اگر فکر می‌کنید یک regex با الگوی bot|crawl|spider برای جداسازی بات از انسان کافی است، آمار سایت شما احتمالاً تا الان چند برابر واقعیت است؛ چون بات‌های مدرن دقیقاً همان چیزی را تقلید می‌کنند که مرورگر شما ارسال می‌کند.

چرا تشخیص بات، مسئله‌ای حیاتی است؟

در یکی از پروژه‌های آماری که روی یک سایت خبری کار می‌کردم، تیم بازاریابی شکایت داشت که «نرخ تبدیل عجیب پایین است». وقتی داده‌ها را دقیق بررسی کردیم، متوجه شدیم حدود ۶۸ درصد بازدیدها از بات‌های اسکرپینگ می‌آمدند که با User-Agentهای جعلی شبیه Chrome خودشان را جا زده بودند. تمام تحلیل‌های قبلی تیم، بر اساس داده‌های آلوده بود. بعد از پیاده‌سازی یک سیستم تشخیص چندلایه، نرخ تبدیل واقعی از ۰.۴٪ به ۱.۳٪ رسید — نه چون سایت تغییر کرده بود، بلکه چون آمار دروغین قبلی حذف شده بود.

این تجربه، اهمیت تشخیص بات را در سطح کسب‌وکار نشان می‌دهد. اما مسئله فقط تحلیل نیست. سه دلیل جدی دیگر هم وجود دارد.

دلیل اول، هزینه‌ی زیرساخت. بات‌ها می‌توانند تا ۷۰٪ پهنای باند و CPU سایت شما را مصرف کنند. اگر آن‌ها را از آمار حذف نکنید، نمی‌توانید تشخیص دهید که بخش بزرگی از هزینه‌ی سرور شما صرف پاسخ دادن به درخواست‌های غیرانسانی می‌شود.

دلیل دوم، امنیت. بسیاری از حملات، مثل credential stuffing و scraping، با User-Agentهای به‌ظاهر انسانی انجام می‌شوند. اگر نتوانید این ترافیک را تشخیص دهید، در برابر این حملات کور هستید.

دلیل سوم، کیفیت داده‌ی کسب‌وکار. اگر تیم محصول بر اساس داده‌های آلوده تصمیم بگیرد، تصمیم‌های اشتباهی گرفته می‌شود. این نوع خطا در پروژه‌های بزرگ، میلیون‌ها تومان هزینه دارد.

تشخیص بات، یک مسئله‌ی فنی نیست؛ یک مسئله‌ی اعتماد به داده است. اگر نتوانید به آمار خودتان اعتماد کنید، تمام تصمیم‌های مبتنی بر آن هم بی‌اعتبار هستند.

این اهمیت، در ساختارهای آماری مثل اپ جنگو برای ردیابی بازدیدکننده به‌طور مستقیم دیده می‌شود. اگر لایه‌ی تشخیص بات درست پیاده نشود، تمام ساختار بالای آن بی‌فایده است.

تکامل User-Agent؛ از یک رشته ساده تا یک جنگ اطلاعاتی

User-Agent یک رشته‌ی متنی است که مرورگر یا کلاینت HTTP با هر درخواست ارسال می‌کند. این رشته، خودش را معرفی می‌کند: چه مرورگری است، چه نسخه‌ای، روی چه سیستم‌عاملی. مفهوم User-Agent از روزهای اول وب وجود داشته و در ابتدا فقط برای سازگاری با مرورگرهای قدیمی استفاده می‌شد.

در سال‌های اول، همه چیز ساده بود. مرورگرها خودشان را معرفی می‌کردند، بات‌ها هم خودشان را به‌عنوان بات معرفی می‌کردند. مثلاً Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) یک User-Agent واقعی بود و Googlebot/2.1 یک بات واقعی. یک regex ساده کافی بود.

اما در دو دهه‌ی گذشته، این تصویر کاملاً تغییر کرده است. سه تحول مهم رخ داده.

تحول اول، جعل آسان User-Agent. امروز هر کلاینتی می‌تواند با یک هدر ساده، خودش را شبیه Chrome 120 معرفی کند. حتی کتابخانه‌های HTTP مثل requests در پایتون، اجازه می‌دهند در یک خط، User-Agent را جعل کنید. این یعنی هر بات مدرن، ظاهر یک کاربر انسانی را دارد.

تحول دوم، ظهور headless browsers. ابزارهایی مثل Puppeteer، Playwright و Selenium می‌توانند یک Chrome واقعی را بدون رابط گرافیکی اجرا کنند. User-Agent این ابزارها دقیقاً همان Chrome واقعی است. تشخیص آن‌ها با regex، غیرممکن است.

تحول سوم، بات‌های هوش مصنوعی. در چند سال اخیر، حجم ترافیک بات‌های مرتبط با هوش مصنوعی چند برابر شده است. این بات‌ها اغلب از IPهای مختلف، User-Agentهای متفاوت و رفتارهای تصادفی استفاده می‌کنند تا از تشخیص فرار کنند.

در نتیجه، تشخیص بات امروز یک مسئله‌ی چندلایه است که به ترکیب چند سیگنال نیاز دارد. مقاله‌ی استخراج نوع و نسخه مرورگر از User-Agent تکنیک‌های پایه‌ای این تحلیل را توضیح می‌دهد، ولی برای تشخیص بات، این تکنیک‌ها نقطه‌ی شروع هستند، نه پایان راه.

روش ساده‌ای که همه استفاده می‌کنند و شکست می‌خورد

اکثر پروژه‌های جنگو، تشخیص بات را با یک regex ساده انجام می‌دهند. کدی شبیه این:

import re


BOT_REGEX = re.compile(r"bot|crawl|spider", re.IGNORECASE)


