资讯动态

用Chrome插件抓取商品信息:从MV3架构到CSV导出实战

发布时间:2026/10/19 5:37:58 来源:尧图企业网站定制
简介这是一款用于淘宝、京东商品详情页信息抓取的Chrome浏览器插件主要面向电商运营、选品分析或需要批量整理商品资料的JavaScript开发者。插件可在浏览商品页时提取标题、价格、优惠价格以及主图与附图等多类字段并将抓取结果提交到指定地址使用时需额外创建 url.js 文件并配置 REQUEST_URL 变量适合具备基础前端调试能力的用户。压缩包共13个文件大小约46KB以JavaScript脚本为核心配合HTML页面、JSON配置、图标图片及说明文档结构精简便于加载和二次修改。插件内部将不同平台采集逻辑分别封装配合后台脚本与弹窗页面完成扩展交互整套流程清晰便于理解轻量级浏览器扩展的常见组织方式。目前已有215人学习下载可作为电商数据采集练习或内部工具改造的参考起点。1. 为什么我写了个插件来抓商品信息做电商选品或竞品调研的人大概都经历过这样的时刻对着商品列表页一条条复制标题、价格、销量再粘贴到表格里。复制三五十条还能忍到了三百条就变成了纯粹的体力活。更难受的是不同平台页面结构不一样上一套复制大法换个站点就失效。我最初写 grabItem 这个 Chrome 插件就是冲着这个痛点去的——用 JavaScript 在浏览器里直接抓取商品结构化信息一键导出表格不用后端、不用爬虫框架、不用处理登录态装上插件打开页面就能用。这个插件解决的场景很清楚你在浏览商品页时想把页面上展示的商品信息标题、价格、主图、销量、评论数、SKU 规格快速落成结构化数据。适合做竞品调研的运营、需要整理商品库的卖家、以及想拿真实页面练手的前端开发者——既能直接复用抓取逻辑也能作为学习 Chrome 扩展 MV3 机制的完整案例。这一篇我会从插件的骨架拆到具体的抓取策略把参数、选择器、常见坑位都铺开讲。2. 从注入到抓取Content Script 的执行边界与消息通道2.1 manifest.json 里的三个关键声明Chrome 扩展在 MV3Manifest V3时代骨架是 manifest.json 一个文件定死的。grabItem 的 manifest 里最重要的三个声明是content_scripts、background和action。content_scripts声明哪些脚本注入到哪些页面background声明扩展的后台逻辑MV3 里是 service workeraction声明工具栏图标的点击行为。建一个项目目录最少需要四个文件manifest.json、content.js注入页面的抓取脚本、background.js处理图标点击和消息中转、popup.html可选做手动触发按钮。先看 manifest{ manifest_version: 3, name: grabItem, version: 1.0.0, permissions: [activeTab, scripting, storage], host_permissions: [all_urls], background: { service_worker: background.js }, action: { default_title: grabItem, default_popup: popup.html }, content_scripts: [ { matches: [all_urls], js: [content.js], run_at: document_idle } ] }注意run_at设成document_idle表示等 DOM 基本解析完成后再注入避免太早拿不到渲染出来的商品节点。host_permissions用all_urls是因为抓取目标可能分布在多个电商域名上测试阶段用全量匹配最省事但等插件要发布到商店时建议收紧成实际要抓的几个域名否则商店审核会追问权限理由。permissions里的activeTab配合scripting是为了支持另一种注入方式用户主动点击图标时再对当前 Tab 注入抓取脚本。这和content_scripts的自动注入不冲突——自动注入负责页面加载后立即挂载监听主动注入负责给用户一个我现在就要抓的按钮。2.2 消息通道为什么决定插件成败Content Script 运行在页面上下文里能直接操作 DOM但它拿不到扩展 API 的大部分能力Background 有扩展 API 权限却碰不到页面 DOM。所以抓数据这活儿必须在 content.js 里做而把抓到的数据存下来或导出文件需要 background 协助。这就必须走消息通道。grabItem 的消息设计很简单content.js 完成抓取后把数据放进chrome.runtime.sendMessagebackground.js 监听onMessage收到数据后触发下载逻辑。// content.js 中抓取完成后发送数据 function sendPayload(payload) { chrome.runtime.sendMessage({ type: GRAB_RESULT, data: payload }, function (response) { if (chrome.runtime.lastError) { console.warn(消息发送失败可能是扩展已更新:, chrome.runtime.lastError.message); return; } if (response response.ok) { console.log(数据已交给后台处理共, payload.length, 条); } }); }// background.js 中接收并触发下载 chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { if (message.type GRAB_RESULT) { const rows message.data; // 转成 CSV 字符串交给下载逻辑 const csvText toCsv(rows); const blob new Blob([\ufeff csvText], { type: text/csv;charsetutf-8 }); const url URL.createObjectURL(blob); chrome.downloads.download({ url: url, filename: grabItem_ Date.now() .csv }); URL.revokeObjectURL(url); sendResponse({ ok: true }); } });第一段代码的sendMessage回调里判断runtime.lastError是个习惯动作——如果扩展在页面加载后被更新旧的 content script 和新 background 之间的消息通道会失效不检查这个字段控制台会出现捕获不到的报错调试时很迷惑。第二段代码用\ufeff这个 BOM字节顺序标记前缀目的是让 Excel 打开 CSV 时能正确识别 UTF-8 编码否则中文标题会直接乱码。这个细节是抓中文商品信息时最容易踩的坑。3. 商品页抓取实现从选择器到多字段提取3.1 定位商品容器的两种思路商品信息的 DOM 结构在不同站点差距很大但几乎都有同一个特征每条商品被包裹在一个容器节点里这个节点有明确的 class 或 data 属性。抓取脚本的核心任务就是先定位容器再在容器内部提取字段。第一种思路是直接按容器 class 选择比如某些平台用.item、.product、.goods。第二种思路是特征属性选择比如[data-spm]、[data-sku]。我一般优先看第二种因为 class 往往带有混淆用的随机后缀比如.item_abc123而 data 属性通常是稳定的业务标识。// content.js 中定位商品容器 function findProductContainers() { // 优先尝试稳定 data 属性再回退到常见 class const selectors [ [data-product-id], [data-spm], .product-item, .goods-item, .item ]; for (const selector of selectors) { const nodes document.querySelectorAll(selector); if (nodes.length 0) { console.log(命中容器选择器:, selector, 数量:, nodes.length); return Array.from(nodes); } } console.warn(未找到商品容器请检查页面是否已渲染出列表); return []; }这段代码的逻辑是按优先级逐个尝试选择器而不是一次性querySelectorAll合并所有选择器。原因是不同平台的商品信息密度不一样有些页面容器命中率高但字段不全有些容器命中率低但单条信息完整。逐个尝试能保证你拿到的是当前页面最合适的容器而不是混着多种结构一起解析解析时字段错位会更严重。3.2 字段抽取的兜底策略容器找到后每个字段的抽取不能只写一条选择器路径。电商前端改版频繁今天能用的.price-text明天可能变成.price--main。我给每个字段都准备主选择器和兜底选择器字段取不到时不会直接返回空而是打一个 warn 日志记录哪个字段在哪个容器里丢失。抓取的字段分为两类文本字段标题、价格、销量、评论数和属性字段链接、图片地址、SKU 规格。文本字段用textContent清洗后取值属性字段用getAttribute取值。// content.js 中提取单条商品信息 function extractProduct(node) { const getText (selectors) { for (const sel of selectors) { const el node.querySelector(sel); if (el el.textContent.trim()) { return el.textContent.trim().replace(/\s/g, ); } } return ; }; const getAttr (selectors, attrName) { for (const sel of selectors) { const el node.querySelector(sel); if (el el.getAttribute(attrName)) { return el.getAttribute(attrName).trim(); } } return ; }; const link getAttr([a[href]], href); const title getText([.title, [class*title], h3, h4]); const price getText([.price, [class*price], .amount]); const sales getText([.sales, [class*sales], .sold, .buy-count]); const rating getText([.rating, [class*rate], .star]); // 绝对化链接保证导出的 href 可被外部直接访问 const absoluteLink link ? new URL(link, window.location.origin).href : ; return { title, price, sales, rating, link: absoluteLink, capturedAt: new Date().toISOString() }; }getText里replace(/\s/g, )很关键——商品标题在 HTML 里经常被换行和空格打断直接textContent取出来会是一坨乱格式压缩成单空格后导出到表格才干净。getAttr里对href做了new URL绝对化处理因为很多页面中的链接是相对路径/item/123.html不补全域名导出的链接点了会 404。capturedAt这个时间戳字段看起来多余实际做竞品价格对比时它是判断同一商品在不同时间点价格变化的基准字段后面有用。3.3 遍历模式与输出格式单页面抓取时有两种遍历模式只抓当前已渲染的商品或者滚动到底触发懒加载后抓全部。懒加载是电商列表页的标配直接抓只能拿到首屏二十来条。处理方式是用定时器逐步滚动到底部每滚一次等待新图片资源加载直到两次滚动之间没有新增商品节点。// content.js 中滚动加载并抓取全部商品 async function grabWithScroll(maxScrolls 12) { let lastCount 0; let scrollCount 0; let allItems []; while (scrollCount maxScrolls) { const containers findProductContainers(); if (containers.length 0 containers.length ! lastCount) { // 有新商品出现增量提取 for (let i lastCount; i containers.length; i) { allItems.push(extractProduct(containers[i])); } lastCount containers.length; } // 滚动到底部触发懒加载 window.scrollTo(0, document.body.scrollHeight); await new Promise(resolve setTimeout(resolve, 1500)); scrollCount; const newContainers findProductContainers(); if (newContainers.length lastCount) { console.log(滚动后商品数量无变化判定到底); break; } } return allItems; }maxScrolls参数设成 12 是经验值——大多数商品列表页在 8 到 10 次滚动后已经到底超过 12 次还在不断出现新品可能是因为页面在做无限流推荐这时候就要停下来避免把推荐流商品也混进结果里。每次滚动之间间隔 1500 毫秒不是懒加载慢而是商品图片较重等待一下能确保后续 DOM 节点稳定不容易出现取到一半数据的情况。这个函数返回的allItems就是最终传入sendPayload的数据源。4. 从 DOM 数据到可用表格清洗规则与批量场景适配4.1 价格和销量的字符串清洗从页面抓到的价格通常是¥129.00或者129.00起销量是已售2000或10万件。这些字符串直接进表格没法做数值计算。grabItem 里我写了一套清洗函数把价格统一转成浮点数把销量中的万转成具体数字。// content.js 中数据清洗函数 function cleanPrice(raw) { if (!raw) return null; // 提取首个数字部分兼容 ¥1,299.00 和 1299.00起 const match raw.replace(/,/g, ).match(/(\d\.?\d*)/); return match ? parseFloat(match[1]) : null; } function cleanSales(raw) { if (!raw) return 0; const match raw.match(/([\d.])\s*万/); if (match) return Math.round(parseFloat(match[1]) * 10000); const plainMatch raw.replace(/,/g, ).match(/(\d)/); return plainMatch ? parseInt(plainMatch[1], 10) : 0; }cleanPrice先把千分位逗号去掉再提取数字是因为部分站点价格渲染成1,299.00不处理的话正则只能匹配到1。cleanSales遇到10万件会先匹配万前面的10乘以 10000 得到 100000遇到已售2000则直接取 2000。注意cleanSales里正则用的是([\d.])\s*万把3.2万这种带小数的情况也覆盖了最后Math.round保证结果是整数。清洗函数的设计原则是宁可返回 null 或 0也不要保留无法解析的原始字符串。导出的表格里出现空值你可以后续人工判断出现undefined或NaN到了 Excel 里会导致整列排序错乱。4.2 批量场景多 Tab 并发抓取做竞品对比时经常要在同一个站点搜多个关键词各开一个 Tab。grabItem 的批量功能是配合 background 的消息机制实现的background 收到当前 Tab 的GRAB_RESULT后不立即下载而是临时存到chrome.storage.local等所有 Tab 都发完数据后合并导出。// background.js 中批量收集逻辑 const pendingResults {}; chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { if (message.type GRAB_RESULT) { const tabId sender.tab ? sender.tab.id : unknown; if (!pendingResults[tabId]) { pendingResults[tabId] []; } pendingResults[tabId] pendingResults[tabId].concat(message.data); sendResponse({ ok: true, count: message.data.length }); } if (message.type FLUSH_ALL) { // 合并所有 Tab 的数据按 Tab 顺序拼接 const allRows []; for (const key of Object.keys(pendingResults)) { allRows allRows.concat(pendingResults[key]); } // 去重按商品链接去重同一个商品被多次抓取时保留最新一条 const seen new Set(); const deduped allRows.filter(item { if (!item.link || seen.has(item.link)) return false; seen.add(item.link); return true; }); triggerDownload(deduped); sendResponse({ ok: true, total: deduped.length }); } });批量收集的核心是去重逻辑。同一商品可能在不同 Tab 的搜索结果里重复出现按链接去重后数据才算是干净的。这里用 Set 记录已见过的链接比filter里用indexOf效率高一个量级商品数量超过一千条时体感差异明显。FLUSH_ALL消息由用户手动触发比如在 popup 里点一个合并导出按钮避免每个 Tab 独立下载一堆小文件。4.3 CSV 表头与字段顺序导出的 CSV 表头顺序不能随意排列。grabItem 固定为title,price,sales,rating,link,capturedAt。这个顺序基于使用习惯——标题放第一列方便浏览价格紧跟其后方便做数值对比链接放后面防止表格预览时超长字符串顶掉可读性。// background.js 中 CSV 生成函数 function toCsv(rows) { if (!rows || rows.length 0) return ; const header [title, price, sales, rating, link, capturedAt]; const lines [header.join(,)]; for (const row of rows) { const cells header.map(key { const value row[key] null || row[key] undefined ? : String(row[key]); // 转义处理字段内出现逗号、引号、换行时用引号包裹 if (value.includes(,) || value.includes() || value.includes(\n)) { return value.replace(//g, ) ; } return value; }); lines.push(cells.join(,)); } return lines.join(\n); }toCsv里的转义是 CSV 导出的标准动作字段内如果包含逗号、双引号或换行必须用双引号包住整字段字段内的双引号再加一个双引号转义。不这么做商品标题里一旦出现iPhone 12, 128G这种带逗号的文本Excel 会把整行数据拆错列。这段代码虽然看着啰嗦但它是决定导出数据能不能直接被表格工具打开的关键。5. 避坑指南抓取过程中最常见的五个翻车现场5.1 页面是异步渲染的脚本跑得太早现象插件注入后findProductContainers()返回空数组控制台提示未找到商品容器。原因run_at虽然设了document_idle但很多电商页面的商品列表是等接口返回后才渲染的DOM ready 时商品节点还不存在。解决用 MutationObserver 监听 body 子节点变化当出现第一个商品容器再启动抓取。改成监听DOMContentLoaded后轮询查找节点每 500ms 查一次连续 10 次无结果才放弃。5.2 价格字段混入促销文案现象价格抓出来是满299减30或券后¥89。原因选择器.price命中的节点里除了实际价格还包含了促销标签节点。解决优先选择价格文本所在的叶子节点而不是父容器。检查节点的childElementCount 0只有没有子元素的节点才是真正包含文本的节点再对该节点做textContent提取。5.3 翻页抓取时数据重复现象列表页翻到第二页、第三页时抓到的部分商品和第一页重复。原因某些平台会混入推荐商品或看了又看模块这些模块里的商品在其他页已经出现过。解决在批量场景里统一走FLUSH_ALL的链接去重逻辑。不要在 content.js 里做去重因为 content.js 只看到当前页面数据跨页面去重必须在 background 汇总后执行。5.4 图片地址是懒加载占位图现象抓到的图片地址形如1px.svg或含placeholder关键字。原因列表页为了首屏性能图片真实地址放在>// background.js 中手动注入抓取函数 chrome.action.onClicked.addListener(async (tab) { if (!tab || !tab.id) return; const result await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: quickGrab, args: [{ includeImages: true, maxItems: 20 }] }); const grabbed result result[0] result[0].result; if (grabbed grabbed.length 0) { sendResultToBackground(grabbed); } }); // 注入到页面的函数在页面上下文执行 function quickGrab(options) { const items findProductContainers().slice(0, options.maxItems).map(extractProduct); return items; }这段代码的注入函数quickGrab定义在 background 里通过func参数传递执行时它在页面上下文里跑能看到 DOM 但看不到 background 的变量。所以findProductContainers和extractProduct这两个依赖函数也必须完整定义在quickGrab内部或者通过args传进去不能假设页面里有这些函数。maxItems限制条数的功能在处理首页大促场景时特别有用——整页可能有 80 个商品你只想要前 20 个做快速确认就不必等滚动加载全部。调大这个值就切换到尽量抓全的批量模式。手动注入的价值在于自动注入经常被页面自身的脚本干扰比如页面重写 DOM 结构而用户主动触发时页面基本处于稳定状态抓取成功率明显更高。从那以后我每次调试 grabItem 都强制走一遍流程先在测试页面开隐身窗口清缓存加载插件后打开控制台确认 content script 注入成功再手动触发一次抓取看数据是否完整最后才会点上自动化滚动抓取。这个习惯帮我过滤掉了一大半的选择器写错问题。希望帮到你如果你也在做类似的商品信息采集需求直接拿这套框架改改选择器就能用。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