当前位置:首页 > 互联网

Python多线程爬虫详解

花晨月夕12个月前 (05-20)互联网1790
网络爬虫程序是一种 IO 密集型程序,程序中涉及了很多网络 IO 以及本地磁盘 IO 操作,这些都会消耗大量的时间,从而降低程序的执行效率,而 Python 提供的多线程能够在一定程度上提升 IO 密集型程序的执行效率。如果想学习 Python 多进程、多线程以及 Python GIL 全局解释器锁…

网络爬虫程序是一种 IO 密集型程序,程序中涉及了很多网络 IO 以及本地磁盘 IO 操作,这些都会消耗大量的时间,从而降低程序的执行效率,而 Python 提供的多线程能够在一定程度上提升 IO 密集型程序的执行效率。

如果想学习 Python 多进程、多线程以及 Python GIL 全局解释器锁的相关知识,可参考《Python并发编程教程》。

多线程使用流程

Python 提供了两个支持多线程的模块,分别是 _thread 和 threading。其中 _thread 模块偏底层,它相比于 threading 模块功能有限,因此推荐大家使用 threading 模块。 threading 中不仅包含了  _thread 模块中的所有方法,还提供了一些其他方法,如下所示:

  • threading.currentThread() 返回当前的线程变量。

  • threading.enumerate() 返回一个所有正在运行的线程的列表。

  • threading.activeCount() 返回正在运行的线程数量。


线程的具体使用方法如下所示:

from threading import Thread#线程创建、启动、回收t = Thread(target=函数名) # 创建线程对象t.start() # 创建并启动线程t.join()  # 阻塞等待回收线程

创建多线程的具体流程:

t_list = []for i in range(5):t = Thread(target=函数名)t_list.append(t)t.start()for t in t_list:t.join()

除了使用该模块外,您也可以使用  Thread  线程类来创建多线程。

在处理线程的过程中要时刻注意线程的同步问题,即多个线程不能操作同一个数据,否则会造成数据的不确定性。通过 threading 模块的 Lock 对象能够保证数据的正确性。

比如,使用多线程将抓取数据写入磁盘文件,此时,就要对执行写入操作的线程加锁,这样才能够避免写入的数据被覆盖。当线程执行完写操作后会主动释放锁,继续让其他线程去获取锁,周而复始,直到所有写操作执行完毕。具体方法如下所示:

from threading import Locklock = Lock()# 获取锁lock.acquire()wirter.writerows("线程锁问题解决")# 释放锁lock.release()

Queue队列模型

对于 Python 多线程而言,由于 GIL 全局解释器锁的存在,同一时刻只允许一个线程占据解释器执行程序,当此线程遇到 IO 操作时就会主动让出解释器,让其他处于等待状态的线程去获取解释器来执行程序,而该线程则回到等待状态,这主要是通过线程的调度机制实现的。

由于上述原因,我们需要构建一个多线程共享数据的模型,让所有线程都到该模型中获取数据。queue(队列,先进先出) 模块提供了创建共享数据的队列模型。比如,把所有待爬取的 URL 地址放入队列中,每个线程都到这个队列中去提取 URL。queue 模块的具体使用方法如下:

# 导入模块from queue import Queueq = Queue() #创界队列对象q.put(url) 向队列中添加爬取一个url链接q.get() # 获取一个url,当队列为空时,阻塞q.empty() # 判断队列是否为空,True/False

多线程爬虫案例

下面通过多线程方法抓取小米应用商店(https://app.mi.com/)中应用分类一栏,所有类别下的 APP 的名称、所属类别以及下载详情页 URL 。如下图所示:

多线程爬虫
图1:小米应用商城

抓取下来的数据 demo 如下所示:

三国杀,棋牌桌游,http://app.mi.com/details?id=com.bf.sgs.hdexp.mi

1) 案例分析

通过搜索关键字可知这是一个动态网站,因此需要抓包分析。

刷新网页来重新加载数据,可得知请求头的 URL 地址,如下所示:

https://app.mi.com/categotyAllListApi?page=0&categoryId=1&pageSize=30

其中查询参数 pageSize 参数值不变化,page 会随着页码的增加而变化,而类别 Id 通过查看页面元素,如下所示

