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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)