背景
想找回一部很久之前看的漫画,网上的资源较少,大多都已被封禁,少数能看的网页加载还超级慢,有些图片显示还非常奇怪,影响正常阅读,遂萌生了下载下来在本地看的想法。后来找到一个网页,显示正常,能看,但是速度有点不稳定,于是就选择爬取该网页,将漫画全部下载到电脑上。
实现思路
漫画下载无非就是图片的抓取下载,也算是一种比较简单的爬虫。分析选为抓取来源的网页的 URL,发现其 URL 规律为host/type/漫画的 ID+漫画章节号+第几页
,可以按照该规律生成所有漫画每一页的 URL,则可以根据每一个 URL 获取里面的漫画图片下载。进一步分析这些页面上漫画图片的 URL,发现是跳转到一个 ASP 页面进行提供,链接为三级 ID 组成,包括漫画 ID,章节号,页码组成。在浏览器直接访问图片的 URL,发现会跳转到 404 页面。根据开发者调试工具的 Network 栏中拦截的请求,发现图片实则来自另一个 URL,并且图片 URL 的规律十分明显。于是问题就转变成根据规律批量生成图片的 URL 并下载。
具体实现:Python
为了简单,就没有使用任何的轮子。直接使用 requests 库访问这些 URL 获取图片资源,并写入到本地文件中保存。
v1
具体的实现思路大概如下。对于漫画的每一话,先创建目录,然后根据该话的序号和页面生成目标资源 URL,发送 GET 请求获取资源,通过文件写入保存到本地,直到访问的 URL 不存在漫画图片,跳转到 404 页面,此处我们通过判断响应首部的内容长度是否等于 404 图片的大小来判断该话是否结束爬取。根据漫画的总数,对每一话进行下载。
|
|
效果
实在是太慢了!
尝试着下了一话大概花费半到一分多钟左右,这里总共有一百多话,两个多小时肯定是走不掉了,而且还没考虑网络不稳定的因素。考虑了一下其中效率的制约因素,最主要为:
- 网络请求。发请求获取资源需要传输时间。
- IO。图片保存到本地需要写入时间。
v2
考虑使用多线程进行并行下载,进而提高速度。虽说 Python 提供的多线程只是伪多线程,实际上还是只能有一个线程被核心处理,但应该还是可以减少其中的等待时间。采用 threading.Thread 对象,将下载任务分成若干个 patch 交由不同的线程完成,每个线程完成 20 话的下载任务。
|
|
效果
并没有提升多少速度。感觉这个多线程并没提高多少并行程度,我开了 7 个线程,但是最开始只创建了 4 文件夹进行下载,在这 4 话中进行调度交替下载。也不知道花了多少时间,下完这 4 话之后,我就强制关掉了,弃掉该方案。
具体实现:Golang
说到多线程,最方便的肯定就是 Go 语言了,直接的关键字支持多线程。于是拾起很久没碰过的 Goland,甚至新电脑上还没安装环境,还需要重新安装 Go 语言,配置环境和开发工具,就下 vscode 的插件都花了点功夫。
实现思路还是同 Python 版本一样,为了简单不使用任何额外的轮子,直接使用 net/http 包进行 http 访问,获取图片,并写入到本地文件。
v3
具体的单话下载代码如下。方法跟 Python 版本的几乎一样,不过 Go 语言做了额外的错误处理,显得有点冗余。
|
|
使用 Go 语言最主要就是要用它的多线程特性。在 Go 中只需要在调用函数前加上关键字 Go 就可以开启新的多线程调用函数。将下载任务分成 20 个为一批的多个 patch,开启了 7 个线程进行下载。此处使用 WaitGroup 进行多线程的等待,避免主线程提前结束。
|
|
效果
速度确实提升了,但是感觉没有到很快的程度。
直到写文章的此刻,跑了两个半小时,下了大概 86 话。打开任务管理器看了看情况,CPU 占用率一直很低,磁盘读写占用也很低,感觉瓶颈就在网络传输上面。另外,觉得 7 个线程也开得有点少了,应该多开一点,榨干电脑的性能,而且开多了也不会有很大的浪费。不过似乎制约速率的瓶颈就这网络传输上面,确实没办法。
思考
- 对于网络爬虫/下载,最为制约效率的因素始终是网络因素,这个也是我们最不能把握的。可能是服务器端的接入速率因素,可能是服务器端的处理计算速度因素,可能是链路的传输速度因素,还可能是墙的因素,有很多的可能性。
- 在程序刚运行的时候,我想到过多的 http 请求会不会把那个站点搞崩。一百多话,每画平均 25 页,接近三千多张图片,需要发三千多个 http 请求,会不会 over 了,不过在当前这个速度下显然是想多了。不过这在以后的爬虫获取数据或者资源的时候确实需要考虑,为他人想想,可以考虑加点间隔时间。
- 东西不用了就会忘记,技能确实需要是不是拾起来使使。
- 有时候问题并不是在选择的方法或工具上面,可能只是简单的自己做错了/做得不够,或者当时的环境不行。
后记
最终因为电脑在休眠的时候自动更新,强行重启了,最终还是没下完,大概下了 120 话,花了 4 个半小时,远远超出我的预期。大概原因为
- 线程设置有误。原本想设置 7 个线程,但没考虑到 Go 里面除法取整,少了一个线程,最后一个 patch 的任务没有执行,实际是六线程运行。
- 代码存在一些不合理的地方,造成了操作上的重复。如判断在每一话当中判断文件夹是否存在,我把他放到了循环当中,每下载一页的图片前都会判断;还有设置 patch 的时候没有考虑开闭区间,且下载的时候没有判断文件是否已经下载,导致首尾的漫画重复下载。
- 后期漫画的图片质量上去了,由前面的一百几十 kb 提升到后面的四五百 kb,所以负责后面漫画下载的线程速度较慢,速度没有达到预期。
- 网络问题。可能昨天晚上的网确实不行,也可能是频繁访问被制约了网速。
今天早上用 Python 把剩下的十几话下载下来,发现其实昨天的多线程代码有问题,修改了一下,顺利下载,而且速度还不错,我开了 6 个线程,下 18 话用了不到 20 分钟。看来还是网络的问题,难顶。