# nty-py **Repository Path**: mzb329/nty-py ## Basic Information - **Project Name**: nty-py - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-28 - **Last Updated**: 2026-08-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # NYT 历史语料采集器(nytcc) 一个面向 **The New York Times 历史文章的大规模批处理采集工具**,通过 NYT Archive API 构建文章清单,再从 Common Crawl(CC)公开存档中定位并精确拉取文章正文,最终输出可直接查询的 Hive 分区 Parquet 语料库。 ```text NYT Archive API ──► 文章清单 ──► CC 索引(capture 定位)──► WARC Range Fetch └─► warcio 解码 ──► 正文提取 └─► 元数据合并 ──► Parquet (year=YYYY/month=MM) ──► DuckDB 查询 ``` --- ## 功能特性 - **文章清单** — 按月从 NYT Archive API 拉取文章元数据,支持退避重试和 429 限流保护。 - **Capture 定位** — 使用 Common Crawl *index-server* CDX 按域名批量查询(`matchType=domain`),而非逐 URL 轮询。自动从 CC 目录(`collinfo.json`)选择最适合文章发布日期的 crawl。 - **WARC Range Fetch** — 精确的 HTTP `Range` 请求,只拉取目标 WARC 记录的字节区间,按声明长度校验完整性。 - **解码 + 提取** — `warcio` 解码 → `news-please`(主)/ `trafilatura`(备选)提取,带质量门控。 - **元数据合并** — 优先级硬编码:**NYT API > HTML 结构化元数据 > 提取器**。 - **存储** — 批量写入 Hive 分区 Parquet(`year=YYYY/month=MM`);DuckDB 视图直接查询;SQLite checkpoint 持久化。 - **容错** — 每篇文章独立错误分类(11 种错误码),checkpoint/resume/retry 机制,单条失败绝不中断整月批量。 - **质量审计** — 按月和汇总质量报告;支持随机抽样 QA。 --- ## 安装 ```bash git clone https://gitee.com/mzb329/nty-py.git && cd nty-py uv sync # 安装依赖 — 或 pip install -e ".[newsplease]" cp .env.example .env # 设置 NYT_API_KEY(可选,用于实时 NYT 清单) ``` `news-please` 是可选的重型依赖:`uv sync --extra newsplease`(或 `pip install -e ".[newsplease]"`)。不安装时 pipeline 仅使用 `trafilatura`。 > **已验证(2026-08-28):** Common Crawl 的所有可达 crawl 均**未暴露 parquet URL 索引**。`nytcc` 改用 index-server CDX 域名批量查询,这是官方推荐的批量机制,满足"不得逐 URL 调 CDX"的要求。 --- ## 快速开始 ```bash # 1. 从 NYT Archive API 生成一个月的文章清单(需要 NYT_API_KEY) nytcc manifest --year 2020 --month 1 # 2. 定位该月的 CC capture nytcc locate --year 2020 --month 1 # 3. 端到端:定位 + 抓取 + 提取 + 写入 Parquet nytcc fetch --year 2020 --month 1 # 4. 跑一个完整的月份区间(带 checkpoint/resume) nytcc run --start 2020-01 --end 2020-03 --workers 8 # 5. 恢复中断的任务(跳过已写入的文章) nytcc resume --start 2020-01 --end 2020-03 # 6. 仅重试之前失败的文章 nytcc retry --start 2020-01 --end 2020-03 # 7. 审计 + 统计 nytcc audit --year 2020 --month 1 nytcc stats nytcc shell ``` ### 先用小样本验证 大规模运行前务必先用小样本验证: ```bash nytcc run --start 2017-01 --end 2017-01 --limit 3 --crawl CC-MAIN-2017-30 ``` `--limit N` 限制清单为 N 篇文章;`--crawl CRAWL_ID` 指定 CC crawl(适用于自动选择不理想时,如复现 `tests/fixtures/real_captures.json` 中的测试数据)。 --- ## CLI 命令参考 | 命令 | 用途 | |-----------|------------------------------------------| | `manifest` | 生成 NYT 文章清单(单月或 `--start`/`--end` 区间) | | `locate` | 定位某月的 CC capture | | `fetch` | 定位 + 抓取 + 提取 + 写入一个月 | | `run` | 跑完整的月份区间(带 checkpoint) | | `resume` | 恢复中断的任务,跳过 `WRITTEN`/`NOT_FOUND` | | `retry` | 重试区间内 `FAILED` 的文章 | | `audit` | 输出按月质量报告 | | `stats` | 通过 DuckDB 查询 Parquet 语料库汇总统计 | | `shell` | 打开 DuckDB SQL 交互查询 | 通用参数:`--year/-y`、`--month/-m`、`--start`、`--end`、`--workers/-w`、`--limit`、`--resume`、`--crawl`、`--log-level/-L`、`--data-root`。 --- ## 真实端到端验证(无需实时 NYT API) NYT Archive API 在某些网络环境下不可达。`nytcc` 可以**完全基于真实 Common Crawl 数据**进行端到端验证: ```bash pytest -m real # 真实 CC 数据:locate → fetch → extract → parquet → DuckDB pytest tests/unit # 纯逻辑单元测试(无网络) pytest # 全部(real 测试需要网络 + CC index 可达) ``` 单元测试使用录制的 NYT API fixture(`tests/fixtures/nyt_archive_2020_01.json`)和可控的 HTTP 后端——它们验证的是**真实客户端代码路径**,而非行为 mock。`real` 标记的测试访问 Common Crawl;当 CC index 服务器宕机时,依赖 locate 的测试会**干净地跳过**(而非假红),保证"红色 = 代码缺陷"的语义。 ### 质量门禁 ```bash uv run ruff check src tests # 代码风格 uv run mypy src # 类型检查 uv run pytest -m real # 真实数据测试 ``` --- ## 项目架构 ```text src/nytcc/ ├── cli.py Typer CLI(所有命令) ├── config.py Pydantic 配置 + env/.env 加载 ├── errors.py ErrorCode 枚举 + NYTCCError 异常体系 ├── models/ article(NYTArticleDoc, Capture, 枚举)、result(ArticleRecord)、state ├── nyt/ NYT Archive API 客户端 + URL 规范化 ├── commoncrawl/ catalog(crawl 选择)、index(CDX 域名查询)、 │ matcher(URL 匹配)、fetcher(异步 Range Fetch)、warc(解码) ├── extract/ 提取管道(news-please 主、trafilatura 备选) ├── pipeline/ manifest、locate、fetch、merge(元数据优先级)、runner(编排) ├── storage/ parquet(Hive 分区)、checkpoint(SQLite/WAL)、duckdb(查询) ├── quality/ report(按月/汇总)、scoring、validation └── utils/ dates(YearMonth)、hashing(去重)、logging ``` ### 关键设计决策 - **元数据优先级**(`pipeline/merge.py`):NYT API 字段为权威来源;HTML/提取器元数据仅填补空白。提取器只提供*正文内容*,绝不覆盖 API 的 headline/section/authors。 - **内容去重**(`utils/hashing.py`):对空白归一化后的正文做 SHA-256;重复抓取保留评分更高的 capture。 - **Checkpoint**(`storage/checkpoint.py`):SQLite WAL 模式;每篇文章状态追踪(`PENDING → MANIFESTED → LOCATED → FETCHED → EXTRACTED → WRITTEN`,以及 `NOT_FOUND`、`FAILED`)。Resume 跳过 `WRITTEN`/`NOT_FOUND`。 - **错误分类**(`errors.py`):`NYT_API_ERROR`、`RATE_LIMITED`、`CC_INDEX_ERROR`、`CAPTURE_NOT_FOUND`、`HTTP_ERROR`、`RANGE_MISMATCH`、`WARC_PARSE_ERROR`、`INVALID_HTML`、`EXTRACTION_EMPTY`、`EXTRACTION_ERROR`、`PARQUET_WRITE_ERROR`,外加配置/恢复冲突码。无裸 `except: pass`。 --- ## 配置 通过环境变量或 `.env` 文件设置(参见 `.env.example`): | 变量 | 默认值 | 说明 | |----------------------|----------------------------------|-------------------------------| | `NYT_API_KEY` | `""` | 实时 NYT 清单所需 | | `NYTCC_DATA_ROOT` | `data` | 所有输出目录的根路径 | | `NYTCC_LOG_LEVEL` | `INFO` | 日志级别 | | `NYTCC_LIVE_NYT` | `false` | NYT 实时页面适配器(合规开关,默认关闭)| `nytcc` 在 `data_root` 下生成以下目录: ```text data_root/ ├── manifest/ 按月文章清单 ├── index/ 已定位的 capture(Hive 分区 Parquet) ├── parquet/ 语料库,year=YYYY/month=MM/part-*.parquet ├── state/ checkpoint.db(SQLite/WAL) ├── logs/ 运行日志 ├── reports/ 按月 + 汇总质量报告(JSON) └── crawl/ 临时文件 ``` --- ## 合规 / 安全边界 本工具是**语料采集器**,不是绕过工具。设计上**不实现**以下任何功能,NYT 实时页面适配器**默认关闭**(`live_nyt_enabled: false`),仅在明确授权后启用: - paywall 绕过 / CAPTCHA 绕过 / 认证绕过 / 反爬虫绕过 / 浏览器指纹伪装 / 隐身爬取 采集完全通过 **NYT 官方 Archive API** 和 **公开存档的 Common Crawl WARC** 进行——两者均为合法、非绕过的公开数据源。 --- ## 输出 Schema(Parquet) 每篇已写入的文章一行(`ArticleRecord`)。分区列 `year`、`month` 由发布日期派生。数据列包括:`nyt_id`、`url`、`normalized_url`、`publication_date`、`headline`、`authors`、`keywords`、`section`、`cc_crawl`、`cc_capture_time`、`cc_match_type`、`warc_filename`、`warc_offset`、`warc_length`、`content_text`、`content_length`、`content_word_count`、`extraction_method`、`extraction_score`、`content_sha256`、`status`、`error_type`、`error_message`、`created_at`。 查询示例: ```sql -- 按月统计文章数 SELECT year, month, COUNT(*) FROM articles GROUP BY 1, 2 ORDER BY 1, 2; -- 查找长文章 Top 10 SELECT headline, content_word_count FROM articles WHERE content_word_count > 500 ORDER BY content_word_count DESC LIMIT 10; ``` --- ## 真实爬取结果(CC-MAIN-2017-30) 以下 8 篇文章从 Common Crawl CC-MAIN-2017-30 中**真实提取**,跨越 90 年(1860–1951): | 年份 | 文章 | 词数 | 内容简要 | |---|---|---|---| | **1860** | 华盛顿通信:国会厅轶事 | 886 | 1860年1月华盛顿政治人物报道 | | **1861** | 总统的回复 | 935 | 林肯政府对南卡罗来纳脱离联邦的答复 | | **1862** | 德兰斯维尔之战 | 909 | 内战报道:邦联视角的战役叙述 | | **1863** | 国际救济委员会 | 80 | 向英国贫困工人募捐的认捐名单 | | **1864** | 州长任命 | 33 | 纽约州法官任命公告 | | **1865** | 破封锁航行记 | 1233 | 美国海军追捕邦联走私船的亲历报道 | | **1950** | 蒙蒂·班克斯去世 | 42 | 意大利默片演员/导演讣告 | | **1951** | 乌拉圭的难民资金 | 45 | 南美"瑞士"——乌拉圭难民资金流动报道 | 8/8 全部成功提取,无失败。Pipeline 正确执行了 Range Fetch → warcio 解码 → Trafilatura 提取 → Parquet 写入 → DuckDB 查询。 --- ## 开发状态 完整分阶段计划见 `docs/NYT Historical Corpus Collector — 开发 TODO.md`。已通过真实 Common Crawl 数据(CC-MAIN-2017-30)端到端验证:locate → Range Fetch(精确字节匹配)→ warcio → Trafilatura → Parquet → DuckDB → checkpoint/resume 全部通过。NYT Archive API 路径通过录制 fixture 验证(原始构建环境中 live API 不可达)。 ### 已知限制 - `index.commoncrawl.org` 间歇性宕机(全量空响应),导致 locate 依赖的测试会干净跳过。 - Python ssl 栈对 CC index 服务不稳定,已改用 curl 子进程传输。 - 1950 年代以后的文章在 CC 存档中可能只包含摘要片段(paywall 限制)。 - 2026 年文章需等 CC index 恢复后才能定位 capture。