python网页数据抓取(Python并发编程教程(二):线程的具体使用方法 )

优采云 发布时间: 2021-12-04 09:13

  python网页数据抓取(Python并发编程教程(二):线程的具体使用方法

)

  网络爬虫程序是一个IO密集型程序。程序涉及到大量的网络IO和本地磁盘IO操作,会消耗大量的时间,从而降低程序的执行效率,而Python提供的多线程可以在一定程度上提高它的执行效率IO 密集型程序。

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

  多线程使用流程 Python提供了两个支持多线程的模块,分别是_thread和threading。其中,_thread 模块处于最底层。与threading模块相比,它的功能有限,所以推荐大家使用threading模块。Threading不仅收录了_thread模块中的所有方法,还提供了一些其他的方法,如下图:

  线程的具体用法如下:

  

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线程类来创建多个线程。

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

  例如,如果您使用多个线程将捕获的数据写入磁盘文件,则必须锁定执行写入操作的线程,以防止写入的数据被覆盖。当线程执行完写操作后,会主动释放锁,并继续让其他线程获取锁,如此循环直到所有写操作完成。具体方法如下:

  

from threading import Lock

lock = Lock()

# 获取锁

lock.acquire()

wirter.writerows("线程锁问题解决")

# 释放锁

lock.release()

  Python多线程的队列队列模型,由于GIL全局解释器锁的存在,只允许一个线程同时占用解释器执行程序。当这个线程遇到IO操作时,会主动放弃解释器,让其他处于等待状态的线程获取解释器来执行程序,线程回到等待状态,主要通过线程调度来实现机制。

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

  

# 导入模块

from queue import Queue

q = Queue() #创界队列对象

q.put(url) 向队列中添加爬取一个url链接

q.get() # 获取一个url,当队列为空时,阻塞

q.empty() # 判断队列是否为空,True/False

  在多线程爬虫的情况下,下面的多线程方法用于抓取()中的应用类别列,所有类别下的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参数值不变,页面会随着页码的增加而变化,可以通过查看页面元素来查看categoryId,如下图

  

游戏

实用工具

影音视听

聊天社交

图书阅读

学习教育

效率办公

时尚购物

居家生活

旅行交通

摄影摄像

医疗健康

体育运动

新闻资讯

娱乐消遣

金融理财

  因此,您可以使用 Xpath 表达式匹配 href 属性来提取类别 ID 和类别名称。表达式如下:

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

  

{

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,因此可以使用总数(计数)来计算每个类别中有多少个页面。

  

pages = int(count) // 30 + 1

  下载详情页地址使用packageName拼接,如下图:

  

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

  2) 完整程序完整程序如下:

  

# -*- coding:utf8 -*-

import requests

from threading import Thread

from queue import Queue

import time

from fake_useragent import UserAgent

from lxml import etree

import csv

from threading import Lock

import json

class 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 + 1

return 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

...

...

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线