异步爬虫
1.高性能异步爬虫
目的:在爬虫中国使用异步实现高性能的数据爬取
2.异步爬虫的方式
2.1 多线程,多进程 ☹☹☹
from threading import Thread
2.1.1 进程与线程
- 进程:运行中的程序,我们每次执行一个程序,操作系统会自动的为这个程序准备一些必要的资源(如:分配内存,创建一个能够执行的线程)
- 线程:程序内,可以直接被CPU调度的执行过程,是操作系统能够进行运算调度的最小单位,包含在进程之中,是进程中的实际运作单位。
2.1.2多线程多进程的利弊
- 好处:可以为相关阻塞的操作单独开启线程或进程,阻塞操作就可以异步执行
- 弊端:无法无限制的开启多线程或者多进程
2.1.3 多线程的使用
写法一:爬虫常用
from threading import Thread
def fun(name):
for i in range(50):
print(f"{name},{i}")
if __name__ == '__main__':
# 在不使用多线程的情况下
print("不使用多线程:")
fun_1 = fun("周润发")
fun_2 = fun("胡歌")
fun_3 = fun("周杰伦")
print("*"*100)
# 创建线程
print("使用多线程执行:")
t_1 = Thread(target=fun,args=("周润发",))
# 在传递参数时,args后面传递的参数必须是元组类型,所以要在括号内再添加一个”,“
t_2 = Thread(target=fun, args=("胡歌",))
t_3 = Thread(target=fun, args=("周杰伦",))
t_1.start()
t_2.start()
t_3.start()
# 在使用多线程时,除了我们所定义的几个多线程以外,还有一个主线程。
print("我是主线程————————————————————————————————————————————————————")
写法二:面向对象
from threading import Thread
# 继承Thread父类
class Mythread(Thread):
def __init__(self,name):
super(Mythread, self).__init__()
self.name=name
def run(self):
for i in range(50):
print(f"{self.name},{i}")
if __name__ == '__main__':
t1 =Mythread("周杰伦")
t2 = Mythread("胡歌")
t3 = Mythread("周润发")
t1.start()
t2.start()
t3.start()
2.1.4多进程的使用
from multiprocessing import Process
def func(name):
for i in range(100):
print(f"我是{name},{i}")
if __name__ == '__main__':
p1 = Process(target=func,args=("周杰伦",))
p2 = Process(target=func, args=("李连杰",))
p1.start()
p2.start()
多进程与多线程类似。
2.1.5 多线程与多进程的选择
- 多线程:任务相对统一,代码相似
- 多进程:任务相对独立,很少有交集。
- IP代理池
- 从各个网站抓取代理IP
- 验证代理IP是否可用
- 准备对外的接口
- IP代理池
2.2线程池、进程池 😐😐😐
from concurrent.futures import ThreadPoolExecutor
2.2.1线进程池的利弊
- 好处:可以降低系统对线程,进程的创建销毁频率,降低系统的开销
- 弊端:池中线程或进程的数量是有上限的
2.2.2 线程池的使用
from concurrent.futures import ThreadPoolExecutor
import time
def func(name):
for i in range(30):
print(f"{name},{i}")
if __name__ == '__main__':
with ThreadPoolExecutor(5) as t:
t.submit(func,"周杰伦")
t.submit(func, "胡歌")
t.submit(func, "周星驰")
此时,通过输出我们发现与多线程,多进程几乎一致,没有什么区别,通过修改 main 函数中的内容,优化线程池,如下:
if __name__ == '__main__':
with ThreadPoolExecutor(5) as t:
for i in range(10):
t.submit(func,f"周杰伦{i}")
因为在多线程中,每个线程都要我们定义。所以当线程数量庞大的时候,对于系统的开销比较大,但是创建线程池,就类似一个线程的缓冲区,我们可以定义线程同时存在的数量,其他线程就在池中待命,优化系统开销。
但是我们使用线程池接受返回值的时候是什么情况呢?
from concurrent.futures import ThreadPoolExecutor
import time
def func(name):
time.sleep(2)
return name
if __name__ == '__main__':
with ThreadPoolExecutor(5) as t:
t.submit(func,"周杰伦")
t.submit(func,"胡歌")
t.submit(func,"周星驰")
通过执行这个代码,我们发现代码执成功,但是并没有输出,没有得到想要的返回值
from concurrent.futures import ThreadPoolExecutor
import time
def func(name,t):
time.sleep(t)
return name
def fn(res):
print(res.result())
if __name__ == '__main__':
with ThreadPoolExecutor(5) as t:
t.submit(func,"周杰伦",3).add_done_callback(fn)
t.submit(func,"胡歌",1).add_done_callback(fn)
t.submit(func,"周星驰",4).add_done_callback(fn)
通过 .add_done_callback(fn)
方法,可以返回执行,该执行顺序是不确定的,返回值的顺序是不确定的,谁完成,谁返回。
from concurrent.futures import ThreadPoolExecutor
import time
def func(name,t):
time.sleep(t)
print(f"我是{name}")
return name
def fn(res):
print(res.result())
if __name__ == '__main__':
with ThreadPoolExecutor(5) as t:
result = t.map(func,["周杰伦","胡歌","周星驰"],[2,1,3])
# result 是一个生成器
print(result)
for r in result:
print(r)
# 此时返回的结果为任务封装时,三个参数的顺序即:周杰伦,胡歌,周星驰,并不是按照时间先后
# map 返回顺序与封装相同。
💎💎💎注意区分.add_done_callback(fn)
和.map()
2.2.线程池案例
利用线程池爬取小说西游戏的全部章节和目录
import requests
import os
from lxml import etree
from concurrent.futures import ThreadPoolExecutor
def download(url):
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
}
resp =requests.get(url=url,headers=headers)
et = etree.HTML(resp.text)
title = et.xpath("/html/body/div[3]/div/div[1]/div[1]/div[1]/h1/text()")
article = et.xpath("/html/body/div[3]/div/div[1]/div[1]/div[2]/p//text()")
t="".join(title)
s = "".join(article).replace("\r\r","\r")
with open(f"西游记/{t}.text","w",encoding="utf-8") as f:
f.write(s)
print(t,"over!!")
if __name__ == '__main__':
if not os.path.exists("西游记"):
os.mkdir("西游记")
with ThreadPoolExecutor(5) as t:
for i in range(1,102):
num = 480+i
url =f"https://www.gushicimingju.com/novel/xiyouji/{num}.html"
t.submit(download,url)
print("all over!!!")
2.3单线程+异步协调 🙂🙂🙂
- event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足某些条件的时候,函数就会被循环执行。
- coroutine:协程对象,我们可以将协程对象注册到事件循环中,他或被事件循环调用,我们可以使用 async 关键字来定义一个方法,这个方法在调用时不会被立即执行,而是返回一个协程对象。
- task:任务,他是协程对象的进一步封装,包含了任务的各个状态。
- future:代表将来执行或者还没有执行的的任务,实际上和 task 没有本质的区别。
- async :定义一个协程。
- await:用来挂起阻塞方法的执行
2.4 多进程与线程池的配合使用
利用多进程与线程池爬取图片
import requests
from lxml import etree
from urllib import parse
from multiprocessing import Process,Queue
from concurrent.futures import ThreadPoolExecutor
import os
# 进程1 爬取每个图片的url地址
# 进程2 根据进程1 的地址下载图片
def Get_src(url,q):
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
}
resp = requests.get(url=url,headers=headers)
resp.encoding = "utf-8"
et = etree.HTML(resp.text)
href_lst = et.xpath('//div[@class = "item_list infinite_scroll"]//div[@class="item_b clearfix"]//a/@href')
for href in href_lst:
child_href = parse.urljoin(url,href)
resp1 = requests.get(url=child_href,headers=headers)
resp1.encoding = "utf-8"
et2 =etree.HTML(resp1.text)
src = et2.xpath('//div[@class="big-pic"]/a/img/@src')[0]
q.put(src)
print(src+"进入队列")
q.put("over")
def Get(q):
with ThreadPoolExecutor(5) as t:
for i in range(2,10):
url =f"https://www.umei.cc/weimeitupian/keaitupian/index_{i}.htm"
t .map(Get_src,[url],[q])
def Downpic(url):
resp = requests.get(url=url)
resp.encoding = "utf-8"
name = url.split("/")[-1]
with open("优美图库/"+name,"wb") as f:
f.write(resp.content)
print(name+"下载完成")
def Download(q):
with ThreadPoolExecutor(5) as t:
while 1:
url = q.get()
if url != "over":
t.submit(Downpic,url)
else:
break
if __name__ == '__main__':
if not os.path.exists("优美图库"):
os.mkdir("优美图库")
q = Queue()
p1 = Process(target=Get,args=(q,))
p2 = Process(target=Download,args=(q,))
p1.start()
p2.start()
print("over!!!")
注:
from urllib import parse
url = parse.urljoin(url,url2)
# url1 = "https://www.umei.cc/weimeitupian/keaitupian/" url2 = "/bizhitupian/xiaoqingxinbizhi/204461.htm"
# ==>url = "https://www.umei.cc/bizhitupian/xiaoqingxinbizhi/204461.htm"
# url1 = "https://www.umei.cc/weimeitupian/keaitupian" url2 = "bizhitupian/xiaoqingxinbizhi/204461.htm"
# ==>url = "https://www.umei.cc/weimeitupian/bizhitupian/xiaoqingxinbizhi/204461.htm"
url 处理模块,使用parse.urljoin(url1,url2)
可以将 url1
url2
两个地址进行拼接,当地址二以 /
开头,则只保留url1
的服务器网址,当url2
不已/
开头,则只将url1
的最后一部分取代。
进程之间相互独立,当使用进程二调用进程一的地址参数时,需要借助第三方队列 Queue ,这个第三方队列需要导入,并且要将q 作为参数传递到进程之中
💎💎💎队列的定义需要定义在进程之前
from multiprocessing import Process,Queue
if __name__ == '__main__':
q = Queue()
p1 = Process(target=Get,args=(q,))