目录
前言一、多进程库(multiprocessing)二、多线程爬虫三、案例实操四、案例解析1、获取网页内容2、获取每一章链接3、获取每一章的正文并返回章节名和正文4、将每一章保存到本地5、多线程爬取文章前言
简单的爬虫只有一个进程、一个线程,因此称为单线程爬虫
。单线程爬虫每次只访问一个页面,不能充分利用计算机的网络带宽。一个页面最多也就几百KB,所以爬虫在爬取一个页面的时候,多出来的网速和从发起请求到得到源代码中间的时间都被浪费了。如果可以让爬虫同时访问10个页面,就相当于爬取速度提高了10倍。为了达到这个目的,就需要使用多线程技术
了。
微观上的单线程,在宏观上就像同时在做几件事。这种机制在I/O(Input/Output,输入/输出)密集型的操作
上影响不大,但是在CPU计算密集型的操作
上面,由于只能使用CPU的一个核,就会对性能产生非常大的影响。所以涉及计算密集型的程序,就需要使用多进程。
(资料图片)
爬虫属于I/O密集型的程序,所以使用多线程可以大大提高爬取效率。
一、多进程库(multiprocessing)
multiprocessing
本身是Python的多进程库
,用来处理与多进程相关的操作。但是由于进程与进程之间不能直接共享内存和堆栈资源,而且启动新的进程开销也比线程大得多,因此使用多线程来爬取比使用多进程有更多的优势。
multiprocessing下面有一个dummy模块
,它可以让Python的线程使用multiprocessing的各种方法。
dummy下面有一个Pool类
,它用来实现线程池。这个线程池有一个map()方法
,可以让线程池里面的所有线程都“同时”执行一个函数。
测试案例计算0~9的每个数的平方
# 循环 for i in range(10): print(i ** i)
也许你的第一反应会是上面这串代码,循环不就行了吗?反正就10个数!
这种写法当然可以得到结果,但是代码是一个数一个数地计算,效率并不高。而如果使用多线程的技术,让代码同时计算很多个数的平方,就需要使用multiprocessing.dummy
来实现:
from multiprocessing.dummy import Pool # 平方函数 def calc_power2(num): return num * num # 定义三个线程池 pool = Pool(3) # 定义循环数 origin_num = [x for x in range(10)] # 利用map让线程池中的所有线程‘同时"执行calc_power2函数 result = pool.map(calc_power2, origin_num) print(f"计算1-10的平方分别为:{result}")
在上面的代码中,先定义了一个函数用来计算平方,然后初始化了一个有3个线程的线程池。这3个线程负责计算10个数字的平方,谁先计算完手上的这个数,谁就先取下一个数继续计算,直到把所有的数字都计算完成为止。
在这个例子中,线程池的map()
方法接收两个参数,第1个参数是函数名,第2个参数是一个列表。注意:第1个参数仅仅是函数的名字,是不能带括号的。第2个参数是一个可迭代的对象,这个可迭代对象里面的每一个元素都会被函数clac_power2()
接收来作为参数。除了列表以外,元组、集合或者字典都可以作为map()
的第2个参数。
二、多线程爬虫
由于爬虫是I/O密集型
的操作,特别是在请求网页源代码的时候,如果使用单线程来开发,会浪费大量的时间来等待网页返回,所以把多线程技术应用到爬虫中,可以大大提高爬虫的运行效率。
下面通过两段代码来对比单线程爬虫和多线程爬虫爬取CSDN首页
的性能差异:
import time import requests from multiprocessing.dummy import Pool # 自定义函数 def query(url): requests.get(url) start = time.time() for i in range(100): query("https://www.csdn.net/") end = time.time() print(f"单线程循环访问100次CSDN,耗时:{end - start}") start = time.time() url_list = [] for i in range(100): url_list.append("https://www.csdn.net/") pool = Pool(5) pool.map(query, url_list) end = time.time() print(f"5线程访问100次CSDN,耗时:{end - start}")
从运行结果可以看到,一个线程用时约69.4s
,5个线程用时约14.3s
,时间是单线程的五分之一
左右。从时间上也可以看到5个线程“同时运行”的效果。
但并不是说线程池设置得越大越好。从上面的结果也可以看到,5个线程运行的时间其实比一个线程运行时间的五分之一(13.88s
)要多一点。这多出来的一点其实就是线程切换的时间。这也从侧面反映了Python的多线程在微观上还是串行的。
因此,如果线程池设置得过大,线程切换导致的开销可能会抵消多线程带来的性能提升。线程池的大小需要根据实际情况来确定,并没有确切的数据。
三、案例实操
从https://www.kanunu8.com/book2/11138/爬取
《北欧众神》
所有章节的网址,再通过一个多线程爬虫将每一章的内容爬取下来。在本地创建一个“北欧众神”文件夹,并将小说中的每一章分别保存到这个文件夹中,且每一章保存为一个文件。
import re import os import requests from multiprocessing.dummy import Pool # 爬取的主网站地址 start_url = "https://www.kanunu8.com/book2/11138/" """ 获取网页源代码 :param url: 网址 :return: 网页源代码 """ def get_source(url): html = requests.get(url) return html.content.decode("gbk") # 这个网页需要使用gbk方式解码才能让中文正常显示 """ 获取每一章链接,储存到一个列表中并返回 :param html: 目录页源代码 :return: 每章链接 """ def get_article_url(html): article_url_list = [] article_block = re.findall("正文(.*?)", html, re.S)[0] article_url = re.findall("", article_block, re.S) for url in article_url: article_url_list.append(start_url + url) return article_url_list """ 获取每一章的正文并返回章节名和正文 :param html: 正文源代码 :return: 章节名,正文 """ def get_article(html): chapter_name = re.findall("(.*?)
", html, re.S)[0] text_block = re.search("(.*?)
", html, re.S).group(1) text_block = text_block.replace(" ", "") # 替换 网页空格符 text_block = text_block.replace("", "") # 替换
中的嵌入的 中的return chapter_name, text_block """ 将每一章保存到本地 :param chapter: 章节名, 第X章 :param article: 正文内容 :return: None """ def save(chapter, article): os.makedirs("北欧众神", exist_ok=True) # 如果没有"北欧众神"文件夹,就创建一个,如果有,则什么都不做" with open(os.path.join("北欧众神", chapter + ".txt"), "w", encoding="utf-8") as f: f.write(article) """ 根据正文网址获取正文源代码,并调用get_article函数获得正文内容最后保存到本地 :param url: 正文网址 :return: None """ def query_article(url): article_html = get_source(url) chapter_name, article_text = get_article(article_html) # print(chapter_name) # print(article_text) save(chapter_name, article_text) if __name__ == "__main__": toc_html = get_source(start_url) toc_list = get_article_url(toc_html) pool = Pool(4) pool.map(query_article, toc_list)
四、案例解析
1、获取网页内容
# 爬取的主网站地址 start_url = "https://www.kanunu8.com/book2/11138/" def get_source(url): html = requests.get(url) return html.content.decode("gbk") # 这个网页需要使用gbk方式解码才能让中文正常显示这一部分并不难,主要就是指明需要爬取的网站,并通过
request.get()
的请求方式获取网站,在通过content.decode()
获取网页的解码内容,其实就是获取网页的源代码。2、获取每一章链接
def get_article_url(html): article_url_list = [] # 根据正文锁定每一章节的链接区域 article_block = re.findall("正文(.*?)
X 关闭
X 关闭
- 1亚马逊开始大规模推广掌纹支付技术 顾客可使用“挥手付”结账
- 2现代和起亚上半年出口20万辆新能源汽车同比增长30.6%
- 3如何让居民5分钟使用到各种设施?沙特“线性城市”来了
- 4AMD实现连续8个季度的增长 季度营收首次突破60亿美元利润更是翻倍
- 5转转集团发布2022年二季度手机行情报告:二手市场“飘香”
- 6充电宝100Wh等于多少毫安?铁路旅客禁止、限制携带和托运物品目录
- 7好消息!京东与腾讯续签三年战略合作协议 加强技术创新与供应链服务
- 8名创优品拟通过香港IPO全球发售4100万股 全球发售所得款项有什么用处?
- 9亚马逊云科技成立量子网络中心致力解决量子计算领域的挑战
- 10京东绿色建材线上平台上线 新增用户70%来自下沉市场