Python爬虫:单线程、多线程和协程的爬虫性能对比!_Python-77的博客-程序员ITS304

技术标签: 爬虫  python  笔记  

大家好,我是漆柒七7!

首先见面礼
Python学习大礼包 点击领取

然后今天我要给大家分享的是如何爬取豆瓣上深圳近期即将上映的电影影讯,并分别用普通的单线程、多线程和协程来爬取,从而对比单线程、多线程和协程在网络爬虫中的性能。

具体要爬的网址是:https://movie.douban.com/cinema/later/shenzhen/

除了要爬入口页以外还需爬取每个电影的详情页,具体要爬取的结构信息如下:
在这里插入图片描述
在这里插入图片描述
爬取测试

下面我演示使用xpath解析数据。
入口页数据读取:

import requests
from lxml import etree
import pandas as pd
import re

main_url = "https://movie.douban.com/cinema/later/shenzhen/"
headers = {
    
    "Accept-Encoding": "Gzip",
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}
r = requests.get(main_url, headers=headers)
r

结果:

<Response [200]>

检查一下所需数据的xpath:
在这里插入图片描述
可以看到每个电影信息都位于id为showing-soon下面的div里面,再分别分析内部的电影名称、url和想看人数所处的位置,于是可以写出如下代码:

html = etree.HTML(r.text)
all_movies = html.xpath("//div[@id='showing-soon']/div")
result = []
for e in all_movies:
    #  imgurl, = e.xpath(".//img/@src")
    name, = e.xpath(".//div[@class='intro']/h3/a/text()")
    url, = e.xpath(".//div[@class='intro']/h3/a/@href")
    # date, movie_type, pos = e.xpath(".//div[@class='intro']/ul/li[@class='dt']/text()")
    like_num, = e.xpath(
        ".//div[@class='intro']/ul/li[@class='dt last']/span/text()")
    result.append((name, int(like_num[:like_num.find("人")]), url))
main_df = pd.DataFrame(result, columns=["影名", "想看人数", "url"])
main_df

结果:
在这里插入图片描述
然后再选择一个详情页的url进行测试,我选择了熊出没·狂野大陆这部电影,因为文本数据相对最复杂,也最具备代表性:

url = main_df.at[17, "url"]
url

结果:

'https://movie.douban.com/subject/34825886/'

分析详情页结构:
在这里插入图片描述
文本信息都在这个位置中,下面我们直接提取这个div下面的所有文本节点:

r = requests.get(url, headers=headers)
html = etree.HTML(r.text)
movie_infos = html.xpath("//div[@id='info']//text()")
print(movie_infos)

结果:

  导演: 丁亮
        编剧: 徐芸 / 崔铁志 / 张宇
        主演: 张伟 / 张秉君 / 谭笑
        类型: 喜剧 / 科幻 / 动画
        
        制片国家/地区: 中国大陆
        语言: 汉语普通话
        上映日期: 2021-02-12(中国大陆) / 2020-08-01(上海电影节)
        片长: 100分钟
        又名: 熊出没大电影7 / 熊出没科幻大电影 / Boonie Bears: The Wild Life
        IMDb链接: tt11654032

接下来就简单了:

row = {
    }
for line in re.split("[\n ]*\n[\n ]*", movie_info_txt):
    line = line.strip()
    arr = line.split(": ", maxsplit=1)
    if len(arr) != 2:
        continue
    k, v = arr
    row[k] = v
row

结果:

{
    '导演': '丁亮',
 '编剧': '徐芸 / 崔铁志 / 张宇',
 '主演': '张伟 / 张秉君 / 谭笑',
 '类型': '喜剧 / 科幻 / 动画',
 '制片国家/地区': '中国大陆',
 '语言': '汉语普通话',
 '上映日期': '2021-02-12(中国大陆) / 2020-08-01(上海电影节)',
 '片长': '100分钟',
 '又名': '熊出没大电影7 / 熊出没科幻大电影 / Boonie Bears: The Wild Life',
 'IMDb链接': 'tt11654032'}

