微信公众号:运维开发故事,作者:素心这里将会以一个例子展开探讨多线程在爬虫中的应用,所以不会过多的解释理论性的东西,并发详情点击连接
爬取某应用商店
当然,爬取之前请自行诊断是否遵循君子协议,遵守就爬不了数据1. 目标
查看robots协议只需要在域名后缀上rebots.txt
即可
例如:
文章图片
- URL:
http://app.mi.com/category/15
- 【python|使用Python抓取动态网站数据】获取“游戏”分类的所有APP名称、简介、下载链接
首先,需要判断是不是动态加载
点击翻页,发现URL后边加上了
#page=1
,这也就是说,查询参数为1的时候为第二页,写一个小爬虫测试一下import requestsurl = "http://app.mi.com/category/15"headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0;
Win64;
x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"}html = requests.get(url=url, headers=headers).content.decode("utf-8")print(html)
在输出的html中搜索“王者荣耀”,发现并没有什么问题,那么第二页呢?将上述代码中的
url = "http://app.mi.com/category/15"
改为url = "http://app.mi.com/category/15#page=1"
再次搜索第二页的内容”炉石传说”,发现并没有搜索出来,那么该网站可能是动态加载
- 抓包分析
打开chrome自带的窃听器,切换到network,点击翻页
文章图片
可以看到该GET请求后缀很多参数
文章图片
经过多次测试发现
-
page
为页数,但是值需要减1才是真实的页数
-
categoryId
为应用分类
-
pageSize
尚不明确,所以将抓到包的URL打开看一下
文章图片
不难发现,pageSize
为每一页显示APP信息的个数,并且返回了一个json字串
复制一段json过来
{"count":2000, "data":
[
{"appId":108048,"displayName":"王者荣耀","icon":"http://file.market.xiaomi.com/thumbnail/PNG/l62/AppStore/0eb7aa415046f4cb838cfe5b5d402a5efc34fbb25","level1CategoryName":"网游RPG","packageName":"com.tencent.tmgp.sgame"
},
{},
...
]
}
所有的信息都不知道是干啥的,暂时保存
2.3 二级页面
点击”王者荣耀”,跳转到APP详情,看下URL是什么样子
http://app.mi.com/details?id=com.tencent.tmgp.sgame
然后这里会惊奇的发现,id的查询参数和上边的
packageName
的值一样,所以详情页就需要拼接URL2.4 获取信息
- APP名称
深圳市腾讯计算机系统有限公司
王者荣耀
......
- APP简介
《王者荣耀》是腾讯第一5V5团队公平竞技手游,国民MOBA手游大作!5V5王者峡谷、公平对战,还原MOBA经典体验;契约之战、五军对决、边境突围等,带来花式作战乐趣!10秒实时跨区匹配,与好友开黑上分,向最强王者进击!多款英雄任凭选择,一血、五杀、超神,实力碾压,收割全场!敌军即将到达战场,王者召唤师快来集结好友,准备团战,就在《王者荣耀》!
新版特性
1.新英雄-马超:五虎将的最后一位英雄,通过“ 投掷-拾取” ,强化攻击在复杂的战场中穿梭。
2.新玩法-王者模拟战(即将上线):在机关沙盘中,招募英雄,排兵布阵,与其他七位玩家比拼策略!
3.新系统-万象天工:整合以往所有的娱乐模式玩法,冒险之旅玩法。未来,用户使用编辑器“ 天工” 创作的优质原创玩法,将有可能会加入到万象天工;
4.新功能-职业选手专属认证:百余位KPL职业选手游戏内官方认证;
5.新功能-不想同队:王者50星以下的排位赛,在结算界面可设置不想同队的玩家;
6.新功能-系统AI托管:玩家在遭遇挂机后可选择AI托管,但AI不会CARRY比赛;
7.新皮肤:沈梦溪-鲨炮海盗猫。
- APP下载地址
直接下载
由以上分析可以得出,使用lxml提取数据将会是不错的选择,有关xpath使用请点击跳转
xpath语法如下:
- 名称:
//div[@class="intro-titles"]/h3/text()
- 简介:
//p[@class="pslide"][1]/text()
- 下载链接:
//a[@class="download"]/@href
import requestsfrom lxml import etreeclass MiSpider(object):
def __init__(self):
self.bsase_url = "http://app.mi.com/categotyAllListApi?page={}&categoryId=15&pageSize=30" # 一级页面的URL地址
self.headers = {"User-Agent":"Mozilla/5.0 (compatible;
MSIE 9.0;
Windows NT 6.1;
Trident/5.0;
"}# 获取响应对象
def get_page(self, url):
reponse = requests.get(url=url, headers=self.headers)return reponse# 解析一级页面,即json解析,得到APP详情页的链接
def parse_page(self, url):
html = self.get_page(url).json()# two_url_list:[{"appId":"108048","dispayName":"..",...},{},{},...]
two_url_list = html["data"]for two_url in two_url_list:
two_url = "http://app.mi.com/details?id={}".format(two_url["packageName"]) # 拼接app详情链接
self.parse_info(two_url)# 解析二级页面,得到名称、简介、下载链接
def parse_info(self, two_url):
html = self.get_page(two_url).content.decode("utf-8")
parse_html = etree.HTML(html)# 获取目标信息
app_name = parse_html.xpath('//div[@class="intro-titles"]/h3/text()')[0].strip()
app_info = parse_html.xpath('//p[@class="pslide"][1]/text()')[0].strip()
app_url = "http://app.mi.com" + parse_html.xpath('//a[@class="download"]/@href')[0].strip()print(app_name, app_url, app_info)# 主函数
def main(self):
for page in range(67):
url = self.bsase_url.format(page)
self.parse_page(url)if __name__ == "__main__":
spider = MiSpider()
spider.main()
接下来将数据存储起来,存储的方式有很多csv、MySQL、MongoDB
数据存储 这里采用MySQL数据库将其存入
建表SQL
/*
Navicat MySQL Data Transfer
Source Server: xxx
Source Server Type: MySQL
Source Server Version : 50727
Source Host: MySQL_ip:3306
Source Schema: MIAPP
Target Server Type: MySQL
Target Server Version : 50727
File Encoding: 65001
Date: 13/09/2019 14:33:38
*/CREATE DATABASE MiApp CHARSET=UTF8;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for app
-- ----------------------------
DROP TABLE IF EXISTS `app`;
CREATE TABLE `app`(
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'APP名称',
`url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'APP下载链接',
`info` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'APP简介'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
1. pymysql 简单介绍一下pymysql 的使用,该模块为第三方,需要用pip安装,安装方法不再赘述。
1.1 内置方法
pymysql方法
connect()
连接数据库,参数为连接信息(host, port, user, password, charset)
-
cursor()
游标,用来定位数据库
-
cursor.execute(sql)
执行sql语句
-
db.commit()
提交事务
-
cursor.close()
关闭游标
-
db.close()
关闭连接
只要涉及数据的修改操作,必须提交事务到数据库
查询数据库需要使用
fet
方法获取查询结果1.3 详情
更多详情可以参考pymsql
2. 存储 创建配置文件(config.py)
'''
数据库连接信息
'''HOST = "xxx.xxx.xxx.xxx"PORT = 3306USER = "xxxxx"PASSWORD = "xxxxxxx"DB = "MIAPP"CHARSET = "utf8mb4"
表结构
mysql> desc MIAPP.app;
+-------+--------------+------+-----+---------+-------+
| Field | Type| Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| name| varchar(20)| YES|| NULL||
| url| varchar(255) | YES|| NULL||
| info| text| YES|| NULL||
+-------+--------------+------+-----+---------+-------+3 rows in set (0.00 sec)
SQL语句
insert into app values(name,url,info);
完整代码
import requestsfrom lxml import etreeimport pymysqlfrom config import *class MiSpider(object):
def __init__(self):
self.bsase_url = "http://app.mi.com/categotyAllListApi?page={}&categoryId=15&pageSize=30" # 一级页面的URL地址
self.headers = {"User-Agent":"Mozilla/5.0 (compatible;
MSIE 9.0;
Windows NT 6.1;
Trident/5.0;
"}
self.db = pymysql.connect(host=HOST, port=PORT, user=USER, password=PASSWORD, database=DB, charset=CHARSET) # 连接数据库
self.cursor = self.db.cursor() # 创建游标
self.i = 0 # 用来计数,无其他作用# 获取响应对象
def get_page(self, url):
reponse = requests.get(url=url, headers=self.headers)return reponse# 解析一级页面,即json解析,得到APP详情页的链接
def parse_page(self, url):
html = self.get_page(url).json()# two_url_list:[{"appId":"108048","dispayName":"..",...},{},{},...]
two_url_list = html["data"]for two_url in two_url_list:
two_url = "http://app.mi.com/details?id={}".format(two_url["packageName"]) # 拼接app详情链接
self.parse_info(two_url)# 解析二级页面,得到名称、简介、下载链接
def parse_info(self, two_url):
html = self.get_page(two_url).content.decode("utf-8")
parse_html = etree.HTML(html)# 获取目标信息
app_name = parse_html.xpath('//div[@class="intro-titles"]/h3/text()')[0].strip()
app_info = parse_html.xpath('//p[@class="pslide"][1]/text()')[0].strip()
app_url = "http://app.mi.com" + parse_html.xpath('//a[@class="download"]/@href')[0].strip()ins = "insert into app(name,url,info) values (%s,%s,%s)" # 需要执行的SQL语句self.cursor.execute(ins, [app_name, app_url, app_info])self.db.commit()self.i += 1
print("第{}APP {}成功写入数据库".format(self.i, app_name))# 主函数
def main(self):
for page in range(67):
url = self.bsase_url.format(page)
self.parse_page(url)# 断开数据库
self.cursor.close()
self.db.close()print("执行结束,共{}个APP成功写入".format(self.i))if __name__ == "__main__":
spider = MiSpider()
spider.main()
文章图片
多线程 爬取上述信息似乎有点慢,如果数据多的话太耗时,而且计算机资源也得不到充分的利用
这就需要用多线程的理念,关于多进程和多线程的概念网上比比皆是,只需要明白一点
进程可以包含很多个线程,进程死掉,线程不复存在
打个比方,假设有一列火车,把这列火车理解成进程的话,那么每节车厢就是线程,正是这许许多多的线程才共同组成了进程python中有多线程的概念
假设现在有两个运算:
n += 1n -= 1
在python内部实际上这样运算的
x = n
x = n + 1n = xx = n
x = n + 1n = x
线程有一个特性,就是会争夺计算机资源,如果一个线程在刚刚计算了
x = n
这时候另一个线程n = x
运行了,那么这样下来全就乱了, 也就是说n加上一千个1再减去一千个1结果不一定为1,这时就考虑线程加锁问题了。每个线程在运行的时候争抢共享数据,如果线程A正在操作一块数据,这时B线程也要操作该数据,届时就有可能造成数据紊乱,从而影响整个程序的运行。如果需要全面性的了解并发,请点击并发编程,在这里只简单介绍使用
所以Python有一个机制,在一个线程工作的时候,它会把整个解释器锁掉,导致其他的线程无法访问任何资源,这把锁就叫做GIL全局解释器锁,正是因为有这把锁的存在,名义上的多线程实则变成了单线程,所以很多人称GIL是python鸡肋性的存在。
针对这一缺陷,很多的标准库和第三方模块或者库都是基于这种缺陷开发,进而使得Python在改进多线程这一块变得尤为困难,那么在实际的开发中,遇到这种问题本人目前用四种解决方式:
- 用
multiprocessing
代替Thead
- 更换
cpython
为jpython
- 加同步锁
threading.Lock()
- 消息队列
queue.Queue()
1. 队列方法
# 导入模块from queue import Queue# 使用q = Queue()
q.put(url)
q.get() # 当队列为空时,阻塞q.empty() # 判断队列是否为空,True/False
2. 线程方法
# 导入模块from threading import Thread# 使用流程t = Thread(target=函数名) # 创建线程对象t.start() # 创建并启动线程t.join() # 阻塞等待回收线程# 创建多线程for i in range(5):
t = Thread(target=函数名)
t.start()
t.join()
3. 改写 理解以上内容就可以将原来的代码改写多线程,改写之前加上
time
来计时文章图片
多线程技术选用:
- 爬虫涉及IO操作较多,贸然改进程会造成计算机资源的浪费。
pass
- 更换
jpython
简直没必要。
pass
- 加锁可以实现,不过针对IO还是比较慢,因为操作文件的话,必须加锁。
pass
- 使用消息队列可有效的提高爬虫速率。
- 既然爬取的页面有67页,APP多达2010个,则考虑将URL入列
def url_in(self):
for page in range(67):
url = self.bsase_url.format(page)
self.q.put(page)
下边是完整代码
import requestsfrom lxml import etreeimport timefrom threading import Threadfrom queue import Queueimport jsonimport pymysqlfrom config import *class MiSpider(object):
def __init__(self):
self.url = "http://app.mi.com/categotyAllListApi?page={}&categoryId=15&pageSize=30"
self.headers = {"User-Agent": "Mozilla/5.0 (X11;
Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3895.5 Safari/537.36"}# 创建URL队列
self.url_queue = Queue()# 把所有要爬取的页面放进队列
def url_in(self):
for page in range(67):
url = self.url.format(page)# 加入队列
self.url_queue.put(url)# 线程事件函数
def get_data(self):
while True:# 如果结果 为True,则队列为空了
if self.url_queue.empty():break
# get地址,请求一级页面
url = self.url_queue.get()
html = requests.get(url=url, headers=self.headers).content.decode("utf-8")
html = json.loads(html) # 转换为json格式
# 解析数据
app_list = [] # 定义一个列表,用来保存所有的APP信息 [(name,url,info),(),(),...]
for app in html["data"]:# 应用链接
app_link = "http://app.mi.com/details?id=" + app["packageName"]
app_list.append(self.parse_two_page(app_link))return app_listdef parse_two_page(self, app_link):
html = requests.get(url=app_link, headers=self.headers).content.decode('utf-8')
parse_html = etree.HTML(html)app_name = parse_html.xpath('//div[@class="intro-titles"]/h3/text()')[0].strip()
app_url = "http://app.mi.com" + parse_html.xpath('//div[@class="app-info-down"]/a/@href')[0].strip()
app_info = parse_html.xpath('//p[@class="pslide"][1]/text()')[0].strip()info = (app_name, app_url, app_info)print(app_name)return info# 主函数
def main(self):
# url入队列
self.url_in()# 创建多线程
t_list = []for i in range(67):
t = Thread(target=self.get_data)
t_list.append(t)
t.start()for i in t_list:
i.join()db = pymysql.connect(host=HOST, user=USER, password=PASSWORD, database=DB, charset=CHARSET)
cursor = db.cursor()ins = 'insert into app values (%s, %s, %s)'app_list = self.get_data()
print("正在写入数据库")
cursor.executemany(ins, app_list)db.commit()
cursor.close()
db.close()if __name__ == '__main__':
start = time.time()
spider = MiSpider()
spider.main()
end = time.time()print("执行时间:%.2f"% (end - start))
当然这里的设计理念是将URL纳入队列之中,还可以将解析以及保存都写进线程,以提高程序的执行效率。
更多爬虫技术点击访问
欢迎各位一起交流
文章图片
推荐阅读
- python|python get请求 url_python实现http get请求
- 服务器|JetBrains全家桶(PyCharm/WebStorm/IntelliJIDEA) Deployment一键部署
- 如何建立一个完美的 Python 项目()
- 详解增强算术赋值(“-=”操作是怎么实现的())
- MyBatis|Springboot整合Mybatis-plus
- GUI 应用(socket 网络聊天室)
- gslb(global server load balance)技术的一点理解
- c++|【C++篇】AVL树
- c++|【C++篇】map和set的简单介绍