企业网站定制开发【Python 爬虫】多线程爬取


文章目录


前言

企业网站定制开发简单的爬虫只有一个进程、一个线程,因此称为企业网站定制开发单线程爬虫。企业网站定制开发单线程爬虫每次只访问一个页面,企业网站定制开发不能充分利用计算机的网络带宽。企业网站定制开发一个页面最多也就几百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)
  • 1
  • 2
  • 3

也许你的第一反应会是上面这串代码,循环不就行了吗?反正就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}')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在上面的代码中,先定义了一个函数用来计算平方,然后初始化了一个有3个线程的线程池。这3个线程负责计算10个数字的平方,谁先计算完手上的这个数,谁就先取下一个数继续计算,直到把所有的数字都计算完成为止。

在这个例子中,线程池的 map() 方法接收两个参数,第1个参数是函数名,第2个参数是一个列表。注意:第1个参数仅仅是函数的名字,是不能带括号的。第2个参数是一个可迭代的对象,这个可迭代对象里面的每一个元素都会被函数 clac_power2() 接收来作为参数。除了列表以外,元组、集合或者字典都可以作为 map() 的第2个参数。


二、爬虫

由于爬虫是 I/O密集型 的操作,特别是在请求网页源代码的时候,如果使用单线程来开发,会浪费大量的时间来等待网页返回,所以把多线程技术应用到爬虫中,可以大大提高爬虫的运行效率。

下面通过两段代码来对比单线程爬虫和多线程爬虫爬取CSDN首页的性能差异:

import timeimport requestsfrom 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}')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22


从运行结果可以看到,一个线程用时约69.4s,5个线程用时约14.3s,时间是单线程的五分之一左右。从时间上也可以看到5个线程“同时运行”的效果。

但并不是说线程池设置得越大越好。从上面的结果也可以看到,5个线程运行的时间其实比一个线程运行时间的五分之一(13.88s)要多一点。这多出来的一点其实就是线程切换的时间。这也从侧面反映了Python的多线程在微观上还是串行的。

因此,如果线程池设置得过大,线程切换导致的开销可能会抵消多线程带来的性能提升。线程池的大小需要根据实际情况来确定,并没有确切的数据。


三、案例实操

从 爬取《北欧众神》所有章节的网址,再通过一个多线程爬虫将每一章的内容爬取下来。在本地创建一个“北欧众神”文件夹,并将小说中的每一章分别保存到这个文件夹中,且每一章保存为一个文件。

import reimport osimport requestsfrom 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('正文(.*?)<div class="clear">', html, re.S)[0]    article_url = re.findall('<a href="(\d*.html)">', 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('<h1>(.*?)<br>', html, re.S)[0]    text_block = re.search('<p>(.*?)</p>', html, re.S).group(1)    text_block = text_block.replace('&nbsp;', '')           # 替换 &nbsp; 网页空格符    text_block = text_block.replace('<p>', '')              # 替换 <p></p> 中的嵌入的 <p></p> 中的 <p>    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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69


四、案例解析

1、获取网页内容

# 爬取的主网站地址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方式解码才能让中文正常显示
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这一部分并不难,主要就是指明需要爬取的网站,并通过 request.get() 的请求方式获取网站,在通过 content.decode() 获取网页的解码内容,其实就是获取网页的源代码。


2、获取每一章链接

"""获取每一章链接,储存到一个列表中并返回:param html: 目录页源代码:return: 每章链接"""def get_article_url(html):    article_url_list = []    # 根据正文锁定每一章节的链接区域    article_block = re.findall('正文(.*?)<div class="clear">', html, re.S)[0]    # 获取到每一章的链接    article_url = re.findall('<a href="(\d*.html)">', article_block, re.S)    for url in article_url:        article_url_list.append(start_url + url)    return article_url_list
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这里需要获取到每一章的链接,首先我们根据正文锁定每一章节的链接区域,然后在链接区域中获取到每一章的链接,形成列表返回。

在获取每章链接的时候,通过页面源码可以发现均为数字开头.html结尾,于是利用正则 (\d*.html) 匹配即可:


3、获取每一章的正文并返回章节名和正文

"""获取每一章的正文并返回章节名和正文:param html: 正文源代码:return: 章节名,正文"""def get_article(html):    chapter_name = re.findall('<h1>(.*?)<br>', html, re.S)[0]    text_block = re.search('<p>(.*?)</p>', html, re.S).group(1)    text_block = text_block.replace('&nbsp;', '')           # 替换 &nbsp; 网页空格符    text_block = text_block.replace('<p>', '')              # 替换 <p></p> 中的嵌入的 <p></p> 中的 <p>    return chapter_name, text_block
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这里利用正则分别匹配出每章的标题和正文内容:

格式化后:


4、将每一章保存到本地

"""将每一章保存到本地: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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这里获取到我们处理好的文章标题及内容,并将其写入本地磁盘。首先创建文件夹,然后打开文件夹以 章节名+.txt 结尾存储每章内容。


5、多线程爬取文章

"""根据正文网址获取正文源代码,并调用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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

这里 query_article 调用 get_sourceget_article 函数获取以上分析的内容,再调用 save 函数进行本地存储,主入口main中创建线程池,包含4个线程。

map()方法,可以让线程池里面的所有线程都“同时”执行一个函数同时map() 方法接收两个参数,第1个参数是函数名,第2个参数是一个列表。这里我们需要对每一个章节进行爬取,所以应该是遍历章节链接的列表(调用 get_article_url 获取),执行 query_article 方法进行爬取保存。

最后运行程序即可!


网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发