def is_bot(user_agent: str) -> bool:
    return bool(BOT_REGEX.search(user_agent or ""))

این کد، چهار مشکل جدی دارد.

مشکل اول، پوشش ناقص. این regex فقط سه کلمه‌ی کلیدی را می‌گیرد. بات‌های مدرن ممکن است خودشان را Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 معرفی کنند. این رشته، سه کلمه‌ی ممنوعه را ندارد، پس از فیلتر عبور می‌کند.

مشکل دوم، عدم حساسیت به مرورگرهای واقعی. برخی User-Agentهای واقعی هم ممکن است شامل کلمه‌ای مثل bot باشند. مثلاً ابزارهای تشخیصی یا افزونه‌های خاص مرورگر. این خطا باعث false positive می‌شود.

مشکل سوم، عدم پوشش بات‌های هوشمند. بات‌هایی که برای فرار از تشخیص طراحی شده‌اند، عمداً کلمات ممنوعه را حذف می‌کنند. این بات‌ها ممکن است ماه‌ها بدون تشخیص به اسکرپینگ ادامه دهند.

مشکل چهارم، هزینه‌ی دیباگ بالا. وقتی این regex خطا داشته باشد، شما هیچ راهی ندارید که بفهمید کدام بات‌ها از فیلتر عبور کرده‌اند. چون داده‌های شناسایی نشده را در دیتابیس ذخیره نکرده‌اید.

در یکی از پروژه‌ها، بعد از سه ماه کار با این regex ساده، متوجه شدیم حدود ۴۵٪ رکوردهای آماری ما از بات‌هایی بود که به‌عنوان Mozilla/5.0 (compatible; YandexBot/3.0) خودشان را معرفی می‌کردند، اما چون کلمه‌ی Bot در آن به‌حروف بزرگ نوشته شده بود و regex ما بدون flag مناسب کار می‌کرد، از فیلتر عبور می‌کردند.

برای رفع این مشکلات، به یک معماری چندلایه نیاز دارید. در لایه‌ی اول، الگوهای شناخته‌شده را با regex جامع تشخیص می‌دهیم. در لایه‌های بعدی، به سیگنال‌های عمیق‌تر متوسل می‌شویم. اگر با الگوی services.py در جنگو آشنا هستید، این معماری در قالب یک سرویس اختصاصی پیاده‌سازی می‌شود.

لایه‌ی اول؛ الگوهای شناخته‌شده با regex جامع

لایه‌ی اول، ارزان‌ترین و سریع‌ترین لایه است. یک regex جامع که تمام الگوهای شناخته‌شده را پوشش می‌دهد، بخش بزرگی از بات‌ها را در کمتر از یک میلی‌ثانیه تشخیص می‌دهد.

import re


KNOWN_BOT_PATTERNS = re.compile(
    r"bot|crawl|spider|slurp|"
    r"bingpreview|facebookexternalhit|facebot|"
    r"whatsapp|telegram|discord|skype|slack|"
    r"semrush|ahrefs|mj12|dotbot|petal|"
    r"yandex|baiduspider|sogou|exabot|"
    r"ia_archiver|archive.org_bot|"
    r"python-requests|python-urllib|"
    r"curl|wget|httpclient|java/|okhttp|"
    r"go-http-client|libwww|axios|node-fetch|"
    r"phantomjs|headlesschrome|puppeteer|playwright|"
    r"uptimerobot|pingdom|statuscake|"
    r"gtmetrix|lighthouse|pagespeed|"
    r"monitoring|healthcheck|"
    r"feedfetcher|feedburner|"
    r"linkedinbot|twitterbot|pinterest|"
    r"applebot|duckduckbot|"
    r"rogerbot|screaming frog|"
    r"seznam|naver|"
    r"postmanruntime|insomnia",
    re.IGNORECASE,
)


def is_known_bot(user_agent: str) -> bool:
    if not user_agent:
        return True
    if len(user_agent.strip()) < 20:
        return True
    return bool(KNOWN_BOT_PATTERNS.search(user_agent))

این regex از چند بخش تشکیل شده که هر بخش، دسته‌ای از بات‌ها را پوشش می‌دهد.

دسته‌ی اول، موتورهای جستجو. Googlebot، Bingbot، Yandex، Baidu، DuckDuckBot، Applebot و بقیه. این بات‌ها معمولاً شناخته‌شده هستند و خودشان را درست معرفی می‌کنند.

دسته‌ی دوم، شبکه‌های اجتماعی. Facebook، WhatsApp، Telegram، Discord، LinkedIn و بقیه. وقتی یک لینک از سایت شما در این پلتفرم‌ها به اشتراک گذاشته می‌شود، این بات‌ها برای گرفتن پیش‌نمایش سر می‌زنند.

دسته‌ی سوم، ابزارهای خط فرمان. curl، wget، httpclient و بقیه. اگر در سایت شما این نوع درخواست‌ها ثبت شده، به احتمال زیاد اسکریپت‌های خودکار هستند.

دسته‌ی چهارم، headless browsers. phantomjs، headlesschrome، puppeteer، playwright. این‌ها ابزارهایی هستند که برای اتوماسیون طراحی شده‌اند.

دسته‌ی پنجم، مانیتورینگ. UptimeRobot، Pingdom، StatusCake، GTmetrix، Lighthouse. این ابزارها برای بررسی سلامت سایت استفاده می‌شوند.

دسته‌ی ششم، فیدخوان‌ها. FeedFetcher، FeedBurner و مشابه‌ها. این‌ها برای خواندن RSS سایت شما می‌آیند.

نکته‌ی مهم: این regex باید در یک تابع مستقل قرار بگیرد که به‌سادگی قابل تست و به‌روزرسانی باشد. اگر با الگوی نوشتن Middleware سفارشی در جنگو آشنا هستید، می‌دانید که این تابع باید در یک سرویس اختصاصی باشد، نه مستقیم در middleware.

