tn3w/is-crawler

GitHub: tn3w/is-crawler

一个零依赖、无正则表达式的超高速 Python 爬虫检测库,在纳秒级完成 UA 识别,并提供 IP 验证、分类标签、中间件和 robots.txt 生成等完整能力。

Stars: 0 | Forks: 0

# is-crawler 在 50 纳秒内从 User-Agent 字符串检测爬虫。零依赖,无正则表达式,防范 ReDoS。 [![PyPI](https://img.shields.io/pypi/v/is-crawler?style=flat-square)](https://pypi.org/project/is-crawler/) [![Python](https://img.shields.io/pypi/pyversions/is-crawler?style=flat-square)](https://pypi.org/project/is-crawler/) [![License](https://img.shields.io/github/license/tn3w/is-crawler?style=flat-square)](https://github.com/tn3w/is-crawler/blob/master/LICENSE) [![Stars](https://img.shields.io/github/stars/tn3w/is-crawler?style=flat-square)](https://github.com/tn3w/is-crawler/stargazers) [![Downloads](https://static.pepy.tech/personalized-badge/is-crawler?period=month&units=international_system&left_color=grey&right_color=blue&left_text=downloads/month&style=flat-square)](https://pepy.tech/project/is-crawler) [![Issues](https://img.shields.io/github/issues/tn3w/is-crawler?style=flat-square)](https://github.com/tn3w/is-crawler/issues) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square)](https://github.com/tn3w/is-crawler/blob/master/CONTRIBUTING.md) [![Buy Me a Coffee](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow?style=flat-square)](https://www.buymeacoffee.com/tn3w)
``` pip install is-crawler ``` ``` from is_crawler import is_crawler is_crawler("Googlebot/2.1 (+http://www.google.com/bot.html)") # True is_crawler("Mozilla/5.0 (X11; Linux x86_64) Firefox/120.0") # False ``` 一次调用,在每个请求上瞬间运行。 ``` \(°o°)/ caught one! /| |\ ``` ## 为什么 爬虫检测处于请求的热路径上。大多数库倾向于使用庞大的正则表达式表,这意味着首次命中缓慢,在恶意 UA 下面临 ReDoS 风险,以及你需要永远承受的毫秒级延迟。 `is_crawler` 对精选的关键词运行 `str.find` 和小型字符扫描。没有回溯,没有数据库负载,没有网络请求。可选的 `crawler_info` 在你需要分类时添加数据库查找。其他所有功能(FCrDNS、IP 段、robots.txt、中间件)都是可选的。 ``` is-crawler ▏ 0.04 µs cua ████████████████████████████████████████████████ 64.00 µs ``` | | is-crawler | crawler-user-agents | ua-parser | | ----------------- | ---------- | ------------------- | --------- | | 热路径正则表达式 | 否 | 是 | 是 | | ReDoS 安全 | 是 | 否 | 否 | | FCrDNS 验证 | 是 | 否 | 否 | | IP 段查询 | 是 | 否 | 否 | | WSGI/ASGI 中间件 | 是 | 否 | 否 | | 预热后的 `is_crawler` | 0.04 µs | 64 µs | 不适用 | ## 实际应用 该 API 在你实际会看到的真实 UA 上返回的结果: | User agent | `is_crawler` | `crawler_name` | `crawler_version` | `crawler_url` | `crawler_signals` | `crawler_info.tags` | | ----------------------------------------------------------------------------------------------------------------- | ------------ | --------------------- | ----------------- | -------------------------------------------------- | ----------------------------------------------------- | ------------------------- | | `Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)` | True | `GPTBot` | `'1.2'` | `'https://openai.com/gptbot'` | `['bot_signal', 'bare_compatible', 'url_in_ua']` | `('ai-crawler',)` | | `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/120.0.0.0 Safari/537.36` | True | `HeadlessChrome` | `'120.0.0.0'` | `None` | `['bot_signal']` | `('browser-automation',)` | | `curl/8.4.0` | True | `curl` | `'8.4.0'` | `None` | `['no_browser_signature']` | `('http-library',)` | | `python-requests/2.31.0` | True | `python-requests` | `'2.31.0'` | `None` | `['no_browser_signature']` | `('http-library',)` | | `Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)` | True | `AhrefsBot` | `'7.0'` | `'http://ahrefs.com/robot/'` | `['bot_signal', 'bare_compatible', 'url_in_ua']` | `('seo',)` | | `facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)` | True | `facebookexternalhit` | `'1.1'` | `'http://www.facebook.com/externalhit_uatext.php'` | `['bot_signal', 'no_browser_signature', 'url_in_ua']` | `('social-preview',)` | | `Mozilla/5.0 (compatible; Nikto/2.5.0)` | True | `Nikto` | `'2.5.0'` | `None` | `['bare_compatible', 'known_tool']` | `('scanner',)` | | `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36` | False | `None` | `None` | `None` | `[]` | `None` | ## 检测 ``` from is_crawler import ( is_crawler, crawler_signals, crawler_info, crawler_has_tag, crawler_name, crawler_version, crawler_url, crawler_contact, ) ua = "Googlebot/2.1 (+http://www.google.com/bot.html)" is_crawler(ua) # True crawler_name(ua) # 'Googlebot' crawler_version(ua) # '2.1' crawler_url(ua) # 'http://www.google.com/bot.html' crawler_signals(ua) # ['bot_signal', 'no_browser_signature', 'url_in_ua'] ua2 = "MyBot/1.0 (contact: bot@example.com)" crawler_contact(ua2) # 'bot@example.com' crawler_contact(ua) # None ``` `is_crawler` 基于三条规则短路判断:正向爬虫信号(如 `bot`/`crawl`/`spider` 等关键字、已知工具、内嵌的 URL/电子邮件)、缺失浏览器签名(无 `Mozilla/`、`WebKit`、OS token 等),或者裸露的 `(compatible; ...)` 块。 `crawler_signals` 暴露了哪些规则被触发,以便进行日志记录和诊断。 ## 分类 `crawler_info` 匹配来自 [monperrus/crawler-user-agents](https://github.com/monperrus/crawler-user-agents) 的 1200 个精选模式及额外内容。这些模式以 48 个条目为一批进行延迟编译。 ``` info = crawler_info(ua) info.url # 'http://www.google.com/bot.html' info.description # "Google's main web crawling bot..." info.tags # ('search-engine',) crawler_has_tag(ua, "search-engine") # True crawler_has_tag(ua, ["ai-crawler", "seo"]) # False ``` 标签:`search-engine`、`ai-crawler`、`seo`、`social-preview`、`advertising`、`archiver`、`feed-reader`、`monitoring`、`scanner`、`academic`、`http-library`、`browser-automation`。 每个标签都提供了单独的包装方法:`is_search_engine`、`is_ai_crawler`、`is_seo`、`is_social_preview`、`is_advertising`、`is_archiver`、`is_feed_reader`、`is_monitoring`、`is_scanner`、`is_academic`、`is_http_library`、`is_browser_automation`。 快速把关: ``` is_good_crawler(ua) # search-engine, social-preview, feed-reader, archiver, academic is_bad_crawler(ua) # ai-crawler, scanner, http-library, browser-automation, seo ``` `advertising` 和 `monitoring` 取决于具体策略,不属于上述任何组别。 ## IP 验证 两种策略,可任选其一或同时使用。仅使用 `socket`,无额外依赖。 ``` from is_crawler.ip import ( verify_crawler_ip, reverse_dns, forward_confirmed_rdns, ip_in_range, known_crawler_ip, known_crawler_rdns, ) verify_crawler_ip("Googlebot/2.1", "66.249.66.1") # True (FCrDNS, UA-name matched) verify_crawler_ip("Googlebot/2.1", "8.8.8.8") # False (spoof) ip_in_range("66.249.66.1") # True (CIDR lookup, offline) known_crawler_rdns("66.249.66.1") # True (rDNS suffix matches any known crawler) reverse_dns("8.8.8.8") # 'dns.google' forward_confirmed_rdns("66.249.66.1", (".googlebot.com",)) # hostname or None ``` `verify_crawler_ip` 执行完整的 FCrDNS 流程:rDNS 查询、与 UA 的供应商进行后缀检查、前向查询、IP 匹配。可捕获 UA 伪装行为。 `ip_in_range` 对来自 39 个官方来源(Google、Bing、OpenAI、Anthropic、Cloudflare、AWS 等)已合并的 CIDR 运行二分查找。低成本且离线运行。 ## 中间件 适用于任何 WSGI 或 ASGI 应用的即插即用方案。零依赖。 ``` from is_crawler.contrib import WSGICrawlerMiddleware, ASGICrawlerMiddleware app = WSGICrawlerMiddleware(app) # Flask, Django app = ASGICrawlerMiddleware(app, block=True, block_tags="ai-crawler") # FastAPI, Starlette # Flask: request.environ["is_crawler"].is_crawler # Django: request.META["is_crawler"].name # FastAPI: request.scope["is_crawler"].verified ``` 两者都会附加一个 `CrawlerMiddlewareResult`,包含 `user_agent`、`ip`、`is_crawler`、`name`、`verified`、`in_ip_range`、`rdns_match`。 标志参数:`block`、`block_tags`、`verify_ip`、`check_ip_range`、`check_rdns`、`trust_forwarded`。一旦 `in_ip_range` 或 `rdns_match` 为真,就会强制 `is_crawler=True`,从而捕获不带 UA 的爬虫。当 `trust_forwarded=True` 时,IP 将依次从 `Forwarded`、`X-Forwarded-For`、`X-Real-IP` 或直接客户端获取。 ## 实用示例 阻止 AI 抓取工具,放行搜索引擎: ``` from fastapi import FastAPI from is_crawler.contrib import ASGICrawlerMiddleware app = FastAPI() app = ASGICrawlerMiddleware(app, block=True, block_tags="ai-crawler", trust_forwarded=True) ``` 从数据库提供实时 `robots.txt`: ``` from flask import Response from is_crawler import build_robots_txt @app.route("/robots.txt") def robots(): return Response(build_robots_txt(disallow=["ai-crawler", "scanner"]), mimetype="text/plain") ``` 在信任之前验证 Googlebot 是否真实: ``` from is_crawler import is_crawler from is_crawler.ip import verify_crawler_ip if is_crawler(ua) and not verify_crawler_ip(ua, ip): abort(403) # spoofed ``` 访问日志中爬虫所占的比例: ``` awk -F'"' '{print $6}' access.log | python -m is_crawler | \ jq -r '.is_crawler' | sort | uniq -c ``` ## robots.txt / ai.txt 根据标签生成指令。名称从数据库模式中提取,跳过仅包含斜杠/URL 的条目。 ``` from is_crawler import build_robots_txt, build_ai_txt, robots_agents_for_tags print(build_robots_txt(disallow=["ai-crawler", "scanner"])) # User-agent: GPTBot # Disallow: / # ... print(build_ai_txt()) # disallows all ai-crawler agents by default # User-Agent: GPTBot # Disallow: / # ... robots_agents_for_tags("ai-crawler") # ['AI2Bot', 'Applebot-Extended', 'Bytespider', 'CCBot', 'ChatGPT-User', ...] ``` `build_robots_txt` 还接受一个 `rules` 列表,包含 `(path, tags)` 对,用于按路径控制: ``` build_robots_txt(rules=[("/api", "scanner"), ("/private", "ai-crawler")]) ``` `assert_crawler(ua)` — 类似于 `crawler_info`,但对于未知的 UA 会抛出 `ValueError`。 ## 命令行工具 ``` python -m is_crawler "Googlebot/2.1 (+http://www.google.com/bot.html)" tail -f access.log | awk -F'"' '{print $6}' | python -m is_crawler python -m is_crawler --help # usage python -m is_crawler --version # show version ``` 每个 UA 对应一个 JSON 对象,包含 `is_crawler`、`name`、`version`、`url`、`contact`、`signals`、`info`。 ## UA 解析器 `parse(ua)` 返回一个包含所有常见字段的 `UserAgent` 对象。零依赖,无正则表达式,带有 4096 条记录的 LRU 缓存。 ``` from is_crawler.parser import parse, parse_or_none ua = parse("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36") ua.browser # 'Chrome' ua.browser_version # '134.0.0.0' ua.browser_major # '134' ua.os # 'Windows' ua.os_version # '10/11' ua.engine # 'Blink' ua.engine_version # '537.36' ua.device # 'Desktop' ua.device_brand # None ua.device_model # None ua.cpu # 'x86_64' ua.is_mobile # False ua.is_tablet # False ua.is_crawler # False ua.languages # [] ua.rendering # 'KHTML, like Gecko' ua.product_token # 'Mozilla/5.0' ua.comment # '(Windows NT 10.0; Win64; x64)' ua.raw # original string ua.to_dict() # all fields as dict ``` `parse_or_none(value)` 规范化 bytes/None/non-str 类型,对于空输入返回 `None`。 ## 基准测试 Python 3.14,Linux x86_64。`cua` = [`crawler-user-agents`](https://pypi.org/project/crawler-user-agents/) v1.47。 **Apache 日志** 42,512 条 UA 记录(8,942 个爬虫,33,570 个浏览器,占比 21%): | 场景 | `is_crawler` | `crawler_info` | `cua.is_crawler` | `cua.crawler_info` | | ---------- | ------------ | -------------- | ---------------- | ------------------ | | 预热缓存 | 0.046 µs | 0.116 µs | 66.234 µs | 1585.007 µs | | 冷缓存 | 0.151 µs | 0.987 µs | — | — | 热路径上快约 1440 倍,`crawler_info` 预热后快约 13700 倍。对 42,512 条 Apache 日志 UA 的完整分类仅需 2.15 毫秒。 **测试用例 UA** 2,149 个爬虫 + 19,910 个浏览器: | 场景 | `is_crawler` (混合) | `crawler_info` | `cua.is_crawler` (混合) | `cua.crawler_info` | | ---------- | -------------------- | -------------- | ------------------------ | ------------------ | | 预热缓存 | 0.04 µs | 1.33 µs | 80.95 µs | 563.53 µs | | 冷缓存 | 2.07 µs | 4.85 µs | 82.00 µs | 581.76 µs | **UA 解析器** 19,910 个真实浏览器 UA 对比 [`ua-parser`](https://pypi.org/project/ua-parser/)(快约 20 倍): | 场景 | `parser.parse` | `ua-parser` | | ---------- | -------------- | ----------- | | 预热缓存 | 21.45 µs | 443.20 µs | | 冷缓存 | 21.20 µs | 443.05 µs | **IP 验证** 预热缓存: | 函数 | 耗时 | | ------------------------ | ------- | | `ip_in_range` | 0.06 µs | | `reverse_dns` | 0.48 µs | | `verify_crawler_ip` | 3.23 µs | | `forward_confirmed_rdns` | 3.69 µs | | `known_crawler_rdns` | 4.27 µs | 每个公共函数都有一个 32k 条记录的 LRU 缓存。首次调用的 rDNS 延迟受限于网络状况。 ## 实现细节 `is_crawler` 使用 `str.find` 和字符扫描,从不使用正则表达式,因此恶意的 UA 无法触发回溯。`crawler_info` 确实使用了 `re`,但仅用于本质上简单的精选上游模式。 数据文件由 `tools/` 中的脚本构建: ``` python3 tools/build_user_agents.py # crawler-user-agents.json from monperrus/crawler-user-agents python3 tools/build_ip_ranges.py # crawler-ip-ranges.json from 39 official sources ``` IP 段的源定义位于 `tools/crawler-ip-ranges.json` 中,可以在不修改构建脚本的情况下进行。 ## 开发 ``` pip install -e ".[dev]" ruff format . && ruff check --fix . npx --yes prettier --write --single-quote --print-width=100 --trailing-comma=es5 --end-of-line=lf "**/*.{md,yml,yaml,html,css,js,ts}" "tools/*.json" ``` 参见 [CONTRIBUTING.md](https://github.com/tn3w/is-crawler/blob/master/CONTRIBUTING.md)。请通过 [GitHub 私人安全建议](https://github.com/tn3w/is-crawler/security) 报告漏洞,而不是在公开问题中报告。参见 [SECURITY.md](https://github.com/tn3w/is-crawler/blob/master/SECURITY.md) 和 [CODE_OF_CONDUCT.md](https://github.com/tn3w/is-crawler/blob/master/CODE_OF_CONDUCT.md)。 ## 许可证 [Apache-2.0](https://github.com/tn3w/is-crawler/blob/master/LICENSE)
标签:Bot管理, CISA项目, pypi包, Python, ReDoS防护, User-Agent解析, WAF, Web安全, web应用防火墙, 反爬虫, 无后门, 机器人检测, 爬虫检测, 网络信息收集, 网络安全, 网络流量分析, 蓝队分析, 逆向工具, 隐私保护, 零依赖