经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Python » 查看文章
Python?多线程爬取案例
来源:jb51  时间:2022/8/16 17:27:34  对本文有异议

前言

简单的爬虫只有一个进程、一个线程,因此称为??单线程爬虫??。单线程爬虫每次只访问一个页面,不能充分利用计算机的网络带宽。一个页面最多也就几百KB,所以爬虫在爬取一个页面的时候,多出来的网速和从发起请求到得到源代码中间的时间都被浪费了。如果可以让爬虫同时访问10个页面,就相当于爬取速度提高了10倍。为了达到这个目的,就需要使用??多线程技术??了。

微观上的单线程,在宏观上就像同时在做几件事。这种机制在 ??I/O(Input/Output,输入/输出)密集型的操作??上影响不大,但是在??CPU计算密集型的操作??上面,由于只能使用CPU的一个核,就会对性能产生非常大的影响。所以涉及计算密集型的程序,就需要使用多进程。

爬虫属于I/O密集型的程序,所以使用多线程可以大大提高爬取效率。

一、多进程库(multiprocessing)

??multiprocessing?? 本身是??Python的多进程库??,用来处理与多进程相关的操作。但是由于进程与进程之间不能直接共享内存和堆栈资源,而且启动新的进程开销也比线程大得多,因此使用多线程来爬取比使用多进程有更多的优势。

multiprocessing下面有一个??dummy模块?? ,它可以让Python的线程使用multiprocessing的各种方法。

dummy下面有一个??Pool类?? ,它用来实现线程池。这个线程池有一个??map()方法??,可以让线程池里面的所有线程都“同时”执行一个函数

测试案例     计算0~9的每个数的平方

  1. # 循环
  2. for i in range(10):
  3. print(i ** i)

也许你的第一反应会是上面这串代码,循环不就行了吗?反正就10个数!

这种写法当然可以得到结果,但是代码是一个数一个数地计算,效率并不高。而如果使用多线程的技术,让代码同时计算很多个数的平方,就需要使用 ??multiprocessing.dummy?? 来实现:

  1. from multiprocessing.dummy import Pool
  2.  
  3. # 平方函数
  4. def calc_power2(num):
  5. return num * num
  6.  
  7. # 定义三个线程池
  8. pool = Pool(3)
  9. # 定义循环数
  10. origin_num = [x for x in range(10)]
  11. # 利用map让线程池中的所有线程‘同时'执行calc_power2函数
  12. result = pool.map(calc_power2, origin_num)
  13. print(f'计算1-10的平方分别为:{result}')

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

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

二、多线程爬虫

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

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

  1. import time
  2. import requests
  3. from multiprocessing.dummy import Pool
  4.  
  5. # 自定义函数
  6. def query(url):
  7. requests.get(url)
  8.  
  9. start = time.time()
  10. for i in range(100):
  11. query('https://www.csdn.net/')
  12. end = time.time()
  13. print(f'单线程循环访问100次CSDN,耗时:{end - start}')
  14.  
  15. start = time.time()
  16. url_list = []
  17. for i in range(100):
  18. url_list.append('https://www.csdn.net/')
  19. pool = Pool(5)
  20. pool.map(query, url_list)
  21. end = time.time()
  22. print(f'5线程访问100次CSDN,耗时:{end - start}')

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

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

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