regex جامع، نقطه‌ی شروع تشخیص بات است، نه پایان آن. هر بات جدیدی که ظاهر شود، باید در این regex اضافه شود، ولی این روش به‌تنهایی برای سایت‌های جدی کافی نیست.

لایه‌ی دوم؛ تحلیل ساختاری User-Agent

بعد از رد کردن بات‌های شناخته‌شده، نوبت به تحلیل ساختاری می‌رسد. بات‌های هوشمند معمولاً User-Agentهایی می‌فرستند که شبیه مرورگر واقعی است، ولی ساختار آن‌ها با مرورگرهای واقعی متفاوت است.

سه سیگنال ساختاری را می‌توانید بررسی کنید.

سیگنال اول، تناقض نسخه‌ی مرورگر و سیستم‌عامل. یک User-Agent که ادعا می‌کند Chrome 120 روی Windows 7 است، مشکوک است، چون Chrome 120 روی Windows 7 پشتیبانی نمی‌شود. این نوع تناقض‌ها در بات‌هایی که User-Agent را از یک منبع قدیمی کپی می‌کنند، شایع است.

سیگنال دوم، حضور نامتناسب کلمات کلیدی. یک User-Agent واقعی Chrome معمولاً الگوی مشخصی دارد. اگر رشته‌ای خارج از این الگو در آن باشد، احتمالاً جعلی است.

def is_structurally_suspicious(user_agent: str) -> bool:
    if not user_agent:
        return True

    ua = user_agent.strip()

    # URL یا نام دامنه در User-Agent
    if "http://" in ua or "https://" in ua:
        return True
    if re.search(r".(com|net|org|ir)", ua, re.IGNORECASE):
        return True

    # کاراکترهای غیرمنتظره
    if re.search(r"[^x20-x7E]", ua):
        return True

    # طول غیرمعمول
    if len(ua) > 1000:
        return True

    # تکرار غیرمعمول یک کاراکتر
    if re.search(r"(.)1{20,}", ua):
        return True

    # Chrome با ادعای Safari بدون WebKit
    if "Chrome" in ua and "Safari" in ua and "AppleWebKit" not in ua:
        return True

    # Firefox با ادعای Chrome
    if "Firefox" in ua and "Chrome" in ua:
        return True

    return False

سیگنال سوم، تناقض بین User-Agent و سایر هدرها. مرورگرهای واقعی مجموعه‌ای از هدرها را با هم می‌فرستند که با هم سازگار است. مثلاً اگر User-Agent ادعا می‌کند Chrome است، ولی هدر Accept-Language خالی است یا هدر Sec-Fetch-* غایب است، احتمالاً بات است.

هدرهای Sec-Fetch-* در سال‌های اخیر توسط مرورگرهای مدرن اضافه شده‌اند و بات‌های قدیمی معمولاً آن‌ها را نمی‌فرستند. حضور یا عدم حضور این هدرها، یک سیگنال قوی است.

نکته‌ی مهم: این تحلیل ساختاری، باید با دقت انجام شود تا false positive ایجاد نکند. برخی مرورگرهای قدیمی یا ابزارهای خاص ممکن است الگوهای غیرمعمول داشته باشند. همیشه نتایج را با داده‌های واقعی سایت خودتان مقایسه کنید. اگر با الگوی تشخیص دستگاه کاربر در جنگو کار کرده‌اید، می‌دانید که این قبیل تحلیلی‌ها همیشه نیاز به کالیبراسیون دارند.

لایه‌ی سوم؛ fingerprinting و ترکیب سیگنال‌ها

بات‌های حرفه‌ای، User-Agent را جعل می‌کنند، ولی اغلب نمی‌توانند تمام سیگنال‌های جانبی را هم جعل کنند. با ترکیب چند سیگنال، می‌توانید یک fingerprint بسازید که تشخیص بات را دقیق‌تر کند.

چهار سیگنال مهم که می‌توانید ترکیب کنید:

سیگنال اول، ترتیب هدرها. مرورگرهای مختلف، هدرهای HTTP را با ترتیب مشخصی می‌فرستند. بات‌ها اغلب این ترتیب را رعایت نمی‌کنند. ترتیب هدرها را می‌توانید در request.META با بررسی request.META.keys() ببینید. هرچند این سیگنال در جنگو به‌طور مستقیم قابل دسترسی نیست، ولی با یک middleware سبک می‌توانید ترتیب اصلی درخواست را استخراج کنید.

سیگنال دوم، هدر Accept. مرورگرهای واقعی هدر Accept مشخصی دارند که به‌ترتیب اولویت مرتب شده است. بات‌ها معمولاً این هدر را ساده می‌کنند یا کپی می‌کنند.

سیگنال سوم، هدر Accept-Language. مرورگرهای واقعی این هدر را با یک یا چند زبان مشخص می‌فرستند. بات‌ها اغلب این هدر را خالی یا با یک مقدار عمومی می‌فرستند.

سیگنال چهارم، هدر Connection. مرورگرهای مدرن معمولاً keep-alive می‌فرستند، ولی بات‌های ساده ممکن است close بفرستند.

def compute_header_score(request) -> int:
    """
    امتیاز 0 (بات قطعی) تا 100 (کاربر قطعی انسانی)
    """
    score = 50

    accept = request.META.get("HTTP_ACCEPT", "")
    if "text/html" in accept:
        score += 10
    if "application/xhtml+xml" in accept:
        score += 5
    if "image/avif" in accept or "image/webp" in accept:
        score += 5

    accept_lang = request.META.get("HTTP_ACCEPT_LANGUAGE", "")
    if accept_lang and "," in accept_lang:
        score += 10
    if accept_lang and len(accept_lang) > 10:
        score += 5

    sec_fetch = request.META.get("HTTP_SEC_FETCH_MODE", "")
    if sec_fetch:
        score += 15
    if sec_fetch in ("navigate", "cors", "same-origin"):
        score += 5

    conn = request.META.get("HTTP_CONNECTION", "")
    if conn.lower() == "keep-alive":
        score += 5

    return min(score, 100)

