动态网站爬虫实战:SpiderFlow 可视化编排与自定义函数
介绍使用 SpiderFlow 可视化工具结合自定义 JavaScript 函数爬取动态加载网站(Coveris)的方法。通过 F12 分析 XHR 请求定位数据接口,模拟滚动触发加载,利用正则解析 JSON 响应提取 URL。流程包含分页抓取、去重判断、详情提取及数据库存储。解决了传统静态解析无法获取异步内容的问题,实现了新闻数据的自动化采集。

介绍使用 SpiderFlow 可视化工具结合自定义 JavaScript 函数爬取动态加载网站(Coveris)的方法。通过 F12 分析 XHR 请求定位数据接口,模拟滚动触发加载,利用正则解析 JSON 响应提取 URL。流程包含分页抓取、去重判断、详情提取及数据库存储。解决了传统静态解析无法获取异步内容的问题,实现了新闻数据的自动化采集。

在爬虫开发过程中,我们经常会遇到动态加载的网站,这类网站采用了现代前端框架(如 React、Vue 或 Angular)构建,数据是通过 JavaScript 异步加载的。传统的基于静态 HTML 解析的爬虫框架往往无法直接定位元素,因为它们只能获取初始 HTML 文档,而无法执行 JavaScript 代码来获取动态生成的内容。
本文将分享一个实际案例,展示如何通过自定义函数 + 可视化爬虫编排的方式,成功爬取动态加载的 Coveris 新闻网站。Coveris 是一家国际包装解决方案提供商,其新闻中心页面采用了典型的 AJAX 动态加载技术,新闻内容通过异步请求加载,页面 URL 保持不变,这给传统爬虫带来了很大挑战。
具体解决方案包括以下几个关键步骤:
在实现过程中,我们特别注意到:
通过这种方法,我们最终成功获取了 Coveris 网站的新闻数据,包括标题、发布日期、正文内容和相关图片链接,为后续的数据分析和商业情报收集提供了可靠的数据源。
挑战:
[开始] → [获取时间范围] → [查询已爬取 URL] → [分页抓取列表] → [解析 URL 列表] → [循环处理] → [去重判断] → [抓取详情] → [提取内容] → [存储数据] → [结束]
列表接口 https://www.coveris.com/en/news/press-releases
列表中拿到内容标题和详情链接:https://www.coveris.com/en/news/coveris-and-tipa-enter-exclusive-agreement-to-deliver-home-compostable-produce-labels
对于动态加载的网站,我们不能直接解析页面 HTML,而是需要找到真实的数据接口。
// 自定义函数:match_coveris
// 功能:从 Ajax 响应中提取新闻 URL
// 输入:htmlStr (Ajax 返回的 JSON 字符串)
// 输出:新闻 URL 数组
// 注册位置:系统管理 → 自定义函数 → 添加函数
function match_coveris(htmlStr) {
// 1. 获取 html 字段中的内容
var htmlContent = htmlStr;
if (!htmlContent) return [];
// 2. 正则匹配:提取 "url" 字段的值
// 正则说明:匹配 "url": "具体 URL" 格式
var regex = /"url"\s*:\s*"(.*?)"/g;
// 3. 执行匹配并提取结果
var matches;
var results = [];
while ((matches = regex.exec(htmlContent)) !== null) {
var sentence = matches[1].trim();
if (sentence) { results.push(sentence); }
}
// 4. 返回 URL 数组
return results;
}
<!-- 爬虫基础配置 -->
<startNode>
<spiderName>000227_抓取 coveris 新闻</spiderName>
<submit-strategy>random</submit-strategy>
<!-- 随机提交策略,避免被识别 -->
<threadCount></threadCount>
<!-- 线程数留空,使用默认值 -->
</startNode>
<!-- 变量节点:动态计算时间范围 -->
<variableNode name="获取时间范围">
<!-- 变量配置 -->
<variable name="start_date" value="${date.format(date.addDays(date.now(),-30),'yyyy-MM-dd')}" description="开始日期:30 天前"/>
<variable name="end_date" value="${date.format(date.addDays(date.now(),1),'yyyy-MM-dd')}" description="结束日期:明天"/>
</variableNode>
<!-- 输出节点:查看时间范围(用于调试) -->
<outputNode name="输出时间范围">
<output-name>["开始时间","结束时间"]</output-name>
<output-value>["${start_date}","${end_date}"]</output-value>
</outputNode>
<!-- 执行 SQL 节点:查询指定时间范围内的已爬取 URL -->
<executeSqlNode name="查询已爬取 URL">
<!-- 数据源配置 -->
<datasourceId>ee975ab73415f54e7872e57ed0031ce9</datasourceId>
<statementType>select</statementType>
<!-- SQL 语句:查询已存在的 URL 用于去重 -->
<sql> SELECT url FROM news WHERE url like '%https://www.coveris.com%' AND (insert_date BETWEEN '${start_date}' AND '${end_date}') </sql>
<!-- 查询结果将存入 rs 变量 -->
</executeSqlNode>
<!-- 输出节点:查看查询结果 -->
<outputNode name="输出查询结果">
<output-name>["开始时间","结束时间","获取结果","结果数量"]</output-name>
<output-value>["${start_date}","${end_date}","${rs}","${list.length(rs)}"]</output-value>
</outputNode>
<!-- 请求节点:抓取列表页 -->
<requestNode name="开始抓取">
<method>GET</method>
<sleep>5000</sleep>
<!-- 请求间隔 5 秒 -->
<timeout>30000</timeout>
<!-- 超时时间 30 秒 -->
<retryCount>3</retryCount>
<!-- 失败重试 3 次 -->
<retryInterval>5000</retryInterval>
<!-- 重试间隔 5 秒 -->
<!-- 请求头配置 -->
<header-name>["User-Agent"]</header-name>
<header-value>["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"]</header-value>
<!-- 动态 URL:使用 page 变量 -->
<url>https://www.coveris.com/app/article/list?page=${page}</url>
<!-- 其他配置 -->
<follow-redirect>1</follow-redirect>
<!-- 自动跟随重定向 -->
<cookie-auto-set>1</cookie-auto-set>
<!-- 自动管理 Cookie -->
</requestNode>
<!-- 变量节点:解析响应内容 -->
<variableNode name="定义变量">
<!-- 分页控制:自动累加页码 -->
<variable name="page" value="${page==null?1:page+1}" description="当前页码"/>
<!-- 调用自定义函数解析 URL 列表 -->
<variable name="news_urllist" value="${match_coveris(resp.html)}" description="新闻 URL 列表"/>
</variableNode>
<!-- 条件分支:判断是否继续分页 -->
<conditionNode name="分页判断">
<!-- 条件:当页码小于等于 2 且 URL 列表不为空时继续 -->
<condition expression="${news_urllist!=null && page<=2}">
<target>开始抓取</target>
<!-- 继续下一页 -->
</condition>
<condition expression="${news_urllist==null || page>2}">
<target>循环处理</target>
<!-- 进入详情处理 -->
</condition>
</conditionNode>
<!-- 循环节点:遍历新闻 URL 列表 -->
<loopNode name="循环">
<!-- 循环配置 -->
<loopCount>${news_urllist.length}</loopCount>
<!-- 循环次数 = URL 数量 -->
<loopStart>0</loopStart>
<!-- 起始索引 -->
<loopEnd>-1</loopEnd>
<!-- 结束索引,-1 表示到最后 -->
<loopVariableName>index</loopVariableName>
<!-- 循环变量名,从 0 开始 -->
</loopNode>
<!-- 变量节点:处理当前新闻 URL -->
<variableNode name="新闻地址">
<!-- 获取当前循环的 URL,并处理转义字符 -->
<variable name="news_url" value="${news_urllist[index].replace('\\', '')}" description="原始 URL 路径"/>
<!-- 构造完整的新闻详情页 URL -->
<variable name="news_urlmap" value="${'https://www.coveris.com' + news_url}" description="完整 URL"/>
<!-- 去重判断:检查 URL 是否已存在 -->
<variable name="query_result" value="${!rs.contains(news_urlmap)}" description="是否为新 URL"/>
</variableNode>
<!-- 输出节点:查看处理结果 -->
<outputNode name="输出 URL 信息">
<output-name>["原始路径","完整 URL","是否为新"]</output-name>
<output-value>["${news_url}","${news_urlmap}","${query_result}"]</output-value>
</outputNode>
<!-- 条件分支:基于去重结果分流 -->
<conditionNode name="去重判断">
<!-- 新 URL:进入详情抓取 -->
抓取新闻详情
输出调试信息
<!-- 请求节点:抓取新闻详情 -->
<requestNode name="抓取新闻详情">
<method>GET</method>
<sleep>5000</sleep>
<!-- 请求间隔 5 秒 -->
<timeout>30000</timeout>
<!-- 超时时间 30 秒 -->
<retryCount>3</retryCount>
<!-- 失败重试 3 次 -->
<retryInterval>5000</retryInterval>
<!-- 重试间隔 5 秒 -->
<!-- 动态 URL:使用构造好的完整 URL -->
<url>${news_urlmap}</url>
<!-- 其他配置 -->
<follow-redirect>1</follow-redirect>
<cookie-auto-set>1</cookie-auto-set>
</requestNode>
<!-- 变量节点:提取详情页内容 -->
<variableNode name="内容提取">
<!-- 使用选择器提取标题 -->
<variable name="title" value="${extract.selector(resp.html, 'h1')}" description="新闻标题"/>
<!-- 作者字段(可根据实际页面调整选择器) -->
<variable name="author" value="${extract.selector(resp.html, '.author')}" description="作者"/>
<!-- 发布日期(可根据实际页面调整选择器) -->
<variable name="release_date" value="${extract.selector(resp.html, '.date')}" description="发布日期"/>
<!-- 提取正文内容 -->
<variable name="content" value="${extract.selector(resp.html, '.text-column.theme-wysiwyg', 'text')}" description="新闻正文"/>
</variableNode>
<!-- 条件判断:内容不为空才保存 -->
<conditionNode name="内容判断">
<condition expression="${content!=null}">
<target>执行 SQL 保存</target>
输出错误日志
<!-- 执行 SQL 节点:保存新闻数据 -->
<executeSqlNode name="保存新闻数据">
<datasourceId>ee975ab73415f54e7872e57ed0031ce9</datasourceId>
<statementType>insert</statementType>
<!-- SQL 语句:插入新闻数据 -->
<sql> INSERT INTO news ( url, -- 新闻 URL
news_id, -- 新闻 ID
author, -- 作者
title, -- 标题
release_date, -- 发布日期
content, -- 内容
source, -- 来源
insert_date -- 插入时间
) VALUES (
'#${news_urlmap}#', -- URL
'#${news_id}#', -- ID(如果有)
'#${author}#', -- 作者
'#${title}#', -- 标题
'#${release_date}#', -- 发布日期
'#${content}#', -- 内容
'#www.coveris.com#', -- 来源
NOW() -- 当前时间
) </sql>
<!-- 说明:使用#号包裹变量,可防止 SQL 注入 -->
</executeSqlNode>
<!-- 输出节点:保存确认 -->
<outputNode name="保存结果">
<output-name>["URL","标题","保存状态"]</output-name>
<output-value>["${news_urlmap}","${title}","保存成功"]</output-value>
</outputNode>
<!-- ForkJoin 节点:同步多个并行分支 -->
<forkJoinNode name="执行结束">
<!-- 等待所有分支执行完毕 -->
<shape>forkJoin</shape>
</forkJoinNode>
-- 创建新闻表
CREATE TABLE `news`(
`id` int(11) NOT NULL AUTO_INCREMENT,
`news_id` bigint(15) DEFAULT NULL,
`url` mediumtext COLLATE utf8mb4_unicode_ci,
`title` mediumtext COLLATE utf8mb4_unicode_ci,
`release_date` mediumtext COLLATE utf8mb4_unicode_ci,
`author` mediumtext COLLATE utf8mb4_unicode_ci,
`content` longtext COLLATE utf8mb4_unicode_ci,
`insert_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`format_release_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '标准格式发布时间',
`source` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT '海外媒体' COMMENT '来源',
`hq_web_release_status` tinyint(2) DEFAULT '0' COMMENT '行情取数状态 (1=取数,0=未取数)',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`relate_rate` int(2) DEFAULT NULL COMMENT '相关度',
PRIMARY KEY (`id`),
KEY `news_id_index` (`id`),
KEY `news_insert_date_index` (`insert_date`)
) ENGINEInnoDB AUTO_INCREMENT CHARSETutf8mb4 utf8mb4_unicode_ci ROW_FORMAT;
<!-- 数据源 ID:ee975ab73415f54e7872e57ed0031ce9 -->
<!-- 对应数据库连接配置 -->
<datasource>
<url>jdbc:mysql://localhost:3306/spider?useSSL=false&characterEncoding=utf8</url>
<username>spider_user</username>
<password>********</password>
<driver>com.mysql.jdbc.Driver</driver>
</datasource>
/app/article/list| 策略 | 配置值 | 说明 |
|---|---|---|
| 请求间隔 | sleep=5000 | 每个请求后等待 5 秒 |
| 重试机制 | retryCount=3, retryInterval=5000 | 失败重试 3 次,间隔 5 秒 |
| User-Agent | Mozilla/5.0 … | 模拟浏览器访问 |
| 随机策略 | submit-strategy=random | 随机化请求特征 |
| 控制点 | 实现方式 | 目的 |
|---|---|---|
| 去重查询 | SELECT url FROM news WHERE … | 避免重复爬取 |
| 实时判断 | ${!rs.contains(news_urlmap)} | 动态去重 |
| 内容验证 | ${content!=null} | 确保数据完整性 |
| 时间范围 | start_date/end_date | 精确控制爬取范围 |
| 变量名 | 类型 | 用途 |
|---|---|---|
| page | 循环变量 | 分页控制 |
| news_urllist | 数组 | 存储 URL 列表 |
| news_url | 字符串 | 当前处理的 URL 路径 |
| news_urlmap | 字符串 | 完整的详情页 URL |
| query_result | 布尔值 | 去重判断结果 |
| rs | 结果集 | SQL 查询结果 |
<!-- 在每个关键节点后添加输出节点,便于跟踪变量值 -->
<outputNode name="调试输出">
<output-all>1</output-all>
<!-- 输出所有变量 -->
</outputNode>
日志管理 → 执行日志日志管理 → 错误日志任务监控 → 实时日志| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| URL 列表为空 | 正则匹配失败 | 检查响应格式,调整正则表达式 |
| 内容提取失败 | 选择器错误 | 使用浏览器开发者工具验证选择器 |
| 去重失效 | 时间范围错误 | 检查 start_date/end_date 计算逻辑 |
| 请求超时 | 网络问题 | 增加超时时间,启用重试机制 |
批量处理
<!-- 批量插入优化 -->
INSERT INTO news (url, title, content)
VALUES
<foreach collection="list" item="item" separator=",">(#{item.url}, #{item.title}, #{item.content})</foreach>
请求间隔优化
<!-- 动态间隔:根据响应时间调整 -->
<sleep>${random.nextInt(3000,8000)}</sleep>
本案例完整展示了如何通过自定义函数 + 可视化编排的方式,优雅地解决动态网站的爬取难题。关键不在于工具本身,而在于对网站加载机制的理解和灵活运用各种技术手段。通过本文的详细配置说明,相信读者能够快速上手类似的动态网站爬取任务。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online