攻克JSON嵌套HTML的双重解析难题:基于多层数据提取的精准爬虫设计

一、引言

在实际爬虫开发中,最复杂的场景之一莫过于:接口返回JSON数据,但JSON中又嵌套着HTML片段,需要先解析JSON,再从HTML中提取目标数据。这种"套娃"式的数据结构,对爬虫的解析能力提出了极高要求。

本文将深入分析一个针对 icis.com 新闻站点的爬虫设计案例。该爬虫创新性地解决了 "JSON嵌套HTML的双重解析""复杂URL正则提取" 两大核心难题,实现了对多层嵌套数据的高效采集。

与之前案例的核心差异:

  • 数据源差异:JSON中嵌套HTML(前几个案例要么是纯HTML,要么是纯JSON)
  • 解析层级差异:需要先JSONPath解析,再CSS选择器提取
  • 正则复杂度差异:需要处理URL路径中的特定模式
  • 分页控制差异:固定4页(反映内容总量有限)

二、系统架构与核心流程

2.1 整体架构设计

该爬虫采用 "JSON解析 → HTML提取 → 双重循环处理" 的架构,整体流程如下:




开始
时间范围计算

90天窗口
查询数据库已有记录
是否有新数据?
GET请求JSON接口

admin-ajax.php
结束流程
JSONPath解析

提取$.data.html
从HTML片段中

提取URL列表
输出URL列表

用于调试
循环处理每个URL
是否已存在?
复杂正则提取ID
抓取新闻详情页
提取结构化数据

标题/时间/作者/内容
存入数据库

2.2 数据流向图

三、关键技术难点与解决方案

难点一:JSON嵌套HTML的双重解析

问题描述:

接口返回的是JSON数据,但真正的列表HTML被包裹在JSON的某个字段中。需要先解析JSON获取HTML字符串,然后再从HTML中提取URL。这种"套娃"结构无法用单一解析方式解决。

解决方案:

采用 "JSONPath + CSS选择器" 双层解析策略:

javascript 复制代码
// 抓取新闻地址列表节点配置
{
    "url": "https://www.icis.com/explore/wp/wp-admin/admin-ajax.php?action=icis-library-get-articles&current_page=${page}",
    "method": "GET"
}

// 新闻地址列表节点 - 双层解析
{
    "variable-name": ["news_urllist"],
    "variable-value": [
        // 第一步:JSONPath提取HTML字符串
        // 第二步:从HTML字符串中用CSS选择器提取href
        "${page==null?null:extract.selectors(
            extract.jsonpath(resp.json, '$.data.html'),  // 先取JSON中的HTML
            '.icis-library-grid-item>div>a',              // 再从HTML中取链接
            'attr', 
            'href'
        )}"
    ]
}

双层解析原理图:
提取结果
HTML层
JSON层
JSONPath: $.data.html
CSS选择器

.icis-library-grid-item>div>a
CSS选择器
{
'data': {
'html': '
...
'

}
}

新闻1

新闻2
/news/12345/abc
/news/12346/def

解析流程示意图:
JSONPath

$.data.html
CSS选择器

.item>a
JSON响应
HTML字符串
URL列表

难点二:复杂URL中的ID正则提取

问题描述:

新闻URL格式为 /news/12345/abc,需要提取中间的12345作为新闻ID。但URL中还有后续的字母部分,简单的数字提取会匹配到不需要的内容。

解决方案:

使用 "零宽断言" 精确匹配特定位置的数字:

javascript 复制代码
// 定义变量节点 - 复杂正则提取
{
    "variable-name": ["news_url", "news_id", "news_urlmap", "query_result"],
    "variable-value": [
        "${news_urllist[index]}",
        // 正则解释: (?<=/) 匹配前面有斜杠的位置
        //           (\d+)   匹配连续数字
        //           (?=/[a-z]) 匹配后面有斜杠+小写字母的位置
        "${news_url.regx('(?<=/)(\\d+)(?=/[a-z])').toInt()}",
        "${{'url': news_url}}",
        "${!rs.contains(news_urlmap)}"
    ]
}

正则提取原理图:
匹配过程
正则表达式
URL字符串
/news/
12345
/abc
(?<=/)
(\d+)
(?=/[a-z])
定位到/news/后面
提取12345
确认后面是/abc
提取结果: 12345

正则分解说明:

正则部分 含义 作用
(?<=/) 正向零宽断言 匹配位置:前面必须是斜杠
(\d+) 捕获组 匹配连续数字并捕获
(?=/[a-z]) 正向零宽断言 匹配位置:后面必须是斜杠+小写字母

示例匹配:

URL 匹配结果
/news/12345/abc ✅ 12345
/news/12345/ ❌ 不匹配(后面没有小写字母)
/blog/12345/abc ❌ 不匹配(前面不是/news/)
/news/abc123/def ❌ 不匹配(数字不在正确位置)

