多线程是在同一个进程中创建多个线程,每个线程都可以执行不同的任务。多线程适合于I/O密集型的任务,比如网络请求、文件读写等,因为在这些任务中,大部分时间都在等待I/O操作完成,而不是在CPU上运行。比如说爬虫,最慢的部分就在与请求网页。
由于多线程的优势在于多网页爬取,我们在此拿博客园作为示例
由于博客园的页数可以在网址上体现,我们可以确定要爬取的内容:
# 我们先暂定所爬取30页博客文章的url
urls = [f'{i}' for i in range(1,31)]
继续编写爬虫部分:
import requests
import threading
import time
header = {"User-Agent":"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (HTML, like Gecko) Chrome/35.0.2117.157 Safari/537.36"}
urls = [f'{i}' for i in range(1,31)]
# 因为thread会多线程的调用方法,所以将请求网页放在craw方法里
def craw(url):
r = requests.get(url=url, headers=header)
print(url, len(r.text))
# 定义多线程方法
def multi_thread():
# 创建任务列表以储存线程对象
threads = []
# 在循环中,对于每个 url,都创建了一个新的线程对象,通过 threading.Thread 类的构造函数,将要执行的任务函数 craw 和参数 url 传递给线程对象。
# 其中target传递的是一个方法名,args传递的是参数元组
for url in urls:
threads.append(
threading.Thread(target=craw, args=(url,))
)
# start() 和 join() 是Thread中的两个方法,分别用于启动线程和等待线程执行完毕。
for thread in threads:
thread.start()
for thread in threads:
thread.join()
# 程序运行
if __name__ == '__main__':
# 开始时间
a = time.time()
# 调用多线程爬取
multi_thread()
b = time.time()
# 打印运行时间
print(b - a)
print('-'*30)
# 不用多线程爬取
# 开始时间
a = time.time()
for url in urls:
craw(url)
b = time.time()
# 打印运行时间
print(b - a)
在启动多个线程后,使用 join() 方法等待所有线程执行完成,这样可以确保所有线程都执行完成后再继续执行主线程中的其他操作。
运行结果:
11 70117
23 70117
16 70117
2 70117
28 70117
18 70117
24 70117
10 70117
7 70117
3 70117
12 70117
27 70117
8 70117
14 70117
15 70117
17 21 70117
70117
22 70117
26 70117
525 70117
13 70117
70117
29 70117
9 70117
1 70117
20 70117
30 70117
6 70117
4 70117
19 70117
0.16265130043029785
-------------
1 70117
2 70117
3 70117
4 70117
5 70117
6 70117
7 70117
8 70117
9 70117
10 70117
11 70117
12 70117
13 70117
14 70117
15 70117
16 70117
17 70117
18 70117
19 70117
20 70117
21 70117
22 70117
23 70117
24 70117
25 70117
26 70117
27 70117
28 70117
29 70117
30 70117
2.7461986541748047
可以看到,两种方法的运行速度相差了十几倍,但是多进程的输出却有一些问题。这是由于多个线程同时向终端输出内容,造成输出内容的交织和覆盖,导致最终输出的结果不可控。这种情况下,我们可以使用线程安全的队列queue来解决这个问题。
下面是一个使用了生产者消费者架构的cnblogs爬虫:
import queue
import requests
from bs4 import BeautifulSoup
import random
import time
import threading
urls = [f'{i}'
for i in range(1, 31)]
header = {"User-Agent": "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (HTML, like Gecko) Chrome/35.0.2117.157 "
"Safari/537.36"}
def craw(url):
r = requests.get(url=url, headers=header)
return r.text
def parse(html):
soup = BeautifulSoup(html, 'html.parser')
datas = soup.find_all('a', class_='post-item-title')
return [(data['href'], data.get_text()) for data in datas]
# 传入两个queue.Queue队列
def do_craw(url_queue: queue.Queue, html_queue: queue.Queue):
# 判断队列是否为空,如果不为空则继续执行
while not url_queue.empty():
# url从队列里取出
url = url_queue.get()
html = craw(url)
# 将文本内容传给html_queue队列
html_queue.put(html)
# 打印当前爬取完成的url
print(url)
time.sleep(random.randint(1, 2))
# 将html队列传入进行处理,并传入一个打开的文本写入信息
def do_parse(html_queue: queue.Queue, out_data):
while True:
html = html_queue.get()
results = parse(html)
for result in results:
out_data.write(str(result[0])+ ',' + str(result[1]) + '\n')
print(html_queue.qsize())
time.sleep(random.randint(1, 2))
if __name__ == '__main__':
out_data = open('cnblogs.csv', 'w', encoding='utf-8')
# 创建两个队列
url_queue = queue.Queue()
html_queue = queue.Queue()
for url in urls:
url_queue.put(url)
创建一个三线程
for num in range(3):
t = threading.Thread(target=do_craw, args=(url_queue, html_queue))
t.start()
创建一个二线程
for num in range(2):
t = threading.Thread(target=do_parse, args=(html_queue, out_data))
t.start()
输出结果:
1
2
3
1
1
0
4
0
5
0
6
7
1
0
8
9
10
11
3
2
12
13
14
4
3
15
16
17
5
4
18
19
20
6
5
21
22
23
24
8
7
25
26
27
9
8
28
8
29
30
9
8
7
6
5
4
3
1
1
0
进程已结束,退出代码-1
由输出结果我们可以看出,数据的爬取和处理是同时进行的,且数据的爬取和处理调用了不同的处理器,也能够加快数据的处理。同时解决了先前多线程出现的问题。
使用queue的四大优点
线程安全:queue.Queue 内置了线程安全的锁机制,可以确保多个线程之间的数据访问和修改是安全的,避免了数据竞争和其他的线程安全问题。
便于控制:queue.Queue 通过内置的阻塞机制可以控制数据的流动,例如在生产者线程添加数据时,如果队列已满,可以阻塞生产者线程,直到队列中有空位可用;在消费者线程获取数据时,如果队列为空,可以阻塞消费者线程,直到队列中有数据可用。这样就可以很方便地控制生产者线程和消费者线程的数量和速度,以达到最优的性能和效果。
灵活性高:queue.Queue 支持多种类型的队列,例如 FIFO(先进先出)队列、LIFO(后进先出)队列、优先级队列等等。同时,可以通过设置队列的最大长度、超时时间、优先级等参数来满足不同的需求。
代码简洁:使用 queue.Queue 实现多线程间的通信和数据共享,代码相对简洁,易于维护和扩展。可以通过多个生产者线程和多个消费者线程来处理复杂的业务逻辑,而无需关心线程间的同步和数据共享问题。
后续学习中我还会演示多进程和多协程的操作实现,本篇文章介绍了多线程threads和队列queue的操作,并用cnblogs进行了实例化演示爬取。