爬虫避坑新思路:别一碰到JS渲染就上无头浏览器,内嵌预载数据才是最简解
评论
收藏

爬虫避坑新思路:别一碰到JS渲染就上无头浏览器,内嵌预载数据才是最简解

经验分享
小可耐教你学影刀RPA【哔哩哔哩同名】
2026-06-04 11:25·浏览量:475
小可耐教你学影刀RPA【哔哩哔哩同名】
影刀高级开发者
发布于 2026-06-04 11:25475浏览

做网页爬虫时绝大多数人的惯性思维:遇到动态渲染空白页面→立刻安装Playwright/Selenium启动浏览器加载JS。但踩过无数坑后才发现:大量主流站点在SSR渲染时,已把全量业务数据预埋进初始HTML源码,只用requests+正则就能抓完全部内容,零浏览器、零环境依赖,语雀知识库全文档爬取就是典型落地案例。

一、常规爬虫踩坑:requests直爬语雀返回空链接

想要批量抓取公开语雀知识库所有正文文档链接,新手起步标准代码如下:

import requests
from bs4 import BeautifulSoup

headers = {"User-Agent":"Mozilla/5.0 Chrome"}
res = requests.get("知识库地址",headers=headers)
soup = BeautifulSoup(res.text,"html.parser")
all_a = soup.find_all("a")

执行结果:页面内所有文章跳转链接全部缺失。
根源在于语雀采用SPA+SSR架构,浏览器初次HTTP请求拿到的HTML只是DOM空骨架,目录、文章、详情等核心数据由前端JS异步请求接口后动态渲染进页面,静态HTML里看不到渲染后的DOM节点,直接解析标签必然一无所获。

绝大多数教程给出的标准答案:启用自动化浏览器框架,启动Chromium内核等待页面资源加载完毕、JS执行渲染完成再解析DOM。
但浏览器方案痛点很明显:环境部署繁琐、启动耗资源、爬取效率低、容易被站点风控识别浏览器特征、容器/云服务器部署还会面临无头驱动兼容难题。

二、关键突破口:藏在HEAD标签里的预埋全局数据

排查动态页面优先打开浏览器开发者工具-元素面板,是爬虫提速的核心习惯。在语雀页面<head>头部script标签中,能发现一行固定格式代码:

window.appData = JSON.parse(decodeURIComponent("%7B%22me%22%3A......"));

平台做服务端渲染优化时,在后端拼接完整知识库数据,经过URL编码后嵌入页面源码,页面不用额外请求后端接口,前端JS直接读取window.appData变量就能渲染整站目录。
这份编码后的JSON,就是全站原始数据源,第一次HTTP请求就能完整获取,不需要等待任何JS、不用模拟浏览器访问接口。

三、三行核心逻辑,拆解预埋数据拿到全量文章

整体解析流程分三步,无需复杂依赖库:

  1. 正则截取编码字符串:通过正则匹配decodeURIComponent("内容"),提取双引号内URL编码字符串;

  2. 解码还原标准JSON:使用urllib解码URL特殊字符,再通过json模块转为Python字典;

  3. 筛选目录数据拼接链接:字典内book.toc存储完整知识库树形目录,区分DOC(正文文档)TITLE(分类标题),过滤正文后拼接域名生成最终访问链接。

精简核心代码:

import re,json,urllib.parse
# 正则捕获编码内容
raw_encode = re.search(r'decodeURIComponent\("([^"]+)"\)',html).group(1)
# URL解码+JSON序列化
data = json.loads(urllib.parse.unquote(raw_encode))
# 筛选文档类型
doc_list = [item for item in data["book"]["toc"] if item["type"]=="DOC"]

四、完整落地脚本:一键抓取、自动落地多格式文件

基于上述原理封装完整爬虫脚本,配置知识库地址即可一键运行,自动导出TXT目录清单与JSON结构化数据,适配Windows本地存储路径:

import re
import json
import urllib.parse
from typing import List, Dict
import requests

