动态网站爬虫新思路:SpiderFlow可视化编排+自定义函数实战
前言
在爬虫开发过程中,我们经常会遇到动态加载的网站,这类网站采用了现代前端框架(如React、Vue或Angular)构建,数据是通过JavaScript异步加载的。传统的基于静态HTML解析的爬虫框架(如SpiderFlow、Scrapy等)往往无法直接定位元素,因为它们只能获取初始HTML文档,而无法执行JavaScript代码来获取动态生成的内容。
本文将分享一个实际案例,展示如何通过自定义函数+可视化爬虫编排的方式,成功爬取动态加载的Coveris新闻网站。Coveris是一家国际包装解决方案提供商,其新闻中心页面采用了典型的AJAX动态加载技术,新闻内容通过异步请求加载,页面URL保持不变,这给传统爬虫带来了很大挑战。
具体解决方案包括以下几个关键步骤:
- 使用浏览器开发者工具(F12)分析XHR请求,定位到新闻数据的API接口
- 通过自定义JavaScript函数模拟滚动事件,触发更多数据的加载
- 利用可视化编排工具设置合理的请求间隔,避免触发反爬机制
- 设计数据解析逻辑,处理嵌套的JSON数据结构
在实现过程中,我们特别注意到:
- 需要设置合适的User-Agent头部信息模拟浏览器访问
- 要处理分页逻辑,通常通过观察API请求参数中的offset或page参数变化
- 对于图片等媒体资源,需要额外处理相对路径转绝对路径的问题
- 要考虑异常处理机制,如请求失败时的重试策略
通过这种方法,我们最终成功获取了Coveris网站的新闻数据,包括标题、发布日期、正文内容和相关图片链接,为后续的数据分析和商业情报收集提供了可靠的数据源。
问题背景
挑战:
- 网站内容采用动态加载技术,新闻列表通过Ajax请求获取
- 无法直接使用SpiderFlow的选择器功能定位元素
- 需要处理分页、去重、数据提取等一系列复杂逻辑
整体架构设计
[开始] → [获取时间范围] → [查询已爬取URL] → [分页抓取列表] → [解析URL列表] → [循环处理] → [去重判断] → [抓取详情] → [提取内容] → [存储数据] → [结束] 爬虫流程图
⭐ 开始 ⭐ │ ▼ ┌─────────────────────────────────┐ │ 【变量节点】获取时间范围 │ │ ┌───────────────────────────┐ │ │ │ start_date = 30天前 │ │ │ │ end_date = 明天 │ │ │ │ ${date.format(...)} │ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ 【执行SQL】查询已爬取URL │ │ ┌───────────────────────────┐ │ │ │ SELECT url FROM news │ │ │ │ WHERE insert_date BETWEEN │ │ │ │ '${start_date}' AND │ │ │ │ '${end_date}' │ │ │ └───────────────────────────┘ │ │ ↓ │ │ 结果存入: ${rs} │ └─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ 【请求节点】分页抓取列表 │ │ ┌───────────────────────────┐ │ │ │ URL: /app/article/list │ │ │ │ ?page=${page} │ │ │ │ Method: GET │ │ │ │ Sleep: 5000ms │ │ │ │ Retry: 3次 │ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ 【变量节点】解析URL列表 │ │ ┌───────────────────────────┐ │ │ │ page = ${page+1} │ │ │ │ news_urllist = │ │ │ │ ${match_coveris(resp.html)}│ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘ │ ┌─────────────────┴─────────────────┐ │ │ ▼ ▼ ┌───────────────────────────┐ ┌───────────────────────────┐ │ 【条件判断】 │ │ 【输出节点】 │ │ ${page<=2 && │ │ 记录当前页数据 │ │ news_urllist!=null} │ │ │ └───────────────────────────┘ └───────────────────────────┘ │ │ (True) │ │ (False/跳过) │ │ ▼ │ ┌───────────────────────┐ │ │ 返回继续下一页 │ │ └───────────────────────┘ │ │ │ └───────────────┬───────────────────┘ ▼ ┌─────────────────────────────────┐ │ 【循环节点】处理URL列表 │ │ ┌───────────────────────────┐ │ │ │ loopCount = │ │ │ │ ${news_urllist.length} │ │ │ │ loopVariable = index │ │ │ │ index从0开始循环 │ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ 【变量节点】构造详情URL │ │ ┌───────────────────────────┐ │ │ │ news_url = │ │ │ │ ${news_urllist[index] │ │ │ │ .replace('\','')} │ │ │ │ │ │ │ │ news_urlmap = │ │ │ │ ${'https://www.coveris.com'│ │ │ │ + news_url} │ │ │ │ │ │ │ │ query_result = │ │ │ │ ${!rs.contains(news_urlmap)}│ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘ │ ┌─────────────────┴─────────────────┐ │ │ ▼ ▼ ┌───────────────────────────┐ ┌───────────────────────────┐ │ 【条件判断】 │ │ 【输出节点】 │ │ ${query_result} │ │ URL已存在,跳过抓取 │ │ (是否为新URL) │ │ │ └───────────────────────────┘ └───────────────────────────┘ │ │ (True) │ │ (False) │ │ ▼ │ ┌───────────────────────┐ │ │ 继续抓取详情 │ │ └───────────────────────┘ │ │ │ └───────────────┬───────────────────┘ ▼ ┌─────────────────────────────────┐ │ 【请求节点】抓取新闻详情 │ │ ┌───────────────────────────┐ │ │ │ URL: ${news_urlmap} │ │ │ │ Method: GET │ │ │ │ Sleep: 5000ms │ │ │ │ Retry: 3次 │ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ 【变量节点】提取新闻内容 │ │ ┌───────────────────────────┐ │ │ │ title = │ │ │ │ ${extract.selector( │ │ │ │ resp.html,'h1')} │ │ │ │ │ │ │ │ author = │ │ │ │ ${extract.selector( │ │ │ │ resp.html,'.author')} │ │ │ │ │ │ │ │ release_date = │ │ │ │ ${extract.selector( │ │ │ │ resp.html,'.date')} │ │ │ │ │ │ │ │ content = │ │ │ │ ${extract.selector( │ │ │ │ resp.html, │ │ │ │ '.text-column.theme-wysiwyg',│ │ │ 'text')} │ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘ │ ┌─────────────────┴─────────────────┐ │ │ ▼ ▼ ┌───────────────────────────┐ ┌───────────────────────────┐ │ 【条件判断】 │ │ 【输出节点】 │ │ ${content!=null} │ │ 内容为空,记录错误 │ │ (内容是否有效) │ │ │ └───────────────────────────┘ └───────────────────────────┘ │ │ (True) │ │ (False) │ │ ▼ │ ┌───────────────────────┐ │ │ 保存到数据库 │ │ └───────────────────────┘ │ │ │ └───────────────┬───────────────────┘ ▼ ┌─────────────────────────────────┐ │ 【执行SQL】保存新闻数据 │ │ ┌───────────────────────────┐ │ │ │ INSERT INTO news ( │ │ │ │ url, news_id, author, │ │ │ │ title, release_date, │ │ │ │ content, source) │ │ │ │ VALUES ( │ │ │ │ '#${news_urlmap}#', │ │ │ │ '#${news_id}#', │ │ │ │ '#${author}#', │ │ │ │ '#${title}#', │ │ │ │ '#${release_date}#', │ │ │ │ '#${content}#', │ │ │ │ '#www.coveris.com#' │ │ │ │ ) │ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ 【ForkJoin】执行结束 │ │ 等待所有分支完成 │ └─────────────────────────────────┘ │ ▼ ⭐ 结束 ⭐ 思路点拨
列表接口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

关键技术点详解
1. 动态内容抓取策略
对于动态加载的网站,我们不能直接解析页面HTML,而是需要找到真实的数据接口。
核心配置:自定义函数 match_coveris

// 自定义函数:match_coveris// 功能:从Ajax响应中提取新闻URL// 输入:htmlStr (Ajax返回的JSON字符串)// 输出:新闻URL数组// 注册位置:系统管理 → 自定义函数 → 添加函数functionmatch_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;}2. 初始化节点配置
2.1 开始节点
<!-- 爬虫基础配置 --><startNode><spiderName>000227_抓取coveris新闻</spiderName><submit-strategy>random</submit-strategy><!-- 随机提交策略,避免被识别 --><threadCount></threadCount><!-- 线程数留空,使用默认值 --></startNode>2.2 获取时间范围节点

<!-- 变量节点:动态计算时间范围 --><variableNodename="获取时间范围"><!-- 变量配置 --><variablename="start_date"value="${date.format(date.addDays(date.now(),-30),'yyyy-MM-dd')}"description="开始日期:30天前"/><variablename="end_date"value="${date.format(date.addDays(date.now(),1),'yyyy-MM-dd')}"description="结束日期:明天"/></variableNode><!-- 输出节点:查看时间范围(用于调试) --><outputNodename="输出时间范围"><output-name>["开始时间","结束时间"]</output-name><output-value>["${start_date}","${end_date}"]</output-value></outputNode>3. 数据查询节点配置
3.1 查询已爬取URL

<!-- 执行SQL节点:查询指定时间范围内的已爬取URL --><executeSqlNodename="查询已爬取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><!-- 输出节点:查看查询结果 --><outputNodename="输出查询结果"><output-name>["开始时间","结束时间","获取结果","结果数量"]</output-name><output-value>[ "${start_date}", "${end_date}", "${rs}", "${list.length(rs)}" ]</output-value></outputNode>4. 分页抓取配置
4.1 分页请求节点

<!-- 请求节点:抓取列表页 --><requestNodename="开始抓取"><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>4.2 解析URL列表节点

<!-- 变量节点:解析响应内容 --><variableNodename="定义变量"><!-- 分页控制:自动累加页码 --><variablename="page"value="${page==null?1:page+1}"description="当前页码"/><!-- 调用自定义函数解析URL列表 --><variablename="news_urllist"value="${match_coveris(resp.html)}"description="新闻URL列表"/></variableNode><!-- 条件分支:判断是否继续分页 --><conditionNodename="分页判断"><!-- 条件:当页码小于等于2且URL列表不为空时继续 --><conditionexpression="${news_urllist!=null && page<=2}"><target>开始抓取</target><!-- 继续下一页 --></condition><conditionexpression="${news_urllist==null || page>2}"><target>循环处理</target><!-- 进入详情处理 --></condition></conditionNode>5. 循环处理节点配置
5.1 循环节点

<!-- 循环节点:遍历新闻URL列表 --><loopNodename="循环"><!-- 循环配置 --><loopCount>${news_urllist.length}</loopCount><!-- 循环次数 = URL数量 --><loopStart>0</loopStart><!-- 起始索引 --><loopEnd>-1</loopEnd><!-- 结束索引,-1表示到最后 --><loopVariableName>index</loopVariableName><!-- 循环变量名,从0开始 --></loopNode>5.2 构造详情页URL


<!-- 变量节点:处理当前新闻URL --><variableNodename="新闻地址"><!-- 获取当前循环的URL,并处理转义字符 --><variablename="news_url"value="${news_urllist[index].replace('\\', '')}"description="原始URL路径"/><!-- 构造完整的新闻详情页URL --><variablename="news_urlmap"value="${'https://www.coveris.com' + news_url}"description="完整URL"/><!-- 去重判断:检查URL是否已存在 --><variablename="query_result"value="${!rs.contains(news_urlmap)}"description="是否为新URL"/></variableNode><!-- 输出节点:查看处理结果 --><outputNodename="输出URL信息"><output-name>["原始路径","完整URL","是否为新"]</output-name><output-value>[ "${news_url}", "${news_urlmap}", "${query_result}" ]</output-value></outputNode><!-- 条件分支:基于去重结果分流 --><conditionNodename="去重判断"><!-- 新URL:进入详情抓取 --><conditionexpression="${query_result}"><target>抓取新闻详情</target></condition><!-- 已存在URL:跳过 --><conditionexpression="${!query_result}"><target>输出调试信息</target></condition></conditionNode>6. 详情页抓取配置
6.1 抓取详情页

<!-- 请求节点:抓取新闻详情 --><requestNodename="抓取新闻详情"><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>6.2 提取新闻内容

<!-- 变量节点:提取详情页内容 --><variableNodename="内容提取"><!-- 使用选择器提取标题 --><variablename="title"value="${extract.selector(resp.html, 'h1')}"description="新闻标题"/><!-- 作者字段(可根据实际页面调整选择器) --><variablename="author"value="${extract.selector(resp.html, '.author')}"description="作者"/><!-- 发布日期(可根据实际页面调整选择器) --><variablename="release_date"value="${extract.selector(resp.html, '.date')}"description="发布日期"/><!-- 提取正文内容 --><variablename="content"value="${extract.selector(resp.html, '.text-column.theme-wysiwyg', 'text')}"description="新闻正文"/></variableNode><!-- 条件判断:内容不为空才保存 --><conditionNodename="内容判断"><conditionexpression="${content!=null}"><target>执行SQL保存</target></condition><conditionexpression="${content==null}"><target>输出错误日志</target></condition></conditionNode>7. 数据存储配置
7.1 SQL插入节点

<!-- 执行SQL节点:保存新闻数据 --><executeSqlNodename="保存新闻数据"><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><!-- 输出节点:保存确认 --><outputNodename="保存结果"><output-name>["URL","标题","保存状态"]</output-name><output-value>[ "${news_urlmap}", "${title}", "保存成功" ]</output-value></outputNode>8. 合并与结束节点
<!-- ForkJoin节点:同步多个并行分支 --><forkJoinNodename="执行结束"><!-- 等待所有分支执行完毕 --><shape>forkJoin</shape></forkJoinNode>完整配置示例
数据库表结构
-- 创建新闻表CREATETABLE`news`(`id`int(11)NOTNULLAUTO_INCREMENT,`news_id`bigint(15)DEFAULTNULL,`url`mediumtextCOLLATE utf8mb4_unicode_ci,`title`mediumtextCOLLATE utf8mb4_unicode_ci,`release_date`mediumtextCOLLATE utf8mb4_unicode_ci,`author`mediumtextCOLLATE utf8mb4_unicode_ci,`content`longtextCOLLATE utf8mb4_unicode_ci,`insert_date`timestampNOTNULLDEFAULTCURRENT_TIMESTAMP,`format_release_date`timestampNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'标准格式发布时间',`source`varchar(32)COLLATE utf8mb4_unicode_ci DEFAULT'海外媒体'COMMENT'来源',`hq_web_release_status`tinyint(2)DEFAULT'0'COMMENT'行情取数状态(1=取数,0=未取数)',`update_time`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',`relate_rate`int(2)DEFAULTNULLCOMMENT'相关度',PRIMARYKEY(`id`),KEY`news_id_index`(`id`),KEY`news_insert_date_index`(`insert_date`))ENGINE=InnoDBAUTO_INCREMENT=107498DEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;数据源配置
<!-- 数据源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>技术要点总结
1. 动态网站爬取核心技巧
- 抓包分析:找到真实的数据接口
/app/article/list - 直接请求API:绕过JavaScript渲染
- 正则解析:使用自定义函数处理JSON响应
2. 防封禁策略
| 策略 | 配置值 | 说明 |
|---|---|---|
| 请求间隔 | sleep=5000 | 每个请求后等待5秒 |
| 重试机制 | retryCount=3, retryInterval=5000 | 失败重试3次,间隔5秒 |
| User-Agent | Mozilla/5.0 … | 模拟浏览器访问 |
| 随机策略 | submit-strategy=random | 随机化请求特征 |
3. 数据质量控制
| 控制点 | 实现方式 | 目的 |
|---|---|---|
| 去重查询 | SELECT url FROM news WHERE … | 避免重复爬取 |
| 实时判断 | ${!rs.contains(news_urlmap)} | 动态去重 |
| 内容验证 | ${content!=null} | 确保数据完整性 |
| 时间范围 | start_date/end_date | 精确控制爬取范围 |
4. 变量命名规范
| 变量名 | 类型 | 用途 |
|---|---|---|
| page | 循环变量 | 分页控制 |
| news_urllist | 数组 | 存储URL列表 |
| news_url | 字符串 | 当前处理的URL路径 |
| news_urlmap | 字符串 | 完整的详情页URL |
| query_result | 布尔值 | 去重判断结果 |
| rs | 结果集 | SQL查询结果 |
调试技巧
1. 输出节点调试
<!-- 在每个关键节点后添加输出节点,便于跟踪变量值 --><outputNodename="调试输出"><output-all>1</output-all><!-- 输出所有变量 --></outputNode>2. 日志查看
- 查看执行日志:
日志管理 → 执行日志 - 查看错误信息:
日志管理 → 错误日志 - 监控实时执行:
任务监控 → 实时日志
3. 常见问题解决
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| URL列表为空 | 正则匹配失败 | 检查响应格式,调整正则表达式 |
| 内容提取失败 | 选择器错误 | 使用浏览器开发者工具验证选择器 |
| 去重失效 | 时间范围错误 | 检查start_date/end_date计算逻辑 |
| 请求超时 | 网络问题 | 增加超时时间,启用重试机制 |
性能优化建议
- 线程数配置
- 单线程执行(避免被封)
- 如需并发,建议不超过3个线程
批量处理
<!-- 批量插入优化 -->INSERTINTO news (url, title, content)VALUES<foreach collection="list" item="item" separator=",">(#{item.url}, #{item.title}, #{item.content})</foreach>请求间隔优化
<!-- 动态间隔:根据响应时间调整 --><sleep>${random.nextInt(3000,8000)}</sleep>结语
本案例完整展示了如何通过自定义函数+可视化编排的方式,优雅地解决动态网站的爬取难题。关键不在于工具本身,而在于对网站加载机制的理解和灵活运用各种技术手段。通过本文的详细配置说明,相信读者能够快速上手类似的动态网站爬取任务。