注意:以下代码去掉了关键信息和url只展示怎么操作,具体实现可采用这个框架自己修改
思路分析:
import requests
from lxml import etree
class Spider():
def __init__(self):
url = "http://www.baidu.com/page/{}" # 注意这里是假url,实际跑不通
# 构建构建每页url地址
self.url_list = [ url.format(i) for i in range(1,14)]
self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"}
def get_html(self,html_url):
# 向标题页发送请求,获取响应内容
html_url_resp = requests.get(html_url, headers=self.headers)
return html_url_resp.content
def get_items(self,html_url_resp):
# 获取每页的标题和标题url
# 获取可以xpath的对象
html_url_element = etree.HTML(html_url_resp)
# 进行xpath提取标题和url
html_url_a = html_url_element.xpath('//a[@class="recmd-content"]')
# print(len(html_url_a))
for a in html_url_a:
item = {}
# print(type(a))
# 通过xpath得到的是列表,[0]是为了将列表中的内容取出来
if a.xpath('./text()') != []: #为空说明是广告
item['title'] = a.xpath('./text()')[0]
item['title_url'] = a.xpath('./@href')[0]
self.save(item)
def save(self,item):
print(item)
def run(self):
for html_url in self.url_list:
# 获取该页的标题详情
html_url_resp = self.get_html(html_url)
# 获取每页标题和标题url
self.get_items(html_url_resp)
if __name__ == '__main__':
start_time = datetime.datetime.now()
spider = Sprider()
spider.run()
end_time = datetime.datetime.now()
print('单线程消耗时间{}'.format(end_time-start_time))
# 单线程消耗时间0:00:06.377732
在前面爬虫基础知识案例中我们发现请求回来的总数据不是太多,时间性对来说还是比较快的,那么如果该网站有大量数据等待爬虫爬取,我们是不是需要使用多线程并发来操作爬虫的网络请求呢?
在python3中,主线程主进程结束,子线程,子进程不会结束
为了能够让主线程回收子线程,可以把子线程设置为守护线程,即该线程不重要,主线程结束,子线程结束(爬虫不能搞守护线程我觉得,不然还没执行的子线程都不爬了)
t1 = threading.Thread(targe=func,args=(,))
t1.setDaemon(True) # 设置为守护线程
t1.start() #此时线程才会启动
from queue import Queue
q = Queue(maxsize=100) # maxsize为队列长度
item = {}
q.put_nowait(item) #不等待直接放,队列满的时候会报错
q.put(item) #放入数据,队列满的时候会阻塞等待
q.get_nowait() #不等待直接取,队列空的时候会报错
q.get() #取出数据,队列为空的时候会阻塞等待
q.qsize() #获取队列中现存数据的个数
q.join() # 队列中维持了一个计数(初始为0),计数不为0时候让主线程阻塞等待,队列计数为0的时候才会继续往后执行
# q.join()实际作用就是阻塞主线程,与task_done()配合使用
# put()操作会让计数+1,task_done()会让计数-1
# 计数为0,才停止阻塞,让主线程继续执行
q.task_done() # put的时候计数+1,get不会-1,get需要和task_done 一起使用才会-1
把爬虫中的每个步骤封装成函数,分别用线程去执行不同的函数通过队列相互通信,函数间解耦
import requests
from lxml import etree
import datetime
from queue import Queue
from threading import Thread
class Spider():
def __init__(self):
self.base_url = "http://www.baidu.com/8hr/page/{}" # 注意这是假url,实际跑不通得换你自己的
# 构建构建每页url地址
self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"}
# 创建队列
self.q_url = Queue(10)
self.q_html = Queue(10)
self.q_item = Queue(10)
# 生产url
def get_url(self):
for i in range(1, 14):
url = self.base_url.format(i)
self.q_url.put(url)
# 获取对应url的response内容
def get_html(self):
while True:
# 向标题页发送请求,获取响应内容
url = self.q_url.get()
html_url_resp = requests.get(url, headers=self.headers)
self.q_html.put(html_url_resp.content)
self.q_url.task_done() # 计数 -1
# 获取每页的标题和url
def get_items(self):
while True:
# 获取每页的响应内容
html_url_resp = self.q_html.get()
# 获取可以xpath的对象
html_url_element = etree.HTML(html_url_resp)
# 进行xpath提取标题和url
html_url_a = html_url_element.xpath('//a[@class="recmd-content"]')
# print(len(html_url_a))
titles = []
for a in html_url_a:
# print(type(a))
# 通过xpath得到的是列表,[0]是为了将列表中的内容取出来
if a.xpath('./text()') != []: #为空说明是广告
item = {}
item['title'] = a.xpath('./text()')[0]
item['title_url'] = a.xpath('./@href')[0]
titles.append(item)
self.q_item.put(titles)
self.q_html.task_done()
def save(self):
while True:
items = self.q_item.get()
for item in items:
print(item)
self.q_item.task_done()
def run(self):
thread_list =[]
t_url = Thread(target=self.get_url)
thread_list.append(t_url)
for i in range(3):
t_html = Thread(target=self.get_html)
thread_list.append(t_html)
for i in range(3):
t_items = Thread(target=self.get_items)
thread_list.append(t_items)
t_save = Thread(target=self.save)
thread_list.append(t_save)
for t in thread_list:
# t.setDaemon(True) # 设置为守护线程
t.start() #此时线程才会启动
for q in [self.q_url, self.q_html, self.q_item]:
q.join() # 主线程阻塞,直到每个q队列计数为0
print('程序结束了')
if __name__ == '__main__':
start_time = datetime.datetime.now()
spider = Spider()
spider.run()
end_time = datetime.datetime.now()
print('多线程消耗时间{}'.format(end_time-start_time))
# 单线程消耗时间0:00:06.377732
# 多线程消耗时间0:00:02.270735
使用和多线程差不多,不同库而已
在一个进程中无论开多少个线程都只能运行在一个CPU的核心之上,这是python的特点,不能说是缺点!
如果我们想利用计算机的多核心优势,就可以用多进程的方式实现,思路和多线程相似,只是对应的api不相同。
from multiprocessing import Process #导入模块
t1 = Process(targe=func,args=(,)) #使用一个进程来执行一个函数
t1.daemon = True #设置为守护进程
t1.start() #此时线程才会启动
多进程中使用普通的队列模块会发生阻塞,对应的需要使multiprocessing提供的JoinableQueue模块,其使用过程和在线程中使用的queue方法相同
import requests
from lxml import etree
import datetime
from multiprocessing import Process
from multiprocessing import JoinableQueue as Queue
class Sprider():
def __init__(self):
self.base_url = "http://www.baidu.com/8hr/page/{}" # 注意这是假url,实际跑不通,得换你自己的
# 构建构建每页url地址
self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"}
# 创建队列
self.q_url = Queue(10)
self.q_html = Queue(10)
self.q_item = Queue(10)
# 生产url
def get_url(self):
for i in range(1, 14):
url = self.base_url.format(i)
self.q_url.put(url)
# 获取对应url的response内容
def get_html(self):
while True:
# 向标题页发送请求,获取响应内容
url = self.q_url.get()
html_url_resp = requests.get(url, headers=self.headers)
self.q_html.put(html_url_resp.content)
self.q_url.task_done()
# 获取每页的标题和url
def get_items(self):
while True:
# 获取每页的响应内容
html_url_resp = self.q_html.get()
# 获取可以xpath的对象
html_url_element = etree.HTML(html_url_resp)
# 进行xpath提取标题和url
html_url_a = html_url_element.xpath('//a[@class="recmd-content"]')
# print(len(html_url_a))
titles = []
for a in html_url_a:
# print(type(a))
# 通过xpath得到的是列表,[0]是为了将列表中的内容取出来
if a.xpath('./text()') != []: #为空说明是广告
item = {}
item['title'] = a.xpath('./text()')[0]
item['title_url'] = a.xpath('./@href')[0]
titles.append(item)
self.q_item.put(titles)
self.q_html.task_done()
def save(self):
while True:
items = self.q_item.get()
for item in items:
print(item)
self.q_item.task_done()
def run(self):
process_list =[]
p_url = Process(target=self.get_url)
process_list.append(p_url)
for i in range(2):
p_html = Process(target=self.get_html)
process_list.append(p_html)
for i in range(2):
p_items = Process(target=self.get_items)
process_list.append(p_items)
p_save = Process(target=self.save)
process_list.append(p_save)
for t in process_list:
# t.setDaemon(True) # 设置为守护线程
t.start() #此时线程才会启动
for q in [self.q_url, self.q_html, self.q_item]:
q.join() # 主线程阻塞,直到每个q队列计数为0
print('程序结束了')
if __name__ == '__main__':
start_time = datetime.datetime.now()
spider = Sprider()
spider.run()
end_time = datetime.datetime.now()
print('多进程消耗时间{}'.format(end_time-start_time))
# 单线程消耗时间0:00:06.377732
# 多线程消耗时间0:00:02.270735
# 多进程消耗时间0: 00:03.296609
上述多进程实现的代码中,multiprocessing提供的JoinableQueue可以创建可连接的共享进程队列。和普通的Queue对象一样,队列允许项目的使用者通知生产者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。 对应的该队列能够和普通队列一样能够调用task_done和join方法。
from multiprocessing.dummy import Pool
pool = Pool(processes=3) # 默认大小是cpu的个数
"""源码内容:
if processes is None:
processes = os.cpu_count() or 1
# 此处or的用法:
默认选择or前边的值,
如果or前边的值为False,就选择后边的值
"""
使用方法pool.apply_async(func)
def exetute_requests_item_save(self):
url = self.queue.get()
html_str = self.parse_url(url)
content_list = self.get_content_list(html_str)
self.save_content_list(content_list)
self.total_response_num +=1
pool.apply_async(self.exetute_requests_item_save)
通过apply_async的方法能够让函数异步执行,但是只能够执行一次,为了让其能够被反复执行,通过添加回调函数的方式能够让_callback 递归的调用自己,同时需要指定递归退出的条件。
def _callback(self,temp):
if self.is_running:
pool.apply_async(self.exetute_requests_item_save,callback=self._callback)
pool.apply_async(self.exetute_requests_item_save,callback=self._callback)
while True: #防止主线程结束
time.sleep(0.0001) #避免cpu空转,浪费资源
if self.total_response_num>=self.total_requests_num:
self.is_running= False
break
self.pool.close() #关闭线程池,防止新的线程开启
# self.pool.join() #等待所有的子线程结束
import requests
from lxml import etree
import datetime
from queue import Queue
from multiprocessing.dummy import Pool
import time
class Spider():
def __init__(self):
self.url = "http://www.baidu.com/8hr/page/{}" # 注意这是假url,实际跑不通,得换你自己的
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36'
}
# 构建构建每页url地址
# self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"}
# 创建队列
self.q_url = Queue()
# 创建进程池对象
self.pool = Pool(5)
# 请求url总数据
self.total_request_num = 0
# 获取响应总数
self.total_response_num = 0
# 停止回调标志
self.is_running = True
# 获取url
def get_url(self):
for i in range(1, 14):
url = self.url.format(i)
# 将生成的url放入到队列中
self.q_url.put(url)
self.total_request_num += 1
# 向标题页发送请求,获取响应内容
def get_html(self,url):
resp = requests.get(url, headers=self.headers)
return resp.content
# 获取每页的标题和标题url
def get_items(self,html_resp):
# 获取可以xpath的对象
html_url_element = etree.HTML(html_resp)
# 进行xpath提取标题和url
html_url_a = html_url_element.xpath('//a[@class="recmd-content"]')
# print(len(html_url_a))
titles = []
for a in html_url_a:
item = {}
# print(type(a))
# 通过xpath得到的是列表,[0]是为了将列表中的内容取出来
if a.xpath('./text()') != []: #为空说明是广告
item['title'] = a.xpath('./text()')[0]
item['title_url'] = a.xpath('./@href')[0]
titles.append(item)
return titles
# 保存
def save(self,titles):
for t in titles:
print(t)
# 完整的执行流程
def execute_request_items_save(self):
url = self.q_url.get()
html_resp = self.get_html(url)
titles = self.get_items(html_resp)
self.save(titles)
self.total_response_num += 1
return '完整执行'
# 回调函数
def _callback(self,xxx):# callback函数必须接收一个参数!
# xxx参数是self.execute_request_item_save的返回值!!!
# 哪怕用不上 也必须接收!
print(xxx)
if self.is_running:
self.pool.apply_async(self.execute_request_items_save, callback=self._callback)
def run(self):
self.get_url()
for i in range(5):
self.pool.apply_async(self.execute_request_items_save, callback=self._callback)
# 退出机制
while True:
#避免cpu空转,浪费资源
time.sleep(0.0001)
if self.total_response_num == self.total_request_num and self.total_request_num == 13:
self.is_running = False
break
self.pool.close()
print('程序执行结束')
if __name__ == '__main__':
start_time = datetime.datetime.now()
spider = Spider()
spider.run()
end_time = datetime.datetime.now()
print('多线程池消耗时间{}'.format(end_time-start_time))
# 单线程消耗时间0:00:06.377732
# 多进程消耗时间0: 00:03.296609
# 多线程池消耗时间0:00:01.761484
协程池模块
import gevent.monkey
gevent.monkey.patch_all()
from gevent.pool import Pool
import gevent.monkey
# monkey补丁要打在发送请求之前
gevent.monkey.patch_all()
import requests
from lxml import etree
import datetime
from queue import Queue
from gevent.pool import Pool
import time
class Sprider():
def __init__(self):
self.url = "http://www.baidu.com/8hr/page/{}" # 注意这是假url,实际跑不通,得换你自己的
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36'
}
# 构建构建每页url地址
# self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"}
# 创建队列
self.q_url = Queue()
# 创建进程池对象
self.pool = Pool(5)
# 请求url总数据
self.total_request_num = 0
# 获取响应总数
self.total_response_num = 0
# 停止回调标志
self.is_running = True
# 获取url
def get_url(self):
for i in range(1, 14):
url = self.url.format(i)
# 将生成的url放入到队列中
self.q_url.put(url)
self.total_request_num += 1
# 向标题页发送请求,获取响应内容
def get_html(self,url):
resp = requests.get(url, headers=self.headers)
return resp.content
# 获取每页的标题和标题url
def get_items(self,html_resp):
# 获取可以xpath的对象
html_url_element = etree.HTML(html_resp)
# 进行xpath提取标题和url
html_url_a = html_url_element.xpath('//a[@class="recmd-content"]')
# print(len(html_url_a))
titles = []
for a in html_url_a:
item = {}
# print(type(a))
# 通过xpath得到的是列表,[0]是为了将列表中的内容取出来
if a.xpath('./text()') != []: #为空说明是广告
item['title'] = a.xpath('./text()')[0]
item['title_url'] = a.xpath('./@href')[0]
titles.append(item)
return titles
# 保存
def save(self,titles):
for t in titles:
print(t)
# 完整的执行流程
def execute_request_items_save(self):
url = self.q_url.get()
html_resp = self.get_html(url)
titles = self.get_items(html_resp)
self.save(titles)
self.total_response_num += 1
return '完整执行'
# 回调函数
def _callback(self,xxx):# callback函数必须接收一个参数!
# xxx参数是self.execute_request_item_save的返回值!!!
# 哪怕用不上 也必须接收!
print(xxx)
if self.is_running:
self.pool.apply_async(self.execute_request_items_save, callback=self._callback)
def run(self):
self.get_url()
for i in range(5):
self.pool.apply_async(self.execute_request_items_save, callback=self._callback)
# 退出机制
while True:
#避免cpu空转,浪费资源
time.sleep(0.0001)
if self.total_response_num == self.total_request_num and self.total_request_num == 13:
self.is_running = False
break
# self.pool.close() # 协程没有过close函数
print('程序执行结束')
if __name__ == '__main__':
start_time = datetime.datetime.now()
spider = Sprider()
spider.run()
end_time = datetime.datetime.now()
print('多协程池消耗时间{}'.format(end_time-start_time))
# 单线程消耗时间0:00:06.377732
# 多进程消耗时间0: 00:03.296609
# 多线程池消耗时间0:00:01.761484
# 多协程池消耗时间0:00:01.689546
对单线程、多进程、多线程、线程池、进程池、协程池的使用进行了总结,但是感觉还是有些缺陷。参考我其他文章把