این score، به‌تنهایی برای تصمیم‌گیری کافی نیست، ولی در کنار سیگنال‌های دیگر، یک تصویر دقیق‌تر می‌سازد. قاعده‌ی کلی این است: هرچه score کمتر، احتمال بات بیشتر.

نکته‌ی عملی: در پروژه‌های واقعی، معمولاً score بین ۳۰ تا ۷۰ برای کاربران انسانی است و زیر ۲۰ برای بات‌ها. این مقادیر باید بر اساس ترافیک واقعی سایت شما کالیبره شود. اگر با الگوی طراحی مدل Visitor و Visit در جنگو کار کرده باشید، می‌دانید که این کالیبراسیون چقدر حیاتی است.

لایه‌ی چهارم؛ تحلیل رفتاری و الگوهای زمانی

دقیق‌ترین لایه‌ی تشخیص بات، تحلیل رفتار است. بات‌ها، صرف‌نظر از اینکه User-Agentشان چقدر خوب باشد، در الگوهای رفتاری خود را لو می‌دهند.

چهار الگوی رفتاری را بررسی کنید.

الگوی اول، سرعت درخواست. یک کاربر انسانی در یک دقیقه نمی‌تواند ۱۰۰ صفحه ببیند. اگر یک IP یا fingerprint در یک دقیقه بیش از ۲۰ درخواست بفرستد، احتمالاً بات است.

from datetime import timedelta
from django.utils import timezone
from analytics.models import PageView


def is_rate_suspicious(visitor, threshold=20, window_seconds=60):
    since = timezone.now() - timedelta(seconds=window_seconds)
    count = PageView.objects.filter(
        visitor=visitor,
        entered_at__gte=since,
    ).count()
    return count > threshold

الگوی دوم، نبود تعامل. کاربران انسانی کلیک می‌کنند، اسکرول می‌کنند، روی دکمه‌ها توقف می‌کنند. بات‌ها صفحات را باز می‌کنند و بدون هیچ تعاملی می‌بندند. اگر ویزیتوری در ۱۰ صفحه‌ی متوالی هیچ Interaction ثبت نکرده باشد، احتمال بات بودنش بالا است. اگر با الگوی ذخیره‌ی PageView و Interaction کار کرده باشید، می‌دانید که این تحلیل، به داده‌ی تاریخی نیاز دارد.

الگوی سوم، الگوی زمانی غیرطبیعی. کاربران انسانی در ساعات شبانه کمتر فعال هستند. اگر یک ویزیتور در ساعات ۳ تا ۵ بامداد به‌طور مداوم درخواست می‌فرستد، احتمال بات بودنش بالا است. این سیگنال به‌تنهایی قوی نیست، ولی در ترکیب با دیگر سیگنال‌ها، مؤثر است.

الگوی چهارم، توزیع مسیر. کاربران انسانی معمولاً مسیرهای متنوعی را می‌بینند. بات‌ها اغلب به یک مسیر خاص (مثلاً /product/list) یا الگوی مشخصی از مسیرها می‌روند.

from collections import Counter


def is_path_pattern_suspicious(visitor, threshold=0.8):
    """
    اگر بیش از threshold از مسیرهای یک ویزیتور تکرار شده باشد،
    احتمال بات بودنش بالا است.
    """
    from analytics.models import PageView

    paths = list(
        PageView.objects.filter(visitor=visitor)
        .values_list("path", flat=True)[:200]
    )
    if len(paths) < 20:
        return False

    counter = Counter(paths)
    most_common_count = counter.most_common(1)[0][1]
    return (most_common_count / len(paths)) > threshold

الگوی رفتاری، دقیق‌ترین سیگنال است، ولی هزینه‌ی محاسباتی آن هم بالاتر است. به همین دلیل، این لایه را فقط بعد از لایه‌های اول و دوم اجرا کنید. این استراتژی چندلایه، در ساختارهای آماری مشابه، مثل اپ جنگو برای ردیابی بازدیدکننده، استاندارد است.

لایه‌ی پنجم؛ محدودیت نرخ و هدرهای HTTP

محدودیت نرخ (Rate Limiting)، یک لایه‌ی دفاعی است که در سطح زیرساخت عمل می‌کند. این لایه به‌جای تشخیص بات بر اساس هویت، بر اساس تعداد درخواست در بازه‌ی زمانی تصمیم می‌گیرد.

سه سطح محدودیت نرخ را در نظر بگیرید.

سطح اول، محدودیت بر اساس IP. هر IP بیش از N درخواست در دقیقه. این سطح، جلوی بات‌های ساده و حملات DOS (Denial of Service) را می‌گیرد.

سطح دوم، محدودیت بر اساس fingerprint. هر ترکیب IP+User-Agent بیش از N درخواست در دقیقه. این سطح، جلوی بات‌هایی که با تغییر IP سعی در دور زدن دارند را می‌گیرد.

سطح سوم، محدودیت بر اساس مسیر. برخی مسیرها (مثل جستجو یا API) باید محدودیت کمتری داشته باشند و برخی مسیرها (مثل ورود یا ثبت‌نام) باید محدودیت بیشتری داشته باشند.

from django.core.cache import cache


