昨天晚上刷抖音的时候我女朋友突然问我,你说你如果不认识我,你现在或者以后会跟别的女生在一起吗?
天塌了,寻思过后我给了他一个十分中立的回答: 这个说不准的
30分钟后…
她突然跟我说,我刚才问你这个问题,你回答让我感到十分的sad,如果你问我这个问题,我肯定会说不会,因为我听到这个问题的时候我的脑子里已经全部是你了
这样的言语让我十分费解,很显然,他的回答已经完全推翻了他问题的先决条件:如果你不认识我,我不禁去思考,如果一个页面上面的数据没有渲染成html元素,那么我要去怎么爬取这个页面上的数据呢?有因必有果,查找了页面的网络请求,我找到了数据请求的接口,并且在接口的返回值里找到了所需要的数据,于是,我就可以通过监听这个接口,来获取到页面上的数据了。
Selenium 监听接口
自动化工具Selenium中并不原生的带有某个可以很方便的去监听接口的方法,通过查找相关的文档,可以通过性能日志(Performance Log)去实现监听的功能。
性能日志主要用于监控和分析网页在加载和运行过程中的各项性能数据,特别适合用来排查性能瓶颈和优化页面加载速度。通常可以通过以下方式启用性能日志(以chrome浏览器为例)。
from selenium import webdriver
options = webdriver.ChromeOptions()
# 启用性能日志
caps = options.to_capabilities()
# 设置日志类别为性能日志;设置性能日志级别为ALL,即记录所有日志
caps['goog:loggingPrefs'] = {'performance': 'ALL'}
# 配置到chrome浏览器
driver = webdriver.Chrome(options=options)
# 获取性能日志
logs = driver.get_log('performance')
# 打印性能日志
for log in logs:
print(log['message'])
如果想要更方便的记录某个操作所产生的接口请求和返回数据,那么在每一步之前可以重复使用driver.get_log('performance')方法,可以达到清除之前操作产生的日志的效果。
driver.get_log('performance')
driver.find_element(By.XPATH, '//button[text()="登录"]').click()
logs = driver.get_log('performance')
for log in logs:
print(log['message'])
这样可以记录出登录按钮点击所产生的接口请求和返回数据。
通过刚才的日志内容,可以捕获到浏览器发出的所有网络请求(通过Network.requestWillBeSent事件)和接收的响应(通过Network.responseReceived事件),这里我举例Network.responseReceived获取指定接口的数据如何使用。
上面获取的logs变量中,message字段是一个json字符串,我们可以通过json模块解析出其中的内容。
import json
for log in logs:
# 解析json字符串
message = json.loads(log['message'])
# 打印响应数据
print(message['message'])
# 可能的一个日志结果为
{
"method": "Network.responseReceived",
"params": {
"response": {
"bodySize": 123,
"connectionId": 123,
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"mimeType": "application/json",
"securityDetails": null,
"status": 200,
"statusText": "OK",
"url": "https://example.com/api/data"
},
"requestId": "123",
...
}
}
于是我们就可以通过response字段中的url字段判断是否是我们所需要的接口,然后通过requestId,使用cdp命令去获取接口的返回数据。
# 获取日志内容
message = json.loads(log['message'])['message']
# 判断是否为接口响应
if message['method'] == 'Network.responseReceived':
# 获取接口返回数据
response = message['params']['response']
# 判断是否为指定接口,这里以"/api/login"举例
if "/api/login" in response['url']:
# 通过requestID获取接口返回数据
request_id = message['params']['requestId']
body = driver.execute_cdp_cmd('Network.getResponseBody', {'requestId': request_id})
print(body)
这样就可以直接获取到接口/api/login的返回数据了。
这种获取数据的方式很适用于页面里没有渲染成html元素的情况,所以就需要从接口里去拿原始数据,再自己处理。当然js逆向是完全可以的,但是大多数都没有那么容易。在我所遇到的情境中,请求数据的接口的请求体中是需要权限token和验证码的,而这个token和验证码是在登录时生成的,所以还需要去逆向登录接口的生成逻辑,有些耗时了(我是菜鸟)。通过自动化工具模拟去操作页面,跳过了请求生成的逻辑,只关注操作页面后接口的返回值,这样做可以节省很多时间。
Playwright
相比于Selenium,Playwright的监听接口的方式更加简单,直接通过page.on('response')方法即可监听所有接口的请求和返回数据,或者通过page.wait_for_response()方法等待指定接口的返回数据。
先举例获取指定接口的返回数据,前提是你得知道你想要的接口是什么。
import asyncio
from playwright.async_api import async_playwright
import json
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
page = await browser.new_page()
# 等待特定接口的响应
response = await page.wait_for_response('https://api.example.com/data')
# 获取响应信息
print(f"状态码: {response.status}")
print(f"URL: {response.url}")
print(f"响应头: {response.headers}")
# 获取响应体
response_body = await response.json() # 对于JSON响应
# 或者 response_body = await response.text() # 对于文本响应
print(f"响应体: {json.dumps(response_body, indent=2)}")
await browser.close()
asyncio.run(main())
核心就只是response = await page.wait_for_response('https://api.example.com/data')这一行,十分简单。
它也可以做到跟Selenium的监听接口的方式一样,等待指定接口的返回数据。
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
page = await browser.new_page()
# 存储所有响应
responses = []
# 监听所有响应事件
def on_response(response):
if '/api/login' in response.url:
response_body = await response.json()
# response_body = await response.text() 针对文本
responses.append({
'url': response.url,
'status': response.status,
'headers': response.headers
'body': response_body
})
print(f"捕获响应: {response.url} - 状态码: {response.status}")
page.on('response', on_response)
# 导航到页面
await page.goto('https://example.com')
# 等待一段时间让所有请求完成
await page.wait_for_timeout(5000)
await browser.close()
asyncio.run(main())
这样也可以做到跟Selenium的监听接口的方式一样,等待指定接口的返回数据,但是值得一提的是,Playwright这种监听方式会让所有的接口都被监听,即使是异步进行的,性能上也会有一定的影响。