难点三:固定页数控制(4页)

问题描述:

该网站新闻总量有限,只需要抓取前4页即可覆盖所有目标内容。

解决方案:

设置固定的分页上限:

javascript 复制代码
// 分页控制条件(蓝色连线)
"condition": "${page<=4}"

// 定义变量节点 - 分页控制
{
    "variable-name": ["page"],
    "variable-value": ["${page==null?1:page+1}"]
}

分页控制对比:

案例 分页上限 原因
chemanalyst 23页 全量抓取
polymerupdate新闻 1页 仅第一页(最近新闻)
bioplasticsnews 动态list.length 每页固定30条
polymerupdate博客 1页 仅第一页
icis新闻 4页 内容总量有限

难点四:90天时间窗口

问题描述:

需要回溯90天的新闻数据,反映该网站新闻更新频率较低。

解决方案:

动态时间范围计算:

javascript 复制代码
// 获取时间范围节点
{
    "variable-name": ["start_date", "end_date"],
    "variable-value": [
        "${date.format(date.addDays(date.now(),-90),'yyyy-MM-dd')}",  // 90天前
        "${date.format(date.addDays(date.now(),1),'yyyy-MM-dd')}"     // 明天
    ]
}

难点五:多层嵌套的空值保护

问题描述:

由于涉及两层解析(JSONPath + CSS选择器),空值保护需要更加谨慎。

解决方案:

链式空值保护:

javascript 复制代码
// 第一层:page为null时不执行任何解析
// 第二层:JSONPath结果为null时,selectors返回空列表
"${page==null?null:extract.selectors(
    extract.jsonpath(resp.json, '$.data.html'),
    '.icis-library-grid-item>div>a',
    'attr',
    'href'
)}"

四、核心代码实现解析

4.1 双层解析器

javascript 复制代码
// 伪代码:双层解析器
class NestedDataParser {
    async fetchAndParse(page) {
        // 1. 请求JSON接口
        const jsonResponse = await this.fetchJson(page);
        
        // 2. JSONPath提取HTML
        const htmlString = this.extractHtmlFromJson(jsonResponse);
        
        // 3. 从HTML中提取URL列表
        const urlList = this.extractUrlsFromHtml(htmlString);
        
        return urlList;
    }
    
    extractHtmlFromJson(json) {
        // JSONPath: $.data.html
        return json?.data?.html || '';
    }
    
    extractUrlsFromHtml(html) {
        // 使用正则模拟CSS选择器
        const regex = /<a[^>]+href="([^"]+)"[^>]*>/g;
        const urls = [];
        let match;
        while ((match = regex.exec(html)) !== null) {
            urls.push(match[1]);
        }
        return urls;
    }
}

4.2 复杂正则提取器

javascript 复制代码
// 伪代码:复杂正则提取器
class RegexIdExtractor {
    constructor(pattern) {
        this.pattern = pattern; // (?<=/)(\d+)(?=/[a-z])
    }
    
    extractIdFromUrl(url) {
        const match = url.match(this.pattern);
        if (match) {
            return parseInt(match[1]); // 捕获组(\d+)的内容
        }
        return null;
    }
    
    // 批量提取
    extractAll(urls) {
        return urls.map(url => this.extractIdFromUrl(url))
                   .filter(id => id !== null);
    }
}

// 使用示例
const extractor = new RegexIdExtractor(/(?<=\/)(\d+)(?=\/[a-z])/);
const id = extractor.extractIdFromUrl('/news/12345/abc'); // 12345

4.3 链式解析安全处理器

javascript 复制代码
// 伪代码:链式解析安全处理器
class SafeChainedParser {
    static safeExtract(json, jsonPath, cssSelector, attribute) {
        // 1. 检查JSON是否存在
        if (!json) return [];
        
        // 2. JSONPath提取
        const html = this.safeJsonPath(json, jsonPath);
        if (!html) return [];
        
        // 3. 从HTML中提取
        return this.safeCssExtract(html, cssSelector, attribute);
    }
    
    static safeJsonPath(json, path) {
        try {
            return extract.jsonpath(json, path);
        } catch (error) {
            console.error('JSONPath解析失败:', error);
            return null;
        }
    }
    
    static safeCssExtract(html, selector, attribute) {
        try {
            return extract.selectors(html, selector, attribute);
        } catch (error) {
            console.error('CSS提取失败:', error);
            return [];
        }
    }
}

五、与前四个案例的对比分析

5.1 核心差异点对比