def check_rate_limit(key: str, limit: int, window: int) -> bool:
    """
    بازگشت True اگر درخواست باید رد شود.
    """
    cache_key = f"ratelimit:{key}"
    current = cache.get(cache_key, 0)

    if current >= limit:
        return True

    if current == 0:
        cache.set(cache_key, 1, timeout=window)
    else:
        cache.incr(cache_key)

    return False

نکته‌ی مهم: از Redis یا Memcached برای این cache استفاده کنید، نه از cache محلی. در محیط چند پروسه‌ای (مثل Gunicorn با چند worker)، cache محلی بین پروسه‌ها به اشتراک گذاشته نمی‌شود و محدودیت نرخ بی‌اثر می‌شود.

علاوه بر محدودیت نرخ، هدرهای HTTP هم می‌توانند سیگنال‌های مهمی باشند. هدر X-Requested-With، هدر Origin، هدر Referer و هدر Content-Type همگی سیگنال‌هایی هستند که می‌توانند در تشخیص بات کمک کنند. اگر با الگوی نوشتن Middleware سفارشی در جنگو کار کرده باشید، می‌دانید که این تحلیل‌ها باید در همان لایه انجام شوند.

لایه‌ی ششم؛ یادگیری ماشین سبک در عمل

در پروژه‌های بزرگ، ممکن است به تشخیص بات مبتنی بر یادگیری ماشین نیاز پیدا کنید. ولی این کار، پیچیدگی‌های عملیاتی زیادی دارد. سه رویکرد را در نظر بگیرید.

رویکرد اول، مدل‌های ساده. یک مدل Logistic Regression که روی چند فیچر ساده (score هدرها، تعداد درخواست، تنوع مسیر، ساعات فعالیت) آموزش دیده باشد، می‌تواند دقت بالایی داشته باشد. مزیت این رویکرد، سرعت پیش‌بینی و قابلیت توضیح‌پذیری است.

رویکرد دوم، Isolation Forest. این الگوریتم بدون نیاز به برچسب (unsupervised)، داده‌های غیرعادی را تشخیص می‌دهد. مناسب برای پروژه‌هایی که داده‌ی برچسب‌دار ندارند.

رویکرد سوم، ترکیب چند مدل. یک ensemble از چند مدل ساده، معمولاً بهتر از یک مدل پیچیده عمل می‌کند. ولی هزینه‌ی نگهداری آن هم بالاتر است.

نکته‌ی مهم: یادگیری ماشین، فقط وقتی ارزش دارد که شما داده‌ی برچسب‌دار با کیفیت داشته باشید. اگر برچسب‌ها اشتباه باشند، مدل هم اشتباه خواهد بود. توصیه می‌کنم ابتدا با لایه‌های اول تا پنجم شروع کنید و فقط اگر این لایه‌ها کافی نبودند، به سراغ یادگیری ماشین بروید. اگر با الگوی بهینه‌سازی جنگو برای ترافیک بالا آشنا هستید، می‌دانید که پیچیدگی اضافه، فقط وقتی ارزش دارد که مسئله را حل کند.

معماری نهایی؛ چطور همه‌ی لایه‌ها را ترکیب کنیم

حالا بیایید همه‌ی لایه‌ها را در یک معماری نهایی ترکیب کنیم. این معماری، در قالب یک سرویس اختصاصی پیاده می‌شود.

# analytics/services/bot_detection.py

from analytics.models import PageView
from analytics.utils import (
    is_known_bot,
    is_structurally_suspicious,
    compute_header_score,
)


BOT_THRESHOLD = 30  # امتیاز کمتر از این، بات


def detect_bot(request, visitor=None) -> dict:
    """
    نتیجه: dict با کلیدهای is_bot, score, reasons
    """
    reasons = []
    score = 100

    ua = request.META.get("HTTP_USER_AGENT", "")

    # لایه 1: الگوهای شناخته‌شده
    if is_known_bot(ua):
        reasons.append("known_bot_pattern")
        score -= 80

    # لایه 2: ساختار مشکوک
    if is_structurally_suspicious(ua):
        reasons.append("suspicious_structure")
        score -= 30

    # لایه 3: امتیاز هدرها
    header_score = compute_header_score(request)
    if header_score < 40:
        reasons.append("low_header_score")
        score -= (40 - header_score)

    # لایه 4: رفتار مشکوک (اگر visitor داریم)
    if visitor is not None:
        if is_rate_suspicious(visitor):
            reasons.append("rate_suspicious")
            score -= 40

        if is_path_pattern_suspicious(visitor):
            reasons.append("path_pattern_suspicious")
            score -= 20

    is_bot = score < BOT_THRESHOLD

    return {
        "is_bot": is_bot,
        "score": max(0, score),
        "reasons": reasons,
    }

و در middleware:

# analytics/middleware.py

class VisitorTrackingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        from analytics.services.bot_detection import detect_bot

        detection = detect_bot(request)

        if detection["is_bot"]:
            request._analytics_skip = True
            return self.get_response(request)

        # ادامه‌ی ردیابی
        ...
        response = self.get_response(request)
        return response

این معماری، سه مزیت کلیدی دارد.

مزیت اول، لایه‌بندی. هر لایه، مستقل از لایه‌های دیگر قابل تست و توسعه است.

مزیت دوم، تصمیم‌گیری مبتنی بر امتیاز. به‌جای یک تصمیم باینری، از یک امتیاز پیوسته استفاده می‌کنیم. این کار، کالیبراسیون را ساده‌تر می‌کند.

مزیت سوم، شفافیت. هر تصمیم، با یک دلیل مشخص ثبت می‌شود. این کار، دیباگ را چند برابر ساده‌تر می‌کند.

