多线程爬虫
其核心原理是利用线程切换技术来实现CPU级别的并发执行。这种并发执行并不等同于多CPU的并行操作,而是在单个CPU上通过快速切换线程来模拟同时处理多个任务的效果。
爬虫使用多线程来处理网络请求,使用多线程来处理队列的URL,然后将URL返回结果放到另外一个队列只能够,其他线程再读取这个队列的数据。这种有种生产者,消费者的概念一样。
了解下queue
Python中的队列(queue)是一种先进先出(FIFO)的数据结构,可以使用标准库中的queue模块实现。以下是一个简单的示例:
import queue
# 创建一个队列
q = queue.Queue()
# 向队列中添加元素
q.put(1)
q.put(2)
q.put(3)
# 从队列中获取元素
print(q.get()) # 输出:1
print(q.get()) # 输出:2
print(q.get()) # 输出:3
多线程案例
-
首先定义两个队列来存放url,另一个用来放访问url后的html信息。
url_queue = queue.Queue()
html_queue = queue.Queue()
urls = [
f"https://www.cnblogs.com/#p{page}"
for page in range(1, 10 + 1)
]
for url in urls:
url_queue.put(url) -
定义一个生产者概念,源源不断往html_queue放数据
def craw(url):
r = requests.get(url)
return r.textdef do_craw(url_queue:queue.Queue, html_queue:queue.Queue):
while True:
url = url_queue.get()
html = craw(url)
html_queue.put(html)
print(threading.current_thread().name,f"do_craw {url},size {url_queue.qsize()}")
time.sleep(random.randint(1, 2)) -
定义一个消费者概念(解析)
def parser(html):
soup = BeautifulSoup(html, 'html.parser')
links = soup.find_all('a', 'post-item-title') # 标签,class
return [(link['href'], link.get_text()) for link in links]def do_parser(html_queue:queue.Queue):
while True:
html = html_queue.get()
results = parser(html)
for result in results:
print(threading.current_thread().name, result,f"html_queue size:{html_queue.qsize()}")
time.sleep(random.randint(1, 2)) -
开启线程
for idx in range(3):
t = threading.Thread(target=do_craw, args=(url_queue,html_queue), name = f'do_craw{idx}')
t.start()for idx in range(2):
t = threading.Thread(target=do_parser, args=(html_queue,), name=f'do_parser{idx}')
t.start() -
整体代码
import queue
import random
import threading
import timeimport requests
from bs4 import BeautifulSoupdef craw(url):
r = requests.get(url)
return r.textdef do_craw(url_queue:queue.Queue, html_queue:queue.Queue):
while True:
url = url_queue.get()
html = craw(url)
html_queue.put(html)
print(threading.current_thread().name,f"do_craw {url},size {url_queue.qsize()}")
time.sleep(random.randint(1, 2))def parser(html):
soup = BeautifulSoup(html, 'html.parser')
links = soup.find_all('a', 'post-item-title') # 标签,class
return [(link['href'], link.get_text()) for link in links]def do_parser(html_queue:queue.Queue):
while True:
html = html_queue.get()
results = parser(html)
for result in results:
print(threading.current_thread().name, result,f"html_queue size:{html_queue.qsize()}")
time.sleep(random.randint(1, 2))if name == 'main':
url_queue = queue.Queue()
html_queue = queue.Queue()
urls = [
f"https://www.cnblogs.com/#p{page}"
for page in range(1, 10 + 1)
]
for url in urls:
url_queue.put(url)for idx in range(3): t = threading.Thread(target=do_craw, args=(url_queue,html_queue), name = f'do_craw{idx}') t.start() for idx in range(2): t = threading.Thread(target=do_parser, args=(html_queue,), name=f'do_parser{idx}') t.start()
多进程爬虫
在计算机系统中,多个CPU同时执行不同的任务,以提高系统的处理能力和效率。在Python中,可以使用multiprocessing模块来实现多CPU并行,其实也叫多进程。
了解下Pool
Pool在Python的multiprocessing模块中是一个非常重要的类,它代表一个进程池。
进程池(Pool)是一种资源管理形式,它可以管理和控制多个进程的创建、执行和终止。通过使用进程池,可以有效地利用系统资源,提高程序运行效率。以下是对Pool类的一些详细解释:
- 资源重用:Pool类可以创建一个进程池,其中的进程可以被重复使用。这意味着不需要为每个任务创建和销毁进程,从而减少了开销。
- 自动管理:Pool类会自动管理进程的生命周期。当任务被提交到进程池时,它会选择一个可用的进程来执行该任务。如果没有可用的进程,任务会在队列中等待,直到有进程可用。
- 任务队列:Pool类使用任务队列来存储待处理的任务。当进程池中有可用进程时,任务会从队列中取出并执行。
- 并行处理:Pool类支持并行处理,即多个任务可以同时在不同的进程中执行。这有助于提高程序的执行速度,特别是在多核处理器上运行时。
- 灵活性:Pool类提供了多种方法来提交任务,如map、imap、apply等,这些方法使得向进程池提交任务变得非常灵活和方便。
- 上下文管理:Pool类支持作为上下文管理器使用,可以使用with语句来自动管理进程池的创建和销毁。
综上所述,理解Pool类是理解Python多进程编程的关键。通过使用Pool类,可以更高效地管理和执行多个进程,从而提高程序的性能。
1. apply
apply方法只能用于提交单个任务,并且需要指定任务的参数。如果要同时提交多个任务,可以使用map或imap方法。
from multiprocessing import Pool
def square(x):
return x * x
if __name__ == "__main__":
with Pool(processes=4) as pool:
result = pool.apply(square, args=(2,))
print("Result:", result)
2. apply_async
apply_async 允许你异步地执行一个函数。这个函数会在一个新的进程中运行,并且返回一个AsyncResult对象,你可以用这个对象来获取函数的结果。
from multiprocessing import Pool
def f(x):
return x*x
if __name__ == '__main__':
with Pool(5) as p:
result = p.apply_async(f, (10,))
print(result.get())
3. map
这将在进程池中的多个进程中并行执行square函数,并将结果存储在一个列表中。
from multiprocessing import Pool
def square(x):
return x * x
if __name__ == "__main__":
with Pool(processes=4) as pool:
results = pool.map(square, range(10))
print("Results:", results)
4. imap
Pool类中的imap方法与map方法类似,都是用于并行地执行函数。不过,imap方法在处理结果时稍有不同,它返回的是一个迭代器,而不是一个列表。这意味着你可以立即开始处理结果,而不需要等待所有任务完成。这对于处理大量数据或长时间运行的任务特别有用,因为它可以帮助减少内存使用并提高程序响应性。
from multiprocessing import Pool
def square(x):
return x * x
if __name__ == "__main__":
with Pool(processes=4) as pool:
# imap返回一个迭代器
results = pool.imap(square, range(10))
# 遍历迭代器来获取结果
for result in results:
print("Result:", result)
多进程案例
import requests
from bs4 import BeautifulSoup
from multiprocessing import Pool
import os
def craw(url):
r = requests.get(url)
results = parser(r.text) # 解析网页并提取所需信息
for result in results:
print("当前进程编号:", os.getpid()) # 打印当前进程编号
print(result)
def parser(html):
soup = BeautifulSoup(html, 'html.parser')
links = soup.find_all('a', 'post-item-title') # 标签,class
return [(link['href'], link.get_text()) for link in links]
if __name__ == '__main__':
urls = [
f"https://www.cnblogs.com/#p{page}"
for page in range(1, 10 + 1)
]
with Pool(processes=4) as pool: # 创建一个包含4个进程的进程池
pool.map(craw, urls) # 使用进程池并发获取网页
多协程爬虫
多协程是指在一个线程中运行多个协程,它们通过任务的暂停和恢复来避免线程切换的开销,并且减少了锁的使用。
asyncio模块只能发tcp级别的请求,不能发http协议。
多协程案例
import asyncio
import aiohttp
from bs4 import BeautifulSoup
async def fetch(url):
connector = aiohttp.TCPConnector(ssl=False)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
f"https://www.cnblogs.com/#p{page}"
for page in range(1, 10 + 1)
]
tasks = [fetch(url) for url in urls]
results = await asyncio.gather(*tasks)
for result in results:
pages = parser(result)
for page in pages:
print(page)
def parser(html):
soup = BeautifulSoup(html, 'html.parser')
links = soup.find_all('a', 'post-item-title') # 标签,class
return [(link['href'], link.get_text()) for link in links]
if __name__ == '__main__':
asyncio.run(main())
首先定义了一个异步函数fetch,它使用aiohttp库异步获取给定URL的内容。然后,我们在main函数中创建了一个任务列表,其中每个任务都是对fetch函数的调用。我们使用asyncio.gather来并发执行所有任务,并等待它们全部完成。