01 这个脚本在做什么?
想象一下:你在链家网站上浏览杭州的租房信息,打开了一个房源详情页。页面加载完毕的那一刻,一个用户脚本已经在后台默默工作了。
它做了三件事:
- 读取页面上的所有房源信息(标题、价格、地铁、设施……)
- 整理成结构化的数据对象
- 在页面右下角放一个浮动按钮,点击即可下载 JSON 文件
这就是一个DOM采集器的基本工作流程。
📊 数据流动画
🔧 代码 ↔ 中文翻译
(async function() { 'use strict'; // 只在详情页运行 if (!/\/HZ\d+\.html/.test(location.pathname)) return; // ... 采集逻辑 ... const detail = scrapeDetailPage(); // ... 创建浮动按钮 ... })();
整个脚本被一个 IIFE 包裹——定义一个函数,然后马上运行它。
这样做的好处:脚本里的变量不会泄露到页面全局环境。
第一行检查:「当前页面是不是房源详情页?」
如果不是(比如是列表页),直接 return 退出,什么都不做。
如果是详情页,才继续执行采集逻辑。
📝 场景测验
02 认识角色
脚本里两个自己写的函数,加上一堆浏览器内置的函数,组成一支分工明确的团队。
它们都是函数——调用方式一模一样:
名字(参数)。唯一区别是「谁写的」:
• 浏览器内置(名字固定,全世界通用):Blob、querySelector、createElement……
→ 就像手机自带的相机、计算器,拿来就用
• 自己写的(名字随便起):scrapeDetailPage、downloadJSON……
→ 就像你自己下载的 App,功能你定义
先认识采集脚本最常用的三个「内置工具」:
🔄 它们怎么配合工作
脚本不直接操作浏览器下载功能,而是先把数据转成标准格式(JSON),再让浏览器处理。这样做的好处:数据是纯净的文本,不依赖浏览器的文件系统 API。
💬 两个函数的对话
看看它们是怎么「聊数据」的:
🔧 代码 ↔ 中文翻译
function downloadJSON(data, filename) { const blob = new Blob([ JSON.stringify(data, null, 2) ], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }
第一步:JSON.stringify() - 格式化数据
把 JavaScript 对象转换成可读的 JSON 字符串,缩进 2 空格。就像把购物清单整理成格式化的清单。
第二步:new Blob() - 装箱
把 JSON 字符串放入 Blob 对象,类型设为 JSON。就像把清单放进快递箱,并贴上「文件类型」标签。
第三步:URL.createObjectURL() - 生成链接
浏览器给 Blob 生成一个临时下载地址(比如 blob:http://...),就像快递生成了运单号。
第四步:createElement('a') + click() - 模拟点击
创建一个不可见的链接标签,设置地址和文件名,然后自动点击。就像快递员按运单号把包裹送给你。
URL.revokeObjectURL(url) 就像快递完成运单后,把运单号销毁,避免号码堆积占用内存。
📝 场景测验
03 怎么读页面
脚本用 DOM 选择器「钓」出页面上想要的元素。
核心 API 是 document.querySelector() 和 document.querySelectorAll():
- querySelector(selector):返回第一个匹配的元素,找不到返回
null - querySelectorAll(selector):返回所有匹配元素的数组,找不到返回空数组
- element?.textContent:读取元素的纯文本内容(不含 HTML 标签)
脚本里实际用到的选择器:
.content__title → 房源标题.content__aside--title span → 价格.piclist li img → 所有房源图片.content__article__info2 li.fl → 配套设施列表🔧 代码 ↔ 中文翻译
// 提取标题 d.title = document.querySelector('.content__title')?.textContent?.trim() || ''; // 提取所有图片 d.pictures = []; document.querySelectorAll('.piclist li img').forEach(img => { d.pictures.push({ url: img.src, type: img.dataset?.type, name: img.dataset?.name }); });
提取标题:
querySelector('.content__title')找到标题元素?.textContent可选链:如果元素不存在,直接返回undefined,不会报错.trim()去掉首尾空格|| ''找不到时,返回空字符串作为默认值
提取所有图片:
querySelectorAll('.piclist li img')返回所有匹配的图片元素(数组).forEach(img => {...})遍历每张图片img.src读取src属性(图片 URL)img.dataset?.type读取data-type自定义属性(链家用这个字段标记图片类型)
注意:dataset 是浏览器访问 data-* 属性的标准方式,会自动把横线转成驼峰命名。
📝 场景测验
04 地铁信息的小技巧
链家页面用百度地图 JS 渲染地铁信息,但脚本选择不依赖它。为什么?因为百度地图的 JS 可能加载慢、网络不稳定、或者被广告拦截器屏蔽。
脚本用了一个 Fallback 模式:直接从页面的纯文本里解析地铁信息。
它这样做:
- 遍历地铁区域的列表项(
.content__article__info4 ul li) - 用 正则表达式
/距离\s*地铁(.+?)\s*[- ]\s*(.+?)\s+(\S+)$/解析文本 - 匹配「距离 地铁X号线 Y站 Z米」的格式
- 提取:线路、站点、距离三个字段
如果文本解析成功,就保存数据;如果失败(文本格式不对),脚本会尝试从 DOM 里获取坐标。
🔧 代码 ↔ 中文翻译
// 遍历地铁列表 info4.querySelectorAll('ul:not(.nav_list) li').forEach(li => { const text = li.textContent.trim().replace(/\s+/g, ' '); const m = text.match(/距离\s*地铁(.+?)\s*[- ]\s*(.+?)\s+(\S+)$/); if (m) { d.subwayStations.push({ line: '地铁' + m[1], station: m[2].trim(), distance: m[3] }); } });
第一步:遍历列表
querySelectorAll('ul:not(.nav_list) li')选中所有地铁区域的列表项,排除导航列表(.nav_list).trim()去掉多余空格replace(/\s+/g, ' ')把多个连续空格合并成一个,避免正则匹配不稳定
第二步:正则匹配
正则表达式 /距离\s*地铁(.+?)\s*[- ]\s*(.+?)\s+(\S+)$/ 的含义:
\s*匹配 1 个或多个空白字符(.+?)非贪婪匹配:问号表示尽可能少匹配(避免吃掉后面的内容)(\S+)匹配非空白字符(线路号、距离单位)$匹配行尾
第三步:提取结果
match() 返回一个数组,如果匹配成功:[null, '1号线', '西湖文化广场站', '300米']。脚本用索引 m[1]、m[2]、m[3] 读取各个分组。
📝 场景测验
05 拼装起来
所有组件整合到一个 IIFE 包裹里,完成整个采集流程。
执行顺序:
- 页面加载时自动采集:IIFE 执行后,马上调用
scrapeDetailPage(),数据保存到全局变量 - 创建浮动按钮:插入到页面,绑定点击事件
- 用户点击按钮:重新采集,附加 URL 和时间戳,触发下载
- 错误处理:用
try-catch包裹核心逻辑,避免意外中断脚本
关键点:脚本用 @run-at document-idle 时机执行——等页面完全渲染完再运行,避免查询不到元素。
🔧 代码 ↔ 中文翻译
document.getElementById('lj-btn').addEventListener('click', () => { try { const data = scrapeDetailPage(); data.url = location.href; data.scrapeTime = new Date().toISOString(); downloadJSON(data, `lianjia-detail-${Date.now()}.json`); } catch (e) { // 错误处理... } });
获取按钮元素:getElementById('lj-btn') 找到浮动按钮
绑定点击事件:addEventListener('click', callback) 当按钮被点击时,执行回调函数
重新采集数据:
scrapeDetailPage()重新读取当前页面 DOM,获取最新数据data.url = location.href附加当前页面 URLdata.scrapeTime = new Date().toISOString()记录采集时间(UTC 格式)
触发下载:用模板字符串生成文件名 lianjia-detail-{时间戳}.json,确保每次下载的文件名唯一
错误处理:try-catch 捕获运行时异常。如果 DOM 结构变化或网络问题导致采集失败,脚本不会崩溃,而是在 catch 块里记录错误或提示用户。