前言
众所周知,pyhon里如果用多线程访问同一个资源是很容易出错的,并且多线程是无序的,也就是一般来讲,我们用多线程需要用线程锁来决定谁来访问,就算用了线程锁,他的无序也决定了我们无法保证内容是按章节顺序存在txt里的,所以为了解决上面两个问题,我们引入了线程池和PriorityQueue重要性队列,按照事件的重要性来判断先后顺序,这样我们同时请求几百条信息的时候赋值给他们自己的重要性数值,就可以决定谁先写入了,下图是1000个线程爬完1400章所需时间,我们可以看到只花了10秒,这个时间是指整个程序开始,一直到文件写入完毕,是非常快的,我也试了单线程,但是单线程实在太慢了,我没耐心等下去,我算了一下,单线程爬完最少需要2分多钟,我是指最少!
一、我们先导入所需要的包
import requests, re
from lxml import etree
from queue import PriorityQueue
from concurrent.futures import ThreadPoolExecutor, as_completed
from time import time
二、定义两个类(生产者和消费者)
这里的重点就是3,4两行了,第三行就是我们的重要性排序的队列,没有他我们不可能在多线程里按顺序写入一个文件内容的,当然,写文件肯定是单线程,但是网页请求是1000个线程,强调一下我是指这个网页请求得来的那1000多个回应是无序的,所以我们单线程写文件时无法按顺序写入,而用到PriorityQueue就可以了,第4行是线程池,我们不能说每一次请求都重新创建一个线程吧,创建线程也是会消耗资源的,所以我们建立一个线程池,保证只有1000个线程来处理你的函数,前一个线程结束了就等待任务到来,而不是关闭再重启,这次你可能体会不到,但是如果别人有10000章的时候,你就能体会到不用线程池时,10000个线程直接把你电脑弄蓝屏的滋味了。
class Spider():
url = 'http://************/txt/111650/index.html'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36',
}
def get_page_urls(self):
rsp = requests.get(self.url, headers=self.headers)
html = etree.HTML(rsp.content)
titles = html.xpath('//dd/a/text()')[0]
links = html.xpath('//dd/a/@href')
links = ['http://**********/txt/111650/'+i for i in links]
return links
class PageJob():
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36',
}
def __init__(self, priority, url):
self.priority = priority
self.url = url
self.GetContent()
return
def __lt__(self, other):
return self.priority < other.priority
def GetContent(self):
rsp = requests.get(self.url, headers=self.headers)
print(rsp.status_code)
if rsp.status_code == 503:
print(rsp.text)
sleep(1)
rsp = requests.get(self.url, headers=self.headers)
html = etree.HTML(rsp.content)
title = html.xpath('//h1/text()')[0]
content = html.xpath('//div[@id="content"]/text()')[:-3]
while '\r' in content:
content.remove('\r')
content = [re.sub('\xa0\xa0\xa0\xa0', '', i) for i in content]
content = [re.sub('\r', '\n', i) for i in content]
self.title = '\n\n'+title+'\n\n'
self.content = content
print(title, content)
这两个类之中Spider用来爬取每一章的链接,相当于生产者,PageJob把生产者产出的每一章链接得到后就发起网页请求把每一章小说都下载下来,相当于消费者。这里我提一下第33-37行是在去除文章中的一些无关代码和加入一些调整格式的字符,其余的都是requests的最基本请求了,我就不提了,不懂的话就看下这份requests官方文档。
三.定义函数
def PutPageJob(para):
q = para[0]
i = para[1]
links = para[2]
q.put(PageJob(i, links[i]))
这个函数其实没有增加实际作用,只是为了当我们在后面把函数放进线程时,能方便的传参数而已。
四.定义函数入点,开始执行
if __name__ == '__main__':
start_time = time()
spider = Spider()
links = spider.get_page_urls()
q = PriorityQueue()
with ThreadPoolExecutor(max_workers=1000) as t: # 创建一个最大容纳数量为1000的线程池
obj_list = []
links = links[12:]
for i in range(len(links)):
para = (q, i, links)
p = t.submit(PutPageJob, para)
obj_list.append(p)
for future in as_completed(obj_list):
data = future.result()
print('*' * 50)
while not q.empty():
next_job = q.get() # 可根据优先级取序列
with open('****.txt', 'a', encoding='utf-8') as f:
f.write(next_job.title)
f.writelines(next_job.content)
print('花费时间:', time()-start_time)
我们可以看到第十一行代码,我们用到了我们定义的那个函数,只有这样才能把参数传进去。第5-17行就是我们的重头戏,启用1000个线程的线程池,多线程并发执行我们的爬虫,并且把数据储存在了第5行我们实例化的PriorityQueue对象中,而且按照重要性排序,这里的重要性参数是i,i越小表示越先执行,因此我们写入时才可以先写入第1章这个样子。
五.完整代码
import requests, re
from lxml import etree
from queue import PriorityQueue
from concurrent.futures import ThreadPoolExecutor, as_completed
from time import time
class Spider():
url = 'http://www.*****/index.html'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36',
}
def get_page_urls(self):
rsp = requests.get(self.url, headers=self.headers)
html = etree.HTML(rsp.content)
titles = html.xpath('//dd/a/text()')[0]
links = html.xpath('//dd/a/@href')
links = ['http://www.******/'+i for i in links]
return links
class PageJob():
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36',
}
def __init__(self, priority, url):
self.priority = priority
self.url = url
self.GetContent()
return
def __lt__(self, other):
return self.priority < other.priority
def GetContent(self):
rsp = requests.get(self.url, headers=self.headers)
print(rsp.status_code)
if rsp.status_code == 503:
print(rsp.text)
time.sleep(1)
rsp = requests.get(self.url, headers=self.headers)
html = etree.HTML(rsp.content)
title = html.xpath('//h1/text()')[0]
content = html.xpath('//div[@id="content"]/text()')[:-3]
while '\r' in content:
content.remove('\r')
content = [re.sub('\xa0\xa0\xa0\xa0', '', i) for i in content]
content = [re.sub('\r', '\n', i) for i in content]
self.title = '\n\n'+title+'\n\n'
self.content = content
print(title, content)
def PutPageJob(para):
q = para[0]
i = para[1]
links = para[2]
q.put(PageJob(i, links[i]))
if __name__ == '__main__':
start_time = time()
spider = Spider() # 实例化对象
links = spider.get_page_urls() # 获取总页面数
q = PriorityQueue() # (优先级队列),即存入数据时候加入一个优先级,取数据的时候优先级最高的取出
with ThreadPoolExecutor(max_workers=20000) as t: # 创建一个最大容纳数量为5的线程池
obj_list = []
links = links[12:] # 前12个链接不要
for i in range(len(links)):
para = (q, i, links) # 传入优先级 链接
p = t.submit(PutPageJob, para) # 通过submit函数提交执行的函数到线程池中,submit函数立即返回,不阻塞
obj_list.append(p)
for future in as_completed(obj_list): # as_completed先完成的任务会先通知主线程
data = future.result()
print('*' * 50)
while not q.empty():
next_job = q.get() # 可根据优先级取序列
with open('***.txt', 'a', encoding='utf-8') as f:
f.write(next_job.title)
f.writelines(next_job.content)
print('花费时间:', time()-start_time)