背景
最近刚开始追了一部 b 站的新番,觉得挺有意思,就打算看看它的漫画,但这漫画也是某站独家,不太想花钱,就打算上网找点资源白嫖(当然大家还是要支持正版的!我是已经支持太多了想省一省 😂)。然而网上找到的资源太少,且大多已经不可用,唯一找到的一个可以看的网站,图片质量太高,加载太慢,而且它的加载方式也很离谱,每一话在一个页面直接丢给你十几二十张图片,而且也没有 lazy load, 全部图片一起加载,体验极差。
所以没有办法,只能重操旧业,把它爬下来,免去了阅读过程中的等待时间。(对上一次的爬取可以看此)
爬取思路
在漫画页的路径规则非常统一,都是那种 http://host/:type/:commic-name/:chapter.html
, 直接根据规则生成 url 就行了。
打开开发者工具看了看页面的文档具体内容,发现漫画图片就直接硬编码嵌入在 table tr td img
中,而且每个图片还直接给了个形如 img2
这样的唯一的 id,XPath 都不需要思考怎么写了,直接 //*[starts-with(@id,'img')]
就可了。于是马上拿出 C# 写了一个函数,解析出单话的所有图片链接,然后下载。
然而发现 xpath 居然找不出了目标元素,一开始还以为是我的 XPath 写得有问题,换了几种写法都不行,看了看获取到的 html 文档全文,才发现这个页面是靠导入 Javascript 脚本,动态插入 DOM 节点来生成完整页面的,所以普通的静态爬取文档方法无效。对这种动态的页面,要么用模拟浏览器,等待它加载执行完 js, 再来解析页面元素,要么就直接深入它动态操作页面的 Javascript 脚本,直接从源头来找数据。因为我算是对 JavaScript 有点熟悉,而且爬虫等待页面加载完再解析页面,性能开销也未免太大(而且 C# 我也不太知道有啥可以像 Python 中的 Selenium 可以模拟浏览器操作的),于是就看了看它导入的一些 js 文件,居然还发现了它所使用的一份来自 2011 年的历史遗留 js 代码。
在现在的角度来看这份代码,简直就是灾难。
- 随意地在随处定义全局变量
- 使用匿名对象的成员来封装函数
- 随心所欲的变量命名:
- 大量的 a, aa, bb, ll 无意义命名
- 英语拼音混杂看不出意义的代码
- 字符串拼接硬编码组成的待插入 html 标签
还好有 IDE, 能够有变量搜索和函数跳转,大概的功能还是看出来了:加载文档时调用一个初始化方法,初始化方法根据一个图片的路径变量,生成一系列的 img 标签,插入到 html 文档里面。
|
|
但是这个变量 qTcms_S_m_murl
我怎么找都没找到它在哪里定义的,用倒是到处都在用,找了几个其它的 js 文件也没有找到,后来发现这居然是在原始的 html 文档直接给出的,原始 html 文档给了一个经过 base64 编码的所有图片链接地址作为一个全局变量,然后导入的 js 文件将该变量中的地址解码再分别取出每个图片的地址,生成 img 标签并插入到源文档中。
发现了它的这个方式之后,工作就很简单了,直接从原 html 文档中找出这个编码后的图片地址合集,解码后分别下载这些图片保存。
实现
这次我选择了用 C# on dotnet core 来实现爬虫任务。按照上面的思路,具体的实现方式也很简单:
- 根据要下载的漫画章节号,生成它的 url
- 发个 Get 请求获取这一话漫画的 html 文档,通过正则获取其中编码后的图片链接合集
- 对于图片链接中的每个图片链接,使用自带的 WebClient.DownloadFileAsync 方法直接下载图片文件到本地
- 然后就是统筹整个下载任务,逐话下载,每话分别获取图片链接,创建文件夹,下载所有漫画图片
这里必须赞一句 dotnet core 对于异步编程的支持,着实做得太好了,自带的 async 和 await 关键字,以及其中用的几乎所有网络请求或者本地 IO 的方法,都给出了异步版本的实现,可以很容易地就实现异步编程,而这种基本是完全交由编译器去处理多线程的调度处理方式,所实现的效果我觉得是比我自己简单地开几个线程分别跑任务的部分是高效得多的。
结果
这个 C# 的实现效率确实太强了,我尝试性下载了一话,二十来张图片,加起来四十多兆,十来秒就下载完了。下载前 80 话漫画,共 1.8G 的图片,总共也就用了 17 分钟,太快了!尤其是对比上一次的下载,下一话居然需要一两分钟,下载了不到一百话就用了两三个小时。当然这里面还有很多因素影响,而且上次的代码的异步编程实现也实现得很糟糕。
不过还是不妨碍我吹一句:我们 dotnet 真是太强啦!