后台列表永远只给你第一页:一次 79 个假缺口和两个真事故我做了一件很自然的事:写脚本打开各平台的「我的文章」列表,抓标题,跟本地稿库比对,算出「哪些稿子还没发到这个平台」。第一次跑出来,缺口79 个。我差点就照着这个清单去补发了。幸好补发前手贱看了一眼作者主页——上面写着「21 篇」,而我的清单里该平台只有 8 篇。8 篇里算出来的 12 个缺口,有 11 个是假的。一、79 个缺口是怎么算出来的逻辑毫无问题:缺口 = 稿库 20 篇 − 平台上已发的标题集合问题出在「平台上已发的标题集合」这一步。我打开的是后台列表页,抓的是页面上出现的所有标题。而后台列表页一页只显示 8 条,我没有翻页。所以每个平台我只看到了最早的 8 篇,剩下 13 篇在我的视野之外,于是被算成「没发过」。八个平台 × 平均每平台只看到 8 条 = 79 个缺口里,绝大多数是这么来的。教训不是「记得翻页」,而是:一个没验证过完整性的数据源,算出来的任何数字都是假的。我当时甚至没觉得需要验证——列表页有分页控件,我默认它是对的。二、分页控件为什么点不动发现清单不对之后,我去补翻页。然后发现:三个平台的后台分页控件都点不动。不是没找到元素。我用的是真实鼠标事件(CDP 的Input.dispatchMouseEvent),坐标取自getBoundingClientRect的中心点,点之前还用elementFromPoint验证过该坐标上没有别的元素遮挡。点击确实命中了「下一页」,页面就是不换。排查下来,原因是这一类:命中元素: DIV.pagination-wrap (宽 640, 高 32) 期望命中: A.page-next (宽 30, 高 24)我用文本匹配「下一页」去找元素,命中的是包裹整个分页区的容器 div——因为容器的innerText里当然也包含「下一页」这三个字。点容器的中心点,落在两个页码之间的空隙上,什么也没触发。修正办法很土但有效:// 只取叶子节点:没有子元素、或者子元素里不含同样文本的那个constisLeaf=e.children.length===0;// 并且优先 A / BUTTON 标签constisClickable=/^(A|BUTTON)$/.test(e.tagName);// 并且先滚到底(分页控件常在视口外,视口外的元素点不到)window.scrollTo(0,document.body.scrollHeight);改完之后,51CTO 能翻了,博客园还是不行(它的分页是异步加载,点完要等 3 秒,而且翻页后 DOM 复用,URL 不变,脚本判断不出「翻成功了没有」)。腾讯云根本没找到分页控件——它的后台压根只给一页。于是我放弃了「点分页」这条路。三、三条能拿到全量的路路一:站内接口(最干净)多数平台的列表页其实是调接口的。开着网络面板点一次列表页,就能看到它请求的接口和返回的字段。这条路我拿下了 CSDN、掘金、知乎、简书四个平台。好处不只是「全量」,还有:返回的是结构化数据,标题、ID、状态、发布时间都有,不用从 HTML 里抠。举个实际收益:接口里有个status字段,能直接区分「已发布」「审核中」「未通过」「草稿」。这是页面上要一个个点进去才看得到的东西。路二:归档页(最稳)博客园有按月归档页,一个月的文章一页列完,没有分页。https://www.cnblogs.com/<用户名>/archive/2026/10.html我 10 月发了 19 篇,打开这个页面,19 条全在。一篇不落,还带日期。这条路的好处是它不依赖登录态和接口签名,是个纯粹的静态页。坏处是只有部分平台提供归档页,而且按月切,跨月要拼好几次。路三:作者主页加后缀(最容易忽略)腾讯云这个卡了我最久。我打开作者主页,只看到几条「动态」,根本不是文章列表。后来发现腾讯云的作者主页分 tab,默认停在「动态」,文章列表在另一个 tab 上,而且这个 tab 是有独立 URL 的:https://cloud.tencent.com/developer/user/<uid> → 动态 https://cloud.tencent.com/developer/user/<uid>/articles → 文章列表(全量)带不带/articles后缀,看到的是两个完全不同的页面。我前面几次抓取全用了不带后缀的那个,所以永远只有 5 条。51CTO 也是同一个路子:直接访问u_17794761看到的是「动态 + 近期文章」,要点开「博客」这个 tab 才是全量列表。四、假缺口的代价:我把发过的稿子又发了一遍上面这些都是技术问题,真正让我肉疼的是后果。在拿到全量之前,我的清单说「51CTO 缺 5 篇」。其中有一篇叫《把 1242 条法规做成离线可核查的知识库》,我记得早期发过,但清单里没有(因为它排在第一页之外),我就信了清单,重新发了一遍。结果:未通过,原因「重复提交」。平台侧的查重不是比标题,是比内容。我换个标题(原标题带了「10 部环境法一夜废止之后」的前缀,新标题没有)没用,正文 90% 相同,照样判重。那一刻我意识到「假缺口」不只是浪费时间,它会造成真实的负资产:一条未通过记录、一个失败草稿、以及平台对你账号的重复提交计数。五、现在我的流程改完之后,算缺口的流程变成这样:1. 取全量(接口 → 归档页 → 主页后缀,三选一,必须验证总数) 2. 验证总数:抓到的条数 ≥ 平台上显示的「N 篇文章」? 不达标就换路子,不许带着残缺数据往下走 3. 比对:用标题的「指纹」(去标点、去空格、取前 12 字)做包含匹配, 不用全等——各平台对标题里的书名号、引号处理不一样 4. 输出清单时同时输出「已发条数」,让人一眼能看出数据是不是全的第 2 步是关键。以前我觉得「抓到什么算什么」,现在这一步是硬性卡点:抓到的条数和平台自己声明的总数对不上,脚本直接报错退出,不许继续。顺便说,第 3 步的指纹匹配也救过我一次。同一篇稿子在 CSDN 上标题末尾多了个空格,在掘金上书名号被转成了直角引号,全等匹配两边都判成「没发过」,用前 12 字包含匹配就对了。六、为什么值得单独写一篇因为这是一个安静的错误。脚本没崩,退出码 0,日志里该有的都有,输出的清单甚至格式漂亮、字段齐全。唯一的问题是数字错了,而且是往「显得还有好多活儿干」的方向错——这正是我最愿意相信的方向。当时我看到 79 个缺口的感觉是「哇,还有这么多可以发」,而不是「等等,这数字对吗」。一个错误如果朝着你希望的方向偏,你不会去怀疑它。所以现在我给所有「统计类」脚本加了一条:输出里必须带上一个能独立验证的基准数字。抓文章就带「平台声明的总数」,抓评论就带「页面上显示的计数」。有这个数字在旁边,偏差自己会跳出来。本篇提到的三个坑(分页控件命中的是容器、归档页、主页后缀)都不是什么高深技术,加起来我花了大概四十分钟才摸清。但如果不是先看到「21 篇」那个数字,我可能至今还在信那 79 个缺口。
后台列表永远只给你第一页:一次 79 个假缺口和两个真事故我做了一件很自然的事:写脚本打开各平台的「我的文章」列表,抓标题,跟本地稿库比对,算出「哪些稿子还没发到这个平台」。第一次跑出来,缺口79 个。我差点就照着这个清单去补发了。幸好补发前手贱看了一眼作者主页——上面写着「21 篇」,而我的清单里该平台只有 8 篇。8 篇里算出来的 12 个缺口,有 11 个是假的。一、79 个缺口是怎么算出来的逻辑毫无问题:缺口 = 稿库 20 篇 − 平台上已发的标题集合问题出在「平台上已发的标题集合」这一步。我打开的是后台列表页,抓的是页面上出现的所有标题。而后台列表页一页只显示 8 条,我没有翻页。所以每个平台我只看到了最早的 8 篇,剩下 13 篇在我的视野之外,于是被算成「没发过」。八个平台 × 平均每平台只看到 8 条 = 79 个缺口里,绝大多数是这么来的。教训不是「记得翻页」,而是:一个没验证过完整性的数据源,算出来的任何数字都是假的。我当时甚至没觉得需要验证——列表页有分页控件,我默认它是对的。二、分页控件为什么点不动发现清单不对之后,我去补翻页。然后发现:三个平台的后台分页控件都点不动。不是没找到元素。我用的是真实鼠标事件(CDP 的Input.dispatchMouseEvent),坐标取自getBoundingClientRect的中心点,点之前还用elementFromPoint验证过该坐标上没有别的元素遮挡。点击确实命中了「下一页」,页面就是不换。排查下来,原因是这一类:命中元素: DIV.pagination-wrap (宽 640, 高 32) 期望命中: A.page-next (宽 30, 高 24)我用文本匹配「下一页」去找元素,命中的是包裹整个分页区的容器 div——因为容器的innerText里当然也包含「下一页」这三个字。点容器的中心点,落在两个页码之间的空隙上,什么也没触发。修正办法很土但有效:// 只取叶子节点:没有子元素、或者子元素里不含同样文本的那个constisLeaf=e.children.length===0;// 并且优先 A / BUTTON 标签constisClickable=/^(A|BUTTON)$/.test(e.tagName);// 并且先滚到底(分页控件常在视口外,视口外的元素点不到)window.scrollTo(0,document.body.scrollHeight);改完之后,51CTO 能翻了,博客园还是不行(它的分页是异步加载,点完要等 3 秒,而且翻页后 DOM 复用,URL 不变,脚本判断不出「翻成功了没有」)。腾讯云根本没找到分页控件——它的后台压根只给一页。于是我放弃了「点分页」这条路。三、三条能拿到全量的路路一:站内接口(最干净)多数平台的列表页其实是调接口的。开着网络面板点一次列表页,就能看到它请求的接口和返回的字段。这条路我拿下了 CSDN、掘金、知乎、简书四个平台。好处不只是「全量」,还有:返回的是结构化数据,标题、ID、状态、发布时间都有,不用从 HTML 里抠。举个实际收益:接口里有个status字段,能直接区分「已发布」「审核中」「未通过」「草稿」。这是页面上要一个个点进去才看得到的东西。路二:归档页(最稳)博客园有按月归档页,一个月的文章一页列完,没有分页。https://www.cnblogs.com/<用户名>/archive/2026/10.html我 10 月发了 19 篇,打开这个页面,19 条全在。一篇不落,还带日期。这条路的好处是它不依赖登录态和接口签名,是个纯粹的静态页。坏处是只有部分平台提供归档页,而且按月切,跨月要拼好几次。路三:作者主页加后缀(最容易忽略)腾讯云这个卡了我最久。我打开作者主页,只看到几条「动态」,根本不是文章列表。后来发现腾讯云的作者主页分 tab,默认停在「动态」,文章列表在另一个 tab 上,而且这个 tab 是有独立 URL 的:https://cloud.tencent.com/developer/user/<uid> → 动态 https://cloud.tencent.com/developer/user/<uid>/articles → 文章列表(全量)带不带/articles后缀,看到的是两个完全不同的页面。我前面几次抓取全用了不带后缀的那个,所以永远只有 5 条。51CTO 也是同一个路子:直接访问u_17794761看到的是「动态 + 近期文章」,要点开「博客」这个 tab 才是全量列表。四、假缺口的代价:我把发过的稿子又发了一遍上面这些都是技术问题,真正让我肉疼的是后果。在拿到全量之前,我的清单说「51CTO 缺 5 篇」。其中有一篇叫《把 1242 条法规做成离线可核查的知识库》,我记得早期发过,但清单里没有(因为它排在第一页之外),我就信了清单,重新发了一遍。结果:未通过,原因「重复提交」。平台侧的查重不是比标题,是比内容。我换个标题(原标题带了「10 部环境法一夜废止之后」的前缀,新标题没有)没用,正文 90% 相同,照样判重。那一刻我意识到「假缺口」不只是浪费时间,它会造成真实的负资产:一条未通过记录、一个失败草稿、以及平台对你账号的重复提交计数。五、现在我的流程改完之后,算缺口的流程变成这样:1. 取全量(接口 → 归档页 → 主页后缀,三选一,必须验证总数) 2. 验证总数:抓到的条数 ≥ 平台上显示的「N 篇文章」? 不达标就换路子,不许带着残缺数据往下走 3. 比对:用标题的「指纹」(去标点、去空格、取前 12 字)做包含匹配, 不用全等——各平台对标题里的书名号、引号处理不一样 4. 输出清单时同时输出「已发条数」,让人一眼能看出数据是不是全的第 2 步是关键。以前我觉得「抓到什么算什么」,现在这一步是硬性卡点:抓到的条数和平台自己声明的总数对不上,脚本直接报错退出,不许继续。顺便说,第 3 步的指纹匹配也救过我一次。同一篇稿子在 CSDN 上标题末尾多了个空格,在掘金上书名号被转成了直角引号,全等匹配两边都判成「没发过」,用前 12 字包含匹配就对了。六、为什么值得单独写一篇因为这是一个安静的错误。脚本没崩,退出码 0,日志里该有的都有,输出的清单甚至格式漂亮、字段齐全。唯一的问题是数字错了,而且是往「显得还有好多活儿干」的方向错——这正是我最愿意相信的方向。当时我看到 79 个缺口的感觉是「哇,还有这么多可以发」,而不是「等等,这数字对吗」。一个错误如果朝着你希望的方向偏,你不会去怀疑它。所以现在我给所有「统计类」脚本加了一条:输出里必须带上一个能独立验证的基准数字。抓文章就带「平台声明的总数」,抓评论就带「页面上显示的计数」。有这个数字在旁边,偏差自己会跳出来。本篇提到的三个坑(分页控件命中的是容器、归档页、主页后缀)都不是什么高深技术,加起来我花了大概四十分钟才摸清。但如果不是先看到「21 篇」那个数字,我可能至今还在信那 79 个缺口。