可以看到成功的切割出了每一项。

下面根据上面的测试基础,我们完善整体的爬虫代码:

单线程爬虫

import requests
from lxml import etree
import pandas as pd
import re

main_url = "https://movie.douban.com/cinema/later/shenzhen/"
headers = {
    
    "Accept-Encoding": "Gzip",
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}
r = requests.get(main_url, headers=headers)
html = etree.HTML(r.text)
all_movies = html.xpath("//div[@id='showing-soon']/div")
result = []
for e in all_movies:
    imgurl, = e.xpath(".//img/@src")
    name, = e.xpath(".//div[@class='intro']/h3/a/text()")
    url, = e.xpath(".//div[@class='intro']/h3/a/@href")
    print(url)
#     date, movie_type, pos = e.xpath(".//div[@class='intro']/ul/li[@class='dt']/text()")
    like_num, = e.xpath(
        ".//div[@class='intro']/ul/li[@class='dt last']/span/text()")
    r = requests.get(url, headers=headers)
    html = etree.HTML(r.text)
    row = {
    }
    row["电影名称"] = name
    for line in re.split("[\n ]*\n[\n ]*", "".join(html.xpath("//div[@id='info']//text()")).strip()):
        line = line.strip()
        arr = line.split(": ", maxsplit=1)
        if len(arr) != 2:
            continue
        k, v = arr
        row[k] = v
    row["想看人数"] = int(like_num[:like_num.find("人")])
#     row["url"] = url
#     row["图片地址"] = imgurl
#     print(row)
    result.append(row)
df = pd.DataFrame(result)
df.sort_values("想看人数", ascending=False, inplace=True)
df.to_csv("shenzhen_movie.csv", index=False)

结果:

https://movie.douban.com/subject/26752564/
https://movie.douban.com/subject/35172699/
https://movie.douban.com/subject/34992142/
https://movie.douban.com/subject/30349667/
https://movie.douban.com/subject/30283209/
https://movie.douban.com/subject/33457717/
https://movie.douban.com/subject/30487738/
https://movie.douban.com/subject/35068230/
https://movie.douban.com/subject/27039358/
https://movie.douban.com/subject/30205667/
https://movie.douban.com/subject/30476403/
https://movie.douban.com/subject/30154423/
https://movie.douban.com/subject/27619748/
https://movie.douban.com/subject/26826330/
https://movie.douban.com/subject/26935283/
https://movie.douban.com/subject/34841067/
https://movie.douban.com/subject/34880302/
https://movie.douban.com/subject/34825886/
https://movie.douban.com/subject/34779692/
https://movie.douban.com/subject/35154209/

爬到的文件:
在这里插入图片描述
整体耗时:
在这里插入图片描述
42.5秒。

多线程爬虫

单线程的爬取耗时还是挺长的,下面看看使用多线程的爬取效率:

import requests
from lxml import etree
import pandas as pd
import re
from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED


def fetch_content(url):
    print(url)
    headers = {
    
        "Accept-Encoding": "Gzip",  # 使用gzip压缩传输数据让访问更快
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
    }
    r = requests.get(url, headers=headers)
    return r.text


url = "https://movie.douban.com/cinema/later/shenzhen/"
init_page = fetch_content(url)
html = etree.HTML(init_page)
all_movies = html.xpath("//div[@id='showing-soon']/div")
result = []
for e in all_movies:
#     imgurl, = e.xpath(".//img/@src")
    name, = e.xpath(".//div[@class='intro']/h3/a/text()")
    url, = e.xpath(".//div[@class='intro']/h3/a/@href")
#     date, movie_type, pos = e.xpath(".//div[@class='intro']/ul/li[@class='dt']/text()")
    like_num, = e.xpath(
        ".//div[@class='intro']/ul/li[@class='dt last']/span/text()")
    result.append((name, int(like_num[:like_num.find("人")]), url))
main_df = pd.DataFrame(result, columns=["影名", "想看人数", "url"])

