多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

浏览器插件实现拼多多订单批量导出为Excel表格

浏览器插件实现拼多多订单批量导出为Excel表格 在浏览器里把拼多多的买家历史订单批量导成 Excel 表格这个需求听起来不大真做起来还挺折腾。我最初是帮一个做电商运营的朋友对账他自己店铺后台的数据好搞但作为买家身份下的历史订单想导出来官方根本没有导出入口。手工复制了几十单后两个人坐在那儿一页页翻整个人都要崩溃了。后来我干脆写了个浏览器插件直接在拼多多订单页里自动加载、解析、导出几分钟就能把上千条订单变成结构化表格。这篇文章就把我踩过的坑、最终跑通的方案完整拆出来代码也会给到属于那种拿来改改就能用的级别。适合谁看你自己经常在拼多多买东西年底要做消费统计、报销、维权举证或者你帮家里人处理订单、做账需要把散在各处的订单聚合起来再或者你本身就是想入门浏览器插件开发拿一个真实需求练手。这篇文章都能给你一条清晰的路径不需要后端、不需要申请任何 API 权限纯浏览器端就能完成。1. 为什么选浏览器插件先盘的几个方案1.1 我最初的三个备选方案动手之前我认真想过三条路一是手工操作就是不断滚动页面、翻页、逐条复制订单号、金额、商品名、收货人最后粘贴到表格里。这个方案对几十单还行一旦订单超过两三百条手指和眼睛都会抗议而且漏单几乎是必然的。二是 RPA 工具比如影刀、按键精灵这类。它们能模拟鼠标键盘操作把页面操作录制成流程。我试过一段时间发现它有明显的笨重感需要额外安装客户端脚本一旦遇到页面改版、弹窗遮挡、异步加载变慢经常录一遍跑一遍结果就不一样了调试成本很高。特别是拼多多订单页的布局一直在微调RPA 里那种固定坐标、固定图标的识别方式比较脆弱。三是调用拼多多开放平台的 API。这条路对商家来说是正路但问题在于我是一个买家身份平台 API 主要服务商家和开发者买家订单数据并没有面向个人开放。申请权限、审核资质、签名加密这一套流程走下来个人玩家基本拿不到。就算拿到了也还得处理 token 过期、接口限流这些事太重了。所以我最后锁定了浏览器插件方案。它本质上是跑在浏览器里的一个扩展程序可以直接操作当前页面的 DOM把页面上已经渲染出来的订单数据读出来再通过脚本整理成 CSV 文件。相比手工它自动化程度高相比 RPA它直接读数据而不是“模仿人点击”稳定性强很多相比 API它不需要任何服务端权限自己用完全够。1.2 插件方案的优势与边界浏览器插件的本质是浏览器官方开放给开发者的“增强浏览器能力”的入口。它比普通网页脚本多出几个能力跨域请求、本地存储、后台常驻、右键菜单、下载管理等等。这里面跟订单导出最相关的是 Content Script内容脚本和 Download下载能力。Content Script 可以注入到拼多多页面里跟页面的 JavaScript 共享同一个 DOM但又拥有独立的运行环境。这意味着我可以直接读取页面上的订单卡片、金额文本、时间文本甚至触发滚动事件让页面加载更多订单。注意这里有一个关键点Content Script 能访问 DOM但不能直接调用页面自身定义的一些全局函数比如 Vue 实例里的方法。不过没关系我导出订单只需要“已经渲染出来的 DOM 文本”不需要调用拼多多内部逻辑这就绕开了限制。当然插件方案也有自己的边界。它是“所见即所得”的方案——页面上没渲染出来的订单它就拿不到。拼多多订单页通常只保留两三年内的订单更早的会归档这个靠插件解决不了。另外如果你需要的是结构化的买家订单接口比如要拿到商品 SKU、支付流水号、售后状态这些 DOM 上不显示的字段插件就无能为力了。好在对于绝大多数记账、报销、统计场景页面上展示的订单号、金额、商品名、下单时间、收货人这些字段已经足够了。2. 插件原理拆解页面脚本、消息通道、DOM 解析2.1 扩展三件套manifest、后台、内容脚本一个现代 Chrome 插件Manifest V3至少包含三个角色清单文件manifest.json、后台服务工作者service worker、内容脚本content script。把它们类比一下manifest 是插件的“身份证”声明插件的权限、入口文件、能在哪些网站上运行后台是插件的“后勤部”负责处理全局事件、跨域请求、下载任务内容脚本是插件的“前线侦察兵”注入到目标网页里干活。订单导出这件事主要发生在“前线”和“后勤”之间。内容是 content script 负责的它在拼多多订单页面加载完成后自动执行自动滚动加载、解析订单列表后勤是后台或 popup弹出窗口负责的它接收 content script 传回来的数据生成 CSV 文件并触发下载。这里我要特别提醒一个 Manifest V3 的变化以前的版本允许后台页面长期驻留V3 里 service worker 是“用时唤起、不用销毁”的你不能指望它保存全局状态。所以在设计上我倾向于不在后台存数据而是让 content script 把数据一次性传给 popup由 popup 负责生成文件。这样规避了 service worker 被系统回收导致数据丢失的问题逻辑也更直观。2.2 核心数据怎么抓文本锚点比选择器更抗改版写爬虫的人都知道最怕的不是反爬是页面改版。今天用的 class 名明天可能就换了一套压缩后的随机字符。拼多多前端恰好特别爱做这类事情。我一开始写解析规则时盯着一堆形如_3JfFjZ、_2QvqEo的 class 名去选结果没过多久页面一改版整个导出工具直接瘫痪。后来我总结出一套更抗打的策略不依赖固定 class而是用“文本锚点”定位。什么意思比如一个订单卡片里必然会有“订单编号”这个文字标签后面的兄弟节点就是订单号“实付款”后面就是金额“收货人”后面就是收件人信息。我只需要在 DOM 里找到含有这些文本的元素再沿 DOM 树往上找到整个订单卡片容器最后在容器内按文本模式提取字段。这个方法的好处是就算拼多多换了 class 名只要页面还展示“订单编号”“实付款”这些肉眼可见的文字解析规则就还能用。实测下来我整套提取逻辑跨过了好几次页面改版依然稳定。2.3 消息通道popup 与 content script 的配合content script 住在页面里popup 住在浏览器工具栏的弹窗里它们不能直接调用对方的函数必须通过 chrome.runtime 的消息机制通信。通信方向有两类popup 主动向 content script 发指令“开始导出”content script 处理完成后把结果发回来“数据如下”。消息可以带对象所以数据量不大的时候直接chrome.tabs.sendMessage把整个订单数组传回来就行。但如果订单数量特别大比如一次导出几千单我建议分批传比如每处理 100 条就发一次popup 侧做累加最后再统一生成文件。这样能避免内存占用过高也方便你在中途看到进度。下面这段是 popup 里发消息的核心骨架// popup.js async function requestExport() { const [tab] await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab || !tab.url.includes(yangkeduo.com) !tab.url.includes(pinduoduo.com)) { alert(请在拼多多订单页使用此工具); return; } try { const response await chrome.tabs.sendMessage(tab.id, { action: startExport }); if (response response.orders) { generateCSV(response.orders); } else { alert(未获取到数据请确认页面已加载出订单列表); } } catch (e) { alert(页面脚本注入失败请刷新订单页后重试); } }对应地content script 里监听这个消息启动自己的“滚动加载 解析”流程结束后把数据回传// content.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) { if (message.action startExport) { runExport().then(orders sendResponse({ orders })); return true; // 告诉浏览器我会异步调 sendResponse } });这里的return true很关键没有它异步的回调还没执行完消息通道就提前关闭了响应会丢失。新手写这块踩坑的特别多。3. 从零搭建导出插件完整实操流程3.1 准备工作目录、开发者模式、加载插件整个项目不需要 npm、不需要打包工具纯手写三个文件就能跑。我建议你建一个独立目录比如order-export-ext里面结构如下order-export-ext/ ├── manifest.json ├── popup.html ├── popup.js ├── content.js然后打开 Chrome地址栏输入chrome://extensions右上角打开“开发者模式”左上角点“加载已解压的扩展程序”选中order-export-ext目录插件就装上了。每次改完代码点一下插件卡片上的刷新按钮再重新打开订单页就能生效。3.2 第一步写最简 manifest.jsonManifest V3 的清单文件长这样{ manifest_version: 3, name: 拼多多买家订单导出, version: 1.0, description: 在拼多多订单页自动加载并导出买家订单数据, permissions: [activeTab, scripting, downloads], action: { default_popup: popup.html, default_title: 拼多多订单导出 }, content_scripts: [ { matches: [*://*.yangkeduo.com/*, *://*.pinduoduo.com/*], js: [content.js], run_at: document_idle } ] }几个地方重点解释。matches控制 content script 在哪些站点生效这里我同时放开了yangkeduo.com和pinduoduo.com因为拼多多的域名经常会跳来跳去只写一个容易出现插件图标置灰的情况。permissions里activeTab解决临时注入的授权问题scripting允许我主动往标签页里注入脚本downloads用来触发文件下载。run_at设为document_idle意思是页面加载得差不多后再注入脚本避免过早注入时 DOM 还没渲染出来。3.3 第二步先验证 content script 是否注入成功第一次搭建时别急着写复杂逻辑先让 content script 简单打印点东西确认通路是通的。打开拼多多订单页按 F12 打开开发者工具切到 Console 面板。正常情况下页面加载后你应该能看到 content.js 打印的日志。如果看不到优先检查matches是否匹配当前 URL。拼多多有时候会跳到mobile.yangkeduo.com如果你的表达式没覆盖这个子域名脚本就不会注入。这种“插件装上了但没反应”的情况八成都是匹配规则没写好。3.4 第三步popup 界面与一键导出popup 是用户看到的操作面板没必要做得多花哨一个按钮加上一个状态提示就够了。我建议加两个按钮一个是“开始导出”另一个是“清空数据”。后者用于重置状态免得反复导出时数据叠加后面排查问题会特别头疼。!-- popup.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 style body { width: 260px; padding: 16px; font-family: system-ui; } button { width: 100%; margin-top: 8px; padding: 8px; } /style /head body h3拼多多订单导出/h3 button idstartBtn开始导出/button button idclearBtn styledisplay:none;清空数据/button div idstatus stylemargin-top: 10px; font-size: 13px; color: #555;就绪/div script srcpopup.js/script /body /htmlpopup.js 里的核心逻辑除了前面提到的消息发送还要处理一个边界content script 可能因为页面没刷新而未运行。这时候sendMessage会抛异常我在代码里用 try/catch 包住并提示用户刷新页面。经验之谈这个环节几乎每次都会遇到特别是你刚改完代码、页面是旧状态的时候。3.5 第四步核心提取逻辑与 CSV 下载这一步是整个插件的灵魂。我在 content.js 里实现了这样一套流程循环滚动页面到底部触发懒加载每次滚动后等待 1.5 到 2.5 秒让订单卡片渲染出来检测列表数量是否还在增加如果连续几次没有新增就认为已经到了底部遍历 DOM 中所有订单卡片按文本锚点提取字段去重后返回数据滚动和等待是订单导出的成败关键。拼多多的订单页是典型的懒加载不滚动就不会加载更多但滚动太快、等待太短服务器还没返回数据DOM 里就还是空的。我最终用的参数是每次滚动 1200 像素等待 2 秒最多尝试 60 次。这个组合我实测了上百单和上千单两种场景都比较稳。提取字段时我优先找“订单编号”“状态”“下单时间”“实付款”这几个标签再在它们所在的卡片容器里提取其他内容。下面是核心提取代码的简化版注释写得很细// content.js async function runExport() { const beforeCount 0; let stableRounds 0; const maxStableRounds 3; const waited []; for (let round 0; round 60; round) { window.scrollBy(0, 1200); await sleep(2000); const currentCount document.querySelectorAll(div[class*order], li[class*order]).length; if (currentCount beforeCount) { stableRounds; if (stableRounds maxStableRounds) break; } else { stableRounds 0; } } const orders extractOrders(); return orders; } function extractOrders() { const orderSet new Map(); const cards document.querySelectorAll(div[class*order], li[class*order]); cards.forEach(card { const text card.innerText || ; const orderNo matchText(text, /订单编号[:\s]*([A-Za-z0-9_-])/); const amount matchText(text, /实付款[:\s]*[¥]?\s*([0-9.])/); const time matchText(text, /下单时间[:\s]*([0-9\-:\s])/); if (orderNo !orderSet.has(orderNo)) { orderSet.set(orderNo, { orderNo, amount, time, title: matchText(text, /商品[:\s]*(.{0,60})/) || , status: matchText(text, /(待发货|待收货|已签收|已取消|退款成功|已评价)/) }); } }); return Array.from(orderSet.values()); } function matchText(text, regex) { const m text.match(regex); return m ? m[1].trim() : ; } function sleep(ms) { return new Promise(resolve setTimeout(resolve, ms)); }注意这里我用innerText拿卡片文本因为innerText会尊重页面布局返回真正对用户可见的文字而textContent会把隐藏元素、脚本里的文本也带进来干扰匹配。拿到数据后popup 侧生成 CSV。生成 CSV 有一个老生常谈的坑Excel 直接打开 UTF-8 编码的 CSV 会中文乱码需要加 BOM 头。我的做法是拼内容时在最前面加上\uFEFF然后用Blob生成文件最后通过chrome.downloads.download触发下载。// popup.js function generateCSV(orders) { const header 订单编号,实付款,下单时间,商品,状态\n; const rows orders.map(o [o.orderNo, o.amount, o.time, o.title, o.status] .map(v ${String(v || ).replace(//g, )}) .join(,) ); const csv \uFEFF header rows.join(\n); const blob new Blob([csv], { type: text/csv;charsetutf-8 }); const url URL.createObjectURL(blob); chrome.downloads.download({ url, filename: pdd_orders_${Date.now()}.csv }); }每一个字段都用双引号包起来再把字段内部的双引号转义成两个双引号。这个细节看起来不起眼但商品标题里经常会带逗号、引号不处理的话CSV 列就会错位整份表格就没法看了。4. 常见问题与排查技巧实录4.1 订单不全、只导出了第一屏这是最常遇到的问题。原因几乎都是滚动加载没触发完整或者页面渲染速度比预设的 2 秒更慢。我的排查顺序是这样的先看 Console 里有没有报错判断是脚本中断还是正常结束再手动滚动页面看看列表底部有没有“已经到底了”的提示如果没有说明滚动的频率或步长不够最后看脚本里统计到的卡片数是否和页面实际数量一致。我把滚动策略改成了“动态等待”每轮滚动后记录当前订单卡片数量和上一次的差值如果连续 3 轮差值都是 0才认定加载完毕。这个方法比固定轮数要靠谱得多特别是在网络环境差的情况下。另外有一个意外发现在订单列表里拼多多默认展示的是“全部”状态但如果你之前手动筛选过“待收货”之类的子页签插件就只能导到当前筛选下的订单。所以开始导出之前先切回“全部”再滚动到顶部保证脚本从完整列表的开头开始加载。4.2 金额、订单号解析出来是空的或乱的如果部分字段是空的但订单数量是对的问题基本出在正则匹配上。我一开始写的正则太严格比如金额用了([0-9.])结果遇到¥12.9这种带货币符号的文本时匹配到的内容没问题但遇到价格被格式化成了1,299.00正则里的[0-9.]会被逗号拦腰截断只能拿到1。解决方案是放宽正则、再做后处理。金额的正则我改成([\d,]\.?\d*)然后在解析函数里去掉逗号再转浮点数。订单号也是同理拼多多的订单号里有下划线和中划线不能只匹配数字。解析时我的建议是先打印出每张卡片的原始文本看着实际内容去调整正则而不是靠猜。一般用 20 条真实订单作为样本把所有正则都调稳了再跑全量。另外如果一个订单卡片里同时出现“实付款”和“优惠”注意“实付款”可能出现在“已退款”的订单里金额是 0 或者负的这表示已退款的实付金额是正常业务逻辑别当成 bug。4.3 打开新页面后插件图标变灰有时候你会遇到这种尴尬插件装在浏览器里了但一打开订单页图标是灰的点不了。问题通常出在 matches 没覆盖拼多多的具体域名或者拼多多用了 iframe 嵌套订单功能你的 content script 注入的是外层页面订单内容在 iframe 里。如果是 matches 的问题把*.yangkeduo.com/*改成*://*/pay*.yangkeduo.com/*这种更宽的模式但别太宽否则任何网站都会注入引入安全问题。如果是 iframe 的问题就得在 content script 里处理 iframe查一下文档里有哪些iframe找到src域名匹配拼多多的那个然后拿到它的contentDocument去解析。这一块比较绕我会在后面的扩展里单独讲。4.4 CSV 乱码、重复导出乱码问题基本就是编码。Excel 对 UTF-8 无 BOM 的 CSV 兼容性很差所以我坚持在文件头加\uFEFF。另外注意如果你在 Windows 上用记事本手动改了 CSV 再保存文件编码可能被改成 ANSI 或 GBK再次用 Excel 打开就不正常了。最好让脚本一次性生成最终文件不要手动二次编辑。重复导出这个坑是我自己埋的。我当时在 popup 里用了全局数组累加数据但没在每次导出前清空结果点第二次“开始导出”的时候订单数量直接翻倍。解决办法是每次开始导出前先把数组重置为空再等待新增数据。更稳的做法是在 content script 侧用Map以订单号为 key 去重天然防止重复。4.5 频率过快触发登录风控说实话拼多多对买家端的反爬比商家端松很多但如果你在几秒内疯狂滚动、频繁发送消息还是可能触发登录状态异常或者滑块验证。我之前调试时脚本一次性滚动了上百次结果订单页直接被弹到了登录页。解决方法是加入随机延时每次滚动后等待时间在 1.5 到 2.5 秒之间随机而不是固定 2 秒。这样动作看起来更像真人操作。另外如果中途弹出了验证码或登录提示脚本应该自动停下来提示用户手动处理而不是继续往下跑。这个兜底逻辑可以放在滚动循环里一旦发现页面 URL 变成了登录页立即break并返回“需要人工处理”的状态。5. 插件之外还能怎么玩5.1 从插件到 RPA、API 的选型边界做完这个插件后我又顺手想了下它在更大自动化体系里的位置。像影刀 RPA 这类工具更适合处理跨系统、跨软件的长流程比如“抓取订单 → 写入 ERP → 生成报表”。而浏览器插件更擅长在单个网页环境里做精准的数据提取和交互开销小、部署快、修改灵活。如果你后续想把订单导出变成定时任务比如每天早上自动拉取昨天的订单可以在插件里加一个chrome.alarms的定时任务在后台定时打开订单页、注入脚本、导出数据。但注意买家端的登录态一般不会长期有效定时任务的维护成本会比手动点按钮高不少。所以我的建议是小批量、偶发场景用插件大批量、需要数据落库再分析的场景考虑 RPA 或正规 API如果只是自己用的数据整理插件完全够了没必要上重武器。5.2 后续扩展自动对账、报销归档、消费分析订单导出来只是第一步后面的用途才是价值所在。我自己在拿到 CSV 后做了两个小工具一个是按月份汇总消费金额做年度预算复盘另一个是识别“已签收但超期未评价”的订单统一处理。这些扩展不需要改插件逻辑CSV 到了手用 Python、Excel、或者现成的记账软件都能继续处理。如果你想全链路自动化也可以在插件端再加一个“导出后自动打开报表页面”的步骤把 CSV 通过 URL 参数传给本地服务服务端再做后续分析。做这些的时候有条底线我得提醒插件导出的数据只属于你自己名下的订单。用同样的技术去抓取他人订单、抓取非本人授权数据性质就完全不同了涉及隐私和数据安全千万别碰。保存数据时建议对包含收货人、手机号、具体地址的文件做加密或至少本地保管不要上传到不可信的第三方网盘。另外有个实操建议在订单导出的同时我会把原始 CSV 备份到本地一个固定目录文件名加日期。这样一个月后即使 CSV 被误删或覆盖还能从旧文件里找回数据。反正生成文件成本很低多做一步备份后面省心很多。最后说个我在实际使用中的体会这种“小工具解决小痛点”的项目最大的价值不在于技术多牛而在于它真的能让你从重复劳动里解放出来。整套插件我断断续续调了一周之后每次给家里人做年度消费盘点从打开订单页到导出 CSV一分钟不到就完成了。如果你照着上面的代码搭了一遍跑通了建议再花点时间把字段调成自己真正需要的那些你会发现导出订单这个动作本身只是开始数据到了自己手里之后能做的事情比想象中多得多。
返回列表