建立一个简单的 Telegram Bot 来存档 Web 内容
在正式内容开始之前,我先来说一下上一篇文章提到的新博客,地址在 https://next.jimmy0w0.me 可以体验到
造一个 Telegram Bot 来存档网页
为什么?事情起因在好几个星期前一名友人找我帮忙存档一些容易 404 的网站快照;可能是一些新闻,或者是在国内论坛上的一些敏感发言。我最初是使用 archive.org 来建立网站快照,但是不知道为什么当时 archive.org 即使我翻了墙访问速度依然很慢,可能是当时线路不好的原因 (现在不慢了)
于是我就萌生了一个想法,既然当时访问 archive.org 的速度那么慢,然后用的还是人家的服务,那为什么不自己写一个
存档网站这种事情方法有非常多种,有的存储下网页的源代码,有的用截屏,然后我选择用 PDF 来存储一个网页的内容
首先是 PDF 可以极大程度的还原网站的样貌,同时可以直接合并成单一文件,文件大小也可以接受;另一个是,PDF 同样具有可以交互的特性,比如你可以复制里面的文字,点击里面的超链接等等;最骚的是,你还可以通过一些工具 (比如 Windows 资源管理器自带的搜索功能) 来搜索 PDF 文件里面的内容,顺便还可以具有高亮文字,注记的特性
用户在 Telegram 中使用特定的命令 + 网页地址参数,就可以在服务端创建出 PDF 文件并且存档到一个地方
大致的构想有了,接下来就是实现了
我不会很详细的说实现的部分,不过我会说一些用 Headless Chromium 时候的一些些坑
Telegraf 到 node-telegram-bot-api
我最早写 Bot 的时候用的是一个叫做 Telegraf 的 Telegram Bot 依赖,这个东西大倒是挺大的,不能直接接收命令参数倒是挺拉跨的
当时我用的版本是 3.x 的,有一个现成的中间件,安装之后就可以接收命令参数了,后来到了 4.x 就不兼容了,报错儿
于是这次写 Bot 我就换了一个依赖,找了一下,node-telegram-bot-api 似乎不错
官方文档直接演示了如何接收命令参数
1 | // Matches "/echo [whatever]" |
match 是一个数组,按需截取即可
Bot 依赖的部分找到了,我还挺喜欢这个依赖的,接下来就是如何生成 PDF 和存储在哪里的问题了
puppeteer
经过之前的搜索,我还是使用了 puppeteer 这个依赖来生成网站的 PDF
puppeteer 是一个 Headless 的 Chromium;简单来说,就是没有 GUI 界面的浏览器
它可以被代码所操控,例如建立一个浏览器实例,打开一个页面,打开 URL,最后生成出 PDF
1 | const puppeteer = require("puppeteer"); |
这样就可以生成出一个名字为 content.pdf 的 PDF 文件,当然,你想保存在一个文件夹里统一管理也是没问题,自己改一下路径即可
page.pdf
那里有几个参数,路径,格式和打印背景
可以参考官方文档来自行修改,反正我就这么写了,目前比较适合用于存档网页快照
顺带一提,page.pdf
也支持使用 width
和 height
参数来调整生成大小,但是当你设置了 format
参数之后,width
和 height
参数会失效
Google Firebase
Firebase Storage
我没有太多的精力来构建一个存储文件的服务器,我也不想买服务器养着供着,所以我会考虑使用一个靠谱的 BaaS (后端即服务)
前面说过,网页存档可能会存档一些容易 404 的内容,这也就说明了内容肯定是非常违规国内国情的
因此像是国内的腾讯云开发 Cloudbase,LeanCloud 我都直接不会考虑使用
剩下来的只有 Google 的 Firebase
好在 Google 还算大方,Firebase 的 Storage 功能免费计划就可以存 5 GB 的内容 (跟现在普通的国外网盘服务差不多,比 Google Drive 小 10 GB)
比较影响 PDF 大小的还是图片,如果单单是文字,其实一般不会达到 1 MB;图文并茂的,例如一篇微信公众号文章,大概也可以稳定在 5 MB 以下
除非是那种大型网站,非常图文并茂,少张图都没办法说明件事的时候,基本上才会达到 10 MB 左右或者更大;不过目前还没见到有存档到这么大的单个 PDF 文件
所以目前,5 GB 可以说是非常宽裕
然后有些事情需要注意,为了让用户可以直接访问到他刚刚存档的文件,通常我们需要返回文件的地址
为了构建出可以直接访问的地址,使用 Firebase Storage Bucket 的 Upload 方法的时候需要自定义文件的访问 Token
Like this
1 | await bucket.upload(path, { |
这个 fileAccessToken
你可以用 uuid 依赖下的 v4 方法来生成
最终地址应该像是这样的
1 | https://firebasestorage.googleapis.com/v0/b/{BUCKET_SPOT_NAME}.appspot.com/o/{FILE_NAME}?alt=media&token={FILE_ACCESS_TOKEN} |
然后按照这样的格式返回给用户,用户就可以直接访问到刚刚存储的 PDF 文件了
Firebase Firestore
然后我还弄了一个文件访问代码的功能,大概就是通过 /get
命令 + 文件访问代码就可以获取回之前的文件
完全就是卵用不大的功能,但是用户在记录存档的文件的时候,长长的 URL 可以被这个文件访问代码取代
文件访问代码是由 7 个随机字符组成的,用的是 random-string 这个依赖
在存储的时候像是这样
1 | await db.collection("archive").doc(fileAccessCode).set({ |
这个 fileAccessCode 就是通过 random-string 生成出来的七个代码
然后 fileAccessUrl 是上面用户可以直接访问到文件的 URL
查询的时候使用
1 | const data = (await db.collection("archive").doc(fileAccessCode).get()).data(); |
就可以访问到文件链接,然后返回给用户
其他功能
我还写了几个逻辑来丰富 Bot 的功能,例如成功上传 PDF 文件之后自动清理 cache 文件夹下的 PDF 文件;以及在出错的时候自动向我的 Telegram 频道汇报错误
坑
在开发到部署的时候碰到了两个坑
-
懒加载资源
-
中文字体
1. 懒加载资源
在我朋友存档一篇微信公众号的文章的时候,我发现上面的图片正确显示,但是下面的图片竟然还是加载的状态
经过实验,微信公众号确实是用了懒加载
也就是,当用户的设备没有往下滑动到图片位置的时候,图片不会加载
这种设计方案是用来减少腾讯服务器的资源浪费
于是我去搜索了一下解决方案,参考这篇 GitHub Issue 目前解决了公众号图片加载问题
1 | const puppeteer = require("puppeteer"); |
目前测试下来,下面的图片都可以正确加载
2. 字体问题
首先感谢 Maverick5g 友情提供的香港 Google Cloud Platform 服务器来部署我的 Bot
GCP 服务器用的 Ubuntu 系统是 English 的,没有安装任何中文字体
因此在首次部署测试的时候,发现出来的 PDF 全部都是方块字
这种就是没有中文字体的后果
解决方案也十分简单,去 Google 搜索如何给 Ubuntu 安装中文字体即可,按照自己喜好安装;我安装的是微软的雅黑字体,因为可以直接从我的 Windows 系统中复制出来,转换成普通的 ttf 字体文件,然后丢到 Ubuntu 中安装
结尾
好,至此,我的简单的存档网页用的 Bot 就开发的差不多了
截止到这篇文章撰写完毕,这个 Bot 已经有了 22 次的 commits
有兴趣了解的,可以来 Telegram 玩玩,搜索 nothing
即可
注:因为在中国某些案件原因,本 Bot 不再向公众开放服务