爬取 Luogu
Luogu 是国内知名的学习网站,为方便学习,现期望爬取其题目数据供本地学习。
技术分析
Luogu 采用的既不是前后端分离,也不是完全的服务端渲染,而是一种将两者结合的技术路线。传回的页面会有一个基本的内容用作没有 JS 的 fallback,而终端版本将会用 Vue 进行渲染。然而,这些数据并不是单独请求,而是服务端已经将数据编码为 JSON 并注入到页面当中。从页面注释,这个基本的 fallback 的主要目的是搜索引擎优化。
爬取数据
Luogu 限制了没有 UA 的请求,因此需要在请求中附带 UA。同时,对请求频率有所限制,需要在请求间进行等待。
爬取后,使用正则表达式获取注入的 JSON。
result = re.search(r'JSON\.parse\(decodeURIComponent\("(.*)"\)\)', response.text)
寻找标签
爬取得到的数据中,有一个tags。不难理解,返回的数字指的是标签对应的序号。但是,这个标签的hashmap在哪里呢?寻找所有请求,都没有发现有关tag的请求;查询所有资源,包括JS,原本的标签文本甚至是Unicode都无法找到对应的字符串。难不成 Luogu采用了量子通信?
仔细思考后,可能是Luogu在首次请求后将相关数据缓存。LocalStorage 和 SessionStorage 都较小,因此 IndexedDB 是最有可能的。果不其然,相关数据存储于名为lfeData数据库的luoguTags项目下。删除这个数据库再次请求,可以发现请求标签字符串的请求,这个请求附带了一个时间戳作为查询值(时间飞逝,时间戳竟然已经以17开头了)。
关键代码
import requests
import re
from urllib.parse import unquote
import json
from datetime import datetime
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"
}
now = datetime.now()
timestamp = datetime.timestamp(now)
response = requests.get(
"https://www.luogu.com.cn/_lfe/tags?_version=%f" % timestamp, headers=headers
)
tags = json.loads(response.text)["tags"]
tags = {i["id"]: i["name"] for i in tags}
infos = []
retry = 0
for i in range(1000, 9868):
response = requests.get("https://www.luogu.com.cn/problem/P%d" % i, headers=headers)
result = re.search(r'JSON\.parse\(decodeURIComponent\("(.*)"\)\)', response.text)
content = result.group(1)
content = unquote(content)
data = json.loads(content)
problem = data["currentData"]["problem"]
infos.append(problem)
with open("luogu.json", "w") as fp:
json.dump(infos, fp, indent=4, ensure_ascii=False)