Greenlet模块、Gevent模块、asynicomok模块(协程函数和协程对象)、案例(爬虫)

【一】Greenlet模块

  • 如果我们在单个线程内有20个任务

    • 要想实现在多个任务之间切换
    • 使用yield生成器的方式过于麻烦(需要先得到初始化一次的生成器,然后再调用send。。。非常麻烦
    • 而使用greenlet模块可以非常简单地实现这20个任务直接的切换
# 【1】安装模块
# pip install greenlet

# 【2】导入模块
from greenlet import greenlet


def eat(name):
    print('%s eat 1' % name)
    g2.switch('hope')
    print('%s eat 2' % name)
    g2.switch()


def play(name):
    print('%s play 1' % name)
    g1.switch()
    print('%s play 2' % name)


g1 = greenlet(eat)
g2 = greenlet(play)

# 可以在第一次switch时传入参数,以后都不需要
g1.switch('chosen')

# chosen eat 1
# hope play 1
# chosen eat 2
# hope play 2

【1】顺序执行和切换执行

from greenlet import greenlet
import time


def add_number_normal():
    res = 1
    for i in range(1,100000):
        res += i

def modify_number_normal():
    res = 1
    for i in range(1,100000):
        res *= i

def main_normal():
    start_time = time.time()
    add_number_normal()
    modify_number_normal()
    end_time = time.time()
    print(f'总耗时:>>>> {end_time - start_time}s')

def add_number_greenlet(modify_number_greenlet_two, modify_number_greenlet_one):
    res = 1
    for i in range(1, 100000):
        res += i
        modify_number_greenlet_two.switch(modify_number_greenlet_two, modify_number_greenlet_one)

def modify_number_greenlet(add_number_greenlet_one, modify_number_greenlet_two):
    res = 1
    for i in range(1, 100000):
        res *= i
        add_number_greenlet_one.switch(add_number_greenlet_one, modify_number_greenlet_two)

def main():
    start_time = time.time()
    # 创建两个 greenlet 对象
    add_number_greenlet_one = greenlet(add_number_greenlet)
    modify_number_greenlet_two = greenlet(modify_number_greenlet)

    # 把两个 greenlet 对象作为参数传递给 函数
    add_number_greenlet_one.switch(modify_number_greenlet_two, add_number_greenlet_one)
    modify_number_greenlet_two.switch(add_number_greenlet_one, modify_number_greenlet_two)
    end_time = time.time()
    print(f'总耗时 :>>>> {end_time - start_time}s')


if __name__ == '__main__':
    main_normal()
    # (顺序执行)总耗时:>>>> 2.6718621253967285s
    main()
    # (切换执行)总耗时 :>>>> 2.6688640117645264s

# greenlet 模块能够帮助我们实现进程或线程之间的切换
# 一旦遇到IO阻塞就开始切换

【2】总结

  • greenlet只是提供了一种比generator更加便捷的切换方式

    • 当切到一个任务执行时如果遇到io
    • 那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。
  • 单线程里的这20个任务的代码通常会既有计算操作又有阻塞操作

    • 我们完全可以在执行任务1时遇到阻塞
    • 就利用阻塞的时间去执行任务2。。。。
    • 如此,才能提高效率,这就用到了Gevent模块。

【二】Gevent模块

  • 贴近异步编程

【1】介绍

  • Gevent 是一个第三方库
  • 可以轻松通过gevent实现并发同步或异步编程
  • 在gevent中用到的主要模式是Greenlet
  • 它是以C扩展模块形式接入Python的轻量级协程。
  • Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

【2】使用

# 【一】安装模块
# pip install gevent
# 【二】使用

import gevent


def func(*args, **kwargs):
    print(args)  # (1, 2, 3)
    print(kwargs)  # {'x': 4, 'y': 5}
    return 'ok'


def func2():
    ...


# 创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
g1 = gevent.spawn(func, 1, 2, 3, x=4, y=5)

# 创建一个协程对象g2
g2 = gevent.spawn(func2)

g1.join()  # 等待g1结束

g2.join()  # 等待g2结束

# 或者上述两步合作一步:gevent.joinall([g1,g2])

# 拿到func1的返回值
result = g1.value

print(result)
# ok

输出结果:
(1, 2, 3)
{'x': 4, 'y': 5}
ok

(1)遇到IO阻塞时会自动切换任务

import gevent


def eat(name):
    print('%s eat 1' % name)
    gevent.sleep(2)
    # time.sleep 是强制休眠 将并行变为串行
    print('%s eat 2' % name)


def play(name):
    print('%s play 1' % name)
    gevent.sleep(1)
    # time.sleep(1)
    print('%s play 2' % name)

start_time = time.time()
g1 = gevent.spawn(eat, 'chosen')
g2 = gevent.spawn(play, name='max')
g1.join()
g2.join()

# 或者gevent.joinall([g1,g2])

print('main')
print(f'总耗时 :>>>> {time.time() - start_time}s')

# chosen eat 1
# max play 1
# max play 2
# chosen eat 2
# main
# 总耗时 :>>>> 2.034878730773926s

(2)兼容其他IO

  • 而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了

  • from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面

    • 如time,socket模块之前
  • 或者我们干脆记忆成:要用gevent,需要将from gevent import monkey; monkey.patch_all()放到文件的开头

from gevent import monkey
import gevent
import time
import threading

monkey.patch_all()




def eat():
    print('eat food 1')
    time.sleep(2)
    print(f"eat 中的 :>>>> {threading.current_thread()}")
    print('eat food 2')


def play():
    print('play 1')
    time.sleep(1)
    print(f"play 中的 :>>>> {threading.current_thread()}")
    print('play 2')

start_time = time.time()
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)
gevent.joinall([g1, g2])

