本文介绍了进程与线程的基本概念和关系;使用threading.Thread实多线程爬虫,在提高爬虫效率的同时,也引发了一些思考。
本案例仅供学习交流使用,请勿商用。如涉及版本侵权,请联系我删除。
目录
一、进程与线程
二、threading模块的使用
1. 创建多线程:threading.Thread类
2. 使用Thread创建多线程
三、某图网单线程下载
1. fake_useragent的使用
2. 获取网站URL
3. 下载图片
4. 添加主函数
四、某图网多线程下载
1. 创建多线程
2. 完整代码
五、多线程爬虫的思考
我们都知道,计算机是由硬件和软件两部分组成的,其中硬件包括中央处理器(CPU)、内存、硬盘、显示器等,软件则包括操作系统和应用程序。
中央处理器(CPU)是计算机的核心部件,它负责执行计算机的所有指令和计算任务。
操作系统是计算机的管理者,它负责协调和管理计算机中的各种资源,包括CPU、内存、硬盘、网络等,以确保它们能够高效地协同工作。操作系统还负责任务的调度和分配,为多任务处理提供支持,并提供用户界面和文件管理等基本功能。
应用程序是运行于操作系统之上的具有某种功能的程序。它们通过操作系统提供的接口访问硬件资源,并利用CPU来执行特定的计算任务,例如文本编辑、图形处理、音频播放等。应用程序可以是系统自带的工具软件,也可以是用户自行安装的第三方软件,它们通常具有各种不同的功能和特点,以满足用户不同的需求。因此,计算机的硬件和软件相互协作,才能完成各种任务和应用。CPU作为计算机的核心部件,通过操作系统协调和管理各种资源,为应用程序提供支持,从而实现了计算机的各种功能。
进程是操作系统中一个基本的概念,进程是一个具有一定独立功能的程序在一个数据集合上依次动态执行的过程。进程是一个正在执行的程序的实例,包括程序计数器、寄存器和程序变量的当前值。在计算机系统中,每个进程都有自己的内存空间和系统资源,如CPU、内存、磁盘等,它们被分配给进程以支持其运行。进程通常由一个可执行文件(例如程序)启动,该文件在操作系统中被加载到内存中。
线程在早期的操作系统中,进程是最小的独立运行单位,也是程序执行的最小单位,因为此时并没有线程的概念。任务调度采用的是时间片轮转的抢占式调度方式,进程是任务调度的最小单位。每个进程拥有独立的一块内存,这使得不同进程之间的内存地址互相隔离。然而,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销变得过大,已经无法满足日益复杂的程序的要求。因此,人们发明了线程。线程是程序执行中的单一顺序控制流程,是程序执行流的最小单元。
CPU 在处理进程和线程时,都会进行来回切换。当 CPU 处理进程时,它会将进程的执行状态保存到进程控制块中,然后切换到另一个进程去执行。这个过程被称为进程切换。进程切换的目的是使 CPU 能够同时执行多个进程,提高系统的并发能力。当 CPU 处理线程时,它也会进行来回切换,但是线程切换的代价要比进程切换小得多。因为线程是共享进程的资源,在一个进程中的多个线程可以同时访问该进程的全局变量、静态变量和堆内存等资源,无需进行额外的拷贝或传输操作。这种共享的方式能够让多个线程之间更加高效地通信和协作,从而提高程序的性能。所以在切换线程时,只需要切换线程的执行状态即可,不需要切换进程的地址空间和其他资源。这使得线程切换比进程切换更加高效。因此,CPU 在处理进程和线程时都需要进行来回切换,但是线程切换的开销要比进程切换小。
属性/方法 | 描述 |
---|---|
Thread(target=None, name=None, args=(), kwargs={}) | Thread类的构造方法,其中target为线程要执行的函数名,name为线程名称,args和kwargs为要传递给函数的参数。 |
start() | 启动线程。 |
run() | 线程要执行的任务函数。需要重写该方法并在其中实现线程的业务逻辑。 |
join(timeout=None) | 当前线程等待该线程执行完毕。timeout为等待的最大时间。 |
is_alive() | 判断线程是否还在运行中。 |
name | 线程的名称。 |
ident | 线程的唯一标识符。 |
Thread类是Python中的多线程编程核心,通过实例化Thread类创建线程对象,并使用start()方法启动线程。同时可以使用join()方法等待线程执行完毕。
import threading
import time
# 定义线程要执行的任务
def worker():
for i in range(5):
print(threading.current_thread().name, i)
time.sleep(1)
# 创建多个线程对象并启动
threads = []
for i in range(3):
t = threading.Thread(target=worker, name=f'Thread-{i}')
t.start()
threads.append(t)
# 等待所有线程执行完毕
for t in threads:
t.join()
print('线程执行完了...')
在上面例中,我们首先定义了线程要执行的任务函数worker。通过循环创建了3个Thread对象,并使用start()方法启动了它们。同时,我们还将每个线程对象保存到一个列表中,方便后续使用join()方法等待它们执行完毕。
fake_useragent 库可以用来生成随机的 User-Agent,进行UA伪装。下面我们介绍fake_useragent 库的使用。
安装 fake_useragent 库
pip install fake_useragent
导入 fake_useragent 库
from fake_useragent import UserAgent
使用 UserAgent.get_random_user_agent() 方法生成随机的 User-Agent。例如:
from fake_useragent import UserAgent
headers = {
'User-Agent': UserAgent().random # 获取随机 User-Agent
}
import os, time
import requests
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
def get_img_urls() -> list:
"""
爬取指定网站 https://www.pkdoutu.com/photo/list/ 上的图片链接,并返回一个链接列表。
Returns:
url_list (list): 图片链接列表,每个元素为一个字符串类型的链接。
"""
url = r'https://www.pkdoutu.com/photo/list/'
headers = {
"User-Agent": UserAgent().random # 设置请求头,使用随机 User-Agent。
}
res = requests.get(url=url, headers=headers).content.decode() # 发送 GET 请求,并获取返回的 HTML 页面。
soup = BeautifulSoup(res, 'lxml') # 解析 HTML 页面,生成 BeautifulSoup 对象。
div_li = soup.find('div', class_='page-content text-center').find_all('a') # 获取所有图片链接所在的 a 标签。
url_list = []
for a in div_li:
img_url = a.find('img', class_='img-responsive lazy image_dta')['data-backup'] # 获取图片链接。
url_list.append(img_url) # 将图片链接添加到列表中。
return url_list # 返回图片链接列表
上面的函数中,使用requests模块获取网页响应,bs4进行网页解析,得到图片URL的列表。
import os, time
import requests
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
def download_img(img_url: str, folder: str):
'''
根据图片的 URL 下载图片,并将其保存到指定文件夹中。
:param img_url: 图片的 URL。
:param folder: 保存图片的文件夹。
'''
# 创建文件夹(如果不存在的话)
if not os.path.exists(folder):
os.makedirs(folder)
# 爬取图片
headers = {
"User-Agent": UserAgent().random # 获取随机 User-Agent
}
res_img = requests.get(img_url, headers=headers)
# 保存图片
file_name = img_url.split('/')[-1]
file_path = os.path.join(folder, file_name)
with open(file_path, 'wb') as f:
f.write(res_img.content)
print(f'{file_name} 下载完成...')
print('-' * 200)
if __name__ == '__main__':
start_time = time.time()
# (1) 爬取当前页的所有的 img_url
img_urls = get_img_urls()
# (2) 根据 img_urls 下载图片
folder = './结果数据/案例03:斗图网多线程采集/'
for img_url in img_urls:
download_img(img_url, folder)
print(f"整体耗时{time.time() - start_time} 秒")
执行程序,下载图片68张,耗时24.72秒。
if __name__ == '__main__':
start_time = time.time()
# (1) 爬取当前页的所有的img_url
img_urls = get_img_urls()
# (2) 根据img_urls下载图片
folder = './结果数据/案例03:斗图网多线程采集/'
t_list = []
for img_url in img_urls:
t = threading.Thread(target=download_img, args=(img_url, folder,))
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(f"整体耗时{time.time() - start_time} 秒")
在上面的程序中,我们创建了68个线程,执行程序,下载图片68张,耗时4.18秒,极大的节约了数据采集时间。
# -*- coding:utf-8 -*-
import os, time
import requests
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
import threading
def get_img_urls() -> list:
"""
爬取指定网站 https://www.pkdoutu.com/photo/list/ 上的图片链接,并返回一个链接列表。
Returns:
url_list (list): 图片链接列表,每个元素为一个字符串类型的链接。
"""
url = r'https://www.pkdoutu.com/photo/list/'
headers = {
"User-Agent": UserAgent().random # 设置请求头,使用随机 User-Agent。
}
res = requests.get(url=url, headers=headers).content.decode() # 发送 GET 请求,并获取返回的 HTML 页面。
soup = BeautifulSoup(res, 'lxml') # 解析 HTML 页面,生成 BeautifulSoup 对象。
div_li = soup.find('div', class_='page-content text-center').find_all('a') # 获取所有图片链接所在的 a 标签。
url_list = []
for a in div_li:
img_url = a.find('img', class_='img-responsive lazy image_dta')['data-backup'] # 获取图片链接。
url_list.append(img_url) # 将图片链接添加到列表中。
return url_list # 返回图片链接列表
def download_img(img_url: str, folder: str):
'''
根据图片的 URL 下载图片,并将其保存到指定文件夹中。
:param img_url: 图片的 URL。
:param folder: 保存图片的文件夹。
'''
# 创建文件夹(如果不存在的话)
if not os.path.exists(folder):
os.makedirs(folder)
# 爬取图片
headers = {
"User-Agent": UserAgent().random # 获取随机 User-Agent
}
res_img = requests.get(img_url, headers=headers)
# 保存图片
file_name = img_url.split('/')[-1]
file_path = os.path.join(folder, file_name)
with open(file_path, 'wb') as f:
f.write(res_img.content)
print(f'{file_name} 下载完成...')
print('-' * 200)
if __name__ == '__main__':
# 多线程爬虫
start_time = time.time()
# (1) 爬取当前页的所有的img_url
img_urls = get_img_urls()
# (2) 根据img_urls下载图片
folder = './结果数据/案例03:斗图网多线程采集/'
t_list = []
for img_url in img_urls:
t = threading.Thread(target=download_img, args=(img_url, folder,))
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(f"整体耗时{time.time() - start_time} 秒")
# 单线程爬虫
# start_time = time.time()
#
# # (1) 爬取当前页的所有的 img_url
# img_urls = get_img_urls()
#
# # (2) 根据 img_urls 下载图片
# folder = './结果数据/案例03:斗图网多线程采集/'
# for img_url in img_urls:
# download_img(img_url, folder)
#
# print(f"整体耗时{time.time() - start_time} 秒")
我们使用Thread创建多线程爬虫,极大的提高了爬虫的效率,提高了程序的响应速度,可以方便的控制线程数量。但是还存在一些弊端,如下:
线程间竞争和协作问题:多个线程同时访问共享资源时,容易出现竞争和协作问题。例如,当多个线程同时对同一个变量进行修改时,可能会导致数据的不一致性。为了解决这个问题,可以使用锁等机制进行同步。
线程切换的开销:线程的切换需要消耗一定的时间和资源,如果线程切换过于频繁,可能会导致程序效率反而下降。此外,由于 Python 解释器的 GIL(全局解释器锁)机制,Python 中的多线程并不是真正的并行执行,而是通过在不同线程之间切换执行来实现的。
内存和资源的占用:多线程在运行时会占用更多的内存和资源,特别是在同时创建大量线程时,可能会导致系统负载过高,甚至出现内存泄漏等问题。
容易触发反爬虫机制:一些网站为了防止爬虫的访问,会设置一些反爬虫机制。如果使用多线程爬虫,可能会因为访问频率过高而被检测到,从而触发反爬虫机制,导致爬虫失败。为了避免这种情况,可以使用代理 IP 等技术进行反反爬虫。
网络拥塞:多线程爬虫同时向服务器发送大量请求,可能会造成网络拥塞,从而导致请求失败或者响应时间过长。这可能会影响到其他用户的网络使用体验,甚至会影响到整个网络的正常运行。
服务器压力:多线程爬虫会给被爬取网站的服务器带来更大的负载压力,如果服务器没有良好的负载均衡和容错机制,可能会导致服务器崩溃或者停机。
因此,在进行多线程爬虫开发时,多线程爬虫虽然可以提高爬虫效率,但也存在多个方面的弊端,需要在实际应用中谨慎使用,并进行合理的优化和调整。例如,设置适当的爬取速度、采用代理 IP、合理规划爬取任务、采用分布式爬虫等。