0%

记一次漫画批量的爬取/下载

背景

想找回一部很久之前看的漫画,网上的资源较少,大多都已被封禁,少数能看的网页加载还超级慢,有些图片显示还非常奇怪,影响正常阅读,遂萌生了下载下来在本地看的想法。后来找到一个网页,显示正常,能看,但是速度有点不稳定,于是就选择爬取该网页,将漫画全部下载到电脑上。

实现思路

漫画下载无非就是图片的抓取下载,也算是一种比较简单的爬虫。分析选为抓取来源的网页的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图片的大小来判断该话是否结束爬取。根据漫画的总数,对每一话进行下载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
baseUrl = 'https://www.xxxxxx.com/1234/1234/{0:0>3d}/{1:0>3d}.jpg'

def getChapter(no):
'''获取并保存第no话的漫画'''
cnt = 1
os.mkdir(saveDir + str(no))
req = requests.get(baseUrl.format(no,cnt))
print(req.status_code, req.headers)
while req.status_code == 200 and req.headers['Content-Length'] != size404:
with open(saveDir + str(no) + '/' + str(cnt)+'.jpg','wb') as f:
f.write(req.content)
cnt += 1
req = requests.get(baseUrl.format(no,cnt))
return cnt - 1

效果

实在是太慢了!

尝试着下了一话大概花费半到一分多钟左右,这里总共有一百多话,两个多小时肯定是走不掉了,而且还没考虑网络不稳定的因素。考虑了一下其中效率的制约因素,最主要为: - 网络请求。发请求获取资源需要传输时间。 - IO。图片保存到本地需要写入时间。

v2

考虑使用多线程进行并行下载,进而提高速度。虽说Python提供的多线程只是伪多线程,实际上还是只能有一个线程被核心处理,但应该还是可以减少其中的等待时间。采用threading.Thread对象,将下载任务分成若干个patch交由不同的线程完成,每个线程完成20话的下载任务。

1
2
3
4
5
6
7
8
def getPatch(start,size,max_chapter):
for i in range(start,min(size,max_chapter)+1):
print('save chapter {:0>3d} page {:0>3d}'.format(i,getChapter(i)))

patch = 20
for i in range(7):
t = threading.Thread(target=getPatch,kwargs={'start':i*patch+1,'size':patch,'max_chapter':136})
t.start()

效果

并没有提升多少速度。感觉这个多线程并没提高多少并行程度,我开了7个线程,但是最开始只创建了4文件夹进行下载,在这4话中进行调度交替下载。也不知道花了多少时间,下完这4话之后,我就强制关掉了,弃掉该方案。

具体实现:Goland

说到多线程,最方便的肯定就是Go语言了,直接的关键字支持多线程。于是拾起很久没碰过的Goland,甚至新电脑上还没安装环境,还需要重新安装Go语言,配置环境和开发工具,就下Vscode的插件都花了点功夫。

实现思路还是同Python版本一样,为了简单不使用任何额外的轮子,直接使用net/http包进行http访问,获取图片,并写入到本地文件。

v3

具体的单话下载代码如下。方法跟Python版本的几乎一样,不过Go语言做了额外的错误处理,显得有点冗余。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func downChapter(no int) {
page := 1
for ;; page++ {
_, err := os.Stat(fmt.Sprintf(saveDir, no))
if err != nil {
fmt.Println(fmt.Sprintf(saveDir, no))
os.Mkdir(fmt.Sprintf(saveDir, no), os.ModePerm)
}
res, err := http.Get(fmt.Sprintf(baseUrl, no, page))
if err != nil {
fmt.Println(err.Error())
return
}
if res.Header.Get("Content-Length") == size404{
break
}
file, err := os.Create(fmt.Sprintf(saveDir, no) + fmt.Sprintf(postfix, page))
if err != nil {
fmt.Println(err.Error())
return
}
io.Copy(file, res.Body)
defer res.Body.Close()
}
fmt.Printf("down chapter %3d with %3d pages\n",no,page)
}

使用Go语言最主要就是要用它的多线程特性。在Go中只需要在调用函数前加上关键字Go就可以开启新的多线程调用函数。将下载任务分成20个为一批的多个patch,开启了7个线程进行下载。此处使用WaitGroup进行多线程的等待,避免主线程提前结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func downPatch(start, patch, maxChapter int) {
defer wg.Done()
cnt:=0
for no:=start;no<=start+patch && no<=maxChapter;no++{
downChapter(no)
cnt++
}
fmt.Printf("down patch from %3d to %3d total %d",start,start+cnt-1,cnt)
}

func main() {
for i:=0;i<maxChapter/patch;i++{
wg.Add(1)
go downPatch(i * patch,patch,maxChapter)
}
wg.Wait()
fmt.Println("finish!")
}

效果

速度确实提升了,但是感觉没有到很快的程度。。。

直到写文章的此刻,跑了两个半小时,下了大概86话。打开任务管理器看了看情况,CPU占用率一直很低,磁盘读写占用也很低,感觉瓶颈就在网络传输上面。另外,觉得7个线程也开得有点少了,应该多开一点,榨干电脑的性能,而且开多了也不会有很大的浪费。不过似乎制约速率的瓶颈就这网络传输上面,确实没办法。

思考

  • 对于网络爬虫/下载,最为制约效率的因素始终是网络因素,这个也是我们最不能把握的。可能是服务器端的接入速率因素,可能是服务器端的处理计算速度因素,可能是链路的传输速度因素,还可能是墙的因素,有很多的可能性。
  • 在程序刚运行的时候,我想到过多的http请求会不会把那个站点搞崩。一百多话,每画平均25页,接近三千多张图片,需要发三千多个http请求,会不会over了,不过在当前这个速度下显然是想多了。不过这在以后的爬虫获取数据或者资源的时候确实需要考虑,为他人想想,可以考虑加点间隔时间。
  • 东西不用了就会忘记,技能确实需要是不是拾起来使使。
  • 有时候问题并不是在选择的方法或工具上面,可能只是简单的自己做错了/做得不够,或者当时的环境不行。

后记

最终因为电脑在休眠的时候自动更新,强行重启了,最终还是没下完,大概下了120话,花了4个半小时,远远超出我的预期。大概原因为

  • 线程设置有误。原本想设置7个线程,但没考虑到Go里面除法取整,少了一个线程,最后一个patch的任务没有执行,实际是六线程运行。
  • 代码存在一些不合理的地方,造成了操作上的重复。如判断在每一话当中判断文件夹是否存在,我把他放到了循环当中,每下载一页的图片前都会判断;还有设置patch的时候没有考虑开闭区间,且下载的时候没有判断文件是否已经下载,导致首尾的漫画重复下载。
  • 后期漫画的图片质量上去了,由前面的一百几十kb提升到后面的四五百kb,所以负责后面漫画下载的线程速度较慢,速度没有达到预期。
  • 网络问题。可能昨天晚上的网确实不行,也可能是频繁访问被制约了网速。

今天早上用Python把剩下的十几话下载下来,发现其实昨天的多线程代码有问题,修改了一下,顺利下载,而且速度还不错,我开了6个线程,下18话用了不到20分钟。看来还是网络的问题,难顶。