نکته‌ی مهم: همیشه نتیجه‌ی تشخیص را در دیتابیس ذخیره کنید، حتی اگر مطمئن نیستید. این داده‌ها برای آموزش مدل‌های آینده حیاتی هستند. اگر با الگوی طراحی مدل Visitor و Visit در جنگو کار کرده باشید، می‌دانید که این داده‌ها باید در یک جدول اختصاصی نگه داشته شوند.

false positive؛ دشمن پنهان تشخیص بات

خطرناک‌تر از رد شدن یک بات از فیلتر، رد شدن یک کاربر انسانی به‌عنوان بات است. این خطا، به «false positive» معروف است و می‌تواند تجربه‌ی کاربر را نابود کند.

چند منبع رایج false positive:

منبع اول، ابزارهای دسترس‌پذیری. ابزارهایی مثل screen readers ممکن است User-Agentهای غیرمعمول داشته باشند. اگر این‌ها را به‌عنوان بات تشخیص دهید، کاربران دارای اختلالات بینایی را از سایت خود محروم کرده‌اید.

منبع دوم، VPN و پروکسی. برخی کاربران انسانی از VPN استفاده می‌کنند که IP آن با برخی بات‌های شناخته‌شده مشترک است.

منبع سوم، بات‌های دوستانه. ابزارهای خودکار مثل IFTTT، Zapier، یا حتی بازدیدهای خودکار تیم تست شما، ممکن است در آمار ظاهر شوند.

منبع چهارم، مرورگرهای قدیمی. کاربرانی که از مرورگرهای قدیمی استفاده می‌کنند، ممکن است User-Agentهای نامتعارف داشته باشند.

برای کاهش false positive، دو راهبرد را در نظر بگیرید.

راهبرد اول، whitelist. یک لیست سفید از IPها یا User-Agentهای معتبر داشته باشید. مثلاً IPهای خودتان، IPهای CDN معتبر، یا User-Agentهای ابزارهای خاص.

راهبرد دوم، احتیاط در تصمیم‌گیری. اگر امتیاز یک درخواست مرزی است، آن را به‌عنوان نامطمئن ذخیره کنید، نه به‌عنوان بات. سپس به‌طور دوره‌ای این موارد را بازبینی کنید.

class BotDetectionResult(models.Model):
    visitor = models.ForeignKey("analytics.Visitor", on_delete=models.CASCADE)
    detected_at = models.DateTimeField(auto_now_add=True)
    score = models.PositiveSmallIntegerField()
    is_bot = models.BooleanField()
    is_uncertain = models.BooleanField(default=False)
    reasons = models.JSONField(default=list)
    reviewed = models.BooleanField(default=False)
    reviewed_as_bot = models.BooleanField(null=True, blank=True)

این مدل، به شما اجازه می‌دهد که بعد از مدتی، موارد نامطمئن را بازبینی کنید و مدل را بهبود دهید. این رویکرد iterative، در پروژه‌های آماری مشابه، مثل ذخیره‌ی PageView و Interaction، استاندارد است.

طراحی داده برای ذخیره‌ی نتیجه‌ی تشخیص

ذخیره‌ی نتیجه‌ی تشخیص، به‌اندازه‌ی خود تشخیص اهمیت دارد. سه سطح داده را باید ذخیره کنید.

سطح اول، نتیجه‌ی فوری. برای هر درخواست، نتیجه‌ی نهایی (bat یا human) باید در دیتابیس ذخیره شود. این کار در middleware انجام می‌شود.

سطح دوم، دلایل تصمیم. چرا این درخواست به‌عنوان بات تشخیص داده شد؟ کدام لایه‌ها فعال شدند؟ این دلایل باید در یک فیلد JSON ذخیره شوند.

سطح سوم، بازبینی دوره‌ای. بعد از مدتی، باید بتوانید تصمیم‌های گذشته را بازبینی کنید. این کار به شما اجازه می‌دهد که مدل را بهبود دهید.

نکته‌ی مهم در طراحی داده: همیشه یک فیلد is_uncertain داشته باشید. این فیلد، به شما اجازه می‌دهد موارد مرزی را جدا کنید. اگر با الگوی انتقال منطق از services.py به templatetags آشنا هستید، می‌دانید که این نوع انعطاف در طراحی داده، در طول زمان ارزش زیادی پیدا می‌کند.

حریم خصوصی و اخلاق در تشخیص بات

تشخیص بات، مرز باریکی با نقض حریم خصوصی دارد. سه اصل را در نظر بگیرید.

اصل اول، حداقل داده. برای تشخیص بات، به داده‌ی زیادی نیاز ندارید. User-Agent، IP و الگوی درخواست‌ها کافی است. نیازی به ذخیره‌ی محتوای درخواست‌ها یا داده‌های هویتی نیست.

اصل دوم، شفافیت. کاربران باید بدانند که سایت شما از سیستم تشخیص بات استفاده می‌کند. این اطلاع‌رسانی، در سیاست حریم خصوصی سایت باید ذکر شود.

اصل سوم، عدم تصمیم‌گیری خودکار بر اساس داده‌های حساس. اگر سیستم شما یک کاربر را به‌عنوان بات تشخیص داد، نباید به‌طور خودکار دسترسی او را قطع کنید. باید یک فرایند بازبینی انسانی وجود داشته باشد.

تشخیص بات، ابزاری برای بهبود کیفیت داده است، نه ابزاری برای محدود کردن کاربران. اگر این مرز را گم کنید، به‌جای محافظت از سایت، به کاربران خود آسیب زده‌اید.

anti-patternهای رایج در پیاده‌سازی تشخیص بات

در بازبینی پروژه‌های مختلف، این اشتباهات را زیاد دیده‌ام:

۱. تصمیم‌گیری باینری. به‌جای امتیاز پیوسته، از یک تصمیم صفر و یک استفاده می‌کنید. این کار، کالیبراسیون را غیرممکن می‌کند.

