最近在给鸿蒙端做信息聚合类功能时又重新把 Flutter 生态里的web_scraper拉出来用了一遍。这个包在轻量级网页抓取这个细分场景里一直挺能打但网上讲它基础用法的文章多真正聊到“怎么适配鸿蒙、怎么做跨端选择器、怎么处理残缺页面数据、怎么把抓取能力接口化”的内容却很少。这篇就围绕这几个点把我在实际项目里踩过的坑、验证过的方案、沉淀下来的代码结构一次性讲清楚。适合看的读者想在 Flutter 项目里快速实现网页抓取、正在做鸿蒙 HarmonyOSohos适配、被动态页面和脏数据折磨得想放弃以及打算把抓取能力抽象成服务给多个端复用的朋友。下面内容都是实践导向不会只贴文档会把“为什么这么做”也讲明白。1. 先搞清楚web_scraper 能做什么不能做什么1.1 这个包为什么敢自称“轻量级”web_scraper核心做的就两件事发 HTTP 请求拿到 HTML 字符串然后用类似 DOM 查询的方式从 HTML 里提取指定节点。它不启动浏览器、不执行页面里的 JavaScript、不渲染 CSS底层依赖是http和html这两个纯 Dart 包。这跟 Playwright、Selenium、Puppeteer 这类无头浏览器方案有本质区别。无头浏览器是“开一个真实浏览器内核去跑页面”能做登录、点击、滚动、等待接口返回但代价非常大内存动辄几百 MB启动耗时按秒算在移动端更是灾难。web_scraper的轻量就轻在这里它不是巨头鲸而是小型的侦察兵适合目标页面是服务端渲染SSR、数据直接在 HTML 里、不需要复杂交互的场景。在鸿蒙端这个特性特别重要。手机 App 内存本来就紧张如果为了抓一个列表页就内嵌一个浏览器内核那体验会很糟糕。web_scraper只发网络请求、只做字符串解析在鸿蒙的 Flutter 运行时里跑起来几乎没有额外负担这也是我选它的首要原因。1.2 鸿蒙适配的真正难点不在 Dart而在平台层很多人一听“HarmonyOS 适配”第一反应是“包能不能用”。实际上web_scraper是纯 Dart 实现没有原生的 Android/iOS 平台通道所以 Flutter 只要能在鸿蒙上跑这个包天然就能编译通过。真正要处理的是三件事第一网络权限。鸿蒙应用要访问网络需要在module.json5里声明ohos.permission.INTERNET很多从 Android 迁过来的项目容易漏掉这个。第二明文 HTTP 限制。如果抓取目标是http://开头而不是https://鸿蒙默认是不放行的需要在网络配置里开启明文流量许可或者干脆只抓 HTTPS 站点。第三User-Agent 和 TLS。不少网站会根据 UA 判断设备类型返回不同结构的 HTML移动端 UA 拿到的是触屏版页面桌面端 UA 拿到的是完整版页面。这些差异会影响选择器表达式是否生效调试时很容易让人误以为是代码写错了。所以做鸿蒙适配别急着改抓取逻辑先把网络栈这层打通。我用 DevEco Studio 建完鸿蒙工程、接入 Flutter 模块后第一步就是验证能不能用 Dart 的http正常请求目标站点这一步通了后续工作才有意义。2. 选择器表达式从“能用”到“跨端通用”2.1 CSS 选择器的能力边界与实际写法web_scraper提取数据用的是 CSS 选择器表达式。基础写法大家都会比如按 class 找元素.product-title按 id 找元素#price但实际页面不会这么友好尤其是电商列表、资讯聚合这类结构复杂的页面。分享几个我实际用得比较多的表达式写法属性选择器div[data-sku12345]适合目标节点没有稳定 class 但自定义属性有规律的情况。后代与子元素组合.list-item .info span.price能精确锁定层级路径避免多个同名 class 互相干扰。伪类选择器.item:first-child、.list li:nth-child(2)适合提取重复结构里的固定位置元素。多选择器兜底h1.title, .article-title a用逗号分隔多个表达式只要其中一个命中就能返回结果对付页面改版很有用。web_scraper的Selector类提供了querySelector和querySelectorAll前者拿单个元素后者拿列表。我通常在定义数据模型时直接用Selector描述“这个字段要从哪里取”而不是写死一串字符串再到处解析。这里的核心思路是把选择器当作配置而不是代码。页面结构一变改配置就行不用动抓取主流程。跨端通用方面有一个容易被忽略的坑同一个网页在手机浏览器和桌面浏览器里打开HTML 可能是完全不同的两套。鸿蒙端如果用的是移动端 UA拿到的是移动版页面如果后端抓取服务用的是桌面 UA拿到的是桌面版页面。写选择器表达式之前先确认两端拿到的 HTML 是同一套否则你在 PC 上调试好的表达式到鸿蒙上全都查不到。2.2 动态页面和无头浏览器的正确组合方式web_scraper不执行 JS所以遇到数据靠 AJAX 动态加载、页面是 Vue/React 客户端渲染的站点直接抓 HTML 会得到一堆空壳数据根本不在里面。这时候就有两个选项一是抓接口而不是抓页面。用浏览器开发者工具看 Network 面板找到返回 JSON 的数据接口直接请求这个接口。这个思路往往比上无头浏览器更省资源数据还更干净我优先推荐。二是真到了必须渲染 JS 才能拿数据的场景才考虑无头浏览器。但无头浏览器不适合直接塞进鸿蒙 App 里更合理的架构是部署一个独立的抓取解析服务服务端跑无头浏览器把页面里你需要的数据抽出来再以 JSON 接口的形式返回给鸿蒙端。有些场景需要配置代理服务比如公司内网的数据源要通过正向代理访问、联调时需要把请求打到本地抓包工具、或者需要轮换出口 IP 来避免触发对方的风控策略。这时候可以在抓取服务里给 HTTP 客户端设置代理地址和端口属于常规的网络配置。我的经验是能用接口抓就抓接口接口不行再上服务端无头浏览器代理配置这层只作为基础设施存在不要成为第一选择。3. 残缺数据提取把“脏数据”收拾干净3.1 残缺数据的典型来源与分析做过网页抓取的人应该都有这种体会代码写得再漂亮一遇到真实网页就崩。问题多半不在选择器而在数据本身残缺。我归类了几个高频来源第一结构缺失。一个商品列表有的商品有促销价有的只显示原价导致span.price节点不存在。第二字段为空。标题里包含多余空白字符、标签文字被截断、时间格式不统一这类问题在直接调用.text之后特别常见。第三编码错乱。目标站点是 GBK/GB2312 编码直接按 UTF-8 解析中文全部变成乱码。第四反爬响应。访问频率太高时网站返回一个验证页或空壳页选择器自然什么都抓不到。应对这些问题的核心原则是把抓取解析部分写“软”不要假设每个字段一定存在、一定合法。所有取值操作都要有兜底所有字符串都要做清洗所有数据类型转换都要包在异常处理里。3.2 提取结果的结构化与兜底策略我写了一套比较固定的数据清洗流程你可以在自己的项目里直接用解析元素后先取文本然后统一做trim把首尾空白去掉。接着做空值判断如果为空就用默认值比如“未知”或者空字符串。再做类型转换比如价格字段把字符串里的“¥”“,”去掉再转 double转不了就返回 0。最后做格式规范化比如时间统一转成yyyy-MM-dd HH:mm:ss。示例代码如下这是我在做商品列表聚合时用过的结构class ProductItem { final String title; final double price; final String link; final bool hasPromotion; ProductItem({ required this.title, required this.price, required this.link, required this.hasPromotion, }); factory ProductItem.fromMap(MapString, dynamic map) { return ProductItem( title: (map[title] ?? 未知商品).toString().trim(), price: _parseDouble(map[price]), link: (map[link] ?? ).toString().trim(), hasPromotion: map[hasPromotion] true, ); } static double _parseDouble(dynamic value) { if (value null) return 0; final cleaned value.toString().replaceAll(RegExp(r[^\d.]), ); return double.tryParse(cleaned) ?? 0; } }这里的关键在于_parseDouble这类“防御型解析函数”。它先把非数字字符清理掉再用tryParse做转换转不了就返回默认值。所有字段都走同一套逻辑就算某个商品没有价格程序也不会因为空指针崩掉。下面这个表格是我整理的常见脏数据形态和对应策略实际排查时可以直接对照脏数据形态常见原因处理策略节点查不到页面结构变化或广告位导致节点缺失用querySelector后判空给默认值字符串带大量空白HTML 格式化产生的缩进和换行统一trim()中文乱码页面编码与请求解码不一致手动指定响应的编码集如utf8.decode(bytes)价格带货币符号和千分位页面展示层格式正则清理后转 double日期格式混乱不同数据源格式不统一用DateTime.tryParse加统一格式化抓到的内容是验证页请求频率过高触发反爬识别特征词并做退避重试4. 数据接口网格化一套抓取逻辑服务多端4.1 为什么需要“网格化”而不是写死一套逻辑把抓取逻辑跟页面绑定写成一个又长又直的函数前期很爽后期很痛。换一个页面、加一个字段、删一个节点都要动核心代码如果这个代码还要被多个端复用那就是灾难。我说的“网格化”是指把抓取能力拆成多个互相独立的单元请求层只负责拿 HTML解析层只负责根据配置提取数据模型层只负责把数据转成结构化对象存储与分发层只负责把结果交给上层。每个单元可以独立替换、独立测试组合起来能覆盖大量页面。具体到代码组织我会定义三种角色DataSource负责网络请求输入是 URL 和请求头输出是 HTML 字符串。Parser负责解析输入是 HTML 和一组Selector配置输出是ListMapString, dynamic。Repository负责编排把 DataSource、Parser 和缓存串起来向上层暴露统一的fetchList()、fetchDetail()等方法。这样的结构带来一个直接好处上层业务完全不知道数据是来自网页抓取、本地缓存还是未来接的第三方接口。你可以在不改变调用方的情况下把抓取源从 A 站切到 B 站这就是接口化的价值。4.2 给鸿蒙端用的抓取服务怎么设计在鸿蒙 App 里Flutter 模块通常被集成到 HarmonyOS 工程中通过FlutterAbility或FlutterFragment承载 UI。抓取结果要给鸿蒙原生页面用走的是 Flutter 与原生之间的消息通道。我的做法是在 Flutter 侧封装一个ScraperService内部暴露MethodChannel供鸿蒙原生侧调用。比如鸿蒙侧发一个方法名叫fetchData的调用参数是目标 URL 和数据模型标识Flutter 侧收到后执行抓取流程解析完的结果通过result.success(jsonString)返回给鸿蒙侧。鸿蒙原生拿到 JSON 字符串后可以直接解析成HashMap或转成 ArkTS 的类对象。如果是“万物互联”的场景比如手机抓取数据后推送到平板、智慧屏或者智能手表上可以利用鸿蒙的分布式能力把标准 JSON 报文同步过去。这里的关键不是用什么传输通道而是抓取结果的格式一定要标准——字段名、类型、嵌套结构都要有严格约定否则多端消费时一定会出现兼容问题。我现在所有抓取结果一律用 Map 结构承载对外输出统一 JSON不直接暴露web_scraper的元素对象就是为了让数据在设备之间流转时不需要重新解析。5. 实操一个完整的鸿蒙适配抓取示例5.1 工程搭建与依赖配置先准备好基础工程用 DevEco Studio 创建 HarmonyOS 工程选择支持 Flutter 的模板。在工程里集成 Flutter 模块SDK 使用兼容鸿蒙的 Flutter 版本。在鸿蒙侧的module.json5中声明网络权限{ module: { requestPermissions: [ { name: ohos.permission.INTERNET } ] } }在 Flutter 侧pubspec.yaml添加依赖dependencies: flutter: sdk: flutter web_scraper: ^0.2.0版本我建议锁定到你验证过的具体版本不要直接用^0.2.0这种浮动版本因为这类轻量包偶尔会有 API 小改动锁版本能避免队友拉代码后编译不过。5.2 核心代码实现下面这个例子抓取一个服务端渲染的新闻列表页提取每条新闻的标题和链接并输出结构化结果。import package:web_scraper/web_scraper.dart; class NewsData { final String title; final String url; NewsData({required this.title, required this.url}); MapString, dynamic toJson() {title: title, url: url}; } FutureListNewsData fetchNewsList(String pageUrl) async { final scraper WebScraper(https://example-news.com); // 如果页面加载成功开始解析 if (await scraper.loadWebPage(pageUrl)) { final titleSelector Selector(div.news-item h2 a, title); final urlSelector Selector(div.news-item h2 a, href); final titles scraper.getElementText(titleSelector); final links scraper.getElementLink(urlSelector); final result NewsData[]; final length titles.length links.length ? titles.length : links.length; for (var i 0; i length; i) { final title titles[i].trim(); if (title.isEmpty) continue; result.add(NewsData(title: title, url: links[i])); } return result; } return []; }几个关键点解释一下loadWebPage返回 bool表示请求是否成功不要忽略这个返回值。Selector的第二个参数是提取属性名如果填title表示取元素文本填href表示取href属性这个参数帮我们省掉了很多手动操作。循环里做了一次空标题过滤因为残缺数据场景里很容易出现某些条目标题为空不过滤会把脏数据带到下一步。WebScraper(https://example-news.com)的构造函数可以设置全局 URL 前缀这能让页面里的相对路径正确拼成绝对地址。实际项目里我会把基地址放到配置中心不同环境用不同域名。5.3 抓取性能和数据保鲜网页抓取最怕的不是慢而是频繁请求导致被对方限制。我的经验是给抓取服务加三层控制第一请求间隔。同一个域名下的请求至少间隔 1 到 2 秒列表页批量抓取时用Future.delayed控制节奏。第二结果缓存。短时间内的重复请求直接读缓存不重新发起网络请求。第三失败退避。连续失败后延迟时间翻倍避免在页面已经反爬的情况下继续硬碰硬。简单缓存示例class SimpleCache { static final MapString, _CacheItem _store {}; static String? get(String key) { final item _store[key]; if (item null) return null; if (DateTime.now().difference(item.time).inMinutes 10) { _store.remove(key); return null; } return item.value; } static void put(String key, String value) { _store[key] _CacheItem(value, DateTime.now()); } } class _CacheItem { final String value; final DateTime time; _CacheItem(this.value, this.time); }抓取前先查缓存命中就直接返回不命中再请求并写入缓存。这在大批量任务里能省下大量请求资源。6. 常见问题与排查技巧实录6.1 抓不到数据不要太早怀疑包有问题遇到抓不到数据我一般按这个顺序排查先用浏览器直接打开目标 URL看页面是否正常再把 URL 放到 Postman 里看返回的 HTML 是不是和浏览器一致最后才回到代码里排查选择器。很多问题的根源是请求被重定向、需要登录、或者返回了验证页而不是web_scraper本身有问题。列一份高频问题对照表症状常见原因排查与解决loadWebPage 返回 false网络不通、DNS 解析失败、目标站拦截抓取请求日志看 HTTP 状态码尝试更换 UA选择器查不到任何元素HTML 结构与预期不符或页面是动态渲染打印 HTML 片段核对实际类名和层级中文全部是乱码页面编码不是 UTF-8手动按 GBK/GB2312 解码后再解析部分字段取不到目标字段所在节点被动态加载优先找后端 JSON 接口而不是硬啃 HTML频繁抓到验证页请求频率过高增加间隔、使用代理池轮换、加缓存鸿蒙端请求超时网络权限未声明或明文 HTTP 被拦截检查 module.json5配置网络明文许可6.2 一些经验之谈合规与长期维护做网页抓取技术只是一半合规意识和工程素养是另一半。有几个原则我从第一年写爬虫时就一直遵守只抓取公开数据不做任何需要绕过登录验证、破解验证码、突破访问控制的事控制请求频率不搞高并发压测抓取前先看目标站点的 robots 协议和服务条款对明确禁止抓取的内容保持克制抓下来的数据如果涉及个人信息要做好脱敏和权限管理不能随意对外分发。长线维护方面建议把所有的 URL、选择器、解析规则都放到配置中心或者独立的配置文件里不要散落在业务代码中。目标页面改版时往往只是选择器失效改一下配置文件就能恢复不需要发版本。这也是我前面反复强调“选择器即配置”的原因。我在实际项目里见到过太多次这样的场景一个人写的抓取函数只有自己能看懂页面一改就手忙脚乱地改正则、改索引最后越改越乱。如果从第一天就把抓取逻辑当作标准的数据管道来设计这些问题都能避免。最后再分享一个小技巧抓取结果的字段命名从一开始就统一用下划线风格还是驼峰风格想清楚并一直坚持下去。这个不起眼的决定会直接影响你后续对接多个端、多个数据模型时的效率。我自己吃过亏后来干脆所有抓取结果统一转成标准 JSON再在各端做一次模型映射后续维护轻松很多。
企业数字化 ERP 产品动态
相关推荐
谷歌浏览器登录与常见问题排查:从账号安全到配置修复一次讲透 谷歌浏览器登录与疑难杂症排查手册:从账号登录到配置恢复的一次讲透打开谷歌浏览器准备登录账号,结果不是提示“此浏览器或账号不安全”,就是网页死活打不开,甚至一开机发现主页被换成了五花八门的导航站。这些年我帮朋友和同事处… · 2026/9/24 19:09:48
2026年蓝牙耳机选购指南:芯片、协议与真实体验全解析 2026年了,还有人问我“什么无线蓝牙耳机好”?说实话,这个问题的难度一年比一年大。以前我们聊耳机,无非是音质、续航、价格三件套,现在你再打开电商页面看看,又是蓝牙版本又是编解码协议,又是降… · 2026/9/24 19:09:48
告别双系统折腾!Windows上跑Linux的5种实用方案详解 2026年了,如果你还在靠重启电脑切到 Ubuntu 来敲几条命令,那我真得劝你一句:这条折腾老路,该彻底放下了。我自己就是从“Win Ubuntu 双系统”时代一路折腾过来的,当年为了给 Ubuntu 扩容、修引导、删分区,… · 2026/9/24 19:09:48
基于YOLOv5的大疆Tello TT无人机目标检测追踪与测距 简介:基于YOLOv5与大疆教育无人机Tello TT构建的毕设级项目包,面向目标识别、检测与追踪测距场景,覆盖旗、圈两类目标的检测任务。数据集包含已标注的旗、圈样本,模型经过训练调优,附带完整源码、PyTorch权重文件、YAM… · 2026/9/24 19:43:10
Score-P与Scalasca实战:HPC并行程序性能分析工具的源码编译与部署指南 前阵子帮组里一位做流体仿真的同事搭性能分析环境,他那边攒了几个跑得很慢的 MPI 作业,程序本身没有报错,但就是能用一半核不干活的那种“匀速慢”。我想着用 Score-P 和 Scalasca 这套组合给他做一次插桩和同步错误检查,结果… · 2026/9/24 19:43:04
UPDATE语句从入门到避坑:事务、锁、索引与批量更新全解析 最近整理SQL学习笔记时,很多同学问过我同一个问题:明明就是一条UPDATE语句,为什么值得连续用两篇笔记来记录?其实答案是,更新数据这个动作看起来简单,背后牵扯的东西一点都不少——从UPDATE的语法细节、WHE… · 2026/9/24 19:43:04
审批状态错乱之谜:并发覆盖下的数据库事务、MVCC与缓存一致性实战 1. 审批状态“倒带”了:现象、复现和第一反应1.1 诡异状态:同意之后又变回审批中在生产环境干过审批流的老哥应该都遇到过这种诡异场景:明明数据库里的审批单已经走完,业务方却拿着截图说状态是“审批中”,再次点同意又… · 2026/9/24 19:43:04
网吧盈利现状与未来潜力深度全景解读(超万字攻略) 一、前言:网吧,从辉煌到转型的世纪旅程
曾几何时,网吧是无数80后、90后青春的记忆——“上网两小时,快乐一整天”,在那个互联网刚起步的年代,网吧是信息的窗口,是游戏的乐园,也是城市… · 2026/9/24 19:42:58
PyTorch3D 点云光栅化完全指南:rasterize_points 参数、原理与实战 PyTorch3D 点云光栅化完全指南:rasterize_points 参数、原理与实战 【免费下载链接】pytorch3d PyTorch3D is FAIRs library of reusable components for deep learning with 3D data 项目地址: https://gitcode.com/gh_mirrors/py/pytorch3d
导读
本文围绕… · 2026/9/24 19:42:51
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44