开发
常见问题
支持的站点列表在哪里?
人类可读的支持站点列表位于仓库的 sites.md 文件。它由主 JSON 文件(其中保存了支持的站点列表)自动生成。
机器可读的 JSON 文件 data.json 位于 resources 目录下。
支持哪些检查账号存在性的方式?
支持以下几种方式(对应 data.json 中的 checkType 值):
message—— 最可靠的方式,检查 HTML 响应中是否包含presenceStrs中任一字符串,同时不包含absenceStrs中的任何字符串status_code—— 检查响应的状态码是否为 2XXresponse_url—— 检查响应是否未发生重定向,且状态码为 2XX
备注
对于某些反爬虫返回的特殊 HTTP 状态码(例如 LinkedIn 的 HTTP 999),Maigret 会原生地将其视为标准的“未找到 / 不存在”信号,而不是抛出基础设施层面的服务器错误,从而优雅地避免误报。
检查机制的具体细节请见 checking.py。
备注
Maigret 目前使用 Majestic Million 数据集来对站点人气进行排序,以替代已停服的 Alexa Rank API。为了与既有配置和解析器保持向后兼容,data.json 以及内部站点模型中的排名字段仍然沿用 alexaRank 和 alexa_rank。
镜像站与 ``--top-sites``: 使用 --top-sites N 限定扫描范围时,Maigret 还会额外纳入*镜像*站点(即 source 字段指向 Twitter、Instagram 等父平台的条目),只要在“也将已禁用站点纳入排名”的前提下,该父平台位于 Majestic Million 的前 N 名内。详见 命令行选项 中 --top-sites 下的 Mirrors 段落。
测试
建议使用 Python 3.10 进行测试。
安装测试依赖:
poetry install --with dev
使用以下命令对 Maigret 进行检查:
# run linter and typing checks
# order of checks:
# - critical syntax errors or undefined names
# - flake checks
# - mypy checks
make lint
# run black formatter
make format
# run testing with coverage html report
# current test coverage is 58%
make test
# open html report
open htmlcov/index.html
# get flamechart of imports to estimate startup time
make speed
站点命名规范
站点名称是 data.json 中的键,也会出现在面向用户的报告中。请遵循以下规则:
默认使用首字母大写(Title Case):
Product Hunt、Hacker News。仅当品牌方本身就这样书写时,才使用全小写:
kofi、note、hi5。不要带域名后缀(
calendly.com→Calendly);除非该域名本身就是品牌名的一部分:last.fm、VC.ru、Archive.org。除非品牌是缩写,否则不要全大写:
VK、CNET、ICQ、IFTTT。名称中不要带
www.或https://前缀。如果品牌名本身带空格,允许保留空格:
Star Citizen、Google Maps。名称中可以使用 {username} 模板:
{username}.tilda.ws。
拿不准时,看看该服务在自家主页上是怎么称呼自己的。
如何修复误报
如果你要处理站点数据库,别忘了启用统计信息更新的 git hook —— 对应命令为:git config --local core.hooksPath .githooks/。
请务必在 maigret 的 git 仓库目录下执行 git 提交,否则该 hook 找不到对应的统计更新脚本。
确定出问题的站点。
如果你已经知道是哪个站点存在误报,并打算专门修它,可以直接跳到下一步。
否则,用一个随机用户名(例如 laiuhi3h4gi3u4hgt)跑一次搜索,然后看结果即可。你也可以使用社区 Telegram 机器人。
在浏览器中打开该账号链接,并检查:
如果站点已彻底下线,将其从列表中移除
如果站点仍在运行但页面已变化,在 data.json 中相应地更新检查方式
如果站点必须登录才能查看主页,则禁用对它的检查
在 data.json 中找到该站点。
如果原先的 checkType 不是 message,而你打算修这个检查,请按下面的方式更新:把 checkType 改为 message;在 absenceStrs 中填入一个仅出现在“账号不存在”响应 HTML 中的关键词;在 presenceStrs 中填入一个仅出现在“账号存在”响应 HTML 中的关键词。
如果你不太能挑出合适的关键词,可以通过 --submit 选项传入账号 URL,让 Maigret 自动检测:
maigret --submit https://my.mail.ru/bk/alex
要禁用对某站点的检查,将 disabled 设为 true,或直接运行:
maigret --self-check --site My.Mail.ru@bk.ru
要基于响应 HTML 来调试检查方式,可以运行:
maigret soxoj --site My.Mail.ru@bk.ru -d 2> response.txt
在不同场景下,data.json 中还有几个有用的可选字段:
engine—— 针对某类站点(如论坛)的预定义检查;参见 JSON 文件中的engines节headers—— 一组额外的请求头,会随请求一并发送给该站点requestHeadOnly—— 如果对该站点用 HEAD 请求即可完成检查,将其设为trueregexCheck—— 用于校验用户名是否合法的正则,适用于频繁出现误报的场景requestMethod—— 指定使用的 HTTP 方法(例如POST)。默认情况下,Maigret 原生使用 GET 或 HEAD。requestPayload—— 用于 POST 请求的 JSON 负载字典(例如{"username": "{username}"}),在对接 GraphQL 或现代 JSON API 时非常有用。protection—— 在该站点上检测到的防护类型列表(详见下文)。
protection(站点防护跟踪)
protection 字段用于记录站点使用了哪种反爬虫防护。Maigret 会读取该字段,并在有对应绕过机制可用时自动启用它。
标签分为两类:
有实际效果的标签。 Maigret 会根据该标签更换 HTTP 客户端或请求头。目前只有
tls_fingerprint(会切换到带 Chrome 级 TLS 的curl_cffi)。仅供记录的标签。 Maigret 不会因该标签改变行为;它只是记录该站点“为什么难”,以便未来引入对应的求解器后,可以直接定位到正确的站点集合,不必再重新审计一遍。
在“仅供记录”这一类标签内部,还有一道进一步的划分,决定该站点是否被设为 disabled: true:
ip_reputation是唯一一个保留站点为启用状态的记录类标签。它的意思是“对大多数用户可用,但从数据中心 / 云 IP 访问会失败”。如果禁用了,那些拥有干净 IP 的用户就会被静默地挡在外面,看不到这个本来正常的站点。修复方法在 Maigret 之外(住宅 IP 或--proxy)。cf_js_challenge、cf_firewall、aws_waf_js_challenge、ddos_guard_challenge、custom_bot_protection、js_challenge都与disabled: true搭配出现。它们的含义是“目前对所有人都不能用”;标签本身用于标明防护提供方,这样一旦相应绕过手段就绪,所有打了该标签的站点就可以一次性重新启用。
支持的取值:
tls_fingerprint(有实际效果;站点保持启用) —— 该站点会对 TLS 握手(JA3/JA4)进行指纹识别,阻挡非浏览器客户端。Maigret 会自动改用带 Chrome 浏览器模拟的curl_cffi来绕过。需要curl_cffi包(已作为依赖一并安装)。示例:Instagram、NPM、Codepen、Kickstarter、Letterboxd。ip_reputation(仅供记录;站点保持启用) —— 不论请求头还是 TLS,只要来自数据中心 / 云 IP 都会被站点拦截。无法自动绕过;请从普通网络(而不是数据中心)上运行 Maigret,或使用--proxy。该站点不会被标为disabled,因为对住宅 IP 的用户它仍然可用。示例:Reddit、Patreon、Figma、OnlyFans。cf_js_challenge(仅供记录;与 ``disabled: true`` 搭配) —— Cloudflare Managed Challenge / Turnstile JS 挑战。症状:HTTP 403,响应头含cf-mitigated: challenge;响应体中含有challenges.cloudflare.com、_cf_chl_opt、window._cf_chl或 "Just a moment"。无法通过curl_cffiTLS 模拟绕过(已在 Chrome 123/124/131、Safari 17/18、Firefox 133/135、Edge 101 等多个版本上验证 —— 全部返回同样的 403 挑战页);必须由真实浏览器执行挑战 JS 才能获得 clearance cookie。在引入 CF 挑战求解器之前,这些站点会一直保持disabled: true。示例:DMOJ、Elakiri、Fanlore、Bdoutdoors、TheStudentRoom、forum.hr。cf_firewall(仅供记录;与 ``disabled: true`` 搭配) —— Cloudflare 防火墙规则 / 机器人分数级封禁(WAF action=block,不是 action=challenge)。症状:由 Cloudflare 返回的 HTTP 403(server: cloudflare、含cf-ray响应头),且响应体中没有 JS 挑战相关标记 —— 内容通常是 "Access denied"、"Attention Required",或一个简单的 1015/1016/1020 错误页。与ip_reputation不同,仅靠住宅 IP 也不足以绕过 —— Cloudflare 会综合机器人分数、TLS 指纹、UA、ASN 以及站点方的自定义规则进行判定,因此即便从住宅线路上用curl_cffi模拟 Chrome,依然会拿到 403。这些站点会一直保持disabled: true,直到为其找到针对性的绕过方式(cookie、真实浏览器,或住宅 + 干净会话)。示例:Fark、Fodors、Huntingnet、Hunttalk。aws_waf_js_challenge(仅供记录;与 ``disabled: true`` 搭配) —— 站点使用 AWS WAF 并启用了 JavaScript 挑战。症状:HTTP 202、响应体为空,且响应头含x-amzn-waf-action: challenge(一个需要执行 CAPTCHA / 挑战 JS 包才能获得 token 的挑战)。无论是curl_cffi的 TLS 模拟,还是更换 User-Agent,都无法绕过 —— 需要真实浏览器,或官方的 AWS WAF challenge-solver SDK。在求解器接入前,这些站点会一直保持disabled: true。示例:Dreamwidth。ddos_guard_challenge(仅供记录;与 ``disabled: true`` 搭配) —— DDoS-Guard(ddos-guard.net)反爬虫页。症状:HTTP 403,响应头含server: ddos-guard,响应体内含 "DDoS-Guard"。DDoS-Guard 会按不同的源 IP 对 UA 进行指纹识别,因此仅在一个环境中替换 User-Agent 并不能在其它环境通用;需要支持执行 JS 的绕过手段,或专门针对 DDoS-Guard 的求解器。在求解器接入前,这些站点会一直保持disabled: true。示例:ForumHouse。js_challenge(仅供记录;与 ``disabled: true`` 搭配) —— 针对无法识别提供方的 JavaScript 挑战系统的兜底标签(即:不是 Cloudflare、AWS WAF 或其它已知厂商的、自研挑战页)。只要能从响应头或响应体特征中确定提供方,就应优先使用对应的具体厂商标签。custom_bot_protection(仅供记录;与 ``disabled: true`` 搭配) —— 针对非 JS 挑战类、自研 / 内部反爬虫系统(非 Cloudflare、非 AWS WAF、非 DDoS-Guard)的兜底标签。典型症状:由站点自身源站返回 HTTP 403(server: nginx、AWS ELB 等),并带有定制的拦截页;无论 TLS 指纹还是住宅 IP 都同样会被挡。无法通用地绕过;需要逐站点研究(cookie、会话、代理地理位置等)。示例:Hackerearth("HackerEarth Guardian")、FreelanceJob(nginx 层封禁)。
原则:优先使用厂商特定的防护标签。 当某站点被一个可识别的反爬虫厂商封禁时,务必在标签中记录该厂商(cf_js_challenge、cf_firewall、aws_waf_js_challenge、ddos_guard_challenge,以及未来可能新增的 sucuri_challenge、incapsula_challenge 等)。通用的 js_challenge 和 custom_bot_protection 标签仅留给自研 / 无法识别的系统。原因在于:绕过求解器在本质上是厂商相关的(一个 Cloudflare Turnstile 求解器对 AWS WAF 没用);提前在标签里记下厂商,就能在某种厂商求解器接入的瞬间一次性把所有相关站点全部启用,而无需再次审计每一个被禁用的站点。其它防护类别在能识别厂商时,也适用同一原则。
示例:
"Instagram": {
"url": "https://www.instagram.com/{username}/",
"checkType": "message",
"presenseStrs": ["\"routePath\":\"\\/"],
"absenceStrs": ["\"routePath\":null"],
"protection": ["tls_fingerprint"]
}
urlProbe(可选的主页探测 URL)
默认情况下,Maigret 会向与 url 相同的地址(也就是面向公众的主页链接模板)发起 HTTP 请求。
如果你在 data.json 中设置了 urlProbe,Maigret 会抓取该 URL 来做存在性检查(API、GraphQL、JSON 接口等);而报告和 ``url_user`` 仍然使用 url —— 即用户应当打开的、人类可读的主页地址。
占位符:{username}、{urlMain}、{urlSubpath}(与 url 一致)。示例:GitHub 的 url 是 https://github.com/{username},而 urlProbe 是 https://api.github.com/users/{username};Picsart 的网页主页是 https://picsart.com/u/{username},探测地址则是 https://api.picsart.com/users/show/{username}.json。
实现位置:checking.py 中的 make_site_result。
借助 LLM 修复站点检查
备注
仓库根目录下的 LLM/ 目录中,以 Markdown 形式保存了编辑站点检查的详细指南:检查清单、关于 checkType / data.json / urlProbe 的完整说明、处理误报的方法、寻找公开 JSON API 的思路,以及面向 socid_extractor 的改动提案日志。
主要文件:
site-checks-playbook.md —— 精简的检查清单
site-checks-guide.md —— 详细指南
socid_extractor_improvements.log —— 身份信息抽取器改进项的模板与记录
每当代码或 data.json 中的检查逻辑发生改动时,这些文件都应同步更新。
激活机制
激活机制用于向那些需要额外认证(cookie、JWT token、自定义请求头等)的站点发起请求。
我们以 Maigret 数据库中的 Vimeo 站点检查记录为例:
"Vimeo": {
"tags": [
"us",
"video"
],
"headers": {
"Authorization": "jwt eyJ0..."
},
"activation": {
"url": "https://vimeo.com/_rv/viewer",
"marks": [
"Something strange occurred. Please get in touch with the app's creator."
],
"method": "vimeo"
},
"urlProbe": "https://api.vimeo.com/users/{username}?fields=name...",
"checkType": "status_code",
"alexaRank": 148,
"urlMain": "https://vimeo.com/",
"url": "https://vimeo.com/{username}",
"usernameClaimed": "blue",
"usernameUnclaimed": "noonewouldeverusethis7"
},
其激活方法如下:
def vimeo(site, logger, cookies={}):
headers = dict(site.headers)
if "Authorization" in headers:
del headers["Authorization"]
import requests
r = requests.get(site.activation["url"], headers=headers)
jwt_token = r.json()["jwt"]
site.headers["Authorization"] = "jwt " + jwt_token
当 JWT token 失效时,激活流程的工作方式如下:
站点检查会带上失效的 token,向
urlProbe发起 HTTP 请求响应中包含
activation/marks字段指定的错误信息一旦检测到该错误,就会触发
vimeo对应的激活函数激活函数获取新的 JWT token,并将其更新到该站点检查记录中
下一次站点检查(无论是重试还是新一次 Maigret 运行)使用新的有效 token,检查随即成功
激活机制的实现示例位于 activation.py 中。
如何发布新版本的 Maigret
需要协作者权限,请联系 Soxoj 获取。
发布新版本时,需要先在仓库中创建一个新分支,提升版本号并补全实际的changelog;然后再创建一次 release,GitHub Action 会自动构建并发布一个新的PyPI 包。
新分支示例:https://github.com/soxoj/maigret/commit/e520418f6a25d7edacde2d73b41a8ae7c80ddf39
Release 示例:https://github.com/soxoj/maigret/releases/tag/v0.4.1
1. Make a new branch locally with a new version name. Check the current version number here: https://pypi.org/project/maigret/. Increase only patch version (third number) if there are no breaking changes.
git checkout -b 0.4.0
2. Update Maigret version in four files manually. All four must be in
sync — the previous bump missed docs/source/conf.py and
snapcraft.yaml and they fell behind by a release.
pyproject.toml——[tool.poetry]下的一行version = "X.Y.Z"。maigret/__version__.py—— 一行__version__ = 'X.Y.Z'。docs/source/conf.py—— 两个 Sphinx 字段。release为完整版本号('X.Y.Z');version为简写的major.minor('X.Y',不包含补丁号)。两处都要更新。snapcraft.yaml—— 一行version: X.Y.Z(不带引号,也不带v前缀)。
改完后,用 grep -rE '0\.5\.|0\.6\.|<old>' 做一次粗略检查,以防还有漏掉的旧版本号。
在 CHANGELOG.md 文件开头新增一个空的段落,并写入当天日期:
## [0.4.0] - 2022-01-03
获取自动生成的 release notes:
点击 Choose a tag,输入 v0.4.0(你的版本号)
点击 Create new tag
点击 + Auto-generate release notes
复制下方描述文本框中的全部内容
粘贴到 CHANGELOG.txt 中你刚刚新建的那个空段落里
删除冗余的 ## What's Changed 行,如果存在 ## New Contributors 段落也一并删除
关闭新建 release 的页面
提交所有改动,push 上去,并创建 pull request
git add -p
git commit -m 'Bump to YOUR VERSION'
git push origin head
合并 pull request
创建新的 release
点击 Choose a tag
按 v0.4.0 格式输入当前实际版本号
同时把当前版本号填入 Release title 字段
点击 Create new tag
点击 + Auto-generate release notes
点击 "Publish release" 按钮
就这些 —— 接下来只需等候推送到 PyPI。你可以在 Action 页面查看进度:https://github.com/soxoj/maigret/actions/workflows/python-publish.yml
文档更新
文档由 docs 目录自动生成、自动部署。
手动更新文档的步骤如下:
修改
docs/source目录下的某个.rst文件。在 docs 目录下执行
python -m pip install -e .进行安装。在 docs 目录下的终端中运行
make singlehtml。在浏览器中打开
build/singlehtml/index.html查看效果。如果你修改了任何英文
.rst正文(而不只是代码块),请刷新各语言的翻译目录 —— 见下文 翻译 一节。跳过这一步会让非英语版本在被改动的字符串上静默回退到英文。如果一切正常,提交并把改动推到 GitHub。
翻译
文档通过 Sphinx 的标准 gettext 工作流进行翻译。英文 .rst 文件是事实来源(source of truth);翻译以 .po 目录形式存放在 docs/source/locale/<lang>/LC_MESSAGES/ 之下(目前只有 zh_CN)。
修改任何英文 .rst 文件后,请刷新一次翻译目录,使已有的翻译与新字符串保持对齐:
cd docs
make intl-update LANG=zh_CN
该命令会先通过 sphinx-build -b gettext 重新生成 .pot 文件,再用 sphinx-intl update 把更新合并到各语言的 .po 文件中。新出现的英文字符串会以空的 msgstr "" 形式列出;发生变化的字符串则会被打上 #, fuzzy 标记,需要由译者复核并重新翻译。
在本地预览翻译后的构建:
make html-zh_CN
open build/html_zh_CN/index.html
CJK 转义空格的坑
reStructuredText 的内联标记(粗体、行内代码、超链接)要求两侧都有空白或标点才能正确闭合。英文里这是自然的:总会跟上一个空格或句点。但在中文 / 日文 / 韩文译文里,下一个字符常常是没有分隔符的 CJK 字符,docutils 就会发出如下警告:
<translated>:1: WARNING: Inline strong start-string without end-string.
<translated>:1: WARNING: Inline interpreted text or phrase reference start-string without end-string.
修复办法是在闭合标记与下一个 CJK 字符之间显式插入一个 RST 转义空格 —— 反斜杠加一个空格。在渲染后的 .rst 里写作 \<space>;在 .po 的 msgstr 内必须写作 \\<space>,因为 .po 解析器会吃掉一层反斜杠。
# WRONG — warning, markup leaks past the bold
msgstr "让**所有**检查请求通过指定代理"
# RIGHT — regular space breaks markup cleanly
msgstr "让 **所有** 检查请求通过指定代理"
# RIGHT — escape-space when no visual space is wanted
msgstr "让\\ **所有**\\ 检查请求通过指定代理"
同样的规则也适用于行内代码后紧跟 CJK 字符的情况,以及超链接后紧跟 CJK 圆括号的情况 —— 一律插入 \\<space>。修改任何 .po 文件后请运行 make html-zh_CN;这类警告只会在构建阶段暴露。
如需新增一种语言,执行 make intl-update LANG=<code>``(例如 ``ja、de、pt_BR)即可生成一份空目录。Read the Docs 会把每种语言作为一个独立项目发布,并挂在父项目下(参见 Localization 指南);维护者需要在 RTD 管理界面中创建一次该翻译项目,设置其语言,并将其标记为主项目 maigret 的翻译版本,这样才能启用语言切换器。
路线图
警告
该路线图需要更新,以反映项目当前状态以及未来计划。