۲. نداشتن log. تصمیم‌های تشخیص را ثبت نمی‌کنید. در نتیجه، وقتی خطایی رخ می‌دهد، نمی‌دانید از کجا آمده.

۳. عدم بازبینی دوره‌ای. مدل را یک‌بار می‌سازید و بعد فراموش می‌کنید. بات‌ها در طول زمان تغییر می‌کنند، پس مدل هم باید تغییر کند.

۴. عدم تست false positive. فقط به false negative (بات‌هایی که رد نمی‌شوند) توجه می‌کنید. false positive (کاربرانی که اشتباهاً به‌عنوان بات تشخیص داده می‌شوند) هم به‌همان اندازه مهم است.

۵. استفاده از لیست‌های ثابت. لیست IPهای بات‌ها یا User-Agentهای بات‌ها به‌سرعت قدیمی می‌شود. باید یک سیستم پویا داشته باشید.

۶. تشخیص در ویو به‌جای middleware. اگر تشخیص بات را در ویو انجام دهید، مجبورید آن را در هر ویو تکرار کنید. اگر با الگوی نوشتن Middleware سفارشی در جنگو کار کرده باشید، می‌دانید که این کار اشتباه است.

۷. عدم هماهنگی با CDN. اگر سایت شما پشت CDN است، IP واقعی کاربر در هدر X-Forwarded-For می‌آید. اگر این را در نظر نگیرید، تشخیص شما بر اساس IP اشتباه خواهد بود.

۸. تصمیم‌گیری بر اساس یک سیگنال. اگر فقط بر اساس User-Agent تصمیم بگیرید، بات‌های حرفه‌ای از فیلتر عبور می‌کنند. همیشه از ترکیب سیگنال‌ها استفاده کنید.

۹. نادیده‌گرفتن بات‌های دوستانه. برخی بات‌ها (مثل Googlebot) برای سئو مفید هستند و نباید مسدود شوند. باید بین بات‌های خوب و بد تفکیک کنید.

۱۰. عدم مستندسازی دلایل. چرا یک بات مسدود شد؟ چرا یک کاربر انسانی به‌عنوان بات تشخیص داده شد؟ بدون مستندسازی، بهبود مدل غیرممکن است.

پرسش‌های پرتکرار درباره‌ی تشخیص بات در جنگو

آیا می‌توانم همه‌ی بات‌ها را مسدود کنم؟ نه، و نباید هم بکنید. بات‌های Googlebot، Bingbot و بقیه برای سئو مفید هستند. همچنین بات‌های شبکه‌های اجتماعی برای پیش‌نمایش لینک ضروری هستند. تمرکز شما باید روی بات‌های اسکرپینگ و بات‌های مخرب باشد.

آیا User-Agent جعل شده را می‌توان تشخیص داد؟ به‌تنهایی سخت است، ولی با ترکیب سیگنال‌های دیگر (هدرها، رفتار، fingerprint) می‌توان به دقت بالایی رسید. هیچ روشی صد درصد نیست.

آیا از سرویس‌های خارجی برای تشخیص بات استفاده کنم؟ برای پروژه‌های کوچک و متوسط، یک سرویس SaaS مثل Cloudflare Bot Management می‌تواند مفید باشد. برای پروژه‌های بزرگ، پیاده‌سازی داخلی بهتر است، چون کنترل کامل روی داده دارید.

چطور false positive را کاهش دهم؟ سه راه: اول، whitelist برای IPها و User-Agentهای معتبر. دوم، احتیاط در تصمیم‌گیری (ذخیره‌ی موارد مرزی به‌عنوان uncertain). سوم، بازبینی دوره‌ای موارد مسدودشده.

آیا باید IP بات‌ها را مسدود کنم؟ فقط در موارد بسیار شدید. مسدود کردن IP می‌تواند برای کاربران انسانی که از VPN استفاده می‌کنند مشکلی ایجاد کند. راه بهتر، محدودیت نرخ است.

چطور بین Googlebot واقعی و جعلی تفکیک کنم؟ Googlebot واقعی از IPهای مشخصی می‌آید که Google منتشر کرده است. می‌توانید با reverse DNS lookup بررسی کنید که درخواست از دامنه‌ی googlebot.com یا google.com آمده است یا نه.

چطور بات‌های هوش مصنوعی را تشخیص دهم؟ این بات‌ها معمولاً خودشان را به‌عنوان GPTBot، ClaudeBot، CCBot یا مشابه معرفی می‌کنند. اگر نمی‌خواهید محتوای شما برای آموزش مدل‌های AI استفاده شود، این User-Agentها را در regex خود اضافه کنید. اگر با الگوی طبقه‌بندی Referrer در جنگو کار کرده باشید، می‌دانید که این نوع طبقه‌بندی، به یک لیست پویا نیاز دارد.

آیا محدودیت نرخ روی همه‌ی IPها اعمال شود؟ نه. برخی IPها (مثل IPهای موبایل اپراتورها) ممکن است به‌طور طبیعی نرخ بالاتری داشته باشند. توصیه می‌کنم آستانه‌ها را بر اساس داده‌ی واقعی سایت خود کالیبره کنید.

چطور می‌توانم بات‌های جدید را شناسایی کنم؟ سه راه: اول، بازبینی دوره‌ای User-Agentهای ناشناخته. دوم، تحلیل الگوهای رفتاری. سوم، اطلاع از منابع صنعتی مثل لیست‌های عمومی بات‌ها.

آیا باید روی درخواست‌های AJAX هم تشخیص بات اعمال کنم؟ بله، ولی با احتیاط. درخواست‌های AJAX معمولاً هدرهای متفاوتی دارند و ممکن است از بات‌ها شبیه‌تر به‌نظر بیایند. توصیه می‌کنم از همان امتیاز پیوسته استفاده کنید، ولی آستانه را برای AJAX بالاتر ببرید.

