执行自定义的 JS 脚本
Pyppeteer Page 对象提供了一系列 evaluate 方法,你可以通过他们来执行一些自定义的 JS 代码,主要提供了下面三个 API:
(1) page.evaluate ( pageFunction [, …args] ) ,返回 pageFunction 执行的结果,pageFunction 表示要在页面执行的函数或表达式, args 表示传入给 pageFunction 的参数
示例:
await page.goto('https://www.baidu.com')
# 输出字符串
await page.evaluate('alert("在浏览器执行js脚本!")')
# 将元素作为参数传入 page.evaluate
element = await page.J('#u1>a[name="tj_trtieba"]')
print(await page.evaluate('el => el.innerHTML', element))
print(await page.evaluate('el => el.href', element))
# 执行函数
el = await page.evaluate('() => document.querySelector("#su").value')
print(el)
(2) page.evaluateHandle(pageFunction[, …args]) ,此方法和 page.evaluate 的唯一区别是此方法返回的是页内类型 ( JSHandle )
示例:
await page.goto('https://www.baidu.com')
el = await page.evaluateHandle('() => document.querySelector("#su").value')
print(type(el))
print(el.toString())
(3) page.evaluateOnNewDocument(pageFunction[, …args]) ,指定的函数在所属的页面被创建并且所属页面的任意 script 执行之前被调用。常用于修改页面 JS 环境。
以下为插入中间 JS ,将淘宝会为了检测浏览器而调用的 JS 修改其结果:
import asyncio
from pyppeteer import launch
async def main():
browser = await launch({
'headless': False,
'args': ['--no-sandbox', '--window-size=1366,768']
})
page = await browser.newPage()
await page.setViewport({'width': 1366, 'height': 768})
await page.evaluateOnNewDocument('''() => {
Object.defineProperty(navigator, 'webdriver', { get: () => false });
}''')
await page.goto('https://login.taobao.com')
await page.evaluate('alert(navigator.webdriver)')
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
元素操作
ElementHandle 表示页内的DOM元素,你可以通过 page.querySelector() 方法创建。DOM 元素具有和 page 相同的某些方法:J()、JJ()、Jeval()、JJeval()、screenshot()、type()、click()、tap()。此外,还有一些好用的方法:
(1) 获取元素边界框坐标:boundingBox(),返回元素的边界框(相对于主框架)=> x 坐标、 y 坐标、width、height
(2) 元素是否可见:isIntersectingViewport()
(3) 上传文件:uploadFile(*filpaths)
(4) ElementHandle 类 转 Frame类:contentFrame(),如果句柄未引用iframe,则返回None。
(5) 聚焦该元素:focus()
(6) 与鼠标相关:hover () ,将鼠标悬停到元素上面
(7) 与键盘相关:press (key[, options]),按键,key 表示按键的名称,option可配置:
-
text (string) - 如果指定,则使用此文本生成输入事件
-
delay (number) - keydown 和 keyup 之间等待的时间。默认是 0
鼠标事件
Mouse 类在相对于视口左上角的主框架 CSS 像素中运行。
(1) page.mouse.down([options]) 按下鼠标,options 可配置:
-
button(str) 按下了哪个键,可选值为 [ left, right, middle ], 默认是 left, 表示鼠标左键
-
clickCount(int) 按下的次数,单击,双击或者其他次数
(2) page.mouse.up([options]) 松开鼠标,options 同上
(3) page.mouse.move(x, y, [options]) 移动鼠标到指定位置,options.steps 表示移动的步长
(4) page.mouse.click(x, y, [options]) 鼠标点击指定的位置,其实是 mouse.move 和 mouse.down 或 mouse.up 的快捷操作
模拟登录的验证码处理
可能用到的方法:
-
ElementHandle.boundingBox()、ElementHandle.hover ()
-
mouse.down()、mouse.move()、mouse.up()、mouse.click()
实例一:淘宝验证码 拖动滑块
(1) 淘宝的验证码验证模块会检测浏览器环境,要注入JS ;
(2) 尽可能模拟用户操作,随机数减慢 Pyppeteer 的执行速度;
示例:
import asyncio
import random
from pyppeteer import launch
async def main():
browser = await launch({
'headless': False,
'args': ['--no-sandbox', '--window-size=1366,768']
})
page = await browser.newPage()
await page.setViewport({'width': 1366, 'height': 768})
await page.evaluateOnNewDocument('''() =>{
Object.defineProperties(navigator,{ webdriver:{ get: () => false } });
}''')
await page.evaluateOnNewDocument('''() =>{
window.navigator.chrome = { runtime: {}, };
}''')
await page.evaluateOnNewDocument('''() =>{
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
}''')
await page.evaluateOnNewDocument('''() =>{
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5,6], });
}''')
await page.goto('https://login.taobao.com')
await asyncio.sleep(2)
try:
await page.click('div.login-links > a.forget-pwd.J_Quick2Static')
except:
pass
await asyncio.sleep(2)
await page.type('#TPL_username_1', '123123123', {'delay': random.randint(60, 121)})
await page.type('#TPL_password_1', '1234567890', {'delay': random.randint(100, 151)})
await asyncio.sleep(1.5)
try:
el = await page.querySelector('#nc_1_n1z')
box = await el.boundingBox()
await page.hover('#nc_1_n1z')
await page.mouse.down()
await page.mouse.move(box['x'] + random.randint(333, 999), box['y'], {'steps': 5})
await page.mouse.up()
except:
pass
await asyncio.sleep(1.8)
await page.click('#J_SubmitStatic')
await asyncio.sleep(5)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
实例二:铁路12306点触验证码
(1) 分析12306的验证码;这个东西是长这样的:
鼠标点击的位置,可以取值各个图片的中心点:
这个值可以计算:
- width :
37
,37 * 3
,37 * 5
,37 * 7
;即 37,111,185,259 - height(0):
70
- height(1):
70 + (190-30) / 2
,即 150
当验证码图片的坐标为 x,y 时;鼠标点击第2、7张图片的位置可以表达为 (x+111, y+70),(x+185, y+150)
示例:
import asyncio
import random
from pyppeteer import launch
async def main():
browser = await launch({
'headless': False,
'args': [f'--window-size=1366,768', '--no-sandbox']
})
page = await browser.newPage()
await page.goto('https://kyfw.12306.cn/otn/login/init',
{'waitUntil': 'networkidle0'})
await page.setViewport({'width': 1366, 'height': 768})
# 等待验证码加载
code = await page.waitForFunction(
'''() => document.querySelector("img.touclick-image")''')
# 验证码截图
await code.screenshot({'path': 'code.png'})
# 获取验证码坐标
box = await code.boundingBox()
await page.waitFor(2 * 1000)
# 点击第2张图片
await page.mouse.click(box['x']+111, box['y']+70)
await page.waitFor(random.randint(567, 3456))
# 点击第7张图片
await page.mouse.click(box['x']+185, box['y']+150)
await page.waitFor(3 * 1000)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
(2) 打码平台:12306的验证码识别有点反人类;对接打码平台是比较不错的选择;原理就是把验证码图片以字节的方式发给他们,返回一个字符串,例如:183,68|193,161
;
超级鹰打码平台API:
chaojiying.py
#!/usr/bin/env python
# coding:utf-8
import requests
from hashlib import md5
class CodeInfo(object):
def __init__(self):
self.username = '用户名'
self.password = md5('密码'.encode('utf8')).hexdigest()
self.soft_id = '96001' # 用户中心 >> 软件ID ,生成一个替换 96001
self.base_params = {
'user': self.username,
'pass2': self.password,
'softid': self.soft_id,
}
self.headers = {
'Connection': 'Keep-Alive',
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
}
def process(self, im, codetype):
url = 'http://upload.chaojiying.net/Upload/Processing.php'
params = {'codetype': codetype}
params.update(self.base_params)
files = {'userfile': ('ccc.jpg', im)}
r = requests.post(url, data=params, files=files, headers=self.headers)
return r.json()
def report(self, im_id):
"""
im_id:报错题目的图片ID
"""
url = 'http://upload.chaojiying.net/Upload/ReportError.php'
params = {'id': im_id}
params.update(self.base_params)
r = requests.post(url, data=params, headers=self.headers)
return r.json()
if __name__ == '__main__':
im = open('code.png', 'rb').read()
"""
9004 验证码类型
参考 http://www.chaojiying.com/price.html
"""
answer = CodeInfo().process(im, 9004)
print(answer)
登录12306的例子:
import asyncio
import random
from pyppeteer import launch
from chaojiying import CodeInfo
def pic_info():
im = open('code.png', 'rb').read()
answer = CodeInfo().process(im, 9004)
print(answer)
return answer['pic_str']
async def main():
browser = await launch({
'headless': False,
'args': ['--window-size=1366,768', '--no-sandbox']
})
page = await browser.newPage()
await page.goto('https://kyfw.12306.cn/otn/login/init',
{'waitUntil': 'networkidle0'})
await page.setViewport({'width': 1366, 'height': 768})
code = await page.waitForFunction(
'''() => document.querySelector("img.touclick-image")''')
await code.screenshot({'path': 'code.png'})
await page.waitFor(2 * 1000)
await page.type('#username', '123456789@qq.com',
{'delay': random.randint(60, 121)})
await page.waitFor(random.randint(345, 1234))
await page.type('#password', '1234567890',
{'delay': random.randint(100, 151)})
pic_str = pic_info()
points = list(set(pic_str.split('|')))
box = await code.boundingBox()
for point in points:
p = point.split(',')
await page.mouse.click(box['x']+int(p[0]), box['y']+int(p[1]))
await page.waitFor(random.randint(567, 3456))
await page.click('#loginSub')
await page.waitFor(5 * 1000)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
键盘事件
Keyboard 提供一个接口来管理虚拟键盘. 高级接口为 keyboard.type, 其接收原始字符, 然后在你的页面上生成对应的 keydown, keypress/input, 和 keyup 事件。
为了更精细的控制(虚拟键盘), 你可以使用 keyboard.down, keyboard.up 和 keyboard.sendCharacter 来手动触发事件, 就好像这些事件是由真实的键盘生成的。
键盘的几个API如下:
- keyboard.down(key[, options]) 触发 keydown 事件
- keyboard.press(key[, options]) 按下某个键,key 表示键的名称,比如‘ArrowLeft’ 向左键;
- keyboard.sendCharacter(char) 输入一个字符
- keyboard.type(text, options) 输入一个字符串
- keyboard.up(key) 触发 keyup 事件
持续按下 Shift 来选择一些字符串并且删除的例子:
import asyncio
from pyppeteer import launch
async def main():
browser = await launch({'headless': False})
page = await browser.newPage()
await page.goto('https://www.baidu.com', {'waitUntil': 'networkidle0'})
el = await page.J('#kw')
await el.focus()
await page.keyboard.type('Hello, World!')
await page.keyboard.press('ArrowLeft')
await page.keyboard.down('Shift')
for _ in ' World':
await page.keyboard.press('ArrowLeft')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
# 结果字符串最终为 'Hello!'
await asyncio.sleep(5)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
按下 A
的例子:
await page.keyboard.down('Shift')
await page.keyboard.press('KeyA')
await page.keyboard.up('Shift')
详细的键名映射可以看源码:
Lib\site-packages\pyppeteer\us_keyboard_layout.py
内嵌框架
可以通过 Page.frames、ElementHandle.contentFrame 方法获取,同时具有和 page 多个方法;
**其它:
- childFrames 获取子框架,返回列表
- parentFrame 返回父框架
- content() 返回框架的 html 内容
- url 获取 url
- name 获取 name
- title() 获取 title
例子:
import asyncio
from pyppeteer import launch
async def main():
browser = await launch({'headless': False})
page = await browser.newPage()
await page.goto('http://www.4399.com', {'waitUntil': 'networkidle0'})
await page.click('#login_tologin')
await asyncio.sleep(1)
frame = page.frames[1]
await frame.type('#username', '123456789')
await frame.type('#j-password', '998765433')
await asyncio.sleep(5)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
或者:
await page.click('#login_tologin')
await asyncio.sleep(1)
element = await page.J('iframe')
frame = await element.contentFrame()