max_workers = main_df.shape[0]
with ThreadPoolExecutor(max_workers=max_workers) as executor:
    future_tasks = [executor.submit(fetch_content, url) for url in main_df.url]
    wait(future_tasks, return_when=ALL_COMPLETED)
    pages = [future.result() for future in future_tasks]

result = []
for url, html_text in zip(main_df.url, pages):
    html = etree.HTML(html_text)
    row = {
    }
    for line in re.split("[\n ]*\n[\n ]*", "".join(html.xpath("//div[@id='info']//text()")).strip()):
        line = line.strip()
        arr = line.split(": ", maxsplit=1)
        if len(arr) != 2:
            continue
        k, v = arr
        row[k] = v
    row["url"] = url
    result.append(row)
detail_df = pd.DataFrame(result)
df = main_df.merge(detail_df, on="url")
df.drop(columns=["url"], inplace=True)
df.sort_values("想看人数", ascending=False, inplace=True)
df.to_csv("shenzhen_movie2.csv", index=False)
df

结果:
在这里插入图片描述
在这里插入图片描述
耗时8秒。

由于每个子页面都是单独的线程爬取,每个线程几乎都是同时在工作,所以最终耗时仅取决于爬取最慢的子页面。

协程异步爬虫

由于我在jupyter中运行,为了使协程能够直接在jupyter中直接运行,所以我在代码中增加了下面两行代码,在普通编辑器里面可以去掉:

import nest_asyncio
nest_asyncio.apply()

这个问题是因为jupyter所依赖的高版本Tornado存在bug,将Tornado退回到低版本也可以解决这个问题。

下面我使用协程来完成这个需求的爬取:

import aiohttp
from lxml import etree
import pandas as pd
import re
import asyncio
import nest_asyncio
nest_asyncio.apply()


async def fetch_content(url):
    print(url)
    header = {
    
        "Accept-Encoding": "Gzip",  # 使用gzip压缩传输数据让访问更快
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
    }
    async with aiohttp.ClientSession(
        headers=header, connector=aiohttp.TCPConnector(ssl=False)
    ) as session:
        async with session.get(url) as response:
            return await response.text()


async def main():
    url = "https://movie.douban.com/cinema/later/shenzhen/"
    init_page = await fetch_content(url)
    html = etree.HTML(init_page)
    all_movies = html.xpath("//div[@id='showing-soon']/div")
    result = []
    for e in all_movies:
        #         imgurl, = e.xpath(".//img/@src")
        name, = e.xpath(".//div[@class='intro']/h3/a/text()")
        url, = e.xpath(".//div[@class='intro']/h3/a/@href")
    #     date, movie_type, pos = e.xpath(".//div[@class='intro']/ul/li[@class='dt']/text()")
        like_num, = e.xpath(
            ".//div[@class='intro']/ul/li[@class='dt last']/span/text()")
        result.append((name, int(like_num[:like_num.find("人")]), url))
    main_df = pd.DataFrame(result, columns=["影名", "想看人数", "url"])

    tasks = [fetch_content(url) for url in main_df.url]
    pages = await asyncio.gather(*tasks)

    result = []
    for url, html_text in zip(main_df.url, pages):
        html = etree.HTML(html_text)
        row = {
    }
        for line in re.split("[\n ]*\n[\n ]*", "".join(html.xpath("//div[@id='info']//text()")).strip()):
            line = line.strip()
            arr = line.split(": ", maxsplit=1)
            if len(arr) != 2:
                continue
            k, v = arr
            row[k] = v
        row["url"] = url
        result.append(row)
    detail_df = pd.DataFrame(result)
    df = main_df.merge(detail_df, on="url")
    df.drop(columns=["url"], inplace=True)
    df.sort_values("想看人数", ascending=False, inplace=True)
    return df

df = asyncio.run(main())
df.to_csv("shenzhen_movie3.csv", index=False)
df

结果:
在这里插入图片描述
耗时仅7秒,相对比多线程更快一点。

由于request库不支持协程,所以我使用了支持协程的aiohttp进行页面抓取。当然实际爬取的耗时还取绝于当时的网络,但整体来说,协程爬取会比多线程爬虫稍微快一些。

