温馨提示:
爬虫玩得好,监狱进得早。数据玩得溜,牢饭吃个够。
《刑法》第 285 条,非法获取计算机信息系统数据罪。
违反国家规定,侵入前款规定以外的计算机信息系统或者采用其他技术手段,获取该计算机信息系统中存储、处理或者传输的数据,或者对该计算机信息系统实施非法控制,情节严重的,处三年以下有期徒刑或者拘役,并处或者单处罚金;情节特别严重的,处三年以上七年以下有期徒刑,并处罚金。
正文:
本章我们来介绍一个新的方式爬虫实用知识, 多线程 ,可能有人会问为什么不用多线程,这个其实说实话,多线程用的会多一点,进程用的少,多线程为主
应用场景
【1】多进程:CPU密集程序
【2】多线程:爬虫(网络I/O)、本地磁盘I/O
创建 队列,等待线程进入
【1】导入模块
from queue import Queue
【2】使用
q = Queue()
q.put(url)
q.get() # 当队列为空时,阻塞
q.empty() # 判断队列是否为空, True/False
【3】q.get()解除阻塞方式
3.1) q.get(block=False)
3.2) q.get(block=True,timeout=3)
3.3) if not q.empty():
q.get()
创建线程模块
# 导入模块
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()
现在我们分析一下小米应用商店的数据,先来打开看一下:
这个网站是一个动态加载的网站,我们上一个案例 豆瓣电影排行榜数据抓取
提到过,这样网站的特点以及怎么抓取数据,这里我们直接进行抓取,没必要观察URL地址的规律,因为响应内容不存在,我们直接 F12 就抓包就好了
我们先来刷新一下,然后点击下一页,多点两下,抓取几个包就够我们用了
这个就是我们抓取的异步的网络数据包,我们从表面就能看到,问好后面的page= 有0、1、2、3… 这个就是一个查询参数,那我们点开一个看看:
这个应该是,这是count有两千个应该是社交app的个数,下面我们也看到了各个应用app的名字,接下来我们换到Headers里面分析各个信息:
这里面我们分析到这个是一个GET请求,那是这样的请求的话我们就直接找URL、headers、Query String Parameters这三个东西,现在URL就是我们找到的框框里面的URL,headers就是Request Headers的信息,最后我们看一下Query String Parameters这里面的东西
这三个,看着也没有加密的东西,也没有时间戳的字符串
page应该是页数
categoryld常规来说这个是类别应用的id
pageSize这个是每页的应用个数
那看一下接下里的数据包
我们看到就是page变了,其他的都是一样的,接下来我们复制抓到的URL地址到浏览器看一下:
那我们就找到了动态加载的数据了,那我们就写写代码吧,URL的话就只是把page后面直接改成花括号就行
导入模块
import requests
import json
from threading import Thread, Lock # 线程模块和线程锁
from queue import Queue # 创建队列
import time
from fake_useragent import UserAgent
定义功能函数,减少重复代码
class XiaomiSpider:
def __init__(self):
self.url = 'http://app.mi.com/categotyAllListApi?page={}&categoryId=2&pageSize=30'
# 队列 锁
self.q = Queue()
self.lock = Lock()
再来一个函数,就是把生成的URL地址,入到队列里面,等待抓取
def url_to_queue(self):
# 生成所有待抓取的URL地址
for page in range(67):
page_url = self.url.format(page)
# 入队列
self.q.put(page_url)
接下来就是创建 线程事件函数 从队列获取地址,请求,解析,数据处理,那我们思维谨慎一点,先来个判断,看看队列里面有没有数据,不是空的就拿出来
if not self.q.empty():
# 获取
url = self.q.get()
下面是取出数据后就是开始请求,解析,数据处理
html = self.get_html(url=url)
html = json.loads(html)
for one_app_dict in html['data']:
item = {
}
item['name'] = one_app_dict['displayName']
item['type'] = one_app_dict['level1CategoryName']
item['link'] = one_app_dict['packageName']
print(item)
创建run函数,把程序运行起来
def run(self):
# URL地址入队列
self.url_to_queue()
# 创建多线程
t_list = []
# 线程个数
for i in range(1):
t = Thread(target=self.parse_html)
t_list.append(t)
t.start()
for t in t_list:
t.join()
这样代码就大体写完了,但是还是有点小问题,我们这个直接t.start开始后就直接到线程事件函数里面,这里面来之后,我们直接一个线程过来了,判断是不是空的就只是get出一个地址,然后在发请求,解析,数据处理后面没有代码直接结束了
也就是说我就获取一个地址让一个线程,进行抓取,这样的话是不对的吧,因为假如我们有几万页的地址,我们不能创建一万个线程吧,所以我们应该让一个线程抓完之后再继续抓其他的,不是停下来不干活了,所以我们直接给他一个while循环
while True:
if not self.q.empty():
url = self.q.get()
html = self.get_html(url=url)
html = json.loads(html)
for one_app_dict in html['data']:
item = {
}
item['name'] = one_app_dict['displayName']
item['type'] = one_app_dict['level1CategoryName']
item['link'] = one_app_dict['packageName']
print(item)
else:
break
这样我们的多线程就是实现了,但是还有一个东西就是我们还是那面创建的线程锁没有用,现在我们也用上它,但是我得考虑一下在哪里加锁,那就看看我们在哪里操作全局变量啊,是不是在这个线程事件函数这里操作啊,是不是在这个上面操作的,或者说几乎同时操作的,
在这里举个例子:
self.q: ['http://page88.html']
Thread-1: if条件成立,进入分支语句
Thread-2: if条件也成立,也进入分支语句
像这样,就是当有最后一个地址的时候,线程1 if 判断,里面有,不是空的,直接进入分支语句,马上就要get获取,就在这时候,线程2 也是 if 判断了,也是判断到里面不是空的,线程2也就是进入分支,最后线程1和线程2都会执行get()语句,但是有一个会发生阻塞,那我们怎么不然它阻塞,只能是加锁控制
while True:
self.lock.acquire()
if not self.q.empty():
url = self.q.get()
print(url)
# 释放锁
self.lock.release()
html = self.get_html(url=url)
html = json.loads(html)
for one_app_dict in html['data']:
item = {
}
item['name'] = one_app_dict['displayName']
item['type'] = one_app_dict['level1CategoryName']
item['link'] = one_app_dict['packageName']
print(item)
else:
# 释放锁
self.lock.release()
break
这样的加锁和释放锁的目的是
加锁一定对应着释放锁,如果进入到if里面了,就在if里面释放锁,如果没有进入到if里面,那就一定会走else里,那就在else里释放锁
最后将全部代码奉上:
import requests
import json
from threading import Thread, Lock
from queue import Queue
import time
from fake_useragent import UserAgent
class XiaomiSpider:
def __init__(self):
self.url = 'http://app.mi.com/categotyAllListApi?page={}&categoryId=2&pageSize=30'
# 队列 锁
self.q = Queue()
self.lock = Lock()
def get_html(self, url):
headers = {
'User-Agent':UserAgent().random}
html = requests.get(url=url,headers=headers).text
return html
def url_to_queue(self):
for page in range(67):
page_url = self.url.format(page)
self.q.put(page_url)
def parse_html(self):
while True:
self.lock.acquire()
if not self.q.empty():
url = self.q.get()
print(url)
# 释放锁
self.lock.release()
html = self.get_html(url=url)
html = json.loads(html)
for one_app_dict in html['data']:
item = {
}
item['name'] = one_app_dict['displayName']
item['type'] = one_app_dict['level1CategoryName']
item['link'] = one_app_dict['packageName']
print(item)
else:
# 释放锁
self.lock.release()
break
def run(self):
# URL地址入队列
self.url_to_queue()
# 创建多线程
t_list = []
for i in range(1):
t = Thread(target=self.parse_html)
t_list.append(t)
t.start()
for t in t_list:
t.join()
if __name__ == '__main__':
start = time.time()
spider = XiaomiSpider()
spider.run()
end = time.time()
print('time:%.2f' % (end - start))