背景
最近的油价波动很大,再加上用车较多,家里领导说想更多了解油价的变化情况,我说,那就弄个爬虫来爬爬看呗。
工具
网页爬虫不是一个新技术,方法也很多,有直接模拟请求的,也有用工具来拦截响应的,通常要根据不同的情况分析来决定最合适的方式。
PlayWright
PlayWright (https://playwright.dev/)通常是用来进行网页自动化交互测试的,它集成了Chromium, Firefox, Webkits, Edge浏览器,可以选择任意一种来进行自动化测试,更重要的是它提供了headless模式,即无界面模式。PlayWright提供了丰富的api,比如可以查找元素,实现点击、移动的效果,并且支持嵌入javascript代码直接在页面里运行。
与Pythong的request包相比,Playwright帮用户管理了所有过程中的cookie和中间状态, 运行的效果跟打开浏览器地址是完全相同。
Playwright提供了Python和Javascript两种sdk,当然, 我这次还是用Python来演示
准备
首先准备python环境, 我假设各位看官已经都具备了,我们来安装playwright和自带的浏览器内核:
pip install playwright
playwright install
上面第二条命令会自动安装多个浏览器内核,chromium, firefox, webkit,你也可以指定浏览器内核名称来安装。注意,这几个内核跟已经安装在你电脑上的任何浏览器都是隔离的,playwright会使用这些内核来运行测试或者页面抓取。
分析
今天要爬取的网站是https://petrolspy.com.au/,我们需要先分析下这个网站,以确定我们需要的数据在哪里
页面加载到浏览器后(我用的是Chrome浏览器,不是刚刚下载的Chromium内核,是安装在我的电脑上的Chrome浏览器),左侧有一个地图,每当拖动地图时,数据会重新加载,我们点击界面右上角的三个点打来Chrome的菜单,选择“开发者工具”
现在切换到“Network”, 并在请求类型中选中“Fetch/XHR"
现在拖动地图,观察“network”请求的列表,会发现有一个“box?“的请求每次都会返回一个复杂的JSON数据
好了,这个就是我们要找的价格数据了。
仔细观察这个请求头,
实际上,它包含了由右上角和左下角GPS坐标围成的BOX,并且有两个时间戳,old坐标是表示地图从之前的区域移到了当前的区域。
这里面的“ts”和“_”两个时间戳是关键,初步估计一个是当前请求的时间戳,一个表示最近更新油价的时间戳,为了不被服务器识别到异常,我打算从页面上取得这两个时间戳再配上我想要的坐标box值来构造请求的参数
设计
现在,我们来设计个自动化流程,它可以借助playwright来自动保存油价数据。
基本的思想是:
我先在地图上找出若干个box,即左上角和右下角的坐标,这一部分我是手工做的,但只需要做一次,注意点是:这个box不要太大,否则服务器会因为数据太多而拒绝返回数据。
然后,用Playwright模拟鼠标拖动地图,每拖动一次,playwright会截获一个价格请求,我的python代码会解析出当时的“ts”和“_“时间戳,再结合我准备好的box 生成一个新的请求,丢给playwright去服务器获取数据,得到响应数据后,保存到本地文件里。
实现
实现过程中有几个要点:
- Playwright可以运行在同步模式下,即每个语句(与页面元素相关的语句)都会自动等待元素加载完成。
- Playwright在处理由javascript发起的Fetch/XHR或者ajax请求时,提供了一个拦截器,我们可以通过拦截器来修改请求的地址,请求头,甚至篡改返回结果,对,这就是Playwright的强大之处。
from playwright.sync_api import sync_playwright, Position, Page, Route
import json
import threading
from urllib.parse import parse_qs, urlencode
from fuel_map_urls import map_urls
from datetime import datetime
def go_page():
with sync_playwright() as p:
browser = p.firefox.launch(headless=False)
context = browser.new_context(
java_script_enabled=True,
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
)
page: Page = context.new_page()
page.goto("https://petrolspy.com.au/map/latlng/-33.8674869/151.2069902")
def box_handler(route: Route):
print(f" ---- route: {route.request.url} ----")
host = route.request.url.split("?")[0]
query = route.request.url.split("?")[1]
query_dict = parse_query_string(query)
new_box = convert_new_box(query_dict, new_loc)
route.request._url = f"{host}?{urlencode(new_box)}"
response = route.fetch()
body = response.json()
with open(f"fuels/fuel_{datetime.now().timestamp()}.json", "w") as fj:
json.dump(body, fj)
route.continue_()
print("==== box handler done ====")
page.route("**/box?*", box_handler)
for map_url in map_urls:
page.wait_for_selector("#map-canvas")
map_view = page.locator("#map-canvas")
map_view.wait_for()
map_box = map_view.bounding_box()
start_point = {
"x": map_box["x"] + map_box["width"] / 2,
"y": map_box["y"] + map_box["height"] / 2,
}
end_point = {
"x": start_point["x"] + offset * direction,
"y": start_point["y"] + offset * direction,
}
page.mouse.move(start_point["x"], start_point["y"])
page.mouse.down()
page.mouse.move(end_point["x"], end_point["y"])
page.mouse.up()
page.wait_for_selector("#map-canvas")
page.wait_for_timeout(2000)
print("==== box handler returned ====")
context.close()
browser.close()
核心是page.route("**/box?*", box_handler)
这句代码第一个参数是url的匹配,第二个参数是是处理函数。
我这里省略了少量函数,比如convert_new_box
因为写的太丑了,不好意思拿出来看,它用来把query_dict
里面的"ts"和”_“放到new_box里面,并且用map_url里面的坐标来代替,返回一个dict,key就是上图Payload那些key
效果
最后,看下效果,我抓下来的json是这样的。
后面我会把这些数据保存到数据库里方便检索和查询更新。