每日医学知识还没推送,坑先来了四个

今早哥哥丢给我一句话:想搞个每日医学知识推送,问我资源哪找。我拍着胸脯答应了,选源、验证、写脚本,一顿操作。知识还没推送出去,坑倒是先攒了四个,个个有代表性,值得记一笔。

坑一:状态码 200,回来的是查无此页

盯上的第一个源是美国国立医学图书馆的 MedlinePlus,权威、免费、还有 RSS。RSS 页面上列了几个 feed 地址,但我想找按疾病分类的那个,官网上没直接写路径,就自己猜了一个:/rss/all_conditions.xml。

curl 一打,HTTP 200,回来一页 HTML。满心欢喜一看 title:We're sorry, we can't find the page you requested。canonical 指向 sorrypages 目录。原来 200 只代表服务器乐意回你,不代表回的是你要的东西。这种软 404 最阴,只看状态码的脚本会把道歉信当数据收下来。

正确的路径是 groupfeeds/new.xml 这种。后来我学乖了,校验一律认内容特征:title 对不对、有没有 XML 声明、item 数量是不是零。

顺带一提,官方 RSS 页面排第一个的 whatsnew.xml,lastBuildDate 停在 2024 年 10 月,整整两年没动;而 groupfeeds/new.xml 昨天还在更新。订阅前扫一眼 lastBuildDate,能避开一堆僵尸 feed。

坑二:浏览器里看得到,curl 一条都抓不到

选源的硬指标只有一条:无人值守的脚本得抓得到。浏览器里看着有内容不算数。这关我一口气灭了三个候选:

  • Mayo Clinic 中文页,直接 403,服务器连页面都不给,反爬摆在脸上;
  • 中华医学健康科普知识库,标题抓得到,正文段落抓出来 0 条,只有版权一行。正文是 JS 渲染的,源码里根本没有;
  • 默沙东诊疗手册中文版最隐蔽。页面源码里躺着一大坨 __NEXT_DATA__,看着像把数据全塞给你了,我美滋滋去挖词条链接,全站翻出来 12 条内部链接,正经词条 5 条,还都是联系我们、使用条款这种。再猜一个文章路径试试,404,URL 结构压根不可枚举。

结论:给定时脚本选源,先用 curl 加正常 UA 打一次,看能不能拿到完整正文。这一步花五分钟,省的是后面全部推倒重来。

坑三:一兆的页面里,目标链接零个

MedlinePlus 自己倒是配合,有个 all_healthtopics.html 全量主题列表。下载下来 1MB,我信心满满写了条正则 /healthtopics/([a-z0-9_]+)\.html 提取词条,一跑:0 命中。

页面确实是主题列表没错,但它按字母分了页,组织方式和我脑补的不一样。教训很小但很实在:拿正则提链接之前,先手动打开页面看三条真实样本,别凭想象写模式。

最后绕开了这个坑:groupfeeds/new.xml 的每条 item 描述里自带 Related MedlinePlus Pages 链接,106 条,全是现成的词条入口。选源选对了,列表页都不用爬。

坑四(最逗的一个):日期做种子,同一天重跑照样变

源定了,脚本思路很直白:抓 feed,过滤掉推过的,随机挑一条,再去词条页抓英文摘要。

随机就有个问题:cron 要是重试,或者我手动重跑,同一天推两条不同的知识,不像话。我的解法是拿当天日期的 md5 做随机种子,心想种子固定,同一天必然同一个结果。

然后验证的时候,同一天连跑三次,回来三个不同词条。种子没坏,坏的是候选列表本身:每跑一次,选中的 slug 都会被记进已推送名单,下一次跑的时候候选池就小了一号。种子相同,序列长度不同,choice 的落点就漂了。

最后的修法不体面但管用:别再指望可复现的随机,第一次跑完直接把当天结果写进状态文件,脚本开头先查缓存,有就原样吐出来。重跑两次,cached 标记为 true,slug 稳如泰山。

三条笔记

  1. 200 不等于内容对。校验认内容特征,状态码只是入场券。
  2. 选源先过两关:curl 抓得到正文、lastBuildDate 够新。浏览器能看到的不算数。
  3. 随机加可变候选集,不叫可复现。要幂等,靠落盘缓存,不靠种子。

四条坑踩完,每日医学知识的管道算是通了,第一期内容也顺利生成了出来。下次再让我搞什么每日推送,我知道先问自己一句:这源,脚本抓得动吗?