回顾

今天我向你演示了,单线程爬虫、多线程爬虫和协程爬虫。可以看到一般情况下协程爬虫速度最快,多线程爬虫略慢一点,单线程爬虫则必须上一个页面爬取完成才能继续爬取。

但协程爬虫相对来说并不是那么好编写,数据抓取无法使用request库,只能使用aiohttp。所以在实际编写爬虫时,我们一般都会使用多线程爬虫来提速,但必须注意的是网站都有ip访问频率限制,爬的过快可能会被封ip,所以一般我们在多线程提速的同时使用代理ip来并发的爬取数据。

彩蛋:xpath+pandas解析表格并提取url

我们在深圳影讯的底部能够看到一个[查看全部即将上映的影片] (https://movie.douban.com/coming)的按钮,点进去能够看到一张完整近期上映电影的列表,发现这个列表是个table标签的数据:
在这里插入图片描述
那就简单了,解析table我们可能压根就不需要用xpath,直接用pandas即可,但片名中包含的url地址还需解析,所以我采用xpath+pandas来解析这个网页,看看我的代码吧:

import pandas as pd
import requests
from lxml import etree

headers = {
    
    "Accept-Encoding": "Gzip",
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}
r = requests.get("https://movie.douban.com/coming", headers=headers)
html = etree.HTML(r.text)
table_tag = html.xpath("//table")[0]
df, = pd.read_html(etree.tostring(table_tag))
urls = table_tag.xpath(".//td[2]/a/@href")
df["url"] = urls
df

结果:
在这里插入图片描述
这样就能到了主页面的完整数据,再简单的处理一下即可。

结语
感谢各位读者,有什么想法和收获欢迎留言评论噢!

Python学习大礼包 点击领取

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m0_52915161/article/details/113921212

智能推荐

Vue 模板是如何编译的_前端同学的博客-程序员ITS304_vue模板编译

认识模板编译我们知道 &lt;template&gt;&lt;/template&gt; 这个是模板,不是真实的 HTML,浏览器是不认识模板的,所以我们需要把它编译成浏览器认识的原生的 HTML这一块的主要流程就是提取出模板中的原生 HTML 和非原生 HTML,比如绑定的属性、事件、指令等等经过一些处理生成 render 函数render 函数再将模板内容生成对应的 vnode再经过 patch 过程( Diff )得到要渲染到视图中的 vnode最后根据 vnode 创建真实的 DOM

sota结果是什么意思_只有达到SOTA的方法才能发文章吗_weixin_39743369的博客-程序员ITS304

最近关注了一个问题:只有达到 state of the art 精度的方法才能发文章吗?得赞同最多的回答分享了ICML审稿人的话,说的非常好:The academic is not an army race. It does not really matter how fancy the model is. It does not really matter whether the model c...

浙江工商大学和杭电计算机哪个好,浙江工业大学、杭电、浙江理工大学、浙江工商大学哪所值得报考?..._Clarlie的博客-程序员ITS304

有人问,浙江工业大学、杭州电子科技大学、浙江工商大学和浙江理工大学,哪个学校更值得选择?这4所大学都是浙江省的普通高校。不过,这几所大学的实力并不太弱,也许要好于部分211大学。原因是,浙江的高等教育跟它的教育强省地位不符合。浙江只有浙大一所985兼211大学。在双一流名单里,也只有3所学校上榜,除了浙大,还有宁波大学和中国美术学院。中国美院实力很强,但属于艺术类学校,对大部分考生不适合。综合性院...

[SPI]SPI协议详解_weixin_30539625的博客-程序员ITS304

转自:https://my.oschina.net/freeblues/blog/674001.SPI协议简介1.1.SPI协议概括  SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信...

JDK、JRE和JVM之间的关系_Sun-yz的博客-程序员ITS304_jdk jre jvm三者之间的关系

作为一个Java开发者,只会用Java,却不知什么是JDK、JRE和JVM是什么,以及他们之间有什么联系。本文总结了JDK,JRE,JVM三者的关系与区别。JDK、JRE和JVM之间的关系一、JDK二、JRE三、JVM四、三者的联系五、三者的区别六、总结一、JDKJDK是Java开发工具包,其中包括编译工具(javac.exe)打包工具(jar.exe)等,也包括JRE。在JDK的安装目录下有一个jre目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工.

初进csdn,写点什么!!!_知知脏的博客-程序员ITS304

以前都是在本站上面当伸手党,我已经觉悟了,要回馈了!!!!!从今往后,我会把自己所总结/踩过的坑,分享出来,让大家遇到和我同样困难的时候,也可以少走弯路,一起成长学习。小白刚上路,大神勿喷!!!!!!!...

随便推点

2021-07-15_sinat_29805979的博客-程序员ITS304

PYQT的使用:一、PYQT的环境搭建:系统环境:centos7开发环境:PYQT5.6学习时间:提示:这里可以添加计划学习的时间例如:1、 周一至周五晚上 7 点—晚上9点2、 周六上午 9 点-上午 11 点3、 周日下午 3 点-下午 6 点学习产出:提示:这里统计学习计划的总量例如:1、 技术笔记 2 遍2、CSDN 技术博客 3 篇3、 学习的 vlog 视频 1 个...

SQL-DAY 9(SQL应用案例:电商用户、商品、平台价值分析)_范儿札记的博客-程序员ITS304

文章目录1.项目背景1.1 分析流程1.项目背景   随着电商⾏业近⼏年的迅猛发展,电⼦商务从早些年的粗放式经营,逐步转化为精细化运营。随着平台数据量的不断积累,通过数据分析挖掘消费者的潜在需求,消费偏好成为平台运营过程中的重要环节。本项⽬基于某电商平台⽤户⾏为数据,在MySQL关系型数据库,探索⽤户⾏为规律,寻找⾼价值⽤户;分析商品特征,寻找⾼贡献商品;分析产品功能,优化产品路径。1.1 分析流程...

关于远程桌面登陆提示“连接被拒绝,因为没有授权此用户账号进行远程登录”_绿萝哥哥的博客-程序员ITS304_远程桌面被拒绝

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 继续跟着教材做实验。在

gpt分区android系统备份,装系统时提示目标分区是动态磁盘的gpt分区,需要在pe环境下进行备份或还原怎么解决..._赖振波的博客-程序员ITS304

很多用户在重装系统的时候,都喜欢借助一些工具来重装,比如一键重装工具,但是有用户在使用一键重装工具装系统时出现了“目标分区是动态磁盘的gpt分区,需要在pe环境下进行备份或还原”的提示,该怎么办呢,本文就给大家带来详细的解决方法。原因分析:因为在uefi gpt模式下,不能直接在本地硬盘通过Onekey ghost、SGI映像总裁、白云、黑云、小白等一键重装工具一键重装,需要用uefi pe启动盘...

jetpack 协程_Jetpack常用组件使用_weixin_39610956的博客-程序员ITS304

Jetpack是Google官方推出的一套Android库,它帮助开发者更方便、更快速的开发出稳健性极佳的软件,简化开发流程与提高效率。Jetpack库常用的如下几个组件,它们都可以单独使用或者组合使用:组建名称介绍Android KTXKotlin扩展程序,包括扩展函数、扩展属性、协程等AppCompat提供向后兼容性的Android组件WorkManager管理应用退出或设备重启时仍应运行等可...

touchgfx程序_TouchGFX使用教程_weixin_39882623的博客-程序员ITS304

TouchGFX使用教程(一)前言TouchGFX背景TouchGFX工程的结构后记前言大家好,本人最近在做项目开发,项目中使用TouchGFX工具进行开发界面,同时使用stm32系列的芯片操作系统使用的是FreeRTOS,之前没有接触过带有界面开发的stm32,也同样没有接触过TouchGFX界面开发工具,所以在开发过程中也遇见过一些坑,对于开发TouchGFX+stm32+stm32cubeM...