tn3w/is-crawler
GitHub: tn3w/is-crawler
一个零依赖、无正则表达式的超高速 Python 爬虫检测库,在纳秒级完成 UA 识别,并提供 IP 验证、分类标签、中间件和 robots.txt 生成等完整能力。
Stars: 0 | Forks: 0
# is-crawler
在 50 纳秒内从 User-Agent 字符串检测爬虫。零依赖,无正则表达式,防范 ReDoS。
[](https://pypi.org/project/is-crawler/)
[](https://pypi.org/project/is-crawler/)
[](https://github.com/tn3w/is-crawler/blob/master/LICENSE)
[](https://github.com/tn3w/is-crawler/stargazers)
[](https://pepy.tech/project/is-crawler)
[](https://github.com/tn3w/is-crawler/issues)
[](https://github.com/tn3w/is-crawler/blob/master/CONTRIBUTING.md)
[](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应用防火墙, 反爬虫, 无后门, 机器人检测, 爬虫检测, 网络信息收集, 网络安全, 网络流量分析, 蓝队分析, 逆向工具, 隐私保护, 零依赖