print(threading.current_thread())

print('主')
print(f"总耗时 :>>> {time.time() - start_time}s")
# 总耗时 :>>> 2.0202083587646484s
# eat food 1
# play 1
# play 中的 :>>>> <_DummyThread(Dummy-1, started daemon 5168389856)>
# play 2
# eat 中的 :>>>> <_DummyThread(Dummy-2, started daemon 5168390016)>
# eat food 2
# <_MainThread(MainThread, started 5168107712)>
# 主


from gevent import spawn, joinall, monkey

monkey.patch_all()

import time


def timer(func):
    def inner(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        print(f'当前程序 {func.__name__} 总耗时 :>>>> {time.time() - start} s')
        return res

    return inner


def task(pid):
    """
    Some non-deterministic task
    """
    time.sleep(0.5)
    print('Task %s done' % pid)


@timer
def synchronous():
    for i in range(10):
        task(i)


@timer
def asynchronous():
    g_l = [spawn(task, i) for i in range(10)]
    joinall(g_l)  # 等价于 [task.join() for task in task_list]


if __name__ == '__main__':
    print('Synchronous:')
    synchronous()
    # Synchronous:
    # Task 0 done
    # Task 1 done
    # Task 2 done
    # Task 3 done
    # Task 4 done
    # Task 5 done
    # Task 6 done
    # Task 7 done
    # Task 8 done
    # Task 9 done
    # 当前程序 synchronous 总耗时 :>>>> 5.034381151199341 s
    
    print('Asynchronous:')
    asynchronous()
    # Asynchronous:
    # Task 0 done
    # Task 1 done
    # Task 2 done
    # Task 3 done
    # Task 4 done
    # Task 5 done
    # Task 6 done
    # Task 7 done
    # Task 8 done
    # Task 9 done
    # 当前程序 asynchronous 总耗时 :>>>> 0.504889726638794 s
  
# 上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn。
# 初始化的greenlet列表存放在数组threads中,此数组被传给gevent.joinall 函数,后者阻塞当前流程,并执行所有给定的greenlet。
# 执行流程只会在 所有greenlet执行完后才会继续向下走。

(3)Gevent应用举例

  • 协程应用:爬虫
from gevent import monkey

monkey.patch_all()
import gevent
import requests
import time


def get_page(url):
    print(f"当前正在获取 :>>>> {url}")
    response = requests.get(url=url)
    if response.status_code == 200:
        print(f'当前响应数据总长度 :>>>> {len(response.text)} 当前链接 :>>>> {url}')


start_time = time.time()
gevent.joinall([
    gevent.spawn(get_page, 'https://www.python.org/'),
    gevent.spawn(get_page, 'https://www.jd.com/'),
    gevent.spawn(get_page, 'https://www.baidu.com/'),
])
stop_time = time.time()
print(f'总耗时 :>>>> {stop_time - start_time}s')

# 当前正在获取 :>>>> https://www.python.org/
# 当前正在获取 :>>>> https://www.jd.com/
# 当前正在获取 :>>>> https://www.baidu.com/
# 当前响应数据总长度 :>>>> 227 当前链接 :>>>> https://www.baidu.com/
# 当前响应数据总长度 :>>>> 182756 当前链接 :>>>> https://www.jd.com/
# 当前响应数据总长度 :>>>> 50995 当前链接 :>>>> https://www.python.org/
# 总耗时 :>>>> 15.546293020248413s



from gevent import monkey

monkey.patch_all()
import gevent
import requests
import time

def get_page(url):
    print(f"当前正在获取 :>>>> {url}")
    response = requests.get(url=url)
    if response.status_code == 200:
        print(f'当前响应数据总长度 :>>>> {len(response.text)} 当前链接 :>>>> {url}')

def main_normal():
    start_time = time.time()
    url_list = ['https://www.python.org/', 'https://www.jd.com/', 'https://www.baidu.com/']
    for url in url_list:
        get_page(url=url)
    stop_time = time.time()
    print(f'总耗时 :>>>> {stop_time - start_time}s')


main_normal()
# 当前正在获取 :>>>> https://www.python.org/
# 当前响应数据总长度 :>>>> 50464 当前链接 :>>>> https://www.python.org/
# 当前正在获取 :>>>> https://www.jd.com/
# 当前响应数据总长度 :>>>> 218539 当前链接 :>>>> https://www.jd.com/
# 当前正在获取 :>>>> https://www.baidu.com/
# 当前响应数据总长度 :>>>> 2443 当前链接 :>>>> https://www.baidu.com/
# 总耗时 :>>>> 0.8800139427185059s

【三】asynico模块

  • asynico模块是 Python 中实现异步的一个模块,该模块在 Python3.4的时候发布
  • 因此,要使用 asyncio模块,建议 Python 解释器的版本不要低于 Python3.5。

【1】协程函数和协程对象

(1)什么是协程函数

# 使用 async 声明的函数就是协程函数
async def fn():
    pass

(2)什么是协程对象

  • 协程函数调用之后返回的对象就叫协程对象
# 使用 async 声明的函数就是协程函数
async def fn():
    pass


# 调用携程函数得到的对象就是协程对象
res = fn()
print(res) # <coroutine object fn at 0x1029684a0>

(3)基本使用

  • await 关键字

  • await 是一个 只能 在协程函数中使用的关键字,用于当协程函数遇到IO操作的时候挂起当前协程(任务),

  • 当前协程挂起过程中,事件循环可以去执行其他的协程(任务)

  • 当前协程IO处理完成时,可以再次切换回来执行 await 之后的代码

import asyncio


# 定义协程函数

async def modify(name):
    print(f'这是modify函数内部 :>>>> {name}')

    return name * name


def modify_one(name):
    print(f'这是modify函数内部 :>>>> {name}')

    return name * name


async def add(name):
    print(f'这是add函数内部 :>>>> {name}')
    # 这个等价于 gevent.sleep(1) 约等于= time.sleep(1)
    # await asyncio.sleep(2)
    # res = await modify(name=name)
    res = modify_one(name=name)
    return res + res


# 开启协程
# 【1】方式一
def main_one():
    # (1)调用协程函数获取得到协程对象
    task_list = [add(i) for i in range(5)]

    # (2)创建一个事件循环
    loop = asyncio.get_event_loop()

    # (3)将上面的任务提交给事件循环
    # run 运行
    # until 直至
    # complete 完成
    # run_until_complete 返回的结果就是当前协程函数返回值
    res = [loop.run_until_complete(task) for task in task_list]
    print(res)


# 【2】方式二
@timer
def main_two():
    # (1)调用协程函数获取得到协程对象
    task_list = [add(i) for i in range(5)]

    # (2)将协程对象交给 run 运行
    res = [asyncio.run(task) for task in task_list]
    print(res)


# 【补充】在gevent内部要用 gevent.sleep()
# async 内部 也要用 asyncio.sleep()
# 如果我想要等待另一个函数的返回值,拿到另一个函数的返回值进行处理
# await 等待函数返回值


if __name__ == '__main__':
    main_two()
    
    
    
  
# 写协程函数是不需要引入 asyncio
# asyncio 里面写了很多和 协程相关的方法
# asyncio.sleep()
# wait 异步阻塞

# 协程函数运行的两种方式
# 【1】创建事件循环对象
# loop = asyncio.get_event_loop()
# loop.run_until_complete(task) ---> 返回值就是异步函数的结果
# 【2】直接通过run方法执行
# asyncio.run(task) ---> 返回值就是异步函数的结果

# 【二】 await 关键字
# 只能在协程函数中使用
# ● await 是一个 只能 在协程函数中使用的关键字,用于当协程函数遇到IO操作的时候挂起当前协程(任务),
# ● 当前协程挂起过程中,事件循环可以去执行其他的协程(任务)
# ● 当前协程IO处理完成时,可以再次切换回来执行 await 之后的代码
# ● 怎么理解呢?请看如下代码:

import asyncio


async def fn():
    print('协程函数内部的代码')

    # 遇到IO操作之后挂起当前协程(任务),等IO操作完成之后再继续往下执行。
    # 当前协程挂起时,事件循环可以去执行其他协程(任务)
    response = await asyncio.sleep(2)  # 模拟遇到了IO操作

    print(f'IO请求结束,结果为:{response}')


def main():
    # 调用协程函数,返回一个协程对象
    res = fn()

    # 执行协程函数
    asyncio.run(res)


if __name__ == '__main__':
    main()

    '''
    运行结果:
    协程函数内部的代码
    IO请求结束,结果为:None
    '''
import asyncio


async def other_tasks():
    print('start')
    await asyncio.sleep(2)  # 模拟遇到了IO操作
    print('end')
    return '返回值'


async def fn():
    print('协程函数内部的代码')

    # 遇到IO操作之后挂起当前协程(任务),等IO操作完成之后再继续往下执行。
    # 当前协程挂起时,事件循环可以去执行其他协程(任务)
    response = await other_tasks()  # 模拟执行其他协程任务

    print(f'IO请求结束,结果为:{response}')


def main():
    # 调用协程函数,返回一个协程对象
    res = fn()

    # 执行协程函数
    asyncio.run(res)


if __name__ == '__main__':
    main()

    '''
    运行结果:
    协程函数内部的代码
    start
    end
    IO请求结束,结果为:返回值
    '''
import asyncio
import time


async def other_tasks():
    print('start')
    await asyncio.sleep(2)  # 模拟遇到了IO操作
    print('end')
    return '返回值'


async def fn():
    print('协程函数内部的代码')

    # 遇到IO操作之后挂起当前协程(任务),等IO操作完成之后再继续往下执行。
    # 当前协程挂起时,事件循环可以去执行其他协程(任务)
    respnse1 = await other_tasks()
    print(f'IO请求结束,结果为:{respnse1}')

    respnse2 = await other_tasks()
    print(f'IO请求结束,结果为:{respnse2}')


def timer(func):
    def inner(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f'总耗时 :>>> {time.time() - start}s')
        return result

    return inner


@timer
def main():
    # 调用协程函数,返回一个协程对象
    cor_obj = fn()

    # 执行协程函数
    asyncio.run(cor_obj)


if __name__ == '__main__':
    main()

    '''
    运行结果:
    协程函数内部的代码
    start
    end
    IO请求结束,结果为:返回值
    start
    end
    IO请求结束,结果为:返回值
    '''
  • TASK 对象

  • Tasks 用于并发调度协程

  • 通过 asyncio.create_task(协程对象) 的方式创建 Task 对象

  • 这样可以让协程加入事件循环中等待被调度执行。

  • 除了使用 asyncio.create_task() 函数以外

  • 还可以用低层级的loop.create_task()ensure_future() 函数。并且不建议手动实例化 Task 对象

  • 本质上是将协程对象封装成 Task 对象

  • 并将协程立即加入事件循环,同时追踪协程的状态。

  • 注意事项:

    • asyncio.create_task() 函数在 Python3.7 中被加入。
    • 在 Python3.7 之前,可以改用低层级的
    • asyncio.ensure_future() 函数。

【2】协程运行方式

(1)方式一

  • async.run()运行协程
  • async.create_task()创建 task
import asyncio


async def other_tasks():
    print('start')
    await asyncio.sleep(2)  # 模拟遇到了IO操作
    print('end')
    return '返回值'


async def fn():
    print('fn开始')

    # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
    task1 = asyncio.create_task(other_tasks())

    # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
    task2 = asyncio.create_task(other_tasks())

    print('fn结束')

    # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
    # 此处的await是等待相对应的协程全都执行完毕并获取结果
    response1 = await task1
    response2 = await task2
    print(response1, response2)


def main():
    asyncio.run(fn())


if __name__ == '__main__':
    main()

    '''
    运行结果:
    fn开始
    fn结束
    start
    start
    end
    end
    返回值 返回值
    '''

(2)方式二

import asyncio


async def other_tasks():
    print('start')
    await asyncio.sleep(2)  # 模拟遇到了IO操作
    print('end')
    return '返回值'


async def fn():
    print('fn开始')

    # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
    task_lis = [
        asyncio.create_task(other_tasks()),
        asyncio.create_task(other_tasks()),
    ]

    print('fn结束')
    # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
    # 此处的await是等待所有协程执行完毕,并将所有协程的返回值保存到done
    # 如果设置了timeout值,则意味着此处最多等待的秒,完成的协程返回值写入到done中,未完成则写到pending中。
    done, pending = await asyncio.wait(task_lis, timeout=None)

    print(f"done :>>>> {done}")
    print(f"pending :>>>> {pending}")


def main():
    asyncio.run(fn())


if __name__ == '__main__':
    main()

    '''
    fn开始
    fn结束
    start
    start
    end
    end
    done :>>>> {<Task finished name='Task-2' coro=<other_tasks() done, defined at /Users/dream/Desktop/PythonProjects/12并发编程/02协程/01.py:4> result='返回值'>, <Task finished name='Task-3' coro=<other_tasks() done, defined at /Users/dream/Desktop/PythonProjects/12并发编程/02协程/01.py:4> result='返回值'>}
    pending :>>>> set()
    '''

(3)获取协程返回值

  • async.gather()获取返回值
import asyncio


async def other_tasks():
    print('start')
    await asyncio.sleep(2)  # 模拟遇到了IO操作
    print('end')
    return '返回值'


async def fn():
    print('fn开始')

    # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
    task_lis = [
        asyncio.create_task(other_tasks()),
        asyncio.create_task(other_tasks()),
    ]

    print('fn结束')
    # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
    # 此处的await是等待所有协程执行完毕,并将所有协程的返回值保存到done
    # 如果设置了timeout值,则意味着此处最多等待的秒,完成的协程返回值写入到done中,未完成则写到pending中。
    await asyncio.wait(task_lis, timeout=None)

    response = await asyncio.gather(task_lis[0], task_lis[1])  # 将task_lis作为参数传入gather,等异步任务都结束后返回结果列表

    print(f'response :>>>> {response}')


def main():
    asyncio.run(fn())


if __name__ == '__main__':
    main()

    '''
    fn开始
    fn结束
    start
    start
    end
    end
    response :>>>> ['返回值', '返回值']
    '''

【3】aiohtpp 对象

  • 我们之前学习过爬虫最重要的模块requests,但它是阻塞式的发起请求,每次请求发起后需阻塞等待其返回响应,不能做其他的事情。

    • 本文要介绍的aiohttp可以理解成是和requests对应Python异步网络请求库,它是基于 asyncio 的异步模块,可用于实现异步爬虫,有点就是更快于 requests 的同步爬虫。
    • 安装方式,pip install aiohttp。
  • aiohttp是一个为Python提供异步HTTP 客户端/服务端编程,基于asyncio的异步库。

    • asyncio可以实现单线程并发IO操作,其实现了TCP、UDP、SSL等协议,
    • aiohttp就是基于asyncio实现的http框架。
import aiohttp
import asyncio

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get("http://httpbin.org/headers") as response:
            print(await response.text())

asyncio.run(main())

【四】案例(不需要掌握只需要看懂)

  • 有涉及爬虫技术

【1】多进程和多线程和正常

import os
import time
from multiprocessing import Process
from threading import Thread
# pip install fake-useragent
from fake_useragent import UserAgent
# pip install requests
import requests

# pip install lxml
from lxml import etree


class BaseSpider(object):
    def __init__(self):
        self.base_area = 'https://pic.netbian.com'
        # 根目录
        self.BASE_DIR = os.path.dirname(os.path.abspath(__file__))
        # 创建请求头
        self.headers = {
            'User-Agent': UserAgent().random
        }

    def __create_etree(self, page_source):
        return etree.HTML(page_source)

    # 获取页面源码数据
    def get_tree(self, url):
        # 对目标地址发起请求并获得响应
        response = requests.get(url=url, headers=self.headers)
        # 因为数据是乱码 所以要对数据进行编码
        response.encoding = 'gbk'
        # 返回当前页面的源码数据
        page_source = response.text
        # 返回当前页面的解析器对象
        tree = self.__create_etree(page_source=page_source)
        return tree

    def spider_all_category(self):
        category_dict = {}
        tree = self.get_tree(url=self.base_area)
        # 直接使用xpath语法
        a_list = tree.xpath('//*[@id="main"]/div[2]/a')
        for a in a_list:
            # //*[@id="main"]/div[2]/a[1]
            # <a href="/4kdongman/" title="4K动漫图片">4K动漫</a>
            # 如果是获取标签的属性 href / title ---> /@属性名
            # 如果是获取标签中间的文本 4K动漫 ---> /text()
            # 上面取到的都是在列表中
            title = a.xpath('./text()')[0]
            href = self.base_area + a.xpath('./@href')[0]
            category_dict[title] = href

        return category_dict

    def spider_all_detail_source(self, url):
        img_data_dict = {}
        tree = self.get_tree(url=url)
        li_list = tree.xpath('//*[@id="main"]/div[4]/ul/li')
        for li in li_list:
            detail_href = self.base_area + li.xpath('./a/@href')[0]
            tree = self.get_tree(url=detail_href)
            img_detail_url = self.base_area + tree.xpath('//*[@id="img"]/img/@src')[0]
            img_title_ = tree.xpath('//*[@id="img"]/img/@title')[0].split(' ')[0]
            img_title = ''
            for item in img_title_:
                if item.isdigit() or item.isspace() or item in ['*', '-', 'x', '+', '\\', '/']:
                    pass
                else:
                    img_title += item
            img_data_dict[img_title] = img_detail_url
        return img_data_dict

    def create_url_list(self, start_page: int, end_page: int, base_url: str):
        url_list = []
        for page in range(start_page, end_page):
            if page == 1:
                # https://pic.netbian.com/4kdongman/
                page_url = base_url
                url_list.append(page_url)
            else:
                # https://pic.netbian.com/4kdongman/index_2.html
                page_url = base_url + f'index_{page}.html'
                url_list.append(page_url)
        return url_list

    def download_image(self, img_url: str, img_title: str, category_input: str):
        file_dir = os.path.join(self.BASE_DIR, category_input)
        os.makedirs(file_dir, exist_ok=True)
        file_path = os.path.join(file_dir, img_title + '.png')
        response = requests.get(url=img_url, headers=self.headers)
        content = response.content
        with open(file_path, 'wb') as fp:
            fp.write(content)
        print(f'当前图片 :>>> 标题 {img_title} 下载成功! 链接 :>>>> {img_url}!')

    @staticmethod
    def show_time(func):
        def inner(*args, **kwargs):
            start_time = time.time()
            result = func(*args, **kwargs)
            end_time = time.time()
            print(f'总耗时 {func.__name__} :>>>> {end_time - start_time} s!')
            return result

        return inner

    def main_download_cls(self, cls, img_data_dict, category_input):
        task_list = []
        count = 0
        for title, url in img_data_dict.items():
            count += 1
            print(f'当前是第 {count} 张图片!')
            task = cls(
                target=self.download_image,
                args=(url, title, category_input)
            )
            task.start()
            task_list.append(task)
        for task in task_list:
            task.join()

    @show_time
    def main_download(self, start_page, end_page, category_href, category_input, func_id):
        # 构建所有目标地址
        target_url_list = self.create_url_list(start_page=int(start_page),
                                               end_page=int(end_page),
                                               base_url=category_href)
        print(f'当前所有目标地址构建完成 :>>>> {target_url_list}')
        print(f'------------------------------------------------')
        img_data_dict = {}
        # 请求每一页的图片详细连接
        for target_url in target_url_list:
            print(f'当前抓取图片首页地址连接为 :>>>> {target_url}')
            img_dict = self.spider_all_detail_source(url=target_url)
            img_data_dict.update(img_dict)
        print(f'当前所有图片连接构建完成 :>>>> {img_data_dict}')
        print(f'------------------------------------------------')
        print(f'开始下载 :>>>> ')
        # 下载每一张图片
        if func_id == '1':
            count = 0
            for title, url in img_data_dict.items():
                count += 1
                print(f'当前是第 {count} 张图片!')
                self.download_image(img_url=url, img_title=title, category_input=category_input)
            # 总耗时 main_download :>>>> 31.696726083755493 s!
            # 1 - 5 页 53 张
        elif func_id == '2':
            self.main_download_cls(Process, img_data_dict, category_input)
            # 总耗时 main_download :>>>> 25.603846788406372 s!
            # 1-5页 54 张
        elif func_id == '3':
            self.main_download_cls(Thread, img_data_dict, category_input)
            # 总耗时 main_download :>>>> 25.290791749954224 s!
            # 1-5页 68张
        print(f'下载结束 :>>>> ')


# 多进程和多线程的相关操作
class SpiderProcessThread(BaseSpider):
    def __init__(self):
        super().__init__()

    def main_chose_category_normal(self):
        # 获取所有的分类
        category_dict = self.spider_all_category()
        while True:
            # 遍历分类获取指定的分类下载
            for index, data in enumerate(category_dict.items(), start=1):
                index = str(index).rjust(len(str(index)) + 3, '0')
                category_title, category_href = data
                print(f'当前编号 :>>> {index} 分类 :>>>> {category_title}')
            # 用户输入下载的分类
            category_input = input("请输入下载的分类 :>>>> ").strip()
            if category_input not in category_dict.keys():
                print(f'当前分类 {category_input} 不存在')
                continue
            # 分类的主链接
            category_href = category_dict.get(category_input)
            start_page = input("请输入起始页码 :>>>> ").strip()
            end_page = input("请输入结束页码 :>>>> ").strip()
            # 起始页码和结束页码
            if not all([start_page.isdigit(), end_page.isdigit()]):
                print(f'页码有误!')
                continue
            func_id = input("请选择版本 ::>> ").strip()
            # 1 正常下载
            # 2 多进程下载
            # 3 多线程下载
            self.main_download(start_page, end_page, category_href, category_input, func_id=func_id)


if __name__ == '__main__':
    s = SpiderProcessThread()
    res = s.main_chose_category_normal()
    print(res)

【2】协程版本

import os
import time
from multiprocessing import Process
from threading import Thread

# pip install fake-useragent
from fake_useragent import UserAgent
# pip install requests
import requests

# pip install lxml
from lxml import etree

import asyncio
# pip install aiohttp
import aiohttp

import aiofiles


# pip install aiofiles

class BaseSpider(object):
    def __init__(self):
        self.base_area = 'https://pic.netbian.com'
        # 根目录
        self.BASE_DIR = os.path.dirname(os.path.abspath(__file__))
        # 创建请求头
        self.headers = {
            'User-Agent': UserAgent().random
        }

    async def __create_etree(self, page_source):
        return etree.HTML(page_source)

    # 获取页面源码数据
    async def get_tree(self, url):
        # 对目标地址发起请求并获得响应
        # async def main():
        #     async with aiohttp.ClientSession() as session:
        #         async with session.get("http://httpbin.org/headers") as response:
        #             print(await response.text())
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                # 因为数据是乱码 所以要对数据进行编码

                # 返回当前页面的源码数据
                page_source = await response.text(encoding='gbk')
                # 返回当前页面的解析器对象
                tree = await self.__create_etree(page_source=page_source)
                return tree

    async def spider_all_category(self):
        category_dict = {}
        tree = await asyncio.create_task(self.get_tree(url=self.base_area))
        print(tree)
        # 直接使用xpath语法
        a_list = tree.xpath('//*[@id="main"]/div[2]/a')
        for a in a_list:
            # //*[@id="main"]/div[2]/a[1]
            # <a href="/4kdongman/" title="4K动漫图片">4K动漫</a>
            # 如果是获取标签的属性 href / title ---> /@属性名
            # 如果是获取标签中间的文本 4K动漫 ---> /text()
            # 上面取到的都是在列表中
            title = a.xpath('./text()')[0]
            href = self.base_area + a.xpath('./@href')[0]
            category_dict[title] = href

        return category_dict

    async def spider_all_detail_source(self, url):
        img_data_dict = {}
        tree = await self.get_tree(url=url)
        li_list = tree.xpath('//*[@id="main"]/div[4]/ul/li')
        for li in li_list:
            detail_href = self.base_area + li.xpath('./a/@href')[0]
            tree = await self.get_tree(url=detail_href)
            img_detail_url = self.base_area + tree.xpath('//*[@id="img"]/img/@src')[0]
            img_title_ = tree.xpath('//*[@id="img"]/img/@title')[0].split(' ')[0]
            img_title = ''
            for item in img_title_:
                if item.isdigit() or item.isspace() or item in ['*', '-', 'x', '+', '\\', '/']:
                    pass
                else:
                    img_title += item
            img_data_dict[img_title] = img_detail_url
        return img_data_dict

    async def create_url_list(self, start_page: int, end_page: int, base_url: str):
        url_list = []
        for page in range(start_page, end_page):
            if page == 1:
                # https://pic.netbian.com/4kdongman/
                page_url = base_url
                url_list.append(page_url)
            else:
                # https://pic.netbian.com/4kdongman/index_2.html
                page_url = base_url + f'index_{page}.html'
                url_list.append(page_url)
        return url_list

    async def download_image(self, img_url: str, img_title: str, category_input: str):
        file_dir = os.path.join(self.BASE_DIR, category_input)
        os.makedirs(file_dir, exist_ok=True)
        file_path = os.path.join(file_dir, img_title + '.png')
        async with aiohttp.ClientSession() as session:
            async with session.get(img_url) as response:
                content = await response.read()
                async with aiofiles.open(file_path, mode='wb') as f:
                    await f.write(content)
        print(f'当前图片 :>>> 标题 {img_title} 下载成功! 链接 :>>>> {img_url}!')

    async def main_download(self, start_page, end_page, category_href, category_input, func_id):
        start_time = time.time()
        # 构建所有目标地址
        target_url_list = await self.create_url_list(start_page=int(start_page),
                                                     end_page=int(end_page),
                                                     base_url=category_href)
        print(f'当前所有目标地址构建完成 :>>>> {target_url_list}')
        print(f'------------------------------------------------')
        img_data_dict = {}
        # 请求每一页的图片详细连接
        for target_url in target_url_list:
            print(f'当前抓取图片首页地址连接为 :>>>> {target_url}')
            img_dict = await self.spider_all_detail_source(url=target_url)
            img_data_dict.update(img_dict)
        print(f'当前所有图片连接构建完成 :>>>> {img_data_dict}')
        print(f'------------------------------------------------')
        print(f'开始下载 :>>>> ')
        stat_download_time = time.time()
        # 下载每一张图片
        if func_id == '1':
            count = 0
            task_list = []
            for title, url in img_data_dict.items():
                count += 1
                print(f'当前是第 {count} 张图片!')
                task = asyncio.create_task(
                    self.download_image(img_url=url, img_title=title, category_input=category_input))
                task_list.append(task)
            # 等待任务完成
            await asyncio.wait(task_list)
        print(f'下载结束 :>>>> ')
        end_time = time.time()
        print(f'总耗时 :>>>> {end_time - start_time} s!')
        print(f'下载总耗时 :>>>> {end_time - stat_download_time} s!')


# 多进程和多线程的相关操作
class SpiderProcessThread(BaseSpider):
    def __init__(self):
        super().__init__()

    async def main_chose_category_normal(self):
        # 获取所有的分类
        category_dict = await self.spider_all_category()
        while True:
            # 遍历分类获取指定的分类下载
            for index, data in enumerate(category_dict.items(), start=1):
                index = str(index).rjust(len(str(index)) + 3, '0')
                category_title, category_href = data
                print(f'当前编号 :>>> {index} 分类 :>>>> {category_title}')
            # 用户输入下载的分类
            category_input = input("请输入下载的分类 :>>>> ").strip()
            if category_input not in category_dict.keys():
                print(f'当前分类 {category_input} 不存在')
                continue
            # 分类的主链接
            category_href = category_dict.get(category_input)
            start_page = input("请输入起始页码 :>>>> ").strip()
            end_page = input("请输入结束页码 :>>>> ").strip()
            # 起始页码和结束页码
            if not all([start_page.isdigit(), end_page.isdigit()]):
                print(f'页码有误!')
                continue
            func_id = input("请选择版本 ::>> ").strip()
            # 1 正常下载
            # 2 多进程下载
            # 3 多线程下载
            await self.main_download(start_page, end_page, category_href, category_input, func_id=func_id)


if __name__ == '__main__':
    s = SpiderProcessThread()
    res = s.main_chose_category_normal()
    asyncio.run(res)
import asyncio
import os
import time

from fake_useragent import UserAgent
import aiohttp
from lxml import etree

headers = {
    'User-Agent': UserAgent().random
}
BASE_DIR = os.path.dirname(__file__)


def create_file_name(path='img'):
    file_name_path = os.path.join(BASE_DIR, path)
    os.makedirs(file_name_path, exist_ok=True)
    return file_name_path


file_name_path = create_file_name()


async def create_url_list():
    url_list = []
    for i in range(1, 5):
        if i == 1:
            index_url = 'https://pic.netbian.com/4kdongman/'
            url_list.append(index_url)
        else:
            index_url = f'https://pic.netbian.com/4kdongman/index_{i}.html'
            url_list.append(index_url)
    return url_list


async def get_tree(page_text):
    tree = etree.HTML(page_text)
    return tree


async def get_page_text(tag_url, encoding='gbk'):
    async with aiohttp.ClientSession() as session:
        # 如果遇到 ssl error 这种错,一般都是 ssl=False
        async with session.get(url=tag_url, headers=headers, ssl=False) as response:
            page_text = await response.text(encoding='gbk')
    return page_text


async def spider_index_tree():
    tree_list = []
    url_list = await create_url_list()
    # url_list = ['https://pic.netbian.com/4kdongman/']
    for url in url_list:
        # 获取每一页的页面源码
        page_text = await get_page_text(tag_url=url)
        tree = await get_tree(page_text=page_text)
        tree_list.append(tree)
    return tree_list


async def get_tree_data(tree):
    img_data_list = []
    li_list = tree.xpath('//*[@id="main"]/div[4]/ul/li')
    # //*[@id="main"]/div[4]/ul/li[1]/a/img
    for li in li_list:
        # ./a/img
        img_title_ = li.xpath('./a/img/@alt')[0]
        img_title = ''
        for item in img_title_:
            if item.isdigit() or item.isspace() or item in ['*', '-', 'x', '+', '\\', '/']:
                pass
            else:
                img_title += item
        img_src = 'https://pic.netbian.com' + li.xpath('./a/img/@src')[0]
        img_data_list.append({'img_title': img_title, 'img_src': img_src})
    print(img_data_list)
    return img_data_list


async def spider_index_img_data():
    img_data_list = []
    tree_list = await spider_index_tree()
    for tree in tree_list:
        img_list = await get_tree_data(tree=tree)
        # [{},{}]
        img_data_list.extend(img_list)
    return img_data_list


async def download(img_src, img_title):
    async with aiohttp.ClientSession() as session:
        async with session.get(url=img_src, headers=headers, ssl=False) as response:
            data_all = await response.read()
            file_path = os.path.join(file_name_path, f'{img_title}.png')
            with open(file_path, mode='wb') as fp:
                fp.write(data_all)
            print(f"当前图片 :>>>> {img_title} 保存成功!")


async def main():
    img_data_list = await spider_index_img_data()
    print(len(img_data_list))
    # 创建Task对象列表
    task_list = [asyncio.create_task(download(img_src=img_data.get('img_src'), img_title=img_data.get('img_title'))) for
                 img_data in img_data_list]
    # 等待任务完成
    await asyncio.wait(task_list)


if __name__ == '__main__':
    start_time = time.time()
    # 启协程
    asyncio.run(main())
    print(f"总耗时 :>>>> {time.time() - start_time} s")

    # 总耗时 :>>>> 6.5860209465026855 s
posted @   光头大炮  阅读(91)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示