开发

常见问题

  1. 支持的站点列表在哪里?

人类可读的支持站点列表位于仓库的 sites.md 文件。它由主 JSON 文件(其中保存了支持的站点列表)自动生成。

机器可读的 JSON 文件 data.json 位于 resources 目录下。

  1. 支持哪些检查账号存在性的方式?

支持以下几种方式(对应 data.json 中的 checkType 值):

  • message —— 最可靠的方式,检查 HTML 响应中是否包含 presenceStrs 中任一字符串,同时不包含 absenceStrs 中的任何字符串

  • status_code —— 检查响应的状态码是否为 2XX

  • response_url —— 检查响应是否未发生重定向,且状态码为 2XX

备注

对于某些反爬虫返回的特殊 HTTP 状态码(例如 LinkedIn 的 HTTP 999),Maigret 会原生地将其视为标准的“未找到 / 不存在”信号,而不是抛出基础设施层面的服务器错误,从而优雅地避免误报。

检查机制的具体细节请见 checking.py

备注

Maigret 目前使用 Majestic Million 数据集来对站点人气进行排序,以替代已停服的 Alexa Rank API。为了与既有配置和解析器保持向后兼容,data.json 以及内部站点模型中的排名字段仍然沿用 alexaRankalexa_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 HuntHacker News

  • 仅当品牌方本身就这样书写时,才使用全小写:kofinotehi5

  • 不要带域名后缀(calendly.comCalendly);除非该域名本身就是品牌名的一部分:last.fmVC.ruArchive.org

  • 除非品牌是缩写,否则不要全大写:VKCNETICQIFTTT

  • 名称中不要www. https:// 前缀

  • 如果品牌名本身带空格,允许保留空格:Star CitizenGoogle Maps

  • 名称中可以使用 {username} 模板:{username}.tilda.ws

拿不准时,看看该服务在自家主页上是怎么称呼自己的。

如何修复误报

如果你要处理站点数据库,别忘了启用统计信息更新的 git hook —— 对应命令为:git config --local core.hooksPath .githooks/

请务必在 maigret 的 git 仓库目录下执行 git 提交,否则该 hook 找不到对应的统计更新脚本。

  1. 确定出问题的站点。

如果你已经知道是哪个站点存在误报,并打算专门修它,可以直接跳到下一步。

否则,用一个随机用户名(例如 laiuhi3h4gi3u4hgt)跑一次搜索,然后看结果即可。你也可以使用社区 Telegram 机器人

  1. 在浏览器中打开该账号链接,并检查:

  • 如果站点已彻底下线,将其从列表中移除

  • 如果站点仍在运行但页面已变化,在 data.json 中相应地更新检查方式

  • 如果站点必须登录才能查看主页,则禁用对它的检查

  1. 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 请求即可完成检查,将其设为 true

  • regexCheck —— 用于校验用户名是否合法的正则,适用于频繁出现误报的场景

  • 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_challengecf_firewallaws_waf_js_challengeddos_guard_challengecustom_bot_protectionjs_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_optwindow._cf_chl 或 "Just a moment"。无法通过 curl_cffi TLS 模拟绕过(已在 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_challengecf_firewallaws_waf_js_challengeddos_guard_challenge,以及未来可能新增的 sucuri_challengeincapsula_challenge 等)。通用的 js_challengecustom_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 的 urlhttps://github.com/{username},而 urlProbehttps://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 的改动提案日志。

主要文件:

每当代码或 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 失效时,激活流程的工作方式如下:

  1. 站点检查会带上失效的 token,向 urlProbe 发起 HTTP 请求

  2. 响应中包含 activation / marks 字段指定的错误信息

  3. 一旦检测到该错误,就会触发 vimeo 对应的激活函数

  4. 激活函数获取新的 JWT token,并将其更新到该站点检查记录中

  5. 下一次站点检查(无论是重试还是新一次 Maigret 运行)使用新的有效 token,检查随即成功

激活机制的实现示例位于 activation.py 中。

如何发布新版本的 Maigret

需要协作者权限,请联系 Soxoj 获取

发布新版本时,需要先在仓库中创建一个新分支,提升版本号并补全实际的changelog;然后再创建一次 release,GitHub Action 会自动构建并发布一个新的PyPI 包。

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>' 做一次粗略检查,以防还有漏掉的旧版本号。

  1. CHANGELOG.md 文件开头新增一个空的段落,并写入当天日期:

## [0.4.0] - 2022-01-03
  1. 获取自动生成的 release notes:

  • 打开 https://github.com/soxoj/maigret/releases/new

  • 点击 Choose a tag,输入 v0.4.0(你的版本号)

  • 点击 Create new tag

  • 点击 + Auto-generate release notes

  • 复制下方描述文本框中的全部内容

  • 粘贴到 CHANGELOG.txt 中你刚刚新建的那个空段落里

  • 删除冗余的 ## What's Changed 行,如果存在 ## New Contributors 段落也一并删除

  • 关闭新建 release 的页面

  1. 提交所有改动,push 上去,并创建 pull request

git add -p
git commit -m 'Bump to YOUR VERSION'
git push origin head
  1. 合并 pull request

  2. 创建新的 release

  • 再次打开 https://github.com/soxoj/maigret/releases/new

  • 点击 Choose a tag

  • v0.4.0 格式输入当前实际版本号

  • 同时把当前版本号填入 Release title 字段

  • 点击 Create new tag

  • 点击 + Auto-generate release notes

  • 点击 "Publish release" 按钮

  1. 就这些 —— 接下来只需等候推送到 PyPI。你可以在 Action 页面查看进度:https://github.com/soxoj/maigret/actions/workflows/python-publish.yml

文档更新

文档由 docs 目录自动生成、自动部署。

手动更新文档的步骤如下:

  1. 修改 docs/source 目录下的某个 .rst 文件。

  2. 在 docs 目录下执行 python -m pip install -e . 进行安装。

  3. 在 docs 目录下的终端中运行 make singlehtml

  4. 在浏览器中打开 build/singlehtml/index.html 查看效果。

  5. 如果你修改了任何英文 .rst 正文(而不只是代码块),请刷新各语言的翻译目录 —— 见下文 翻译 一节。跳过这一步会让非英语版本在被改动的字符串上静默回退到英文。

  6. 如果一切正常,提交并把改动推到 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>;在 .pomsgstr 内必须写作 \\<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>``(例如 ``jadept_BR)即可生成一份空目录。Read the Docs 会把每种语言作为一个独立项目发布,并挂在父项目下(参见 Localization 指南);维护者需要在 RTD 管理界面中创建一次该翻译项目,设置其语言,并将其标记为主项目 maigret 的翻译版本,这样才能启用语言切换器。

路线图

警告

该路线图需要更新,以反映项目当前状态以及未来计划。

https://i.imgur.com/kk8cFdR.png