浏览器功能定制2:显示网页正文的阅读模式
在《浏览器功能定制1:将网页内容保存为 Markdown》中,为了保证 HTML 转 Markdown 的效果,其中有一步,通过提取网页正文来简化 HTML 结构。
这让我意识到,这一步应当提取出来,作为一个单独的步骤,实现一种类似『阅读模式』。
之所以说“类似”的阅读模式,与浏览器所自带的阅读模式不同。浏览器自带的阅读模式,会滤出掉文章的所有样式,转为一种统一的“文章”样式进行展示。
而我的实现方案更加简单一些:识别出文章所在的 div,将这个元素充满屏幕展示即可,换句话说,隐藏掉出文章 div 外的其它内容。
在本文中,记录了实现这个方案进行的探索。
识别网页正文
在《浏览器功能定制1:将网页内容保存为 Markdown》中,我调研了许多网页正文提取库。这些网页正文提取库,能够根据一定的算法,将 HTML 的正文提取出来。
但是不太符合我的需求,我需要的不是纯文字,而是代表文章正文的 div。goose3 和 gne 都具备文本提取,以及对应的 div 的 HTML 提取,非常方便。
2023-11-08:在《浏览器功能定制4:qutebrowser userscipts机制及自带readability》中,我了解到了 qutebrowser 内置的阅读模式实现方案,其中同时提供了 readability 库的 Python 和 JavaScript 实现。而 JavaScript 实现由 mozilla 官方实现,看起来要更好一些。
调研
TODO:将《浏览器功能定制1:将网页内容保存为 Markdown》中的调研搬至这里。
使用 readability.js 提取正文
经过一番调研,mozilla/readability 是最香的,能获得 Firefox 同款。其他 Python 的移植版本都没有 JavaScript 版本香。
我打算用它作为网络正文提取器。在《浏览器功能定制4:qutebrowser userscipts机制及自带readability》中提到,qutebrowser 通过 userscripts 机制是提供网络正文提取器(阅读模式)的,并且可以选择基于 JavaScript 版本的 readability。
但是我打算重新实现,原因是 userscripts 跟我的需求不太吻合。它默认行为会触发打开一个新 Tab。而我需要的是一个命令行工具,通过 Python 脚本调用,并返回给我处理后的结果。
于是寻找 readability.js 命令行版的封装,找到一个 phxql/readability-cli。实际使用中,我发现它在处理管道输入时有点问题,会报错 EAGAIN
。需要用如下代码替换:
const run = (url) => {
let data = '';
process.stdin.setEncoding('utf-8');
process.stdin.on('readable', () => {
let chunk;
while ((chunk = process.stdin.read()) !== null) {
data += chunk;
}
});
process.stdin.on('end', () => {
const sanitizedDom = DOMPurify.sanitize(data, purifyOptions);
if (debug) {
console.error('url: ', url)
}
const options = {
features: {
FetchExternalResources: false,
ProcessExternalresources: false,
},
virtualConsole: jsdomConsole,
url: url,
};
const article = readability(new JSDOM(sanitizedDom, options), url);
if (article == null) {
process.exit(1);
} else {
console.log(JSON.stringify(article));
}
});
};
Python 侧的调用代码如下:
def readability_js(html: str, url: str):
print("=======readability_js=======")
# print(html)
# 指定index.js脚本的完整路径
script_path = "/home/maxiee/Code/readability-cli/index.js"
# 构建命令,确保使用绝对路径调用node脚本
command = ["node", script_path, url]
# 开启进程
with subprocess.Popen(
command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
) as process:
# 发送输入数据并获取输出
stdout, stderr = process.communicate(input=html)
# 打印标准输出和错误(如果有)
print(stdout)
if stderr:
print("Error:", stderr)
# 打印返回码
print("Return code:", process.returncode)
ret = json.loads(stdout)
return ret['content']
实际使用下来,效果令人满意!
正文提取库的实际效果
我发现,正文提取不是每个站都能成功。比如 goose3,在有的站点能够成功,有的站点则解析失败。
好在我发现,有时 goose3 解不出来的站点,用 gne 能解出来。(也许 gne 更加强大,我应该调换下顺序,先 gne,后用 goose3 做兜底尝试)。
因此,我选择同时采用两个提取器,以加大提取成功率。
代码实现如下:
@cmdutils.register(instance='command-dispatcher', scope='window')
def main_content(self):
cur_tab: browsertab.AbstractTab = self._cntwidget()
url = self._current_url()
def on_html(html: str):
from goose3 import Goose
# 首先尝试使用 Goose 解析
g = Goose()
article = g.extract(raw_html=html)
content_html = article.top_node_raw_html
# 如果没解出来,再尝试使用 GNE 解析
if content_html is None:
from gne import GeneralNewsExtractor
extractor = GeneralNewsExtractor()
result = extractor.extract(
html=html,
with_body_html=True)
content_html = result['body_html']
print(content_html)
cur_tab.dump_async(on_html)
这会自动在 qutebrowser 中创建一条指令 main-content
。通过键盘即可触发,非常 Geek style。
更新页面
找到正文后,接下来该是更新页面内容,忽略掉其他元素,只展示正文部分。
我最直接的想法,是拿到 content_html
后直接 set_html
。尝试了一下,怎么说呢,正文是展示出来了,但是比较错乱,可读性比较差。
此时应该有一个 html simplify 的过程。去掉各种样式,恢复为最朴素的 HTML(对的,我希望的就是最朴素的 HTML)。
html simplify
借助于 GPT 和 BeautifulSoup,我实现了一个 html 简化算法:
def html_simplify(html: str):
# 使用 BeautifulSoup 解析 HTML
soup = BeautifulSoup(html, 'html.parser')
# 遍历所有元素并删除 'style' 属性,限制图片大小
for element in soup.recursiveChildGenerator():
if isinstance(element, Tag):
# 删除样式属性
if 'style' in element.attrs:
del element.attrs['style']
# 如果是 img 标签,则设置宽度属性
if element.name == 'img':
element.attrs['style'] = 'max-width: 80%; height: auto;'
cleaned_html = soup.prettify()
print(cleaned_html)
# 输出清理后的 HTML
return cleaned_html
在 main_content
的回调中,再加一行 set_html
:
cur_tab.set_html(html_simplify(content_html))
效果
我本担心 set_html
会不会过于粗暴。
实际验证下来,效果非常不错!
以本数字花园的文章为例,正文阅读模式的效果:
是不是很不错!
有了如此干净的 HTML,后续《浏览器功能定制1:将网页内容保存为 Markdown》转 Markdown 时也会简单很多。
对于日常阅读,我喜欢这样的极简样式,能够让我更加专注、严肃地阅读。
今天收获满满,希望我的探索也对你有所启发、帮助。
本文作者:Maeiee
本文链接:浏览器功能定制2:显示网页正文的阅读模式
版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!
喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!