博客订阅功能重构

随着友链交换越来越多,不支持 RSS 订阅的网站也越来越多。如果每一个网站都单独写一个爬虫维护成本太大。同时部分网站界面改得也太频繁了,每次都需要重新处理下,点名批评 @云樾

页面内容获取

有些页面使用服务端渲染,有些则是客户端渲染,因此分为两种获取内容的形式。

首先先使用简单的 get 获取页面内容,不执行 JS,如果可以拿到内容那么万事大吉。
而如果发现无法分析出结果,则说明可能这个网站是客户端渲染的,还需要加载 JS 才行。这里调用 chromedp 来实现相关的渲染。

为了避免页面奇怪的问题卡住,两种方式都设置了 120 秒超时
为了节省维护成本,所以选择自动提取页面的标题来实现订阅

日期处理

考虑到目前每个博客的时间格式差别比较大,还需要对不同的形式都进行检查
(在这里面甚至有 2021-01-1 这种月份补 0,日期不补 0 的奇怪格式)

文章部分抓取

HTML 内容分析

这里需要的信息包含三部分

  • 文章标题
  • 文章链接
  • 发布/更新时间

最简单的突破口就在文章链接,在 HTML 里,链接必然是个 <a> 标签。或许会有人用 <div> 实现一切,但是在跳转这方面,<a> 的语义化以及对搜索引擎爬虫的友好度是无法替代的

找到所有的 <a> 标签后,就面临下一个问题,哪些是文章的链接?毕竟一个正常的博客必然会有大量乱七八糟的链接,可能是跳转到特殊页面的,也可能是跳转到站外的。
首先对这些标签进行分类

  • 站内导航: 跳转到首页、归档页、标签页或其他地方
  • 文章跳转: 跳转到文章里
  • 站外跳转: 跳转到其他网站(如友链、Github)

最后一种相对比较好筛选,直接判断域名的 host 是否相同即可
对于前两种可能不太好区分,毕竟每个博客文章的 URL 构成都不一样,有些站点确实和文章页很像,在这里可以考虑两种策略

  • 如果同一组的链接里包含跳转首页的链接,则这一组都不是文章
  • 如果同一组的链接文字内容都很短,那么这一组都不是文章(一般来说,导航都在 4 个字左右,而文章标题通常会更长)

在这里,引入了同一组标签的概念,由于页面的生成存在规律,一个优秀的页面,类似的内容必定存在类似的格式。比如如果是文章列表,那么每个文章最外层的标签必然互为邻居标签(可能会被塞进去几个广告)。从代码体现上来看,就是 xpath 路径相同,也即很可能是类似 html/body/div/div/div/ul/li/div/a 的形式。将所有的 <a> 标签的位置进行聚合,这样就可以实现分组。同时一般而言,如果不考虑站外(host 不同)、标签(长度一般很短)时,组内元素最多的就是文章列表(导航一般很少)

RSS 处理

上面提到的都是下策,实际上如果有 RSS 订阅,那么最省事的是直接从解析 RSS 文件

简单的判断思路时判断地址里是否有 rssatomfeedxml 的字符,如果存在则大概率时 RSS 订阅地址。

JSON 处理

作为另一套预备方案,如果网站为客户端渲染,通常使用 JSON 传输网站内容,可以直接分析 JSON 数据。
受限于每个网站格式不一,因此这里考虑通过如下形式寻找文章列表,优先遍历 postposts 字段,如果存在,且这部分内容为数组或对象(虽然大概率不会,但是万一有类似 {1: {}, 2: {}} 的邪门歪道写法呢),那么只处理该部分字段

对得到的数据结构,遍历 title 字段。一般来说别的字段都可能会有其他叫法,标题都比较固定。

然后就是优先枚举 urllink 字段作为链接。如果不存在这些字段,则检查所有可能为链接的字段。

最后枚举所有字段并检查是否可能是时间。因为 JSON 这里有可能以时间戳形式传输,所以分别以秒计、毫秒级、微秒级、纳秒级判断时间是否为 2000 年到现在的时间段内(2000 年前的博客应该不会被抓取吧……)

有一种额外要考虑的就是,可能部分网站本身链接是拼接的,JSON 里不包含链接,而是只传递了一个 ID。对于这种只能寄希望于其他手段有效吧
(如果一个网站,没有 RSS、页面也提取不到文章、JSON 格式还很诡异,可能站长本来就不想被爬吧……)

如何配置

一般来说,有 RSS 优先贴 RSS

没有 RSS 优先贴符合规则的 JSON

都没有就贴归档页(archive),通常归档页比较整齐,干扰因素比较少

如果归档页都没有,那么去首页看缘分抓取吧……

结果

从目前的情况来看,大部分页面都可以正常处理,部分博客由于未知原因,还需要逐步调整

部分已知问题

  • @iMyShare 页面内容太复杂,经常会错误选取到其他的内容
  • @野宁新之助 不确定是不是 Docker 环境问题,总是加载失败
  • @TonyHe 无法识别日期,不过日期本身不影响抓取
  • @Oldpan 首页东西太乱,总是抓错,RSS 订阅头部返回格式错误。这里强制请求开启压缩可以解决
  • @勇杰的博客 日期不包括年份,抓取失败,不影响结果
  • @flypig 部分 RSS 格式解析失败,不过其他的没问题(实际上即使是失败的格式,理论上再按照解析 html 的思路做一次应该也可以实现)
  • @极客兔兔 请求失败,从测试来看,服务器上的 Docker 请求了一个 IPv6 地址,但是本地没问题,应该是 Docker 配置问题,后续可以考虑禁用 IPv6
  • @Sky 看起来没啥问题,但是服务器上总是访问失败,另外 RSS 格式有问题
  • @镜旅博客 内容太复杂提取错误

总得来说,问题大部分是因为首页太乱提取失败,可以考虑增加更多的判断来解决;另一部分就是 Docker 环境下奇怪的请求超时,需要进一步寻找解决方案。

未来可能可以考虑使用机器学习来分析,不过就目前效果来看,似乎不太必的样子,可能引入后并不会提升准确率