InvesResearch · 卫星互联网投研 · twikit 采集链路

跟踪 X 账号的抓取程序

一个基于 twikit 的航天/卫星互联网 X 账号采集器:cookie 无头登录、按种子清单批量抓 profile 与时间线、归一化后落进本机 SQLite,既出中英对照报告,又作为 Satellite Agent 的每日信号源——两侧通过一份只读的 x.sqlite3 彻底解耦。

95
种子账号
3,123
已抓推文
5
SQLite Schema
9
账号分类
2
下游链路
01

概述

这是什么、解决什么问题、现状与部署。

这是什么。 expert/X/ 是一个基于 twikit(非官方 X GraphQL 客户端)的航天 X 账号抓取器。它用一次性登录得到的 cookie 做无头登录,按种子清单批量拉取账号 profile 与时间线,把 twikit 的松散对象归一化后落进本机 SQLite(x.sqlite3),并可产出中英对照的 Markdown / HTML / PDF 报告。

解决什么问题。 为「Satellite Agent」(agent/,全球卫星互联网产业决策 agent)提供每日的 X 侧信号源。抓取器只负责「把推文抓进 SQLite」,agent 侧再只读增量地把新推文映射成事件(events),喂进 decide / 日报 / wiki 回写。

当前数据规模。 SQLite 已存 95 个账号 / 3123 条推文(2026-06-06 实测,NEXT_STEPS.md:3);其中 5 个种子账号报 user_not_found(handle 停用/改名)。账号按 9 大分类组织。

◈ 部署状态

抓取器与 agent 入库链均为 operator 本机 macOS 资产,脚本硬编码 /Users/john/InvesResearch/expert/X本仓库只是代码副本data/ 全部 gitignore——x.sqlite3cookies.json、快照、报告都不在此机。

Tier 2 整合(XSqliteSource + x-ingest)已交付 2026-06-06,端到端实测 30 条推文真入库。Tier 3(X → wiki 单向增量回写)仍待启动:x_to_wiki.py 已具备但作为手动脚本、不在调度链。

02

系统架构

两大块,共用底层 expert/X/data/x.sqlite3,去向不同。