<ul class="category-list"><li><a class="current" href="/category/15">游戏</a></li><li><a href="/category/5">实用工具</a></li><li><a href="/category/27">影音视听</a></li><li><a href="/category/2">聊天社交</a></li><li><a href="/category/7">图书阅读</a></li><li><a href="/category/12">学习教育</a></li><li><a href="/category/10">效率办公</a></li><li><a href="/category/9">时尚购物</a></li><li><a href="/category/4">居家生活</a></li><li><a href="/category/3">旅行交通</a></li><li><a href="/category/6">摄影摄像</a></li><li><a href="/category/14">医疗健康</a></li><li><a href="/category/8">体育运动</a></li><li><a href="/category/11">新闻资讯</a></li><li><a href="/category/13">娱乐消遣</a></li><li><a href="/category/1">金融理财</a></li></ul>

因此,可以使用 Xpath 表达式匹配 href 属性,从而提取类别 ID 以及类别名称,表达式如下:

基准表达式:xpath_bds = '//ul[@class="category-list"]/li'
提取 id 表达式:typ_id = li.xpath('./a/@href')[0].split('/')[-1]
类型名称:typ_name = li.xpath('./a/text()')[0]

点击开发者工具的 response 选项卡,查看响应数据,如下所示:

{
count: 2000,
data: [
{
appId: 1348407,
displayName: "天气暖暖-关心Ta从关心天气开始",
icon: "http://file.market.xiaomi.com/thumbnail/PNG/l62/AppStore/004ff4467a7eda75641eea8d38ec4d41018433d33",
level1CategoryName: "居家生活",
packageName: "com.xiaowoniu.WarmWeather"
},
{
appId: 1348403,
displayName: "贵斌同城",
icon: "http://file.market.xiaomi.com/thumbnail/PNG/l62/AppStore/0e607ac85ed9742d2ac2ec1094fca3a85170b15c8",
level1CategoryName: "居家生活",
packageName: "com.gbtc.guibintongcheng"
},
...
...

通过上述响应内容,我们可以从中提取出 APP 总数量(count)和 APP (displayName)名称,以及下载详情页的 packageName。由于每页中包含了 30 个 APP,所以总数量(count)可以计算出每个类别共有多少页。

pages = int(count) // 30 + 1

下载详情页的地址是使用 packageName 拼接而成,如下所示:

link = 'http://app.mi.com/details?id=' + app['packageName']

2) 完整程序

完整程序如下所示:

# -*- coding:utf8 -*-import requestsfrom threading import Threadfrom queue import Queueimport timefrom fake_useragent import UserAgentfrom lxml import etreeimport csvfrom threading import Lockimport jsonclass XiaomiSpider(object):def __init__(self):self.url = 'http://app.mi.com/categotyAllListApi?page={}&categoryId={}&pageSize=30'# 存放所有URL地址的队列self.q = Queue()self.i = 0# 存放所有类型id的空列表self.id_list = []# 打开文件self.f = open('XiaomiShangcheng.csv','a',encoding='utf-8')self.writer = csv.writer(self.f)# 创建锁self.lock = Lock()def get_cateid(self):# 请求url = 'http://app.mi.com/'headers = { 'User-Agent': UserAgent().random}html = requests.get(url=url,headers=headers).text# 解析parse_html = etree.HTML(html)xpath_bds = '//ul[@class="category-list"]/li'li_list = parse_html.xpath(xpath_bds)for li in li_list:typ_name = li.xpath('./a/text()')[0]typ_id = li.xpath('./a/@href')[0].split('/')[-1]# 计算每个类型的页数pages = self.get_pages(typ_id)#往列表中添加二元组self.id_list.append( (typ_id,pages) )# 入队列self.url_in()# 获取count的值并计算页数def get_pages(self,typ_id):# 获取count的值,即app总数url = self.url.format(0,typ_id)html = requests.get(url=url,headers={'User-Agent':UserAgent().random}).json()count = html['count']pages = int(count) // 30 + 1return pages# url入队函数,拼接url,并将url加入队列def url_in(self):for id in self.id_list:# id格式:('4',pages)for page in range(1,id[1]+1):url = self.url.format(page,id[0])# 把URL地址入队列self.q.put(url)# 线程事件函数: get() -请求-解析-处理数据,三步骤def get_data(self):while True:# 判断队列不为空则执行,否则终止if not self.q.empty():url = self.q.get()headers = {'User-Agent':UserAgent().random}html = requests.get(url=url,headers=headers)res_html = html.content.decode(encoding='utf-8')html=json.loads(res_html)self.parse_html(html)else:break# 解析函数def parse_html(self,html):# 写入到csv文件app_list = []for app in html['data']:# app名称 + 分类 + 详情链接name = app['displayName']link = 'http://app.mi.com/details?id=' + app['packageName']typ_name = app['level1CategoryName']# 把每一条数据放到app_list中,并通过writerows()实现多行写入app_list.append([name,typ_name,link])print(name,typ_name)self.i += 1# 向CSV文件中写入数据self.lock.acquire()self.writer.writerows(app_list)self.lock.release()# 入口函数def main(self):# URL入队列self.get_cateid()t_list = []# 创建多线程for i in range(1):t = Thread(target=self.get_data)t_list.append(t)# 启动线程t.start()for t in t_list:# 回收线程   t.join()self.f.close()print('数量:',self.i)if __name__ == '__main__':start = time.time()spider = XiaomiSpider()spider.main()end = time.time()print('执行时间:%.1f' % (end-start))

运行上述程序后,打开存储文件,其内容如下:

在我们之间-单机版,休闲创意,http://app.mi.com/details?id=com.easybrain.impostor.gtx

粉末游戏,模拟经营,http://app.mi.com/details?id=jp.danball.powdergameviewer.bnn

三国杀,棋牌桌游,http://app.mi.com/details?id=com.bf.sgs.hdexp.mi

腾讯欢乐麻将全集,棋牌桌游,http://app.mi.com/details?id=com.qqgame.happymj

快游戏,休闲创意,http://app.mi.com/details?id=com.h5gamecenter.h2mgc

皇室战争,战争策略,http://app.mi.com/details?id=com.supercell.clashroyale.mi

地铁跑酷,跑酷闯关,http://app.mi.com/details?id=com.kiloo.subwaysurf
...
...


扫描二维码推送至手机访问。

版权声明:本文由花晨月夕发布,如需转载请注明出处。

本文链接:https://www.856syz.top/?id=13

分享给朋友:

“Python多线程爬虫详解” 的相关文章

Python爬虫实现Cookie模拟登录

Python爬虫实现Cookie模拟登录

在使用爬虫采集数据的规程中,我们会遇到许多不同类型的网站,比如一些网站需要用户登录后才允许查看相关内容,如果遇到这种类型的网站,又应该如何编写爬虫程序呢?Cookie 模拟登录技术成功地解决了此类问题。Cookie 是一个记录了用户登录状态以及用户属性的加密字符串。当你第一次登陆网站时,服务端会在返…

解锁Parrot OS图形界面,轻松提升你的黑客技能

解锁Parrot OS图形界面,轻松提升你的黑客技能

Parrot OS是一款基于Debian的Linux发行版,专为渗透测试、数字取证和安全审计而设计。虽然它的默认界面是命令行,但许多用户可能会发现图形界面(GUI)在执行日常任务时更为便捷。本文将指导您如何解锁Parrot OS的图形界面,并简要介绍一些可以帮助您提升黑客技能的工具。…

掌握Linux下载技巧,轻松安装软件大揭秘

掌握Linux下载技巧,轻松安装软件大揭秘

Linux操作系统因其开源、稳定和安全性高等特点,在全球范围内拥有庞大的用户群体。在Linux系统中,安装软件的方式与Windows系统有所不同,需要通过命令行进行。本文将详细介绍Linux下载技巧,帮助您轻松安装软件。…

Linux与macOS的兼容之道:跨越技术鸿沟,轻松实现系统互操作

Linux与macOS的兼容之道:跨越技术鸿沟,轻松实现系统互操作

在当今的计算机世界里,Linux和macOS作为两大主流操作系统,各自拥有庞大的用户群体。虽然它们在内核架构、文件系统、软件生态等方面存在差异,但用户往往需要在不同系统间进行数据交换和软件兼容。本文将深入探讨Linux与macOS的兼容之道,帮助用户跨越技术鸿沟,实现系统互操作。…

Linux容器镜像:打造高效灵活的轻量级应用基石

Linux容器镜像:打造高效灵活的轻量级应用基石

在当今的云计算和DevOps环境中,容器技术已经成为一种流行的应用部署方式。Linux容器镜像作为容器技术的核心组件,承载着构建、打包和分发应用的重要任务。本文将深入探讨Linux容器镜像的原理、构建方法以及在实际应用中的优势。…

揭秘Linux系统:如何轻松实现硬件性能的极限优化

揭秘Linux系统:如何轻松实现硬件性能的极限优化

Linux系统以其稳定性和灵活性在服务器和嵌入式系统等领域得到了广泛应用。为了充分利用硬件资源,实现硬件性能的极限优化,我们需要从多个方面进行考虑。本文将详细探讨在Linux系统中如何进行硬件性能优化。…

发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。