5.1多线程与多进程

异步爬虫

1.高性能异步爬虫

目的:在爬虫中国使用异步实现高性能的数据爬取

2.异步爬虫的方式

2.1 多线程,多进程 ☹☹☹

from threading import Thread
2.1.1 进程与线程
  • 进程:运行中的程序,我们每次执行一个程序,操作系统会自动的为这个程序准备一些必要的资源(如:分配内存,创建一个能够执行的线程)
  • 线程:程序内,可以直接被CPU调度的执行过程,是操作系统能够进行运算调度的最小单位,包含在进程之中,是进程中的实际运作单位。
2.1.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是否可用
      • 准备对外的接口

2.2线程池、进程池 😐😐😐

from concurrent.futures import ThreadPoolExecutor
2.2.1线进程池的利弊
  1. 好处:可以降低系统对线程,进程的创建销毁频率,降低系统的开销
  2. 弊端:池中线程或进程的数量是有上限的
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单线程+异步协调 🙂🙂🙂

  1. event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足某些条件的时候,函数就会被循环执行。
  2. coroutine:协程对象,我们可以将协程对象注册到事件循环中,他或被事件循环调用,我们可以使用 async 关键字来定义一个方法,这个方法在调用时不会被立即执行,而是返回一个协程对象。
  3. task:任务,他是协程对象的进一步封装,包含了任务的各个状态。
  4. future:代表将来执行或者还没有执行的的任务,实际上和 task 没有本质的区别。
  5. async :定义一个协程。
  6. 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,))
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值