上游 · expert/X 抓取库
📄 种子账号清单
航天相关X账号汇总表_豆包AI生成.xlsx — 分类 / 账号名 / @用户名 / 中文简介 · accounts.load_seed_accounts()
⚙️ cli scrape → run_scrape() twikit
cookies.json 无头登录 → 逐账号抓 profile + 时间线(Tweets/Replies) → 归一化 _normalize_user / _normalize_tweet / _normalize_media
🗄️ x.sqlite3 commit-per-account
accounts / tweets / media / translations / runs 表 · 每账号一次 commit 做 checkpoint
报告支线
cli report reports.py
report.md / .html / .pdf
中英对照,HTML 自包含
快照 / 翻译
data/snapshots/run_NNNNN/*.json
原始快照,离线审计
translations 表 dump/import 翻译工作流

下游 agent 侧只读 x.sqlite3绝不重抓、绝不写 X 的表。分两条链路(沿用 x-bridge-operator-runbook.md 字母约定):

下游 · agent 管道
链路 A — 轻量桥(零服务器)
run_x_scrape.sh export_x_events.py
复用 XSqliteSource · 近30天 · 脱敏5字段
data-live/x-events-archive.jsonl git push
GitHub Actions refresh_live_data
oaf.world 看板(每6h)+ 飞书日报(08:37)
链路 B — 本机 launchd
launchd(daily) run_x_ingest.sh
(1) x-ingest-daily = _action_x_ingest
XSqliteSource 增量读(first_seen_at>since) classify(5主线)+去重 events 表
(2) x-freshness(断供监控,|| true
(3) daily-report(复用 weekly_report 7d)
events 表 decide() · x_to_wiki.py(手动)
链路 A:进线上看板/日报 链路 B:进本机 agent.db 的 events
✓ 关键隔离

x.sqlite3(含 cookie / 全量原文)永不进 git;GitHub Actions 只见脱敏 JSONL,服务器侧看不到本体。

◈ 定时说明(已更正)

jobs.py:536 注释与 cron-runbook 写的是 launchd 03:00(遗留本机时点);production-runbook.md:84 记录当前生产调度为 06:4545 6 * * * run_job.sh x-ingest-daily),以此为准。抓取(run_x_scrape.sh故意不进任何调度链——单次 30–60 分钟、撞 429 会 sleep 900s,与 1–2 秒的 ingest 量级不同,默认 operator 手动跑。

03

目录与文件地图

上游抓取库 + agent 桥接的关键文件。

作用路径
包初始化 · 首先 import 补丁expert/X/x_agent/__init__.py
argparse 入口 · 6 子命令expert/X/x_agent/cli.py
种子清单加载 SeedAccountexpert/X/x_agent/accounts.py
twikit 抓取核心expert/X/x_agent/scraper.py
twikit 2.3.3 两处 monkey-patchexpert/X/x_agent/_twikit_patch.py
全部 SQLite 持久化 · 5 表expert/X/x_agent/db.py
md/html/pdf 报告(只读)expert/X/x_agent/reports.py
翻译工作流:导出待译 / 写回expert/X/scripts/dump_for_translate.py · import_translations.py
配置模板(复制为 config.yaml)expert/X/config.example.yaml
项目依赖 · requires-python>=3.10expert/X/pyproject.toml
产物根(全 gitignored)expert/X/data/  →  x.sqlite3 / cookies.json / snapshots / reports
只读增量消费 → RawEventagent/satellite_agent/sources/x_sqlite.py
x-ingest / x-freshness / daily-reportagent/satellite_agent/jobs.py
逐条入库 + 去重agent/satellite_agent/ingest_pipeline.py
5 主线本体 Threadagent/satellite_agent/ontology.py
operator scrape / launchd 链 / 可移植入口agent/scripts/run_x_scrape.sh · run_x_ingest.sh · run_job.sh
回写 wiki / 导出桥 / occurred_at 回填agent/scripts/x_to_wiki.py · export_x_events.py · backfill_x_occurred_at.py
04

数据模型(SQLite)

DDL 集中在 db.py:11-94,全 IF NOT EXISTSinit_schema 可反复执行。共 5 表 + 3 索引;时间戳统一由 utcnow_iso() 生成(UTC 秒级 ISO8601)。

4.1 accounts — 账号 db.py:12-29

主键 username。字段分三组:抓取回填、种子导入、运维状态。无外键,也无「建行时间」字段。

字段类型含义
usernameTEXT PK去 @ 的用户名(种子导入建行的键)
user_idTEXTX 数字 id(回填,COALESCE 保护不被 NULL 覆盖)
display_name / bioTEXT显示名 / 简介原文(回填自 name / description)
followers/following/tweets_countINTEGER三项计数(following = following_count or friends_count
verifiedINTEGER0/1;verified OR is_blue_verified(蓝标也算)
profile_created_at / location / urlTEXT资料字段(回填)
source_categoryTEXT种子表分类标签(9 大分类,供 agent source 标签)
source_name / source_bio_zhTEXT种子表账号名 / 中文简介
last_scraped_at / last_errorTEXT末次抓取时间 / 末次错误(截断 500 字,成功清空)

4.2 tweets — 推文 db.py:31-55

主键 id(TEXT)。唯一外键 author_username → accounts(username)。三索引:author / created_at / type。

字段类型含义
idTEXT PK推文 id(强转 str)
author_usernameTEXT NN时间线拥有者(种子账号),RT 时非原作者
text / created_at / langTEXTfull_text or text / 推文原始时间(非 ISO) / 语言码
tweet_typeTEXToriginal|retweet|quote|reply_classify,RT 优先)
in_reply_to_usernameTEXT⚠️ 名不副实:实存 in_reply_to_user_id_str(数字 id 非 handle)
retweeted / quoted_tweet_idTEXTRT 原推 id / 引用推 id
*_count ×6INTEGERfavorite/retweet/reply/quote/view/bookmark 互动计数
urls_json / hashtags_jsonTEXTjson.dumps(... ensure_ascii=False)
scraped_atTEXT末见时间(每次 upsert 刷新为 now)
first_seen_atTEXT首见时间(增量判定水位,见 4.6)
run_idINTEGER首见的那次 run(不随复看刷新)

4.3 media / 4.4 translations / 4.5 runs

  • mediadb.py:61-71):复合主键 (tweet_id, media_key)无到 tweets(id) 的外键——即便 foreign_keys=ON 也可能写入孤儿媒体行。photo 存图片直链,video/gif 存最高码率 mp4 直链(_best_video_url)。
  • translationsdb.py:73-81):复合主键 (kind, ref_id)kind ∈ {tweet,bio}db.py 本身只有 DDL、无读写函数——写在 import_translations.pyINSERT OR REPLACE),读在 reports.pyx_sqlite.py
  • runsdb.py:83-93):抓取运行账本。finished_at=NULL 即 run 未正常收尾(中断/崩溃),据此判僵尸 run。记 total/ok/failed 与 tweets_new/seen。

4.6 first_seen_at 增量判定 核心不变量

✓ 整个增量体系的基石 · upsert_tweet db.py:222-276

is_new 靠插入前一次 SELECT 1 FROM tweets WHERE id=?db.py:230)判定,结果为 None → 新增;同连接内即使未 commit 也自见,账号内跨 tab 去重可靠。

ON CONFLICT(id) DO UPDATE 冲突分支只更新 text + 6 个互动计数 + scraped_at=nowfirst_seen_atrun_id 刻意不在更新列表db.py:241-249)。

结果:first_seen_at 恒为首见时间、scraped_at 刷成末见时间。下游 agent 正是靠 first_seen_at > since 做开区间增量。

05

核心抓取逻辑

scraper.py

ScrapeConfig(frozen dataclass)默认:tweets_per_account=40每个 tab 的目标条数)、include_replies=Truesleep_between_accounts=(3,7)rate_limit_sleep=900max_retries=3

5.1 登录 / cookie

两条路径,登录态与抓取解耦,凭证只在登录时用:interactive_loginscraper.py:53)一次性交互登录产出 cookie JSON(支持 totp_secret 2FA);make_clientscraper.py:42)无头路径,cookie 不存在直接 RuntimeError 提示先 cli login,不带任何明文凭证。Cookie 复用规避重复登录挑战与风控。

5.2 归一化层(松散对象 → 稳定 dict)

全字段走 getattr(..,None) / .get(default) 容错,X 掉字段也不崩:_classify 按 RT>reply>quote>original 优先级判类型;_best_video_url 挑最高 bitrate 的 mp4;_normalize_tweetauthor_username 恒为传入种子账号(RT 时非原作者)。

5.3 分页 _fetch_timeline · scraper.py:188

⚠ 关键不对称

首页 get_tweets(191 行)不套 try,异常向上抛;分页 result.next()(196 行)被except Exception 吞掉直接 break——翻页途中撞 429 或任何错都只是「提前截断」,不触发退避重试。X 实际每页约 20,须靠 next() 翻页凑够 40。

5.4 单账号流程 scrape_account · scraper.py:205

返回 (new, seen, error):① get_user_by_screen_name 本地 try 只捕 UserNotFound/Unauthorized 成软错误(不含 429,故 429 冒泡到 run 层退避);② upsert profile;③ 抓 Tweets tab(不套 try),若 include_replies 再抓 Replies——仅此处try/except NotFound 兜 Replies 端点 404,保留 Tweets-only;④ 两 tab 都 fetch 完才跑写库循环(逐条 normalize+upsert_tweet+upsert_media);⑤ 写 {username}.json 快照。

5.5 run 编排 run_scrape · scraper.py:275

  • --only 过滤:不在 seed 清单的账号显式 warn(修此前静默跳过的已知问题 #1);全不命中 → SystemExit
  • 逐账号重试循环TooManyRequestsattempt<max_retries 则 sleep 900s 后从 get_user 重跑整账号(upsert 幂等不重复);BadRequest/TwitterException 与兜底 Exception 记错 break,绝不掀翻整轮
  • commit-per-account(347 行):每账号后 conn.commit() checkpoint,配合 WAL 让 stats/report 在 run 进行中就读到已完成账号,中途崩溃不丢前面成果。

5.6 twikit 补丁 _twikit_patch.py

__init__.py:2 在任何 Client 使用前 import 生效(强依赖此导入路径,绕过则每请求 Couldn't get KEY_BYTE indices 全线失败)。两处:get_indices 适配 X 2026-03 后 ondemand.s 拆 index+hash 两段(upstream #408);User.__init__ 换成全字段 .get(default) 版容忍缺字段(#417),含 following_count = legacy.friends_count 映射。

5.7 限流 / 重试的陷阱(均已核查更正)

⚠ 四个必须知道的行为边界

退避覆盖范围:rate_limit_sleep=900 对任何从 scrape_account 冒泡的 429 生效(含 get_user、两 tab 首页 get_tweets),唯独分页 next() 的 429 被裸 except 吞掉。单账号被限流最坏 3×900s ≈ 45 分钟串行阻塞整轮。

限流失败不落半份推文:写库循环在两 tab fetch 完之后才跑,429 只可能在此之前抛出,故限流账号最多落 profile 行 + mark_account_error,不写任何推文。

cookie 过期无全局熔断:失效时首个调用抛 Unauthorized → 被本地捕成软错误 → 不 abort,会把整份 seed 逐个跑成 failed,浪费一整轮。

include_retweets 是死配置:已完整接线(config→cli→ScrapeConfig)但抓取逻辑内无任何分支读它(原始时间线本就含 RT),改动无行为效果。

06

CLI 与配置

进程唯一入口 maincli.py:263):parse_args → _load_config → _resolve_paths → args.func(args,cfg,paths)。子命令统一签名,返回值即退出码。

子命令参数作用
init从 xlsx 种子表灌 accounts
login--username/--email/--password/--totp交互登录一次,产出 cookies.json
import-cookiespath浏览器导出 cookie JSON 归一后写入
scrape--only · --tweets-per-account抓全部/指定种子账号的 profile+推文
stats只读打印 DB 汇总
report--out-dir · --no-pdf从 DB 渲染 md/html/(pdf)

6.1 config.yaml(全可选,缺文件用默认)

配置键默认说明
data_dirROOT/data输出根(ROOT = expert/X)
db_path / cookies_path / snapshots_root相对 data_dir⚠️ 只改 db_path 不改 data_dir 会路径分裂
credentials.*仅 login 用;优先级 args > config > env(X_USERNAME…
scrape.include_replies示例 false / 代码默认 True2026-06-08 端点 404 后示例配置关掉
scrape.rate_limit_sleep / max_retries900 / 3仅对 429 生效

6.2 cookie 导入 & 种子加载

_extract_from_browser_exportcli.py:72)支持 3 种导出格式归一成 {name:value},按 _X_DOMAINS 过滤,必需 {auth_token, ct0}(缺则 WARN 不 fail——真失败要到 scrape 才暴露)。load_seed_accountsaccounts.py:18)读活动 sheet,首行前 4 列须严格等于 (分类,账号名称,@用户名,简介核心内容) 否则 ValueError;按去 @ 用户名去重。seed_accountsON CONFLICT DO UPDATE 只覆盖 source_* 三列,重导种子不清已抓数据。

07

报告生成

reports.py

write_reportsreports.py:735):collect() 得 Report → 渲染 md + 自包含 html → 可选 headless Chrome 转 pdf。md/html 必出,pdf 可缺。

collect(唯一读库处,reports.py:118:6 条 SQL 一次性把 5 张表读进内存 shape 成 Report dataclass,渲染层不再碰 DB。派生:by_category 分桶后桶内按 followers 降序;top = 全部推文按 Tweet.score 降序取 30。

报告结构

统计头部 → 目录 → 全局 Top 30 推文按分类展开(每账号 profile + 该账号全部推文按 score 降序,含媒体缩略图,中英对照)→ 失败账号表。HTML 内联 CSS+JS,含深/浅/print 三主题、搜索过滤、中英显隐、?expand=1 展开。

⚠ 实现坑

Tweet.scorereports.py:69)= view_count or (fav×3 + rt×5 + reply + quote×2):有/无浏览数两组量级混排;view_count==0 会误回退到加权分。

TOP_TWEETS_PER_ACCOUNT=999:实质不限量,大账号会撑爆 HTML。

分类数由数据决定(已更正):由 accounts.source_category 数据决定、非代码写死「9」;缺失兜底 (未分类)。媒体是远程 <img src> 非内嵌,在线可能 404、PDF 中被 @media print 隐藏。

PDF 靠本机 Chrome(_find_chrome 先查 macOS 四个 .appshutil.which),命令 chrome --headless=new --print-to-pdf,找不到则静默降级只出 md+html。

08

下游集成:接入 Satellite Agent

XSqliteSourcex_sqlite.py:136)把 x.sqlite3 作为只读、增量的 Source 接进 agent 抓取层。

8.1 增量映射 XSqliteSource.fetch

  • 只读打开_open_ro):3 段 fallback file:?mode=ro&immutable=1(WAL 下唯一稳,读 last commit 不抢锁)→ ?mode=ro → 普通 connect;db 不存在设 last_error 返回 None 降级不抛。
  • 增量 SQLtext<>'' 恒成立,since 非空再加 (first_seen_at IS NULL OR first_seen_at > ?)开区间;NULL 行永远放过,靠下游去重挡),LEFT JOIN accounts 取 category/display_name,ORDER BY first_seen_at DESC NULLS LAST(老 SQLite 去词 fallback),LIMIT(source 默认 500 / job 默认 200)。
  • 过滤:默认过滤 reply / retweet;可选 categories 白名单;二次空文本过滤。
  • 中英拼接:若 translations 有 zh 译,text = 原文 + "\n\n[zh] 译文",让 classifier 同看中英提升中文命中。
◈ RawEvent 映射要点

occurred_at = _to_iso(created_at)——来自推文 created_at 而非 first_seen_at_to_iso 把 Twitter ctime 风格 'Wed May 20 13:00:33 +0000 2026'(年在末尾,非标准 RFC 2822,仅 parsedate_to_datetime 容忍)转 ISO;解析失败留 None,decide 的 7d/30d 窗口会跳过。companies/numeric_overrides/next_indicators 全空,让下游 match_companies/extract_numeric 兜底。

8.2 9 分类 → 主线(关键设计:不映射)

KNOWN_CATEGORIES 9 大分类:全球卫星运营商 / 其他细分航天企业 / 军工航天巨头 / 各国官方航天机构 / 在轨专项航天项目账号 / 民营航天企业(美国)/ 航天从业者/宇航员 / 航天媒体&资讯博主 / 非洲航天相关。这 9 分类不映射到主线——只拼进 source 标签 X · {category} · @{handle} 供反查,主线判定完全由下游 classifier 读 text 决定(媒体博主/在轨专项常跨主线,强绑会误分)。

◈ 已更正 · 主线数

agent 本体 ontology.Thread 现有 5 主线——核心网 / 终端 / 芯片 / 运营支撑 / 运载发射(ontology.py:11-16,2026-06-08 新增 LAUNCH_VEHICLE)。x_sqlite.pyontology.py docstring 仍写「4 主线」系遗漏更新,以 5 为准。这不影响 source 行为(它本就不做 category→主线映射)。

8.3 x-ingest cron 链 · 24h 时序

抓取是 operator 手动触发的(不在时钟上),产出 x.sqlite3 后,两条链路各自按固定时刻消费。下图按每日时钟排布 4 类执行者的调度点。

Daily schedule · launchd / cron / GitHub Actions
时刻为各执行主机本地时钟;GitHub Actions 标注 北京时间launchd(本机)ECS cron(生产) 是链路 B 的两套独立部署,互不依赖;生产库靠 03:30 rsync 从本机同步。
Operator 本机(手动 / rsync) launchd · macOS 本机 cron · ECS 生产服务器 GitHub Actions
⟳ Ad-hoc 触发 · 全链数据源
operator 手动跑 run_x_scrape.sh(建议 1–2 天一次)
twikit 抓 95 账号,单次 30–60 分钟(撞 429 sleep 900s)→ 写 x.sqlite3。随后(链路 A)export_x_events.py 脱敏 → x-events-archive.jsonlgit push main不在固定时钟上;隔太久下游每天重读 stale 库(由 x-freshness 把这种「假活」变告警)。
每6h北京
GitHub Actions · data-refresh
x-events-archive.jsonl → oaf.world 看板
00:45 · 06:45 · 12:45 · 18:45 四档 · refresh_live_data.pydocs/sample/*.json → Cloudflare Pages 部署。Actions 永远看不到 x.sqlite3 本体。
03:00本机
launchd · run_x_ingest.sh
(1) x-ingest-daily → (2) x-freshness → (3) daily-report
limit=500 无 since,靠 (url,occurred_at) 去重重跑不双写 → 本机 agent.db events。链路各步 || true,仅主步透传退出码。
03:30本机
operator crontab · rsync
x.sqlite3 → 生产服务器
把本机抓好的库同步到 ECS,供服务器 06:45 的 x-ingest 消费(production-runbook.md:147)。
06:45服务器
ECS cron · run_job.sh x-ingest-daily
since=auto 增量 → 服务器 events
从 events 表 MAX(occurred_at) WHERE source LIKE 'X ·%' 反推水位(减 24h 缓冲)做增量,重叠由去重消化。
07:00服务器
⚠ ECS cron · x-freshness
断供监控 · max_age_hours=48
最近一条 X 事件 created_at(入库时间)距今 >48h → job exit 1 +(可选)飞书红卡。注册须 --max-failures 999,否则报警 3 次自 disable。
08:37北京
GitHub Actions · daily-feishu
卫星互联网日报候选池
读同一份 x-events-archive.jsonl,命中主线的 X · 条目进次日早报候选。

各 action 明细:

环节位置做什么
x-ingest 主步jobs.py:188,604_action_x_ingestsince=auto 解水位 → XSqliteSource → ingest_from_source 入 events;SQLite 缺失静默 yield 0 仍标 success
since 水位x_sqlite.py:106MAX(occurred_at) WHERE source LIKE 'X ·%' - margin(24h);语义偏移(seen≥created)由 margin 吸收
逐条入库+去重ingest_pipeline.py:29,74ingest_raw:classify+match_companies+extract_numeric;find_event_by_url_and_time 去重(消化重叠窗,重跑不双写)
x-freshnessjobs.py:451events.created_at(入库时间)判上游断供,超 48h 告警。监控 job 须配 --max-failures 999
daily-reportjobs.py:532复用 weekly_report(7d) 落 markdown + 「信息源构成」表让 X 贡献可见
decide 消费decision.py:52,507不在链上,按需触发;依赖 ISO occurred_at 才能正确切窗

8.4 export / backfill / 回写 wiki(均手动,不在 cron)

  • export_x_events.py:复用 XSqliteSource 读近 30 天 → 按 shared_keywords 过滤 → 脱敏 5 字段 JSONL → data-live/x-events-archive.jsonl(原子落盘)→ push → Actions 读它。Actions 永远看不到 x.sqlite3 本体。
  • backfill_x_occurred_at.py:一次性把历史 Twitter 格式 occurred_at 回填成 ISO(幂等、--dry-run);因 decide 窗口/排序按字符串比 occurred_at,非 ISO 给错时序。
  • x_to_wiki.py:读近 14 天 → 用 wiki 实体别名 matcher 做 NER → 命中 slug 整段替换目标 md 的 ## X 动态 段(原子写);整账号零命中 → 起草 _drafts/x_new_entities/<account>.md 供人审。
09

运维要点

  • 登录 / 换 cookie:首次 cli loginimport-cookies(校验 auth_token+ct0)。cookie 失效表现为整份 seed 逐个跑成 unauthorized——见到即重导 fresh cookie。
  • 续跑 / 省 quota:撞 429 或部分失败后用 scrape --only <handle…> 只抓未成功账号(未命中会 WARN)。
  • 429 应对:等 1–2 小时让限流窗口过 → --only 续跑 → 实在卡死重导 cookie。降概率:把 sleep_between_accounts[3,7] 拉到 [8,15](上轮在第 63 账号 429)。
  • 定时窗口:抓取手动(不进调度链);ingest 链 launchd 03:00(本机)/ 生产 06:45;x-freshness 断供监控 48h,注册须 --max-failures 999
  • 翻译工作流dump_for_translate.py(去已译、CJK≥60% 跳过、每 250 条一批)→ 人工/子 agent 译 → import_translations.py_lenient_parse 容忍未转义引号,INSERT OR REPLACE)。
  • 路径:脚本硬编码 /Users/john/InvesResearch/expert/XNEXT_STEPS.md:29 残留老路径 /Users/john/lichao/X,换机器要改。
10

已知问题与后续计划

问题现状 / 应对
X 端点漂移(Replies 404)示例配置默认关 replies;scraper.py:235 已 try/except 兜底,保留 Tweets-only
5 个账号 user_not_foundstokespace/slingshot_aero/HTVXA/NorwegianSpaceAg/LM_Space handle 停用/改名,需在 xlsx 更正
cron 假活operator 几天不抓 → ingest 每天重读 stale 库,表面 success 实则无增量;x-freshness 已变显式告警
首页/翻页 429 不对称翻页 429 被裸 except 吞成静默少抓(scraper.py:196),无退避补偿
孤儿媒体风险media 表无到 tweets(id) 外键,可能写入孤儿行
upsert_account_profile 纯 UPDATEusername 未先 seed 建行则回写影响 0 行、资料丢失——强依赖「种子先行」顺序
Tier 3(X→wiki 回写)x_to_wiki.py 已具备但未进调度、仍待启动
运行栈约束expert/X 与 agent 均 requires-python>=3.10,不能被 ECS 宿主 python 3.6 运维脚本直接 import
11

关键代码索引

散落的重要 file:line 速查表。

主题位置说明
run 顶层编排scraper.py:275run_scrape:--only / 重试循环 / checkpoint
单账号流程scraper.py:205scrape_account(new,seen,err)
429 退避重试scraper.py:327对所有冒泡 429 生效,最坏 3×900s
commit-per-accountscraper.py:347每账号 checkpoint,中途 kill 不丢数据
Replies 404 降级scraper.py:236仅 Replies 套 except NotFound
分页裸 exceptscraper.py:196翻页 429 被吞成提前截断
twikit 补丁生效点__init__.py:2 · _twikit_patch.py:15,89KEY_BYTE 双段匹配 / friends_count 映射
SQLite schemadb.py:11-945 表 + 3 索引
is_new 判定 / first_seen 保留db.py:230 · db.py:241插入前 SELECT 1 / 冲突分支不更新 first_seen_at
CLI 入口 / 路径派生cli.py:263 · cli.py:33main 分发 / 全默认路径
cookie 归一 / xlsx 表头校验cli.py:72 · accounts.py:23三格式 / 前 4 列严格匹配
报告入口 / 唯一读库处reports.py:735 · reports.py:118write_reports / collect 6 SQL
Tweet.score / PDF 渲染reports.py:69 · reports.py:708量级混排坑 / headless Chrome
agent Source / 增量 SQLx_sqlite.py:136 · x_sqlite.py:228XSqliteSource / first_seen_at>since
occurred_at 归一 / 只读打开x_sqlite.py:40,310 · x_sqlite.py:186_to_iso / mode=ro&immutable=1
source 标签 / since=auto 水位x_sqlite.py:77,297 · x_sqlite.py:106X·分类·@handle / MAX(occurred_at)-margin
5 主线本体ontology.py:11核心网/终端/芯片/运营支撑/运载发射
x-ingest / x-freshness / daily-reportjobs.py:188,604 · 451 · 532三个 action + register
逐条入库 + 去重ingest_pipeline.py:29,74ingest_raw + find_event_by_url_and_time
launchd 链 / operator scraperun_x_ingest.sh · run_x_scrape.sh:44ingest→freshness→report / 透传 cli scrape
导出桥 / 回填 / 回写 wikiexport_x_events.py:62 · backfill:25 · x_to_wiki.py:63,91脱敏 JSONL / 幂等回填 / ## X 动态

X 账号抓取程序 · 技术文档 — 覆盖 expert/X/ 抓取库与 agent/ 下游桥接。全部字段名、行号以源码为准;标「已更正」处为经独立核查修订后的结论。

配套完整文档:expert/X/ARCHITECTURE.md。数据规模引自 NEXT_STEPS.md(2026-06-06 实测 95 账号 / 3123 推文)。