前言
爬取小说时,以每一个章节为一个线程进行爬取,如果不加以控制的话,保存的时候各个章节之间的顺序会乱掉。
当然,这里说的是一本小说保存为单个txt文件,如果以每个章节为一个txt文件,自然不会存在这种情况。
不仅仅是小说,一些其他的数据在多线程爬取时也有类似情况,比如:
漫画:漫画其实是由大量图片组成,一般一本漫画会保存为一个pdf文件,在此过程要保证图片的顺序。
视频:现在网络上的视频大部分是由多个ts文件拼合,最后保存为一个mp4文件,要保证ts文件的顺序。
它们都有一个共同的特点,那就是:爬取目标时有多个链接,但保存后却只有一个文件,且无法在保存完毕后进行二次排序。
单线程时,这一类文件的爬取不会出现任何问题;
多线程时,因为爬取顺序的不确定性,导致保存的顺序紊乱。
解决方案
问题的根本原因在于:因为爬取和保存的一致性,混乱的爬取顺序使得保存顺序也变得混乱。
解决方案:将爬取过程和保存过程分离,多线程爬取数据,但按顺序保存数据。
比如一本小说,在爬取章节的过程中可以使用多线程,但不要在爬取之后立即保存,等待时机,精准写入。
那这个时机是什么呢?
可以在爬取章节的过程中,给每个章节一个带序号的标记,这个序号即小说章节的序号;保存时,从序号0开始保存,记录这一个序号,再检测当前爬取的章节中有没有比记录的序号刚好大于1的(大于1相当于下一个章节),有就写入,没有就等待。
具体过程:
爬取目录页,抽取出所有的章节链接。
将所有待爬取的链接扔到一个队列里面去,同时给每个链接一个标记。
开5个线程,不断地从队列里面拿链接进行爬取。
单个章节爬取之后,让爬取这个链接的线程阻塞。
申明一个成员变量,表示保存的章节序号,从-1开始
当前线程的链接标记是否刚好比章节序号大于1,是就保存,不是就继续阻塞
因为是从队列中取数据,就能够保证这5个章节是还没有被爬取的前5个章节
具体实现
爬虫代码用的python实现,自定义的线程类,这里以/8_8455/爬取为例。
代码如下:
importrequestsfrom lxml importetreefrom threading importThreadfrom queue importQueueclassMyThread(Thread):def __init__(self, q):
Thread.__init__(self)
self.q=qdefrun(self):globalindexwhile notself.q.empty():
data=self.q.get()
url= root + ''.join(data[1])
response= requests.get(url, headers=headers)
page=etree.HTML(response.content)
chapter= page.xpath("//h1/text()")
chapter= ''.join(chapter)print("爬取 -> %s" %chapter,index)
content= page.xpath("//div[@id='content']/text()")
content= '\n'.join(content)
content= content.replace("\xa0\xa0\xa0\xa0", "\t")#如果当前标记比保存的小说章节序号大于1,阻塞
while data[0] > index + 1:pass
#刚好大于1时,通过,保存章节
if data[0] == index + 1:print("保存 -> %s" %chapter,index)
f.write('\n' + chapter + '\n')
f.write(content)
index+= 1
if __name__ == '__main__':
root= "/8_8455/"headers={'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
index= -1 #章节标记,表示保存的章数
response= requests.get(root, headers=headers)
page=etree.HTML(response.content)
title= ''.join(page.xpath("//h1/text()")) #小说名
print(title)
with open("%s.txt" % title, 'w', encoding='utf8') as f:
f.write(title)#先写入小说名
hrefs = page.xpath("//div[@id='list']/dl/dt[2]/following-sibling::dd/a/@href")
q=Queue()for i,href inenumerate(hrefs):
q.put((i,href))
ts=[]for i in range(5):
t=MyThread(q)
t.start()
ts.append(t)for t ints:
t.join()
代码整体上并不复杂,用的也是requests+lxml的经典组合,主要是队列数据写入和线程阻塞那里,能够理解整个也就不难了。
总结
该说的也都说了,最后说一句:
如果是锻炼技术的话可以多线程爬这玩玩,但如果要写全网爬虫,还是写个单线程吧,尤其是在自己时间足够充裕的情况下,毕竟人家租个服务器也不容易。