Featured image of post 手把手教学:如何用PlayWright实现爬虫来爬取实时油价

手把手教学:如何用PlayWright实现爬虫来爬取实时油价

背景

最近的油价波动很大,再加上用车较多,家里领导说想更多了解油价的变化情况,我说,那就弄个爬虫来爬爬看呗。

工具

网页爬虫不是一个新技术,方法也很多,有直接模拟请求的,也有用工具来拦截响应的,通常要根据不同的情况分析来决定最合适的方式。

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去服务器获取数据,得到响应数据后,保存到本地文件里。

实现

实现过程中有几个要点:

  1. Playwright可以运行在同步模式下,即每个语句(与页面元素相关的语句)都会自动等待元素加载完成。
  2. 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是这样的。

后面我会把这些数据保存到数据库里方便检索和查询更新。

By 大可出奇迹