Day 194 — AI 增强与数据管道
阶段:Phase 12 — 综合实战
项目:AI 赋能反爬数据平台 · 第 4/5 天
子主题:反爬风险预测 · 特征工程 · 代价敏感阈值 · SimHash 去重 · 分桶索引 · 管道 · 质量门禁
代码:1 个共享库 + 4 个可独立运行脚本,全部带--self-test,无一依赖外部服务
实测环境:Linux 7.0.0-34-generic / Python 3.12.3 / numpy 2.0.0 / scikit-learn 1.9.0 / 2026-10-24 06:00–06:50 CST
今天在项目里的位置
Day 191 引擎骨架 → 队列、限速、重试、落库 ✅
Day 192 代理池 → IP 轮换、健康检查、按目标分配出口 ✅
Day 193 浏览器与验证码 → JS 渲染、指纹、验证码识别 ✅
Day 194 AI 增强 → 风险预测、内容去重、清洗入库管道 ← 今天
Day 195 运维与交付 → 调度、加密、鉴权、Docker + CI/CD
前三天解决的是"怎么把数据拿回来"。今天要解决的是拿回来的东西能不能用:
哪一页该停、哪两篇是同一篇、哪些字段脏到不能入库。
一句话概括:分类器决定要不要继续抓,SimHash 决定要不要再存一份,质量门禁决定能不能入库。
概念速览
完整图解见 diagrams/README.md。
| 概念 | 一句话 | 为什么重要 |
|---|---|---|
| 软封(soft block) | HTTP 200,内容是空壳/验证页 | 没有任何单点特征,规则必然漏 |
| 特征工程 | 把响应变成一行数字 | 模型上限由特征决定,不由算法决定 |
| 标准化 | 按训练集均值方差缩放 | 量纲差 5 个数量级时梯度会失控 |
| 类别不平衡 | 正类只占 25.4% | accuracy 会骗人,要看 P/R/AUC |
| 代价敏感阈值 | 漏报比误报贵 5 倍 | 阈值 0.5 是默认值,不是最优值 |
| 数据泄漏 | 用了线上拿不到的特征 | 离线 AUC 虚高,上线即失效 |
| SimHash | 64 位指纹,近似文本距离小 | 精确哈希抓不住转载 |
| 汉明距离 | 两个指纹不同的位数 | 阈值 k 必须自己扫,没有普适值 |
| 鸽巢分桶 | 切成 bands 段,任一段相同才比对 | 把 O(n²) 压到候选集 |
| 粗筛 + 精排 | SimHash 找候选,Jaccard 定终判 | 单靠阈值拿不到高精度+高召回 |
| 幂等 | 重跑结果不变 | 幂等 ≠ 正确(见翻车实验 1) |
| 质量门禁 | 只判"能明确判定错"的规则 | 主观判断不该进自动门禁 |
一、概念解释
1. 为什么要"预测"而不是继续写 if
规则能覆盖显式拒绝:403、429、验证码、JS 挑战。但真实风控里最贵的是软封——
服务器返回 200,页面正常渲染,内容却是空的或是个"请稍后再试"。
硬封:403 + WAF 页面 → if status == 403: 停 ✅ 规则能抓
限速:429 + Retry-After → if 429: 退避 ✅ 规则能抓
软封:200 + 空壳 + 正常头 → if ???: ??? ❌ 没有单点特征
软封只能靠多个弱信号的组合:响应体异常小、延迟偏高、响应头偏少、偶发验证码。
这就是分类问题:输入一行特征,输出"这一页还能不能继续抓"的概率。
为什么不用大模型? 每抓一页调一次 LLM,成本是每千页几块钱、延迟是几百毫秒,
而逻辑回归是每页 7 微秒、零边际成本、系数可解释。这类高频二分类的正确解法是
特征工程 + 轻量模型,LLM 适合的是低频、需要语义理解的环节(比如判断页面主题)。
2. 特征选择的原则:只要"决策时就能拿到"的
FEATURES 里的 10 个字段全部来自响应本身:状态码、响应体大小、延迟、验证码、
Retry-After、WAF cookie、响应头数量、JS 挑战、重定向跳数、UA 是否命中爬虫库。
反面例子是 block_page_title、waf_reason 这类字段——它们只在被拦之后才存在,
拿它们训练等于让模型抄答案。本次实测(见实验 1 第 3 节)把生成器的隐藏变量
_propensity 加进去,AUC 从 0.9299 抬到 0.9340:增益只有 +0.0041,因为这份数据里
10 个特征本身已经很强。增益小不代表没风险——换成弱特征数据集,同一个字段能让
离线 AUC 虚高 0.2–0.3,而你在离线阶段完全看不出来。
3. 类别不平衡与代价不对称
正类(需要停抓)只占 25.4%。如果模型全判"安全",accuracy 也有 74.6%,但一条软封都抓不到。
所以要看的指标是 precision / recall / AUC,而不是 accuracy。
更关键的是两类错误的代价不同:
- 误报(正常页判成被封)→ 少抓几页,代价 1
- 漏报(被封了还在抓)→ 继续撞风控,可能整段 IP 段被封,代价 5
阈值 0.5 是数学默认值,不是业务最优值。实测把阈值下调到 0.34,总代价从 194 降到 144。
4. 去重:精确哈希为什么不够
转载场景里,同一篇文章会被改标题前缀、换标点、加推广尾巴。SHA256 对这种改动
完全无感——一个字节不同,哈希就完全不同。实测精确哈希召回只有 0.164。
SimHash 的思路是把文档压成 64 位指纹:内容相近 → 指纹只差几位。
于是"是否重复"变成"汉明距离是否 ≤ k"。代价是 k 必须自己定:
- k 太小 → 召回低(改几个词就抓不到)
- k 太大 → 精度崩(无关文档也撞上)
而 k 的合适值随文档长度变化:174 token 的文档最优 k=10,10 token 的商品标题
k=3 召回只有 3%。所以生产系统里 k 是按长度分层的参数,不是常数。
5. 管道:分阶段是为了"错误可定位"
把所有逻辑塞进一个函数,出错时你只知道"结果不对"。分成
采集 → 清洗 → 抽取 → 去重 → 质检 → 入库 六段之后,每一段都有明确的输入输出计数,
脏数据在哪一段消失、因为什么消失,一目了然。
配套的两条硬规则:
- 脏数据不静默丢:进
quarantine表并带 reason code,否则你永远不知道丢了多少。 - 入库必须幂等:同一批数据跑两遍,行数必须一样。但注意——幂等只保证可重复,
不保证正确(Day 194 的 URL 规范化事故就是"幂等但错")。
二、原理深入
1. 逻辑回归:从线性打分到概率
模型先算一个线性分:z = w·x + b,再用 sigmoid 压到 (0,1):
p = 1 / (1 + e^(-z))
训练目标是最小化交叉熵(加 L2):
L = -(1/n) Σ [ y·log(p) + (1-y)·log(1-p) ] + (λ/2)·||w||²
梯度很干净:
∂L/∂w = (1/n)·Xᵀ(p - y) + λ·w
∂L/∂b = mean(p - y)
代码里就是这 4 行,1500 轮批量梯度下降,在 2100×10 的训练集上 156 ms 收敛,
loss 从 0.6932(等价于瞎猜)降到 0.2370。
2. 为什么必须先标准化
body_len 在 10⁵ 量级,status_code 只有 3 位。不标准化时:
- 梯度里 body_len 方向的步长远大于其它维度
- 同一个学习率下,小量纲特征几乎不动,大量纲特征来回震荡
实测证据:第一次跑 sklearn 对照时忘了标准化,lbfgs 直接报
failed to converge after 1000 iterations,还贴心地提示 "You might also want to scale the data"。
把 StandardScaler 放进 Pipeline 后,5 折交叉验证一次通过,AUC 0.9279 ± 0.0082。
另外一条容易忽略的规矩:标准化统计量只能来自训练集。用全量数据算均值方差,
测试集的信息就漏进了训练过程。
3. SimHash 的位运算
对每个 token 取一个稳定的 64 位哈希(必须用 blake2b/sha,不能用内置 hash()——
它受 PYTHONHASHSEED 影响,跨进程不可复现),然后逐位投票:
for each token t with weight w_t:
for bit i in 0..63:
votes[i] += +w_t if (hash(t) >> i) & 1 else -w_t
fingerprint = Σ (1 << i) for votes[i] > 0
两篇文档改了几个词,只有那几位附近的投票会翻转,所以距离小。
中文没有空格,本课用字符二元组("新能源" → 新能/能源),零依赖且对未登录词鲁棒。
英文和数字用 [a-z0-9]+ 切词。
4. 鸽巢分桶:把 n² 变成候选集
把 64 位切成 bands 段。若两篇文档距离 ≤ bands-1,那么至少有一段完全相同
(因为最多只有 bands-1 位不同,不可能每段都被污染)。于是只需查"同段同值"的桶。
实测代价(1200 篇):
| bands | 每段位宽 | 保证召回的 k | 平均候选/篇 | 候选对 | 与全量比对 |
|---|---|---|---|---|---|
| 4 | 16 | 3 | 1.5 | 285 | ✅ 一致 |
| 8 | 8 | 7 | 1.8 | 454 | ✅ 一致 |
段越多 → 保证阈值越高,但每段取值空间越小、桶里越挤。这就是取舍点。
5. 两阶段:粗筛 + 精排
单靠 SimHash 阈值在 1200 篇上是这样的曲线:
k=3 P=1.000 R=0.539 ← 一个都不敢误报,但漏一半
k=10 P=0.923 R=0.970 ← 召回够了,误报 43 对
k=15 P=0.153 R=1.000 ← 全召回,但精度崩了
正确做法是分工:SimHash 只负责把 719,400 对压到 454 对候选,
最终判定交给 Jaccard(token 集合交并比 ≥ 0.6)。实测:
候选对 454 / 719,400(0.06%)
precision=1.000 recall=0.853 f1=0.920
误报 0 / 漏报 78,耗时 127 ms
6. 质量门禁:只写"能明确判定错"的规则
规则分两类:
- 可判定:标题为空、价格不是数字、价格 ≤ 0、日期格式不对、正文 < 50 字、乱码 → 直接拦
- 需判断:内容是否有价值、图片是否清晰、描述是否合规 → 不进自动门禁,进人工队列
本课 8 条规则全部属于第一类,误报率可控(自检里用一条正常记录验证"不误报")。
乱码检测是个例外,它天生是启发式:
信号 1:出现 U+FFFD 或 Ã. / †/ � 组合
信号 2:Latin-1 补充区字符占比 ≥ 15%
信号 3:整段能用 cp1252 编码、再用 utf-8 解码得到汉字(可逆修复)
阈值取 0.15 是刻意的:取 0.05 会把正常法语(Le café crème est très bon)当成乱码隔离。
三、API 速查
pipeline_lib(本课自建,零外部依赖)
| 函数 / 类 | 作用 | 关键参数 |
|---|---|---|
clean_html(raw) |
去脚本/标签/实体/零宽字符,NFKC 归一 | — |
canon_url(url) |
只删已知追踪参数,保留 sku/page | extra_drop |
tokenize(text) |
中文二元组 + 英文单词 | — |
simhash(text) |
64 位指纹 | bits |
hamming(a, b) |
不同位数 | — |
SimHashIndex(bands=4) |
分桶索引 | bits, bands |
.add(h, payload) / .query(h, k) |
建索引 / 查候选(k ≤ bands-1) |
— |
hash_to_signed(h) / signed_to_hash(v) |
uint64 ↔ SQLite INTEGER | bits |
check_record(rec) |
质量规则,返回 [Issue] |
— |
synth_responses(n, seed) |
带标签的响应样本 | — |
synth_corpus(n, dup_ratio, seed) |
原创 + 转载混合语料 | — |
numpy / sklearn 常用点
| 调用 | 说明 |
|---|---|
np.bitwise_count(x) |
numpy ≥ 2.0 的 popcount,算汉明距离 |
np.triu_indices(n, 1) |
上三角下标,避免重复计数 |
StandardScaler() + make_pipeline |
标准化必须放进 Pipeline |
cross_val_score(..., scoring='roc_auc') |
5 折 AUC |
np.argsort(score, kind='mergesort') |
AUC 秩和公式要求稳定排序 |
四、图解
完整版见 diagrams/README.md,这里放两张核心图。
风险预测的数据流
────────────────────────────────────────────────────────
响应 ──► 特征提取 ──► 标准化 ──► w·x+b ──► sigmoid ──► p
│ 10 维 训练集统计量 线性分 │
│ ▼
└─ status/body_len/latency/captcha/... p ≥ 阈值 ? 继续 : 停抓
│
阈值 0.34(不是 0.5)
两阶段去重(1200 篇 → 454 候选 → 终判)
────────────────────────────────────────────────────────
全部 719,400 对
│ 汉明距离 ≤ 7(8 段索引,鸽巢保证)
▼
454 对候选(0.06%)
│ Jaccard ≥ 0.6
▼
precision 1.000 / recall 0.853
五、实战实验手册
所有实验都在本机真实执行,输出为原样粘贴。清理命令在每个实验末尾。
实验 1:反爬风险分类器
目的:验证"规则基线 vs 逻辑回归"的差距,并量化代价敏感阈值与推理延迟。
环境:Python 3.12.3 / numpy 2.0.0 / scikit-learn 1.9.0(仅用于交叉验证对照)
准备:
cd ~/code/Learn-Python/days/day-194-ai-data-pipeline/code
python3 pipeline_lib.py --self-test
执行:
python3 01-risk-classifier.py --self-test
python3 01-risk-classifier.py
实际结果(2026-10-24 06:12 CST):
====================================================================
数据集:3000 条合成响应 | 正类(需停抓)占比 25.4%
特征:10 个 → status_code, body_len, latency_ms, captcha, retry_after ...
====================================================================
[1] 规则基线 (403/429/503 或 验证码 或 JS挑战)
precision=0.435 recall=0.750 f1=0.551
漏报 57 条软封 / 误报 222 条正常页
[2] 逻辑回归(手写 GD,1500 轮)
训练耗时 156 ms | 首末 loss 0.6932 → 0.2370
AUC=0.9299 accuracy=0.9533 precision=0.979 recall=0.833 f1=0.900
代价敏感阈值:0.50 → 0.34(漏报代价=误报×5)
代价 194 → 144 | 漏报 26 误报 14
单条推理 6.9 µs | 批量 0.02 µs/条(899 行/批)
[3] 反面教材:把生成器的隐藏变量 _propensity 当特征
只用泄漏列:AUC=0.8957(一个字段顶整套特征工程)
特征+泄漏:AUC=0.9340(比诚实模型 0.9299 高 +0.0041)
[4] 标准化系数 Top5(正=更像被封)
body_len -1.289
status_code +0.985
header_count -0.759
latency_ms +0.268
ua_known_bot -0.134
[5] sklearn 5 折 AUC = 0.9279 ± 0.0082
手写模型单次切分 AUC = 0.9299(单次切分会波动 ±0.02 左右)
结论:
- 规则基线 F1 0.551,主要亏在 57 条软封漏报和 222 条误报;
- 逻辑回归把 F1 抬到 0.900,且单条推理 6.9 µs,完全够"每页判一次";
- 代价敏感阈值把总代价从 194 降到 144——阈值是业务参数,不是超参。
清理:本实验不落盘,无残留。
实验 2:SimHash 去重与分桶索引
目的:量化精确哈希的召回、SimHash 阈值曲线、分桶等价性与两阶段收益。
执行:
python3 02-simhash-dedup.py --self-test
python3 02-simhash-dedup.py
实际结果(同日 06:20 CST,节选):
语料:1200 篇 | 转载/改写 376 篇 | 真实重复对 529
文档长度:123 ~ 229 token(中位 174)
[1] 精确哈希(SHA256 + 标点归一化)
precision=1.000 recall=0.164 f1=0.282
抓到 87 对 / 漏 442 对 —— 转载加个前缀就逃掉了
[2] SimHash 阈值扫描(1200 篇全量两两比对)
k precision recall f1
3 1.000 0.539 0.700
7 0.993 0.853 0.918
8 0.970 0.921 0.945
10 0.923 0.970 0.946
15 0.153 1.000 0.265
[3] 分桶索引:段数决定'保证召回的 k'与候选规模
bands 保证k 平均候选/篇 候选对 与全量一致
4 3 1.5 285 ✅ (7 ms)
8 7 1.8 454 ✅ (29 ms)
[4] 两阶段去重:SimHash 粗筛(k≤7)+ Jaccard 精排(≥0.6)
候选对 454 / 全量 719,400(0.06%)
precision=1.000 recall=0.853 f1=0.920
误报 0 对 / 漏报 78 对 | 耗时 127 ms
[5] 吞吐量:20000 篇文档(合成语料,单进程无并发,numpy 版 SimHash)
生成哈希 3.55 s (5,636 篇/s)
建索引 0.06 s (339,987 篇/s)
全量查询 9.73 s (2,055 篇/s, 平均 2.0 个候选/篇)
桶数量 1,929(8 段 × 2^8)
[6] 短文本:200 条商品标题,仅把价格改掉(应判为同一商品)
平均 token 数 10.0
汉明距离分布:2:2, 3:4, 4:12, 5:13, 6:23, 7:32, 8:39, 9:31 ...
k=3 召回 3.0%
k=5 召回 15.5%
k=8 召回 62.5%
k=10 召回 87.5%
对照组(600 条无关标题,179,700 对):k=3 误判 0.01% | k=8 误判 2.30%
结论:
- 精确哈希召回 0.164,转载场景不可用;
- SimHash 单独使用时,k 无法同时满足精度与召回(k=10 精度 0.923,k=15 精度 0.153);
- 两阶段把候选压到 0.06%,同时拿到 precision 1.000 / recall 0.853;
- 短文本(10 token)对单点改动过于敏感:k=3 只召回 3.0%,而放大到 k=8 时
无关标题的误判升到 2.30%——短文本该走集合相似度或规则。
清理:本实验只在内存与 /tmp 临时文件中操作,无残留。
实验 3:URL 规范化——被删掉的 419 个页面
目的:复现"删掉全部查询参数"导致的静默数据丢失,并验证白名单修复。
执行:
python3 03-pitfall-canonicalize.py
实际结果(同日 06:26 CST):
抓取样本:2709 条 URL | 真实页面(去重后应有):820 个
[1] 旧规则:删掉全部查询串
[legacy] 规范化后不同 key:401 个
真实页面 820 个 → 能表示出来的 401 个
发生合并的 key:401 个,涉及真实页面 820 个
静默丢失 419 个页面(没有任何报错)
例:https://shop.example.com/product/106 ← ['/product/106?sku=blue', '/product/106?sku=red']
[2] 新规则:只删已知追踪参数(utm_* / spm / ref / from ...)
[fixed] 规范化后不同 key:820 个
真实页面 820 个 → 能表示出来的 820 个
发生合并的 key:0 个,涉及真实页面 0 个
[3] 入库结果对比
旧规则 401 行 | 新规则 820 行 | 应有 820 行
[4] 重跑两遍:幂等 ≠ 正确
旧规则重跑 401 行(与第一次相同 → 幂等)
新规则重跑 820 行(与第一次相同 → 幂等)
结论:旧规则把 820 个页面压成 401 个,重跑行数稳定、日志零报错,
只有把 key 和真实页面集合对照才看得出来。修复方式是白名单删除 + 参数排序 +
入库前断言(key 数不应小于原始 URL 数的 1/10)。
清理:
rm -f /tmp/day194_old.db /tmp/day194_new.db /tmp/day194_st.db
实验 4:端到端管道(3000 条脏数据)
目的:验证五阶段管道的吞吐、漏斗、质量门禁与幂等性。
执行:
python3 04-pipeline-e2e.py --self-test
python3 04-pipeline-e2e.py --report /tmp/day194-report.json
实际结果(同日 06:34 CST):
Day 194 管道 | 3000 条输入 | dry_run=False | db=/tmp/day194.db
── 阶段耗时 ──
s1_fetch 0.0 ms ( 0.0%)
s2s3_clean_extract 175.8 ms ( 6.9%)
s4_dedup 2255.4 ms ( 88.2%)
s5_gate 44.5 ms ( 1.7%)
s6_store 80.8 ms ( 3.2%)
合计 2556.5 ms (1,173 条/s)
── 数量漏斗 ──
采集 3000
去重丢弃 349 (11.6%)
质检拦截 885 (29.5%)
入库 2115
── 拦截原因(按条计,一条可命中多个规则)──
TEXT_TOO_SHORT 291
TITLE_EMPTY 136
MOJIBAKE 135
PRICE_OUTLIER 124
DATE_BAD 117
DATE_OUT_OF_RANGE 44
PRICE_NOT_NUMBER 38
── 幂等性 ──
第一次入库 2115 行 | 第二次重跑后 2115 行 | ✅ 一致
隔离区行数 885 → 1770(每轮都会追加,需按批次清理)
结论:
- 去重占 88.2% 的时间——这是全链路唯一值得上索引和并行的阶段;
- 入 2115 + 拦截 885 = 采集 3000:漏斗是串联的,被去重丢掉的不再进入质检;
- 每轮质检都会向隔离区追加,隔离区要按批次号清理,否则会无限增长。
清理:
rm -f /tmp/day194.db /tmp/day194.db-journal /tmp/day194-report.json
六、翻车实验
这一节是本次编写时真实踩到的坑,按发生顺序记录。
翻车 1:SQLite 存不下 64 位哈希
现象:
OverflowError: Python int too large to convert to SQLite INTEGER
假设:SimHash 是 64 位,SQLite INTEGER 也是 64 位,应该能存。
验证:sqlite3 的 INTEGER 是有符号 64 位(−2⁶³ ~ 2⁶³−1),
而 SimHash 是无符号 64 位(0 ~ 2⁶⁴−1)。超过 2⁶³ 的值直接溢出。
根因:无符号/有符号边界没对齐。
修复:入库前 hash_to_signed() 减 2⁶⁴,读出来后 signed_to_hash() 加回去,
并加了 0、1、2⁶³−1、2⁶³、2⁶⁴−1 五个边界的往返自检。
复测:自检 5 项全过,3000 条全部入库。
翻车 2:整页文本去重 → 80.6% 被误判重复
现象:第一次跑管道,3000 条里 2419 条被判重复(80.6%),而注入的重复只有 8%。
假设 A:SimHash 阈值太大。把 k 从 7 降到 3 试了一下,召回掉光,误判只降到 70%+。
假设 B:Jaccard 阈值太低。调到 0.8 后误判仍高。
验证:打印两篇"被判重复"的正文前 120 字,发现导航、价格、版权声明、
模板句全都参与相似度计算,正文只有一小段不同。
根因:去重对象选错了——拿"整页 clean_html 之后的文本"做哈希。
站点模板的占比远大于正文差异。
修复:新增 extract_body(),只取 <article> 容器清洗后参与去重。
复测:仍 89.2%——因为我的生成器正文复用同一个句子池,正文本身也高度雷同。
进一步把正文改成"随机 5 个片段 + 6 个唯一 SKU",降到 11.6%(注入 8% + 模板碰撞)。
同时暴露了一个新问题:TEXT_TOO_SHORT 从 0 涨到 291——正文抽取后,
"空壳页"终于现形了(此前靠导航文字蒙混过关)。这是修复带来的正确结果。
翻车 3:我自己的语料把 Jaccard 指标搞失真了
现象:给去重语料第一次做评估时,纯 Jaccard 的 precision 只有 0.006。
假设:Jaccard 阈值 0.6 太低。
验证:抽 20,000 对无关文档算 Jaccard,中位数 0.55、p90 0.75——
无关文档本来就高度相似。
根因:生成器只用了 6 个模板句子,词汇量太小。错的是语料,不是算法。
修复:改成 40 个片段的池子、每篇抽 7–10 段。修复后无关对 Jaccard 中位数降到 0.13,
真重复对中位数 0.99,指标才有意义。
教训:当指标看起来"荒谬地差"时,先怀疑评估集,再怀疑算法。
翻车 4:sklearn 不收敛(其实是没标准化)
现象:lbfgs failed to converge after 1000 iterations,五条一模一样的警告刷屏。
根因:直接把原始量纲的 X 喂给 LogisticRegression。
修复:make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000)),
并用 warnings.simplefilter("error") 让收敛警告直接抛异常——警告刷屏然后继续跑,
是最容易骗过自己的做法。
七、思考题
- 你的规则基线在测试集上 F1 是 0.55,模型是 0.90。上线后模型真的会更好吗?
需要额外验证哪三件事? - 为什么 SimHash 的 k 要随文档长度变化?请用"token 数 → 单 token 权重的期望位移"
这个角度解释,并设计一个分长度分桶的方案。 - 分桶索引
bands=8保证 k≤7 的召回。如果业务需要 k=15 且不能用全量比对,
你会怎么改?(提示:多个排列、多组 bands) - 质量门禁把
TEXT_TOO_SHORT设为 50 字。如果目标站点有大量"短公告"(20 字但有效),
会发生什么?规则应该怎么写才不误杀? - 幂等 ≠ 正确。请构造一个"重跑结果一致但数据错误"的管道,并给出能在 CI 里自动发现它的断言。
八、清理与收尾
本次实验产生的临时文件与清理命令:
# 数据库与报告
rm -f /tmp/day194.db /tmp/day194.db-journal /tmp/day194-report.json
rm -f /tmp/day194_old.db /tmp/day194_new.db /tmp/day194_st.db
# 运行日志(编写过程用)
rm -f /tmp/r1.txt /tmp/r2.txt /tmp/r2st.txt /tmp/r4.txt /tmp/lib.txt /tmp/probe*.py
# 编译缓存
find ~/code/Learn-Python/days/day-194-ai-data-pipeline -name __pycache__ -type d -exec rm -rf {} +
确认无残留:
ls /tmp/day194* 2>/dev/null || echo "clean"
pgrep -fa '04-pipeline-e2e|02-simhash|01-risk' || echo "no process left"
本课不启动任何常驻服务、不占用端口、不写系统目录。