三、案例实操

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

  1. import re
  2. import os
  3. import requests
  4. from multiprocessing.dummy import Pool
  5.  
  6. # 爬取的主网站地址
  7. start_url = 'https://www.kanunu8.com/book2/11138/'
  8. """
  9. 获取网页源代码
  10. :param url: 网址
  11. :return: 网页源代码
  12. """
  13. def get_source(url):
  14. html = requests.get(url)
  15. return html.content.decode('gbk') # 这个网页需要使用gbk方式解码才能让中文正常显示
  16.  
  17. """
  18. 获取每一章链接,储存到一个列表中并返回
  19. :param html: 目录页源代码
  20. :return: 每章链接
  21. """
  22. def get_article_url(html):
  23. article_url_list = []
  24. article_block = re.findall('正文(.*?)<div class="clear">', html, re.S)[0]
  25. article_url = re.findall('<a href="(\d*.html)" rel="external nofollow" rel="external nofollow" >', article_block, re.S)
  26. for url in article_url:
  27. article_url_list.append(start_url + url)
  28. return article_url_list
  29.  
  30. """
  31. 获取每一章的正文并返回章节名和正文
  32. :param html: 正文源代码
  33. :return: 章节名,正文
  34. """
  35. def get_article(html):
  36. chapter_name = re.findall('<h1>(.*?)<br>', html, re.S)[0]
  37. text_block = re.search('<p>(.*?)</p>', html, re.S).group(1)
  38. text_block = text_block.replace('?', '') # 替换 ? 网页空格符
  39. text_block = text_block.replace('<p>', '') # 替换 <p></p> 中的嵌入的 <p></p> 中的 <p>
  40. return chapter_name, text_block
  41.  
  42. """
  43. 将每一章保存到本地
  44. :param chapter: 章节名, 第X章
  45. :param article: 正文内容
  46. :return: None
  47. """
  48. def save(chapter, article):
  49. os.makedirs('北欧众神', exist_ok=True) # 如果没有"北欧众神"文件夹,就创建一个,如果有,则什么都不做"
  50. with open(os.path.join('北欧众神', chapter + '.txt'), 'w', encoding='utf-8') as f:
  51. f.write(article)
  52.  
  53. """
  54. 根据正文网址获取正文源代码,并调用get_article函数获得正文内容最后保存到本地
  55. :param url: 正文网址
  56. :return: None
  57. """
  58. def query_article(url):
  59. article_html = get_source(url)
  60. chapter_name, article_text = get_article(article_html)
  61. # print(chapter_name)
  62. # print(article_text)
  63. save(chapter_name, article_text)
  64.  
  65. if __name__ == '__main__':
  66. toc_html = get_source(start_url)
  67. toc_list = get_article_url(toc_html)
  68. pool = Pool(4)
  69. pool.map(query_article, toc_list)

四、案例解析

1、获取网页内容

  1. # 爬取的主网站地址
  2. start_url = 'https://www.kanunu8.com/book2/11138/'
  3. def get_source(url):
  4. html = requests.get(url)
  5. return html.content.decode('gbk') # 这个网页需要使用gbk方式解码才能让中文正常显示

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

2、获取每一章链接

  1. def get_article_url(html):
  2. article_url_list = []
  3. # 根据正文锁定每一章节的链接区域
  4. article_block = re.findall('正文(.*?)<div class="clear">', html, re.S)[0]
  5. # 获取到每一章的链接
  6. article_url = re.findall('<a href="(\d*.html)" rel="external nofollow" rel="external nofollow" >', article_block, re.S)
  7. for url in article_url:
  8. article_url_list.append(start_url + url)
  9. return

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

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

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

  1. def get_article(html):
  2. chapter_name = re.findall('<h1>(.*?)<br>', html, re.S)[0]
  3. text_block = re.search('<p>(.*?)</p>', html, re.S).group(1)
  4. text_block = text_block.replace('?', '') # 替换 ? 网页空格符
  5. text_block = text_block.replace('<p>', '') # 替换 <p></p> 中的嵌入的 <p></p> 中的 <p>
  6. return chapter_name,

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

格式化后:

4、将每一章保存到本地

  1. """
  2. 将每一章保存到本地
  3. :param chapter: 章节名, 第X章
  4. :param article: 正文内容
  5. :return: None
  6. """
  7. def save(chapter, article):
  8. os.makedirs('北欧众神', exist_ok=True) # 如果没有"北欧众神"文件夹,就创建一个,如果有,则什么都不做"
  9. with open(os.path.join('北欧众神', chapter + '.txt'), 'w', encoding='utf-8') as f:
  10. f.write(article)

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

5、多线程爬取文章

  1. """
  2. 根据正文网址获取正文源代码,并调用get_article函数获得正文内容最后保存到本地
  3. :param url: 正文网址
  4. :return: None
  5. """
  6. def query_article(url):
  7. article_html = get_source(url)
  8. chapter_name, article_text = get_article(article_html)
  9. # print(chapter_name)
  10. # print(article_text)
  11. save(chapter_name, article_text)
  12.  
  13. if __name__ == '__main__':
  14. toc_html = get_source(start_url)
  15. toc_list = get_article_url(toc_html)
  16. pool = Pool(4)
  17. pool.map(query_article, toc_list)

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

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

最后运行程序即可!

到此这篇关于Python 多线程爬取案例的文章就介绍到这了,更多相关Python 多线程爬取内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号