需求
爬取 小米应用商店 全站应用数据
知识点
-
队列Queue:使用队列存放所以的url,让线程从队列中取出url后发起请求,拿动态数据。线程的队列使用from queue import Queue,区别于进程的队列from multiprocessing import Queue
-
多线程
-
线程锁
-
xpath
-
sv写入
步骤
-
确认是否为动态页面
在网页源码中搜索你要获取的数据中的关键词,发现源码中没有,确认为动态数据 -
先实现在一个分类(游戏)下抓取所有应用:即页面数和categoryId是固定的
-
抓包
进入控制台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 -
找query string params中变化的参数的规律
page: 1 #页面数,从0开始,递增,一共67页,就是0 – 66
categoryId: 15 #应用分类,15为游戏,在同一分类下不变
pageSize: 30 #每页中应用的个数,不变 -
打开后端接口网页,查看后端返回的json串中数据的格式和内容,找到你想要的数据:名称,类别,packagename,思考数据预处理的方式
-
再实现所有分类全站抓取:即categoryId和页面数是不固定的,需要从网页中抓取这两个数据
- 发现页面数是动态数据,去抓包中找该数据,发现和json串中的一个count值(每个分类下的app总数)的关系:每页中存放30个应用,若应用数正好为30的整数,则页面数=count // 30,若应用数不为30的整数,则页面数=count // 30 + 1
- 类别categoryld:
- 此时类别发生变化,将url中的类别改为变量,如下:
http://app.mi.com/categotyAllListApi?page={(page-1)}&categoryId={}&pageSize=30 - 在源码中找分类的关键词,发现源码中能找到,所以是静态数据,思考可以通过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() -->再获取到文本
- 此时类别发生变化,将url中的类别改为变量,如下:
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))