$ ls ~yifei/notes/

python-readability 源码阅读

Posted on:

Last modified:

Readability 是一个可以从杂乱无章的网页中抽取出无特殊格式,适合再次排版阅读的文章的库。比如,我们常见的手机浏览器的阅读模式很大程度上就是采用的这个库,还有 evernote 的 webclipper 之类的应用也都是利用了类似的库。Readability 的各个版本都源自 arc90 的 readability.js 这个库,之前尝试阅读过 js 版本,无关的辅助函数太多了,而且 js 的 dom api 实在称不上优雅,读起来晦涩难通,星期天终于有时间拜读了一下 python-readability 的代码。

readability 核心是一个 Document 类,这个类代表了一个 HTML 文件,同时可以输出一个格式化的文件。

几个核心方法和概念

Readability 目录下的文件:

readability
├── __init__.py
├── browser.py
├── cleaners.py
├── debug.py
├── encoding.py
├── htmls.py
└── readability.py

核心的是 readability.py 文件。其他的都可以理解为辅助类。

下面介绍 Document 中的方法。

_html

summary

summary 方法是核心方法,可以抽取出一篇文章,可能需要对文章抽取多次才能获得符合条件的文章。这个方法的核心思想是:

  1. 第一次尝试抽取设定为 ruthless,也就是强力模式,可能会误伤到一些标签
  2. 把给定的 input 解析一次并记录到 self.html,并去除所有的 script,sytle 标签,因为这些标签并不贡献文章内容
  3. 如果在强力模式,使用 remove_unlikely_candidates 去掉不太可能的候选
  4. transform_misused_divs_into_ps 把错误使用的 div 转换成 p 标签,这样就不用考虑 div 标签了,其实这步挺关键的。其实还有一些其他的处理需要使用。
  5. 使用 score_paragraphs 给每段(paragraph)打分
  6. 使用 select_best_candidates 获得最佳候选(candidates)
  7. 选出最佳候选,如果选出的话,调用 get_article 抽取文章
  8. 如果没有选出,恢复到非强力模式再试一次,还不行的话就直接把 html 返回
  9. 清理文章,使用 sanitize 方法
  10. 如果得到的文章太短了,尝试恢复到非强力模式重试一次

强力模式和非强力模式的关键区别在于是否调用了 remove_unlikely_candidates

对于以上的核心步骤,已经足够应付大多数比较规范的网页。但是还是会有不少识别错误。公司内部的改进做法在于:

此处省略 1000 个字。

下面按照在 summary 出场顺序依次介绍~

remove_unlikely_candidates

匹配标签的 class 和 id,根据 unlikelyCandidatesRe 和 okMaybeItsACandidate 这个两个表达式删除一部分节点。

unlikelyCandidatesRe:combx|comment|community|disqus|extra|... 可以看出是一些边缘性的词汇
okMaybeItsACandidateRe: and|article|body|column|main|shadow... 可以看出主要是制定正文的词汇

transform_misused_divs_into_paragraphs

  1. 对所有的 div 节点,如果没有 divToPElementsRe 这个表达式里的标签,就把他转化为 p
  2. 再对剩下的 div 标签中,如果有文字的话,就把文字转换成一个 p 标签,插入到当前节点,如果子标签有 tail 节点的话,也把他作为 p 标签插入到当前节点中
  3. 把 br 标签删掉

socore_node

  1. 按照 tag、class 和 id 如果符合负面词汇的正则,就剪掉 25 分,如果符合正面词汇的正则,就加上 25 分
  2. div +5 分, pre、td、backquote +3 分
  3. address、ol、ul、dl、dd、dt、li、form -3 分
  4. h1-h6 th -5 分

score_paragraphs

  1. 首先定义常量,MIN_LEN 最小成段文本长度
  2. 对于所有的 p,pre,td 标签,找到他们的父标签和祖父标签,文本长度小于 MIN_LEN 的直接忽略
  3. 对父标签打分(score_node),并放入排序队列
  4. 祖父标签也打分,并放入排序队列
  5. 开始计算当前节点的内容分(content_socre) 基础分 1 分,按照逗号断句,每句一分,每 100 字母 +1 分,至少三分
  6. 父元素加上当前元素的分,祖先元素加上 1/2
  7. 链接密度 链接 / (文本 + 链接)
  8. 最终得分 之前的分 * (1 - 链接密度)

注意:

  • 当前标签并没有加入 candidates,父标签和祖父标签才加入。
  • 累计加分,如果一个元素有多个 p,那么会把所有子元素的 content score 都加上

select_best_candidate

就是 ordered 中找出最大的

get_article

对于最佳候选周围的标签,给予复活的机会,以避免被广告分开的部分被去掉,阈值是 10 分或者最佳候选分数的五分之一。如果是 p 的话,node_length > 80 and link_density < 0.25 或者 长度小于 80,但是没有连接,而且最后是句号

思考

readability 之所以能够 work 的原因,很大程度上是基于 html 本身是一篇文档,数据都已将在 html 里了,然后通过操作 DOM 获得文章。而在前端框架飞速发展的今天,随着 react 和 vue 等的崛起,越来越多的网站采用了动态加载,真正的文章存在了页面的 js 中甚至需要 ajax 加载,这时在浏览器中使用 readability.js 虽然依然可以(因为浏览器已经加载出了 DOM),但是如果用于抓取目的的话,需要执行一遍 js,得到渲染过的 DOM 才能提取文章,如果能够有一个算法,直接识别出大段的文字,而不是依赖 DOM 提取文章就好了~

除去 Readability 之外,还有一些其他的文章抽取工具,都调研了一下:

  • readability

  • goose3,性能不好。而且只支持中文、英文、阿拉伯语和汉语。

  • newspaper,非常不好使,什么也抽不出来

  • textract,从各种文件中抽取文字,可以作为没有特定抽取器时的 baseline.

  • html2text,Arron 的遗作,还好有 Python3 移植。这个并不能抽取正文,只是转换成 markdown,不过从转换出来的 Markdown 似乎很好找出正文。

网上还有一些按照文本密度抽取的想法,实在是考虑得太少了,基本属于想当然。

除了 buriy/python-readability 之外,另外两个值得参考的实现是:bookieio/breadabilitymozilla/readability。前者有一些测试用例值得参考,后者有最新的 js 实现。

参考

  1. https://github.com/goose3/goose3
  2. https://github.com/deanmalmgren/textract
  3. https://github.com/aaronsw/html2text
  4. https://github.com/chrislinan/cx-extractor-python
  5. https://github.com/Alir3z4/html2text
  6. https://github.com/matthewwithanm/python-markdownify
  7. Breadability, 又一个 readability 移植,有些测试值得参考
WeChat Qr Code

© 2016-2022 Yifei Kong. Powered by ynotes

All contents are under the CC-BY-NC-SA license, if not otherwise specified.

Opinions expressed here are solely my own and do not express the views or opinions of my employer.

友情链接: MySQL 教程站