爬虫
1 selenium模块
1.1 简介
selenium模块一般用于基于浏览器的自动化测试工作,也可以用于爬虫。 使用selenium模块进行爬虫:
- 可以方便地捕获动态加载的数据,页面可见即可得;
- 容易实现模拟登录。
优点:可见即可得,方便; 缺点:速度慢。
1.2 基本操作
1.2.1 安装
安装selenium模块
pip install selenium
下载安装谷歌浏览器驱动
http://chromedriver.storage.googleapis.com/index.html
1.2.2 基本操作案例 搜索京东商品
目标:在京东搜索Macbook pro。
from selenium import webdriver
from time import sleep
# 实例化浏览器对象
chrome_obj = webdriver.Chrome(executable_path='./chromedriver')
# 发起请求
chrome_obj.get('https://www.jd.com/')
# 进行标签定位,搜索框
search_input = chrome_obj.find_element_by_xpath('//*[@id="key"]')
# 向搜索框中输入关键字
search_input.send_keys('Macbook pro')
# 定位搜索按钮
serach_btn = chrome_obj.find_element_by_xpath('//*[@id="search"]/div/div[2]/button')
# 点击搜索按钮
serach_btn.click()
sleep(1)
# 操作js代码
chrome_obj.execute_script('window.scrollTo(0, document.body.scrollHeight)')
# 获取当前页面的源码数据
page_text = chrome_obj.page_source
print(page_text)
sleep(2)
# 关闭浏览器
chrome_obj.quit()
1.2.3 爬取动态加载数据案例 化妆品生产许可信息
化妆品生产许可信息管理系统服务平台:http://125.35.6.84:81/xk/ 目标:获取前5页中每一页的第一条数据中的企业名称。
from selenium import webdriver
from lxml import etree
from time import sleep
chrome_obj = webdriver.Chrome(executable_path='./chromedriver')
chrome_obj.get('http://125.35.6.84:81/xk/')
sleep(1)
page_text_list = [chrome_obj.page_source]
for i in range(6):
next_page_btn = chrome_obj.find_element_by_xpath('//*[@id="pageIto_next"]')
next_page_btn.click()
sleep(1)
page_text_list.append(chrome_obj.page_source)
for each_page_text in page_text_list:
tree_obj = etree.HTML(each_page_text)
first_enterprise_name_per_page = tree_obj.xpath('//*[@id="gzlist"]/li[1]/dl/a/text()')[0]
print(first_enterprise_name_per_page)
chrome_obj.quit()
1.2.4 动作链操作案例 移动菜鸟工具中的方块
动作链:一系列连续的操作。
目标:移动菜鸟工具中的方块。菜鸟工具:https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable
from selenium import webdriver
from selenium.webdriver import ActionChains
from time import sleep
chrome_obj = webdriver.Chrome(executable_path='./chromedriver')
chrome_obj.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')
sleep(1)
# 页面嵌套问题:如果定位的标签存在于iframe标签下的子页面中,常规的标签定位方法会报错。
# 使用如下操作
chrome_obj.switch_to.frame('iframeResult') # 参数是iframe标签的id
draggable_div = chrome_obj.find_element_by_id('draggable')
# 实例化一个动作链对象,将其绑定给浏览器。
action_chains_obj = ActionChains(chrome_obj)
# 对指定的标签进行点击且长按操作
action_chains_obj.click_and_hold(draggable_div)
# 多次移动操作
for i in range(5):
action_chains_obj.move_by_offset(20, 40).perform() # perform方法让动作链立即执行
sleep(0.5)
action_chains_obj = ActionChains(chrome_obj)
chrome_obj.quit()
1.2.5 获取cookie
chrome_obj.get_cookies()
1.2.6 无头浏览器
无头浏览器是指没有可视化图形界面,通常由代码进行操作控制的浏览器。 推荐使用谷歌无头浏览器。
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from time import sleep
# 创建一个参数对象,用来控制chrome浏览器以无界面模式打开。
chrome_options_obj = Options()
chrome_options_obj.add_argument('--headless')
chrome_options_obj.add_argument('--disable-gpu')
# 创建浏览器对象,配置参数。
chrome_obj = webdriver.Chrome(executable_path='./chromedriver', chrome_options=chrome_options_obj)
# 上网
target_url = 'http://www.baidu.com/'
chrome_obj.get(target_url)
sleep(1)
print(chrome_obj.page_source)
# 截屏
chrome_obj.save_screenshot('baidu.png')
chrome_obj.quit()
1.3 模拟登录12306
12306登陆页面:https://kyfw.12306.cn/otn/login/init
难点:图片验证,根据验证码图片顶部的文字提示点击对应的图片。
解决方案:超级鹰验证码识别(http://www.chaojiying.com/)
def parse_code_img(img_path, img_type):
chaojiying = Chaojiying_Client('username', 'password', '899370')
im = open(img_path, 'rb').read()
return chaojiying.PostPic(im, img_type)['pic_str']
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from PIL import Image
from time import sleep
chrome_obj = webdriver.Chrome(executable_path='./chromedriver')
target_url = 'https://kyfw.12306.cn/otn/login/init'
chrome_obj.get(target_url)
sleep(1)
# 获取验证码图片时注意,不可以对验证码图片的url再次发送请求,因为会请求到另一张不同于本次登录页面的验证码图片。
# 采用画面截取+裁剪的方式获取验证码图片
chrome_obj.save_screenshot('main.png') # 截取整个页面
img_tag = chrome_obj.find_element_by_xpath('//*[@id="loginForm"]/div/ul[2]/li[4]/div/div/div[3]/img') # 验证码图片标签
img_location = img_tag.location # 验证码图片左下角的坐标
img_size = img_tag.size # 验证码图片尺寸
# 验证码图片的左下角坐标+右上角坐标
img_rangle = (
int(img_location['x']),
int(img_location['y']),
int(img_location['x'] + img_size['width']),
int(img_location['y'] + img_size['height'])
)
# 根据img_rangle表示的裁剪区域进行图片裁剪
i = Image.open('./main.png')
frame = i.crop(img_rangle)
frame.save('./code.png')
# 识别验证码
# 9004:坐标多选,返回1~4个坐标,例如'x1,y1|x2,y2|x3,y3'
all_loc_str = parse_code_img('./code.png', 9004)
print(all_loc_str) # 需要在验证码图片中点击的位置坐标
# 'x1,y1|x2,y2|x3,y3' => [[x1,y1], [x2,y2], [x3,y3]]
all_loc_list = []
if '|' in all_loc_str:
list_1 = all_loc_str.split('|')
count_1 = len(list_1)
for i in range(count_1):
xy_list = []
x = int(list_1[i].split(',')[0])
y = int(list_1[i].split(',')[1])
xy_list.append(x)
xy_list.append(y)
all_loc_list.append(xy_list)
else:
x = int(all_loc_str.split(',')[0])
y = int(all_loc_str.split(',')[1])
xy_list = []
xy_list.append(x)
xy_list.append(y)
all_loc_list.append(xy_list)
# 循环的次数就是点击的次数
for each_loc in all_loc_list:
x = each_loc[0]
y = each_loc[1]
# 点击对应的图片
ActionChains(chrome_obj).move_to_element_with_offset(img_tag, x, y).click().perform()
sleep(1)
# 输入用户名和密码,点击登录按钮。
chrome_obj.find_element_by_id('username').send_keys('test_username')
sleep(1)
chrome_obj.find_element_by_id('password').send_keys('test_password')
sleep(1)
chrome_obj.find_element_by_id('loginSub').click()
sleep(1)
chrome_obj.quit()
2 js加密
2.1 案例 中国空气质量在线监测平台数据爬取
中国空气质量在线监测平台: https://www.aqistudy.cn/html/city_detail.html 案例涉及的技术包括:
- 动态请求数据
- 加密响应数据
- js混淆
- js加密
- js逆向
2.2 分析
在页面中输入查询条件后,点击查询按钮,会发起ajax请求,向后台发送请求获取指定的数据。
Request URL: https://www.aqistudy.cn/apinew/aqistudyapi.php
Request Method: POST
Form Data
d: tdg...
问题
- 请求参数d是通过js代码动态生成的;
- 响应数据是经过加密的密文数据。
解决思路
- 如何处理动态变化的请求参数;
- 如何对响应密文数据进行解密。
核心ajax请求的代码中存在生成请求参数以及处理响应数据的代码。
查找流程 ajax请求动作是点击查询按钮后触发的,首先查询按钮绑定的点击事件。
$("#btnSearch").click(function(){
getData();
});
function getData() {
...
getAQIData();
getWeatherData();
}
函数getAQIData和getWeatherData的定义中没有处理请求和响应数据的ajax代码,内部发视了函数getServerData的调用。
function getWeatherData() {
...
getServerData(method, param, function(obj) {
...
}, 0.5);
}
下一步是查找函数getServerData的定义。 位于向https://www.aqistudy.cn/js/jquery-1.8.0.min.js?v=1.2发送get请求后的响应数据中,代码经过加密,无法直接读取。 此时遇到了反爬机制js混淆。
js混淆是指对核心代码进行了加密处理。 js反混淆,采用暴力破解的方式处理加密代码。 将响应数据中含有getServerData函数名的那一行代码复制到js反混淆中进行破解。
function getServerData(method, object, callback, period) {
const key = hex_md5(method + JSON.stringify(object));
const data = getDataFromLocalStorage(key, period);
if (!data) {
var param = getParam(method, object);
$.ajax({
url: '../apinew/aqistudyapi.php',
data: {
d: param
},
type: "post",
success: function (data) {
data = decodeData(data);
obj = JSON.parse(data);
if (obj.success) {
if (period > 0) {
obj.result.time = new Date().getTime();
localStorageUtil.save(key, obj.result)
}
callback(obj.result)
} else {
console.log(obj.errcode, obj.errmsg)
}
}
})
} else {
callback(data)
}
}
结果
- 函数getParam(method, object)返回动态变化的请求参数d;
- data是加密后的响应数据,函数decodeData(data)用于对经过加密的响应数据进行解密。
2.2 js逆向
js逆向是指使用Python等其它语言调用js的代码。 方式1:将js代码改手动改写成Python代码,困难; 方式2:使用PyExecJS工具类实现自动逆向。
使用PyExecJS工具类需要安装Nodejs开发环境。
流程: 先将待执行的js代码保存到一个js文件(例如test.js)中,再对js文件进行编译,最后调用所需的函数。
2.2.1 动态生成请求参数
目标:调用函数getParam获取动态生成的请求参数。
对函数getParam进行进一步封装,在test.js后添加自定义函数getPostParamCode。 目的,getParam接受的参数格式分别是字符串和js字典,而getPostParamCode将所有传入的参数格式统一为字符串。js字符串格式与Python字符串格式是相通的。
function getPostParamCode(method, city, type, startTime, endTime) {
var param = {};
param.city = city;
param.type = type;
param.startTime = startTime;
param.endTime = endTime;
return getParam(method, param);
}
import execjs
import requests
# 1. 实例化对象
node = execjs.get()
# 2. 对js源文件进行编译
file = 'test.js'
ctx = node.compile(open(file, encoding='utf-8').read())
# 准备函数getPostParamCode的参数
method = 'GETCITYWEATHER'
city = '北京'
type = 'HOUR'
start_time = '2018-01-25 00:00:00'
end_time = '2018-01-25 23:00:00'
# 准备js代码
js = 'getPostParamCode("{0}", "{1}", "{2}", "{3}", "{4}")'.format(method, city, type, start_time, end_time)
# 3. 使用方法eval执行编译后的js函数,获得动态生成的请求参数d
params = ctx.eval(js)
# 4. 发送post请求,获取加密后的响应数据
url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'
headers = {
'User-Agent': 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'
}
data = {
'd': params
}
response_text = requests.post(url=url, headers=headers, data=data).text
2.2.2 处理加密后的响应数据
目标:执行函数decodeData(data)对上面获取的响应数据进行解析。 在上面的基础上添加第5步。
import execjs
import requests
# 1. 实例化对象
node = execjs.get()
# 2. 对js源文件进行编译
file = 'test.js'
ctx = node.compile(open(file,encoding='utf-8').read())
# 准备函数getPostParamCode的参数
method = 'GETCITYWEATHER'
city = '北京'
type = 'HOUR'
start_time = '2018-01-25 00:00:00'
end_time = '2018-01-25 23:00:00'
# 准备js代码
js = 'getPostParamCode("{0}", "{1}", "{2}", "{3}", "{4}")'.format(method, city, type, start_time, end_time)
# 3. 使用方法eval执行编译后的js函数,获得动态生成的请求参数d
params = ctx.eval(js)
# 4. 发送post请求,获取加密后的响应数据
url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'
response_text = requests.post(url, headers=headers, data={'d': params}).text
# 5. 对加密的响应数据进行解密
js = 'decodeData("{0}")'.format(response_text)
decrypted_data = ctx.eval(js)
print(decrypted_data)