上周三下午做运营的老同学在群里甩来一句很短的话PHD 里的数据到底怎么抓出来我问他第一句话不是你想用哪个工具而是你现在打开 PHD是浏览器里的一个网页还是桌面上那个装好的客户端。他愣了一下说两种都有网页端看汇总客户端拉明细。这个反问很关键——从 PHD 抓取数据这件事八成的成败在你动手写第一行代码之前就已经定了定在你有没有先搞清楚数据的出口在哪。后面我花了两个晚上帮他把流程跑通顺便把踩过的坑整理成了这篇东西。它讲的不是某个特定版本的 PHD而是当你面对一套叫 PHD 的业务系统、需要把里面的数据批量搬出来做分析时一个从业者真正会走的路径怎么判断形态、怎么找到接口、接口堵死之后有哪些退路、跑起来之后怎么保证不漏不错、以及哪些线不能碰。适合每天和报表打交道但不想再手工复制粘贴的人也适合刚接手数据对接任务、还没什么抓取经验的同学。1. 动手前先定性你手里的 PHD 是网页、客户端还是别的形态很多人卡在这一步一上来就搜PHD 抓取脚本搜到的方案要么跑不通要么对不上号。原因很简单PHD这三个字母在不同团队指的东西差别极大——可能是公司自研的网页版数据后台可能是采购来的一套带安装包的 Windows 客户端也可能是嵌在企业微信里的小程序甚至是手机 App。形态不同可选的技术路线几乎是两套完全不同的体系所以第一步永远是定性而不是选工具。1.1 四种常见形态对应的可行方案我把见过的几类情况整理成了一张表你可以直接对号入座。看的时候注意落地难度这一列它比能不能做更重要——能做的方案有很多但维护成本差的不是一点半点。形态典型特征首选方案落地难度网页版后台浏览器打开列表会翻页右上角常有导出直接调它的数据接口低但接口会变桌面客户端装在电脑上登录后看数据可能带打印抓本机流量或读本地缓存文件中小程序 / 移动端只能在手机或企业微信里看界面自动化RPA为主中高有导出/订阅功能菜单里有导出 Excel、报表订阅优先用官方出口不写抓取最低看这张表的时候有个细节容易被忽略有导出功能和导出功能够用是两件事。很多系统的导出只给你当前页、只给固定字段、或者限制一次最多两千行这种时候你依然要回到接口那条路上去。但它的价值在于——导出按钮本身往往就是一个接口点一次就能在流量里看到它等于系统主动把出口位置告诉你了这比你自己去几十个请求里翻找要省事得多。1.2 三分钟定位法从数据从哪来倒着推定性不用搞得很复杂我通常按下面这个顺序问三个问题基本三分钟能定位。第一个问题这些数据是页面加载时就带着的还是滚动/点击之后才出现的如果是加载时就带着说明它在页面的初始数据里右键查看网页源代码搜关键词就能找到这种最省事。如果是点一下才出来那它一定是通过一个异步请求拿回来的去浏览器的开发者工具里找。第二个问题客户端有没有本地缓存Windows 客户端的安装目录下通常会有data、cache、localdb这类文件夹里面可能有.db、.mdf、.dat文件。我遇到过好几次辛辛苦苦抓了两小时流量结果发现数据本来就以明文存在本地一个 SQLite 文件里读一下就行效率差了十几倍。第三个问题有没有人做过这件事内网搜一下、问问隔壁组很多抓取需求其实已经有同事写过脚本了只是没沉淀成文档。重复造轮子的成本不只是写代码那几天还包括后面接口一变你要维护两份。1.3 为什么我总劝人先找官方出口说一个不太讨喜但很真实的结论抓取脚本的平均寿命比大多数人预期的短得多。稍微正规一点的系统接口三个月到半年就会调整一次字段改名、加签名、换分页方式你的脚本就废了。而导出功能、报表订阅、开放接口这些官方出口虽然可能字段少一点、格式土一点但它们是被维护的。所以我给自己定的顺序是官方导出够用就直接用不够用再看官方有没有开放接口或报表订阅两条都没有才考虑抓取。这个顺序还有一个隐性好处当你需要向系统负责人申请权限时我要用导出功能做月度分析比我想抓你们的接口容易获批得多。抓取本身不是问题没有沟通就抓才是问题。2. 网页版 PHD先把请求解剖清楚再决定写不写代码如果确认是网页版恭喜你这是所有形态里成本最低的一种。但低不代表随便写就行我见过太多人打开编辑器就开始堆 requests结果调了半天发现方向错了。正确的姿势是先花二十分钟做接口分析把数据长什么样、怎么要、要几次这三件事搞清楚写代码只是最后十分钟的事。2.1 Network 面板里真正需要盯住的只有五样打开开发者工具的 Network 面板勾上 XHR 或 Fetch 过滤然后刷新列表页。这时候你会看到一堆请求不用全看按下面的标准筛看返回内容点开 Response如果是一段结构化的 JSON并且数组长度和页面列表的行数差不多那就是它了。HTML 片段说明你找错了。看 Request URL记下完整地址注意路径里的关键词比如/api/report/list、/data/query这种。看 Method 和 Payload是 GET 还是 POST参数叫什么名字。分页参数一般叫page、pageIndex、pageNo、offset这几个名字之一每页条数叫pageSize、limit、rows。看请求头重点看Cookie、Authorization、Referer、X-Token这类字段它们决定了你能不能带着登录态去请求。看耗时和状态码如果某个请求返回 200 但内容是空的多半是参数没给对或者没有权限。这一步做完你手头应该有一份最小可复现请求一个 URL、一个方法、一组参数、一份请求头。有了它后面就是在代码里把浏览器刚才做的事重演一遍。2.2 三个经常能捡到的便宜分析请求的时候有三条捷径值得先试成本极低但收益很高。第一条是导出按钮反查。找到页面上的导出按钮点一下然后立刻去 Network 看新增的请求。很多时候它就是一个返回文件流的接口你把参数换成更大的时间范围一次就能拿到全量数据比自己翻页拼接干净得多。注意有些系统会在这个接口上加个一次性 token那就不能直接复用得看具体情况。第二条是把 pageSize 调大。列表页默认每页 20 条你把请求里的pageSize改成 200 或 500 再发一次如果返回条数跟着变那你的请求次数直接少一个数量级。但要注意服务端通常有上限常见的是 100 到 1000 之间超过就被截断或者直接报错试探的时候小步走别一上来写 99999容易被风控盯上。第三条是缩小时间范围再放大。有些报表接口的参数是起止日期如果系统对单次查询的跨度有限制你可以按天循环一天一个请求逻辑简单、失败了也好重跑。2.3 登录态到底怎么带着走这是新手最容易翻车的地方。请求头里的那个Cookie或者Authorization本质上是你在系统里的身份凭证它的特点是会过期。常见的过期时间从几小时到几天不等有的系统你关掉浏览器就失效。我的处理方式是第一次手工登录从开发者工具里把完整的 Cookie 复制出来放进配置跑通流程然后观察它大概多久失效再决定要不要做自动续期。这里有个经验——不要试图去自动化登录过程本身尤其是涉及验证码、短信、扫码的场景投入产出比极低而且容易触发风控。更稳的做法是让脚本跑短周期、跑完就停需要长期跑的时候人工换一次凭证一天也就花一分钟。至于 Token 放在哪判据很简单在 Network 里找到那个数据请求把它的请求头逐条对比一个普通静态资源的请求头多出来的那几条就是关键照抄即可。2.4 把它变成能跑的脚本下面这段代码是一个可以直接改参数用的最小骨架。它做三件事按页循环、把结果写进 CSV、每页之间随机停顿。写法上我特意用了utf-8-sig编码这样导出的 CSV 直接用 Excel 打开不会出现乱码这个细节能省掉后面很多解释成本。import csv import random import time import requests BASE_URL https://phd.example.com/api/report/list HEADERS { User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36, Referer: https://phd.example.com/report/index, Content-Type: application/json, Cookie: sessionid替换成你自己的; csrftoken替换成你自己的, } PAYLOAD_TPL { pageIndex: 1, pageSize: 100, beginDate: 2024-01-01, endDate: 2024-12-31, } def fetch_page(page: int) - list: payload dict(PAYLOAD_TPL) payload[pageIndex] page resp requests.post(BASE_URL, jsonpayload, headersHEADERS, timeout20) if resp.status_code ! 200: raise RuntimeError(f第 {page} 页请求失败状态码 {resp.status_code}) body resp.json() return body.get(data, {}).get(rows, []) def main(): all_rows [] for page in range(1, 200): rows fetch_page(page) if not rows: print(f第 {page} 页返回空认为已到末页) break all_rows.extend(rows) print(f已抓取第 {page} 页累计 {len(all_rows)} 条) time.sleep(random.uniform(0.8, 1.5)) if not all_rows: print(没有拿到任何数据先回去检查请求头和参数) return with open(phd_rows.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnameslist(all_rows[0].keys())) writer.writeheader() writer.writerows(all_rows) print(f完成共 {len(all_rows)} 条已写入 phd_rows.csv) if __name__ __main__: main()几个我自己加进去的小习惯你可以直接沿用timeout一定要给不给的话网络一抖脚本就挂在那儿每页之间sleep一个随机区间而不是固定值比固定间隔更接近人的操作节奏判断末页用返回空列表而不是抓够 N 页因为实际总页数往往和你估的不一样。3. 接口抓不到或者参数被加密了还有三条退路不是所有 PHD 都这么友好。你会遇到几种情况请求体是一串看不出含义的密文、带上一个几分钟就变的签名、或者干脆连列表数据都是服务端渲染好直接塞进 HTML 的。这时候不要硬啃按成本从低到高往下退。3.1 抓本机流量Charles 的三步配置和它的边界抓包工具的价值在于它不需要你去猜请求长什么样只要流量从你机器上过它就能给你看。以 Charles 为例在 Mac 上抓本机自己的流量其实只有三步。第一步打开 Charles进入 Proxy 设置确认监听端口默认 8888。第二步把 macOS 的系统代理指向127.0.0.1:8888这一步告诉系统我的网络请求先交给 Charles。第三步安装并信任根证书——在 Charles 菜单里选择安装根证书到系统钥匙串然后在钥匙串访问里把这张证书的信任设置改成始终信任。不装这一步你只能看到一堆 HTTPS 域名的连接建立记录看不到里面的内容因为流量本身是加密的。配置完之后你在浏览器或客户端里点一次列表页Charles 左侧就会列出一串域名找到对应的那条点开就能看请求和响应。这里有个实用技巧用 Charles 的过滤功能只保留目标域名否则请求列表刷得飞快眼睛都跟不上。需要说清楚的是这条路只适用于你自己的机器、你自己有权限访问的系统。抓包本身是排查问题的常规手段但把它用在别人账号的流量上性质就完全变了这条线不能越。3.2 桌面客户端往往把数据落在了本地这是被最多人忽略的一条路。Windows 上的客户端程序为了实现离线查看、快速加载经常会把服务端数据缓存到本地文件里。你要做的是找到那个文件。找法有几种打开客户端安装目录看有没有data、cache、db、local这类文件夹按修改时间排序你刚刷新过页面的那几个文件就是目标也可以看客户端的配置文件里面常写着数据存储路径还可以用系统自带的资源监视器看客户端进程打开了哪些文件句柄直接定位到具体文件。找到之后如果是.db后缀八成是 SQLite用 DB Browser for SQLite 打开就能看表结构如果是.mdf可能是 SQL Server 的本地库如果是一堆看不懂的二进制先看有没有配套的日志文件有时候明文就写在日志里。注意打开之前一定先把文件复制一份出来在副本上操作。客户端正在运行时锁着库文件直接打开可能报错强行操作还有损坏数据的风险。3.3 参数加密时先算一笔账再决定要不要硬啃遇到签名或者加密参数很多人第一反应是去逆向那段 JavaScript。我的建议是先冷静算一笔账这个接口多久变一次你需要的是一次性全量还是每天持续拉取如果是一次性任务数据量不大逆向花三天显然不划算用界面自动化慢慢跑反而更快。如果是长期需求逆向的维护成本也要算进去人家改一次混淆你的代码就得重来一遍。真正稳妥的长期方案往往是去找系统负责人谈一个正式的接口或者数据同步方案——这听起来不如写代码酷但它能让你三个月后不用半夜起来修脚本。4. 一行代码不写用 RPA 在界面上把数据搬出来如果接口这条路走不通又不值得投入逆向那就让程序去操作界面。这类工具常见的有影刀、UiPath 这一类统称 RPA思路是模拟人的操作打开程序、点登录、输条件、点查询、翻页、把表格读出来、写进 Excel。4.1 RPA 的甜区和禁区先说它擅长什么再说它不擅长什么这样你能快速判断该不该用。场景是否适合 RPA原因系统只有客户端没有接口适合这是 RPA 的主战场数据需要跨多个页面拼接适合页面是给人看的字段位置固定单次数据量几百到几千条适合跑几十分钟能接受单次数据量几十万条不适合界面渲染速度是瓶颈跑不完页面结构频繁改版不适合元素定位会失效维护成本高需要 7×24 高频采集不适合稳定性撑不住也容易影响别人使用我见过一个很典型的例子是抓取货源平台的运费数据做对账。这类平台通常要求登录查询条件复杂运费还要从详情页里逐条读数据量一晚上也就几百条——用接口的话得逆向一堆参数用 RPA 反而半天就能搭好而且对方页面改版了你改两下选择器就行改动量比接口小。这就是 RPA 的价值区间数据量不大、字段来源分散、页面相对稳定。4.2 分页循环的最小骨架怎么搭不管用哪个工具分页采集的思路都是一样的我把它拆成五步你照着搭就行。第一步把打开到查询出结果这一段录成一条完整流程确保它能稳定重复执行三次不报错。第二步在结果表格上做数据提取先只提三列验证一下字段对不对别一上来就把二十列全配上。第三步加一个循环循环的判断条件用下一页按钮是否可点击比用固定页数安全。第四步每轮把提取到的数据追加写入同一个表格文件不要攒在内存里最后一次性写。第五步在翻页动作前后各加一个等待等待条件用表格内容发生变化或者加载遮罩消失而不是写死三秒。第五步特别重要。写死等待时间是 RPA 脚本最大的稳定性杀手网络慢的时候等不够网络快的时候白等。用元素状态作为等待条件脚本会自己适应速度变化。4.3 两个必须提前想清楚的问题第一个是登录态和验证码。多数这类系统会要求登录有的还带验证码。常见的处理是让脚本第一次运行时停下来人工登录然后复用这个会话往下跑而不是想办法去自动识别验证码——后者的合规风险和实现成本都不值得。会话过期了就让脚本停下来提示你重新登录这比强行续期要好。第二个是运行环境。RPA 脚本对屏幕分辨率、窗口大小、缩放比例很敏感。你今天在自己电脑上调通了明天放到同事的电脑上可能就点错位置。所以搭好之后一定要换个环境测一遍把分辨率、缩放这些写进说明里。如果要在多台机器上跑优先用固定分辨率的环境。5. 能跑通只是起点限速、断点和数据校验脚本第一次跑通的那个瞬间很容易让人兴奋但真正决定这套东西能不能长期用的是它跑第二十次的时候还准不准。我自己踩的坑几乎都集中在这个阶段所以把这部分单独拿出来说。5.1 限速怎么定用一个简单的算式限速不是凭感觉设的可以算。假设你要抓三千条记录每页一百条那就是三十次请求。如果每次请求间隔一秒加上请求本身的耗时假设 0.5 秒总耗时大约是 45 秒。这个量级对大多数系统来说完全无感你完全可以放慢到每次间隔两秒总耗时也就一分半没必要去省这一分钟。真正需要警惕的是你自己加并发的那一刻。单线程跑十分钟很稳的场景你开十个线程去跑很可能一分钟就被拦了。我的经验值是这样对不明底细的系统先按每分钟二十到三十次请求起步观察半小时没有任何异常没有 429、没有要求验证码、响应时间没有明显变长再考虑往上加。加到出现异常就退回去这个上限就找到了。另外抓取的时段也有讲究。业务系统在上班时间本来就有很多人用你这个时候大批量请求一是容易被发现二是实实在在地影响了同事的体验。能挪到晚上和周末跑的就挪过去。5.2 断点续爬别让一次网络抖动毁掉两小时抓取任务最气人的场景是跑到第 800 页报了个超时脚本退出之前的数据没保存。避免这个问题的办法很简单就是边抓边落盘并且记录进度。落盘那部分前文的代码里已经体现了每页抓完就 append 到列表里最后统一写文件——但如果跑的是几千页的任务更好的做法是每抓几十页就写一次文件或者直接写到 SQLite 里。进度记录建议单独建一张表结构大概是这样CREATE TABLE crawl_state ( task_name TEXT PRIMARY KEY, last_page INTEGER, begin_date TEXT, end_date TEXT, updated_at TEXT );每次成功抓完一页就更新last_page脚本启动时先读这张表从上次中断的地方继续。这套东西写起来不到三十行代码但它能让你在遇到任何意外时都不用从头再来。我现在的习惯是哪怕只是抓几百条也把这个状态表加上因为临时抓一下的任务最后往往会变成每周都要跑。5.3 抓完之后必须做的三项校验数据抓回来不等于能用。我在交付之前一定会做三件事。第一件条数对账。大多数系统的列表页底部会显示总条数比如共 2,847 条。你抓到的条数应该等于或者略大于这个数考虑到抓取期间有新数据进来。差得多就说明翻页逻辑有问题最常见的原因是分页时数据在变动导致重复或漏项。第二件主键去重。抓回来的数据里按唯一标识去重一下看看有没有重复。重复通常来自两个地方翻页时排序不稳定同一行出现在两页里或者重试机制把同一页抓了两次。去重前统计一下重复数量如果比例超过百分之一就值得回头看看接口的排序参数。第三件抽样人工比对。随机挑十条打开 PHD 界面逐字段核对。这一步听起来笨但它能发现前面两步发现不了的问题——比如某个字段被截断了、日期格式变了、金额单位从元变成了分。我遇到过最隐蔽的一次是某个字段在超过四位数字时会被服务端科学计数法格式化肉眼一看条数没错、去重也没问题抽样一比才发现几十条数据的金额全错了。6. 权限、脱敏与留痕几条不能碰的线技术上的事讲完了最后说几句容易被跳过的。抓取这件事本身是中性的问题几乎都出在边界上。我的原则是三条非常简单。第一条只抓你有权限看的数据。你能在界面上看到的不等于你可以批量拿走。如果这份数据本身有访问审批流程那你抓取的范围就不该超过审批给你的范围。要扩大范围走流程申请别用脚本绕。第二条不碰个人信息碰了就必须脱敏。如果抓回来的数据里包含手机号、身份证、地址这类字段先问一句分析真的需要它们吗多数情况下不需要那就在入库前做哈希或者掩码处理。既不需要又留着是纯粹的风险。第三条留痕。把脚本、抓取的时间范围、抓取频率、字段映射关系记录下来最好写在同一个目录的说明文件里。这样做有两个好处一是三个月后你自己还看得懂二是万一有人问起这份数据从哪来的你能立刻说清楚。听起来像走形式但真出问题时这份记录就是你和说不清之间的区别。另外补一句关于网络层面的合规不要为了访问某些资源去改动网络出口配置、绕过访问控制。抓取的前提是你本来就该能访问绕过去的那些操作无论技术上多简单都不在这个话题的讨论范围内也不该在你的方案里出现。我个人在实际操作中的体会是这类活儿最花时间的从来不是代码而是前面那二十分钟的定性和后面那三十分钟的校验。把这两段做扎实中间那段写起来几乎是顺理成章的反过来跳过前面直接开写你大概率会在某个下午对着一份不知道哪里错的 CSV 发呆。至于长期方案我的建议始终是同一个能谈下一个正式接口就谈脚本的寿命真的没那么长留好字段映射表和注释换接口的时候你会感谢现在的自己。