thomasproject-stack/scrapekit

GitHub: thomasproject-stack/scrapekit

一个免费优先的 Python 网页抓取工具包,通过多层成本递增的抓取级联与硬性费用上限,在保障数据真实性的同时避免代理预算失控。

Stars: 0 | Forks: 0

# scrapekit **一个依赖注入的 Python 抓取工具包,具备免费优先的抓取级联、无凭证开放数据发现助手,以及主题驱动的社会工程/OSINT(开源情报)引擎 — 旨在为 GCC(海湾合作委员会)房地产供应追踪器提供数据馈送,绝不捏造数值或耗尽代理预算。** 每一层都是导入安全且惰性的:缺失的可选依赖项会直接顺延至下一个选项,未配置的付费层是严格的 `$0` 空操作,而任何无法获取真实内容的采集器都会返回 `None` / `[]`,而不是凭空捏造。 ## 为什么它很有趣 大多数抓取代码只是一堆一次性的脚本,每个脚本都在重复解决“网站封锁了我”的问题,并且在你强行加上代理的那一刻就会悄悄地花钱。scrapekit 将其转变为一个小巧、打包好的库,具有三个经过深思熟虑的设计立场: - **免费优先,付费出口为可选并设有上限。** 抓取级联会首先尝试成本最低的客户端(纯 `httpx`),只有在免费路径真正被*封锁*时,才会升级到浏览器 TLS、隐身 Chrome 或按流量计费住宅代理 —— 设有严格的单次运行和每日兆字节上限及熔断开关,因此对受围墙保护目标的重试循环绝不会悄无声息地消耗掉千兆字节的流量。这一点已作为 [ADR-0001](docs/adr/0001-residential-egress-free-first-escalation.md) 记录在案。 - **HTTP 200 并不代表成功。** 每一层都会通过相同的反机器人指纹检查(`datadome`、`_incapsula_resource`、`cf-challenge`、`just a moment`、`captcha`…)来运行响应。一个带有 HTTP 200 状态码的验证墙会被视为一次 *miss*,因此级联会继续升级,而不是将 CAPTCHA 页面缓存为“数据”。 - **结构上的诚实留空。** 六个社交数据填充器和每个发现助手在无法获取真实内容或缺少凭证时都会返回 `None` / `[]`(并附带一条日志)。下游的任何内容都不必猜测某一行数据是否真实。 ## 抓取级联的精确工作原理 该级联位于 `scrapekit/fetch/` 中。每一层都是一个独立的模块,暴露相同的契约 —— `fetch_html(url, *, proxy=None, timeout=...) -> str | None` —— 因此某一层可以从已安装的包中*导入*,或者原封不动地*复制*到另一个项目的 `src/` 目录中。`cascade.py` 是按照成本从低到高将它们链接起来的运行器: | 顺序 | 层 | 成本 | 相比前一层增加的内容 | |---|---|---|---| | 1 | `httpx` | 免费 | 从数据中心 IP 发起普通 GET 请求 | | 2 | `curl_cffi` | 免费 | 真实的浏览器 **TLS/JA3 指纹**(击败简单的 TLS 封锁) | | 3 | `nodriver` | 免费 | 无头隐身 **Chrome** —— 清除 JS 挑战墙 | | 4 | `flaresolverr` | 免费* | 一个 Cloudflare "Just a moment" 验证求解器 sidecar(可选的 Docker 服务) | | 5 | `wayback` | 免费 | 当线上网站不可达时的最后手段 **Internet Archive** 快照 | | — | `residential` | **付费,有上限** | 仅作为升级出口,从不在默认顺序中(见下文) | 两条规则使其变得健壮,而不仅仅是一个列表: **围墙即未命中。** 如果响应体为空、短于 `min_len`(默认 **200** 个字符),或者前 6000 个字符中包含任何反机器人标记,`looks_blocked(html)` 就会返回 `True` —— 因此级联会越过返回 200 状态码但带有 CAPTCHA 的响应,而不是直接将其返回。 ``` BLOCK_MARKERS = ("_incapsula_resource", "incident id", "datadome", "cf-challenge", "just a moment", "captcha", "px-captcha", "access denied", "attention required", "enable javascript and cookies") ``` **付费出口是免费优先且具有硬上限的。** `fetch_escalating()` 会先运行整个免费级联,只有在返回为空时才会升级到住宅代理。单个 URL 字符串是唯一的代理真实性来源;每一层都会为其自身的客户端对其进行格式化。升级受三个由环境变量驱动的守卫限制: ``` can_proxy = (allow_residential and res.is_configured() and not res.cap_reached()) force = domain in ALWAYS_PROXY_DOMAINS # closed set that blocks 100% of the time # free pass 仅对 `force` 域名 SKIPPED(反正注定失败);其他所有内容都优先使用 free-first ``` `residential.py` 将每个代理传输的字节计量到一个仅追加的账本中,并在达到 `RESIDENTIAL_MAX_MB_PER_RUN`(默认 50 MB ≈ $0.05)或 `RESIDENTIAL_MAX_MB_PER_DAY`(默认 200 MB ≈ $0.20)时,或者当 `RESIDENTIAL_PROXY_DISABLED=1` 时拒绝升级。**未配置 ⇒ 严格的空操作:** 在没有凭证的情况下,`fetch_escalating` 与免费级联在字节上是完全相同的,且不花费任何成本。`fetch_json_escalating()` 是发现助手使用的 JSON-API 孪生方法,因此开放数据门户上的数据中心 IP 封锁也会以同样的方式自动降级处理。 ## 另外两个家族 **`discover/` — 无凭证开放数据助手。** 针对大多数门户背后已有的结构化数据源,提供廉价、免登录的读取器:`arcgis`(Esri FeatureServer/MapServer 分页)、`opendatasoft`(Explore v2.1)、`ckan`(`package_search`/`datastore_search`)、`sitemap` → `jsonld`(开发者项目页面)、`next_data`(`__NEXT_DATA__` + Next.js 数据路由)和 `algolia`(捕获密钥后进行索引浏览)。`catalog.py` 将数据源研究过程转化为一个开箱即用的注册表 —— 每个条目都指定了数据源、区域、采集器类型和参数 —— 因此 `catalog.fetch("ajman_projects")` 或 `catalog.verify_all()` 即可直接运行。 **`social/` — 一个主题驱动的发现引擎。** 在原始的按平台采集器之上,有一个中立的引擎,它接收消费者定义的 `Subject`(关键字 + 平台),构建 Google 风格的 dork,将每个结果 URL 路由到其所属的平台,并将其填充为统一的 `SocialPost`。它不导入任何消费项目的代码:调用者注入自己的 `search_fn`(例如 SearXNG 级联)和可选的 `fetch_fn`。所有六个填充器都是免费的 —— X 使用 fxtwitter/oembed/syndication,LinkedIn 使用 OG 片段,YouTube 使用 `yt-dlp`,reddit 使用 `.json`,Telegram 使用公共的 `t.me//?embed=1` 小部件,Instagram 使用匿名的 `instaloader` —— 并且 **无法获取真实内容的填充器会返回 `None`,因此引擎会丢弃该命中,而不是生成捏造的帖子。** 存在一个付费回退(`social/paid/`),但默认为 OFF 且受密钥限制:在设置密钥之前不会有任何网络请求。 一个单行 CLI 封装了所有这些: ``` scrapekit social discover "ROSHN Sedra launch" --platforms x,linkedin --langs en,ar scrapekit social follow x emaardubai # account-scoped discovery, no login scrapekit social monitor --per-handle 25 # sweep the curated GCC handles ``` ## 架构 ``` ┌─────────────────────────────────────────────┐ consumer / tracker │ injects: search_fn, fetch_fn, proxy URL │ (or the scrapekit CLI)│ never the other way round — scrapekit │ │ imports nothing from the consuming project │ └───────────────┬─────────────────────────────┘ │ ┌────────────────────────────────┼────────────────────────────────┐ ▼ ▼ ▼ fetch/ (HTML) discover/ (JSON, no creds) social/ (records) ┌──────────────────────┐ ┌───────────────────────┐ ┌──────────────────────┐ │ cascade.fetch() │ │ arcgis · opendatasoft │ │ subjects → dork → │ │ httpx │ │ ckan · sitemap·jsonld │ │ search_fn → routing → │ │ → curl_cffi (TLS) │ │ next_data · algolia │ │ 6 free hydrators → │ │ → nodriver (Chrome) │ │ │ │ │ SocialPost │ │ → flaresolverr │ │ catalog.py registry │ │ (None ⇒ hit dropped) │ │ → wayback │ └───────────┬───────────┘ └───────────┬──────────┘ │ │ │ │ │ │ looks_blocked() ────┤ a 200-with-a-wall is a MISS, keep escalating │ │ │ │ │ │ │ fetch_escalating() ─┼── free-first ───────┴── fetch_json_escalating() ─────┘ │ │ │ │ ▼ only if free path BLOCKED and under cap extract/ │ residential exit ── metered → ledger → hard caps + kill-switch trafilatura | defuddle └──────────────────────┘ unconfigured ⇒ strict $0 no-op ``` ## 技术栈 Python 3.11+ · `curl_cffi` · `nodriver` · `httpx` · `trafilatura` + `lxml`/`beautifulsoup4` · `yt-dlp` · `praw` · `telethon` · `twscrape` · `instaloader` · Node.js(`defuddle` 提取器)· SearXNG · FlareSolverr · pytest 导入 scrapekit 只需要标准库;每一个沉重的依赖项都是可选的,在使用它的层内部进行惰性导入,并声明为 `pyproject` 中的扩展依赖,以便使用者只安装它实际调用的内容。 ## 在本地运行 ``` python3 -m venv .venv && source .venv/bin/activate # 仅安装你需要的内容(optional-extras),或安装所有内容: pip install -e ".[fetch]" # just the cheap browser-TLS layer pip install -e ".[fetch,stealth,clean]" # + stealth Chrome + trafilatura extract pip install -e ".[all]" # every harvester (yt-dlp, praw, telethon, …) # 使用 fetch cascade python3 -c "from scrapekit.fetch import cascade; r = cascade.fetch('https://example.com'); print(r.layer if r else 'blocked')" # 发现开放数据(无需 credentials) python3 -m scrapekit.catalog --list python3 -m scrapekit.catalog --fetch ajman_projects 5 # 社交发现(需要一个可访问的 SearXNG 实例;使用 SCRAPEKIT_SEARXNG_URL 覆盖) scrapekit social discover "off plan Dubai" --limit 15 # 运行离线测试套件(无网络) python3 -m pytest tests/ -q ``` 可选的服务和凭证记录在 `.env.example` 中。Node `defuddle` 提取器是一个自安装的 sidecar:`cd scrapekit/extract && npm install`(如果缺少 Node,它会回退到 `trafilatura`)。 ## 数据与隐私 此代码库**仅包含代码**。它不发布任何抓取的页面、数据库、采集的帖子或凭证 —— 包含真实代理账户详细信息的 `.env` 从不会被包含在内;只有 `.env.example`(变量名,空值)。scrapekit 是一组抓取/解析的构建块:你自己将其指向公开的数据源,它会在本地生成数据。`social/handles.py` 中精心整理的账户列表仅引用了公开的品牌/政府账户(Emaar、ROSHN、Aldar、DAMAC、DLD……)。这里没有任何捏造的数据 —— 当每一层没有真实内容时,都会返回一个诚实的空值。 ## 项目布局 ``` scrapekit/ fetch/ free-first HTML cascade + residential escalation (cascade, curl_cffi, nodriver, flaresolverr, wayback, reader, residential) discover/ no-credential open-data helpers (arcgis, opendatasoft, ckan, sitemap, jsonld, next_data, algolia) social/ subject-driven engine + 6 free hydrators + CLI + paid (off by default) extract/ defuddle (Node) vs trafilatura main-content extraction catalog.py turnkey registry of verified free GCC data sources docs/adr/ architecture decision records bench/ reproducible defuddle-vs-trafilatura extractor benchmark tests/ ~6.4k LOC of fully offline pytest (fixtures, no network) ``` ## 许可证 MIT — 见 [LICENSE](LICENSE)。
标签:MITM代理, 安全规则引擎, 运行时操纵, 逆向工具