# 配置区:替换为目标语雀知识库首页地址
BOOK_URL = "https://www.yuque.com/pengzhiqiang999/xiaokenai"
BASE_DOMAIN = "https://www.yuque.com"
# 知识库空间路径,跟随链接自定义
SPACE_PATH = "pengzhiqiang999/xiaokenai"

def get_page_html(url: str) -> str:
    """模拟浏览器请求,获取页面原始HTML源码"""
    req_headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
        "Accept-Language": "zh-CN,zh;q=0.9",
        "Referer": "https://www.yuque.com/"
    }
    resp = requests.get(url, headers=req_headers, timeout=15)
    resp.raise_for_status()
    return resp.text

def parse_app_data(html_content: str) -> dict:
    """正则提取并解码window.appData全局数据"""
    reg_rule = r'decodeURIComponent\("([^"]+)"\)'
    match_res = re.search(reg_rule, html_content)
    if not match_res:
        raise Exception("页面数据格式变更,未匹配到appData编码字段")
    encode_str = match_res.group(1)
    json_str = urllib.parse.unquote(encode_str)
    return json.loads(json_str)

def filter_article_list(raw_data: dict) -> List[Dict]:
    """从原始数据过滤正文文档,剔除分类标题"""
    # 兼容语雀多版本数据结构
    toc_data = raw_data.get("book",{}).get("toc") or raw_data.get("toc",[])
    article_arr = []
    for node in toc_data:
        # DOC代表正文文章,TITLE为目录分组
        if node.get("type") == "DOC" and node.get("url"):
            article_arr.append({
                "title": node.get("title","无标题文档"),
                "slug": node["url"],
                "full_url": f"{BASE_DOMAIN}/{SPACE_PATH}/{node['url']}"
            })
    return article_arr

def main():
    print(f"开始爬取知识库:{BOOK_URL}\n")
    html = get_page_html(BOOK_URL)
    source_data = parse_app_data(html)
    # 保存原始全量JSON,方便后续调试字段变更
    with open("app_data_raw.json","w",encoding="utf-8") as f:
        json.dump(source_data,f,ensure_ascii=False,indent=2)
    article_info = filter_article_list(source_data)
    if not article_info:
        print("未抓取到文章,查看app_data_raw.json核对toc字段路径")
        return
    # 控制台打印结果
    print(f"共成功抓取{len(article_info)}篇正文文档:")
    print(f"{'序号':<4}{'文档标题':<42}{'访问链接'}")
    print("-"*110)
    for idx,art in enumerate(article_info,1):
        print(f"{idx:<4}{art['title']:<42}{art['full_url']}")
    # 落地存储:TXT文档清单 + JSON结构化文件
    save_txt_path = r"D:\桌面\公司产品\article_links.txt"
    save_json_path = r"D:\桌面\公司产品\article_links.json"
    with open(save_txt_path,"w",encoding="utf-8") as f:
        for art in article_info:
            f.write(f"{art['title']}\t{art['full_url']}\n")
    with open(save_json_path,"w",encoding="utf-8") as f:
        json.dump(article_info,f,ensure_ascii=False,indent=2)
    print(f"\n✅ 数据落地完成:\n文本清单:{save_txt_path}\n结构化JSON:{save_json_path}")

if __name__ == "__main__":
    main()

五、通用爬虫方法论:动态页面排查标准顺序

以后遇到SPA动态渲染页面,遵循先查预埋数据→再查接口→最后启用浏览器的排查顺序,大幅降低爬虫开发成本:

  1. 检索全局预埋变量:页面源码/开发者工具搜索关键词:__NEXT_DATA__(NextJS站点)window.__INITIAL_STATE__(Vue/React站点)appDatawindow.__PRELOADED_STATE__,小某书、知乎、掘金、语雀等主流平台普遍采用该方案;

  2. 抓接口请求:若无预埋数据,打开F12-网络面板筛选XHR/Fetch,找到后端数据接口,直接调用API拿JSON;

  3. 兜底方案浏览器渲染:前两种方式失效后,再使用Playwright、Selenium启动浏览器渲染页面。

依托站点SSR预埋数据爬取,代码体量小、运行速度快、资源占用极低,是动态页面爬虫性价比最高的实现方案。

收藏11
全部评论1
最新
发布评论
评论