python爬虫 多线程抓取小米应用商店全站应用信息(动态数据) --保存到csv文件

需求

爬取 小米应用商店 全站应用数据

知识点

  1. 队列Queue:使用队列存放所以的url,让线程从队列中取出url后发起请求,拿动态数据。线程的队列使用from queue import Queue,区别于进程的队列from multiprocessing import Queue

  2. 多线程

  3. 线程锁

  4. xpath

  5. sv写入

步骤

  1. 确认是否为动态页面
    在网页源码中搜索你要获取的数据中的关键词,发现源码中没有,确认为动态数据

  2. 先实现在一个分类(游戏)下抓取所有应用:即页面数和categoryId是固定的

  3. 抓包
    进入控制台Network -->xhr
    多刷几次翻页抓包后->Preview,找到我们需要的数据所在的包
    在Network – > xhr --> Headers --> General -->Request URL,将后端返给前端数据的接口拿到,如下:
    http://app.mi.com/categotyAllListApi?page=1&categoryId=15&pageSize=30
    写成格式化字符串:http://app.mi.com/categotyAllListApi?page={(page-1)}&categoryId=15&pageSize=30

  4. 找query string params中变化的参数的规律
    page: 1 #页面数,从0开始,递增,一共67页,就是0 – 66
    categoryId: 15 #应用分类,15为游戏,在同一分类下不变
    pageSize: 30 #每页中应用的个数,不变

  5. 打开后端接口网页,查看后端返回的json串中数据的格式和内容,找到你想要的数据:名称,类别,packagename,思考数据预处理的方式

  6. 再实现所有分类全站抓取:即categoryId和页面数是不固定的,需要从网页中抓取这两个数据

    1. 发现页面数是动态数据,去抓包中找该数据,发现和json串中的一个count值(每个分类下的app总数)的关系:每页中存放30个应用,若应用数正好为30的整数,则页面数=count // 30,若应用数不为30的整数,则页面数=count // 30 + 1
    2. 类别categoryld:
      1. 此时类别发生变化,将url中的类别改为变量,如下:
        http://app.mi.com/categotyAllListApi?page={(page-1)}&categoryId={}&pageSize=30
      2. 在源码中找分类的关键词,发现源码中能找到,所以是静态数据,思考可以通过re或者xpath将数据抓取出来
        检查页面元素节点
      <li><a class="current" href="/category/15">游戏</a></li>
      #写xpath或者右键copy xpath表达式
      //div[@class="sidebar"]/div[2]/ul[1]/li -->先获取到整个节点对象
      ./a@href -->再获取到href
      ./a/text() -->再获取到文本
      
import requests
import json
from threading import Thread,Lock
from queue import Queue
import time
import random
from lxml import etree
from fake_useragent import UserAgent
import csv


class XiaomiSpider:
	def __init__(self):
		#需要请求的页面
		self.url = 'http://app.mi.com/categotyAllListApi?page={}&categoryId={}&pageSize=30'
		#创建队列,用于多线程抓取url入队列(作用类似scrapy中的调度器)
		self.q = Queue()
		self.i = 0
		#创建线程互斥锁,控制线程的原子性
		self.lock = Lock()

		#打开一个csv文件,准备向里面写入数据
		self.f = open('xiaomiAppShop.csv','a')
		#创建写入对象
		self.writer = csv.writer(self.f)

	#2. 请求获取响应内容的方法
	def get_html(self,url):
		headers = {'User-Agent':UserAgent().random}
		html = requests.get(url=url,headers=headers).text

		#因为打印出来不一定是json串,打印出来看下返回的是什么格式
		print(html)
		
		return html

	#1. url准备:将需要抓取的数据包的url全都入队列,页面数:total
	def url_in(self,id):
	
		#获取某种分类的页面总数
		total = self.get_total(id)
		
		for page in range(0,total):
			url = self.url.format(page,id)
			#将该分类下的所有页面url入队列
			self.q.put(url)

	#1. url准备:获取每个分类的页面总数的函数
	def get_total(self,id):
		#由于发现和json串中的count(该分类下应用的总数)是xxx关系,取出count的值(每个分类下的count值是固定的)
		url = self.url.format(0,id)
		html = json.loads(self.get_html(url))
		count = int(html['count'])

		if count % 30 ==0:
			total = count // 30
		else:
			total = count // 30 + 1
		return total

	#1. url准备:获取所有分类对应的id(静态数据,用xpath获取),并拼接url地址,入队列
	def get_id(self):
		#1. 请求分类数据所在url并获取响应
		url = 'http://app.mi.com/category/15#page=2'
		html = self.get_html(url)
		
		#2. 使用xpath解析出分类数据
		p = etree.HTML(html)
		li_list = p.xpath('//div[@class="sidebar"]/div[2]/ul[1]/li')
		#确认下xpath是否匹配成功了,打印出来应该是对象的地址
		print(li_list)
		
		#从节点列表中提取分类id和值,返回的都为列表
		for li in li_list:
			#href="/category/15"
			id = li.xpath('./a/@href')[0].split('/')[-1]	#直接取最后一个数
			type_name = li.xpath('./a/text()')[0]

			#拼接数据包Url,然后将其入队列
			self.url_in(id)
	

	#最终:线程的事件函数:请求+解析+数据预处理+数据持久化
	def parse_html(self):
		#让线程循环取 url干活
		while True:
			#当队列中不为空时执行get()
			if not self.q.empty():
				url = self.q.get()
				#请求,获取动态数据响应
				#收到的动态数据响应为json串(后端返给前端的数据)
				#将json串转为python数据类型{key1:value1,data:[{key1:value1,key2:value2,...},{...},...]}
				html = json.loads(self.get_html(url))
				#从返回的数据中取出想要的数据
				item = {}
				#准备一个列表,将抓下来的数据作为元组放进去:最终数据格式[(name1,type,link),(),(),..]
				app_list = []
				for app in html['data']:
					item['name']  = app['displayName']
					item['type'] = app['level1CategoryName']
					item['link'] = app['packageName']
					app_list.append(
								(item['name'],item['type'],item['link'])
								)
					
					print(item)
				
				#线程操作共享资源csv文件,加互斥锁
				self.lock.acquire()
				#多行写入,io操作少
				self.writer.writerows(app_list)
				self.lock.release()

				#让一个线程抓取一页,就随机休眠
                		time.sleep(random.uniform(0,1))
					
			#当队列为空时,队列会阻塞,break跳出循环
			else:
				break
				
		
	#入口函数
	def run(self):
		#1. url准备:获取所有分类对应的id ,并拼接url地址、入队列
		self.get_id()
			
		#2. 解析+数据持久化
		#创建 n个线程启动事件函数
		#注:多线程从队列中拿了url,谁先执行不确定,即先拿哪个页面的数据是无序的
		t_list = []
		for i in range(4):
			t = Thread(target=self.parse_html)
			t_list.append(t)
			t.start()
	
		#回收
		for j in t_list:
			j.join()
			
		#所有线程都写完了,关闭csv文件
		self.f.close()
	
if __name__ == '__main__':
	#计时
	begin = time.time()
	
	spider = XiaomiSpider()
	spider.run()

	end = time.time()
	print('执行时间:%.2f' %(end-begin))
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值