Python提供了多种方法来创建、执行和管理线程,并且需要注意线程安全性和性能方面的问题。其中使用threading模块创建线程,并获取其执行的函数返回值的方法有:
- 使用concurrent.futures模块:提供了高级API,可以将返回值和异常从工作线程传递到主线程。但可能比使用threading模块更耗费资源。
- 使用multiprocessing.pool模块:提供了类似的接口,可以使用进程或线程池,并使用apply_async方法异步地执行函数并获取结果。但需要序列化和传递数据,而且不能共享内存。
- 使用可变对象作为参数传递给线程的构造器,并让线程将其结果存储在该对象的指定位置。但可能会导致竞争条件。
- 使用Thread的子类:重写run和join方法,使得join方法可以返回目标函数的返回值。但需要访问一些私有的数据结构。
在选择方法时,需要考虑具体需求和场景。以下是需要注意的一些方面:
- concurrent.futures模块可以简化线程的创建和管理,但可能比使用threading模块更耗费资源。
- multiprocessing.pool模块可以利用多核处理器并行执行函数,但需要序列化和传递数据,而且不能共享内存。
- 使用可变对象作为参数传递给线程可能会导致竞争条件,即多个线程同时修改同一个对象,造成数据不一致或错误。
- 使用Thread的子类来返回目标函数的返回值可能会破坏Thread的原有设计,而且需要访问一些私有的数据结构。
- Python的线程受到全局解释器锁(GIL)的限制,即在任何时刻只有一个线程能够执行Python字节码,因此对于计算密集型的任务,线程并不能提高性能。
- Python的线程在执行I/O操作或其他阻塞调用时会释放GIL,因此对于I/O密集型的任务,线程可以提高性能。
- Python的线程需要注意线程安全性,即避免多个线程同时访问或修改共享的资源,否则可能会造成数据损坏或不一致。
- Python提供了一些工具来保证线程安全性,例如锁(Lock)、信号量(Semaphore)、定时器(Timer)和屏障(Barrier)等。
例如用”汽车”和“冰淇淋”作为关键词对B站进行搜索,将返回的视频标题进行采集整理并写入数据库,同时计算数据总量,以此进行热点事件分析,代码如下:
代码语言:python代码运行次数:0复制# 导入所需的模块
import requests
import re
import sqlite3
import threading
# 定义一个函数,根据关键词和页码获取B站搜索结果页面的HTML内容
def get_html(keyword, page):
# 构造请求头和参数
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36"
}
params = {
"keyword": keyword,
"page": page,
"order": "totalrank"
}
#设置亿牛云爬虫代理加强版 代理IP的服务器地址、端口、用户名和密码
proxyHost = "www.16yun.cn"
proxyPort = "31111"
# 代理验证信息
proxyUser = "16YUN"
proxyPass = "16IP"
proxyMeta = "http://%(user)s:%(pass)s@%(host)s:%(port)s" % {
"host" : proxyHost,
"port" : proxyPort,
"user" : proxyUser,
"pass" : proxyPass,
}
# 设置 http和https访问都是用HTTP代理
proxies = {
"http" : proxyMeta,
"https" : proxyMeta,
}
# 发送GET请求,获取响应内容,使用代理IP和用户名密码
response = requests.get("https://search.bilibili.com/all", headers=headers, params=params, proxies=proxies)
# 返回HTML内容
return response.text
# 定义一个函数,从HTML内容中提取视频标题,并将其写入数据库
def extract_and_save(html):
# 连接数据库,创建游标
conn = sqlite3.connect("bilibili.db")
cursor = conn.cursor()
# 创建数据表,如果已存在则忽略
cursor.execute("CREATE TABLE IF NOT EXISTS videos (title TEXT)")
# 从HTML内容中提取视频标题,使用正则表达式匹配
titles = re.findall(r"<a title="(.*?)" href=", html)
# 遍历每个标题,将其插入数据表中
for title in titles:
cursor.execute("INSERT INTO videos VALUES (?)", (title,))
# 提交事务,关闭连接
conn.commit()
conn.close()
# 定义一个函数,计算数据库中的数据总量,并打印结果
def count_and_print():
# 连接数据库,创建游标
conn = sqlite3.connect("bilibili.db")
cursor = conn.cursor()
# 查询数据表中的记录数
cursor.execute("SELECT COUNT(*) FROM videos")
count = cursor.fetchone()[0]
# 打印结果
print(f"共采集了{count}条数据")
# 关闭连接
conn.close()
# 定义一个主函数,使用线程进行快速I/O操作
def main():
# 定义关键词和页码范围
keyword = "汽车 冰淇淋"
pages = range(1, 11)
# 创建一个空列表,用于存储线程对象
threads = []
# 遍历每个页码,创建一个线程对象,执行get_html和extract_and_save函数,并将其添加到列表中
for page in pages:
thread = threading.Thread(target=lambda: extract_and_save(get_html(keyword, page)))
thread.start()
threads.append(thread)
# 等待所有线程结束
for thread in threads:
thread.join()
# 调用count_and_print函数,计算并打印数据总量
count_and_print()
# 调用主函数
if __name__ == "__main__":
main()
总体来说,这段代码使用了多线程技术,使用多个线程并发地访问B站的搜索结果页面,提取其中的视频标题,并将其写入数据库,将网络请求和数据库操作分别放到不同的线程中执行,从而实现了快速爬取和处理大量数据的目的。同时,该代码还使用了爬虫代理IP,提高了爬虫的稳定性和安全性。