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站进行搜索,将返回的视频标题进行采集整理并写入数据库,同时计算数据总量,以此进行热点事件分析,代码如下:
# 导入所需的模块importrequestsimportreimportsqlite3importthreading# 定义一个函数,根据关键词和页码获取B站搜索结果页面的HTML内容defget_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内容returnresponse.text# 定义一个函数,从HTML内容中提取视频标题,并将其写入数据库defextract_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) # 遍历每个标题,将其插入数据表中fortitleintitles: cursor.execute("INSERT INTO videos VALUES (?)", (title,)) # 提交事务,关闭连接conn.commit() conn.close() # 定义一个函数,计算数据库中的数据总量,并打印结果defcount_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操作defmain(): # 定义关键词和页码范围keyword="汽车 冰淇淋"pages=range(1, 11) # 创建一个空列表,用于存储线程对象threads= [] # 遍历每个页码,创建一个线程对象,执行get_html和extract_and_save函数,并将其添加到列表中forpageinpages: thread=threading.Thread(target=lambda: extract_and_save(get_html(keyword, page))) thread.start() threads.append(thread) # 等待所有线程结束forthreadinthreads: thread.join() # 调用count_and_print函数,计算并打印数据总量count_and_print() # 调用主函数if__name__=="__main__": main()
总体来说,这段代码使用了多线程技术,使用多个线程并发地访问B站的搜索结果页面,提取其中的视频标题,并将其写入数据库,将网络请求和数据库操作分别放到不同的线程中执行,从而实现了快速爬取和处理大量数据的目的。同时,该代码还使用了爬虫代理IP,提高了爬虫的稳定性和安全性。