维度 icis新闻 polymerupdate博客 polymerupdate新闻 bioplasticsnews chemanalyst
数据源类型 JSON嵌套HTML 纯JSON HTML HTML HTML
解析方式 JSONPath + CSS JSONPath CSS选择器 CSS选择器 CSS选择器
解析层级 2层 1层 1层 1层 1层
正则复杂度 复杂(零宽断言) 简单 简单 简单 简单
分页上限 4页 1页 1页 动态 23页
时间窗口 90天 30天 7天 90天 7天
ID提取方式 复杂正则 对象属性 URL正则 article的id属性 URL正则

5.2 差异化技术难点

icis新闻
JSON嵌套HTML解析
复杂正则提取
4页固定分页
polymerupdate博客
JSON接口解析
对象数组处理
bioplasticsnews
双列表协同提取
路径参数分页
polymerupdate新闻
POST表单分页
代理分层策略
chemanalyst
GET参数分页
23页全量抓取

5.3 解析复杂度演进

纯HTML解析

chemanalyst
纯JSON解析

polymerupdate博客
JSON嵌套HTML

icis新闻


六、性能优化与最佳实践

6.1 双层解析优化

javascript 复制代码
// 缓存HTML字符串避免重复解析
class CachedParser {
    constructor() {
        this.htmlCache = new Map();
    }
    
    parse(json) {
        const html = json?.data?.html;
        if (!html) return [];
        
        // 使用HTML作为缓存key
        if (this.htmlCache.has(html)) {
            return this.htmlCache.get(html);
        }
        
        const urls = extract.selectors(html, '.item>a', 'attr', 'href');
        this.htmlCache.set(html, urls);
        return urls;
    }
}

6.2 正则表达式优化

正则类型 表达式 性能 适用场景
贪婪匹配 .* 不推荐
懒惰匹配 .*? 一般场景
零宽断言 (?<=/)(\d+)(?=/) 精确定位

6.3 空值处理链

javascript 复制代码
// 完整的空值处理链
const urls = page != null 
    ? (json?.data?.html 
        ? extract.selectors(json.data.html, '.item>a', 'attr', 'href')
        : [])
    : [];

七、总结与经验分享

7.1 核心收获

  1. 双层解析技术:JSON + HTML嵌套结构的处理技巧
  2. 复杂正则提取:零宽断言精确定位URL中的ID
  3. 链式空值保护:多层解析时的安全处理
  4. 固定分页控制:根据内容总量灵活设置分页上限

7.2 可复用经验

  1. 识别嵌套结构:通过浏览器开发者工具分析数据来源,识别是否是JSON嵌套HTML
  2. 分层解析策略:先JSONPath后CSS,分步处理
  3. 正则精确匹配:使用零宽断言提高提取精度
  4. 空值保护链:每层解析都要考虑空值情况

7.3 适用场景

该爬虫设计模式适用于:

  • 接口返回JSON但包含HTML片段的网站
  • URL结构复杂需要精确定位的场景
  • 需要多层解析的数据源
  • 内容总量可控的网站

八、附录:核心配置对照表

节点类型 核心作用 关键技术点
获取时间范围 动态计算90天窗口 date.addDays(now,-90)
执行SQL(查询) 获取已抓取记录 like '%icis.com%'
抓取新闻地址列表 GET请求JSON接口 admin-ajax.php?action=...
新闻地址列表 双层解析 JSONPath + CSS选择器
新闻地址 复杂正则提取 (?<=/)(\d+)(?=/[a-z])
开始抓取 获取新闻详情页 无代理
内容 结构化提取 标题/时间/作者/内容
执行SQL(插入) 存储数据 source='icis'

通过以上设计,该爬虫成功应对了JSON嵌套HTML和复杂正则提取的双重挑战,实现了对icis.com新闻网站的高效增量抓取。其中的双层解析技术、复杂正则零宽断言等思路,对于处理多层嵌套数据结构的爬虫开发具有很高的参考价值。

相关推荐
前端小趴菜~时倾2 小时前
自我提升-python爬虫学习:day05-函数与面向对象编程
爬虫·python·学习
进击的雷神2 小时前
攻克JSON接口分页与对象数组处理:基于AJAX数据源的精准博客爬虫设计
爬虫·ajax·json·spiderflow
LJianK12 小时前
vxe-table 的 checkbox复选框
前端·html
圣光SG4 小时前
奶茶店网页(纯HTML和CSS)
前端·css·html
予你@。4 小时前
vue 使用html2canvas + jsPDF 将html导出为pdf (延伸问题)
vue.js·pdf·html
不会写DN5 小时前
Go 生态最快 JSON 库 - jsoniter
开发语言·golang·json
KevinCyao5 小时前
批量发短信接口的数据格式设计:CSV、JSON还是XML?
xml·前端·前端框架·json
电商API&Tina5 小时前
主流跨境平台多站点 API 接入流程:淘宝天猫京东API接口
java·大数据·网络·数据库·人工智能·sql·json
西门吹牛5 小时前
RV1126B移植mosquitto并且写C语言调用
c语言·开发语言·json