چطور با تغییرات User-Agent بات‌ها هماهنگ شوم؟ regex خود را به‌طور دوره‌ای به‌روز کنید. یک کامند بنویسید که User-Agentهای تکرارشونده با score پایین را جمع‌آوری کند و به شما نشان دهد. اگر با الگوی ساخت پنل ادمین با کارت‌های quick view کار کرده باشید، می‌دانید که این نوع پنل، برای بازبینی دوره‌ای بسیار مفید است.

آیا از یادگیری ماشین استفاده کنم؟ برای پروژه‌های کوچک و متوسط، احتمالاً نه. پیچیدگی عملیاتی این کار، بیشتر از مزیتش است. برای پروژه‌های بزرگ که داده‌ی برچسب‌دار با کیفیت دارند، یادگیری ماشین می‌تواند مفید باشد.

چطور بات‌های حرفه‌ای را که از headless browser استفاده می‌کنند تشخیص دهم؟ این بات‌ها سخت‌ترین هستند، چون User-Agentشان دقیقاً مثل Chrome واقعی است. راه‌حل: ترکیب سیگنال‌های هدرها، تحلیل رفتاری و محدودیت نرخ. همچنین برخی کتابخانه‌ها مثل fingerprintjs2 می‌توانند در سمت کلاینت، تفاوت‌های ظریف را تشخیص دهند.

آیا باید همه‌ی بات‌ها را در دیتابیس ذخیره کنم؟ توصیه می‌کنم فقط نتیجه‌ی نهایی (score و is_bot) و دلیل را ذخیره کنید. ذخیره‌ی داده‌ی خام هر درخواست بات، حجم دیتابیس را چند برابر می‌کند. اگر با الگوی حذف رکوردهای یتیم با batch delete کار کرده باشید، می‌دانید که پاک‌سازی دوره‌ای چقدر اهمیت دارد.

نگاهی از منظر مهندس داده در مقیاس میلیونی

در مقیاس میلیون‌ها درخواست در روز، تشخیص بات تبدیل به یک مسئله‌ی مهندسی داده می‌شود، نه فقط یک مسئله‌ی کدنویسی. سه مفهوم بنیادین را باید بازتعریف کنید.

مفهوم اول، latency بودجه. تشخیص بات نباید بیش از ۵ میلی‌ثانیه به هر درخواست اضافه کند. اگر لایه‌های شما بیش از این مقدار زمان می‌گیرند، باید به فکر معماری توزیع‌شده باشید. یک راه‌حل رایج، استفاده از Bloom filter برای تشخیص سریع IPهای شناخته‌شده است. Bloom filter در حافظه‌ی کم، احتمال false positive پایینی دارد و سرعت lookup آن O(1) است.

مفهوم دوم، ذخیره‌سازی ستونی. در مقیاس بالا، ذخیره‌ی نتایج تشخیص در یک دیتابیس رابطه‌ای ممکن است گران تمام شود. دیتابیس‌های ستونی مثل ClickHouse یا BigQuery برای این نوع داده طراحی شده‌اند. این دیتابیس‌ها، کوئری‌های تحلیلی را چند صد برابر سریع‌تر اجرا می‌کنند.

مفهوم سوم، جریان پیوسته. در معماری‌های مدرن، تشخیص بات به‌جای یک تصمیم لحظه‌ای، یک جریان پیوسته است. هر تصمیم، به یک event stream فرستاده می‌شود و مدل، به‌طور مداوم به‌روز می‌شود. این الگو، در سیستم‌های بزرگ استاندارد است، ولی پیاده‌سازی آن پیچیده است.

نکته‌ی آخر: در مقیاس بالا، هیچ مدلی صد درصد دقیق نیست. هدف، رسیدن به یک trade-off قابل قبول بین false positive و false negative است. بهترین کاری که می‌توانید بکنید این است که این trade-off را صریح کنید و به‌طور مداوم آن را اندازه بگیرید. اگر با الگوی بهینه‌سازی جنگو برای ترافیک بالا کار کرده باشید، می‌دانید که این نوع رویکرد، در تمام لایه‌های پروژه ضروری است.

آن سؤالی که در پایان باید بپرسید

قبل از اینکه سیستم تشخیص بات خود را نهایی کنید، یک پرسش را از خودتان بپرسید: «اگر امروز یک کاربر انسانی به‌اشتباه مسدود شود، آیا متوجه می‌شوم؟» اگر پاسخ منفی است، یعنی سیستم شما به یک بازبینی انسانی نیاز دارد. اگر پاسخ مثبت است، یعنی سیستم شما به‌درستی طراحی شده است.

تشخیص بات، در نهایت یک تصمیم کسب‌وکار است، نه یک تصمیم فنی. قبل از انتخاب هر مدل یا هر الگوریتم، باید بدانید چه سطحی از دقت برای شما کافی است. سایت‌های خبری با ۱۰ میلیون بازدید روزانه، تحمل کمتری برای false positive دارند، چون هر false positive معادل از دست دادن یک کاربر است. سایت‌های فروشگاهی با ترافیک کمتر، می‌توانند سختگیرتر باشند.

در پایان، اگر این تجربه را در پروژه‌ی خودتان داشته‌اید — مثلاً جایی که بات‌های حرفه‌ای از فیلتر شما عبور کرده‌اند یا جایی که کاربران انسانی به‌اشتباه مسدود شده‌اند — برایم جالب است بدانید. مخصوصاً اگر راه‌حل خاصی برای یک سناریوی خاص پیدا کرده‌اید، چون همان راه‌حل‌ها می‌توانند به خواننده‌ی بعدی کمک کنند. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید.