PHD 这套系统在很多团队里都不是给外部人用的——它是内部主数据平台、生产管理系统或者某个业务中台的代称,界面上能点、能筛、能看,偏偏不给你一个顺手的批量导出。你要的那几张明细表、那几个按条件过滤后的统计口径,界面上要么一页一页翻到手指发酸,要么导出的 CSV 字段被砍掉一半。这就是“如何从 PHD 抓取数据”这个问题在内部群里反复被问到的原因。它不是一个纯技术问题,而是一个“先搞清楚数据从哪来、再选一条成本最低的路把它稳定拿下来”的工程问题。这篇文章面向的是需要定期从 PHD 取数做报表、做对账、做二次分析的人:会一点 Python 更好,完全不会代码也能走 RPA 那条路。我把接口直连、浏览器渲染、RPA 三条路线全部拆开讲,包括抓包定位、参数逆向、断点续抓、增量调度和一堆踩过的坑,你按自己的场景对号入座即可。
1. 先搞清楚 PHD 到底是什么,再谈怎么抓
抓数据这件事,90% 的失败不是死在代码上,而是死在一开始就没弄清目标系统的形态。PHD 在不同团队里指代的东西差别极大,访问方式、鉴权机制、数据组织方式完全不同,用同一套脚本硬套必然翻车。
1.1 PHD 在常见团队里的三类典型形态
第一类是内部 Web 管理系统,部署在内网或者某个专线域名下,需要账号密码登录,登录后靠 Session 或 Cookie 维持状态,页面主体是表格加分页器。这类系统的数据一般藏在 XHR 接口里,页面点一次“下一页”,背后就是一个带页码参数的接口调用。这是最容易抓的一类,只要把接口和参数还原出来,用脚本跑比手点快一百倍。
第二类是报表平台或数据看板,比如某些 BI 工具挂载的 PHD 数据集。它的特点是页面渲染重、图表多,数据可能在页面初始化时一次性塞进一个 JSON 变量里,也可能通过 WebSocket 分片推送。这类系统你直接看源码往往能看到一大坨压缩过的数据,取出来的成本极低,但字段名是短码,需要对照前端映射关系翻译。
第三类是桌面客户端或者 C/S 架构的客户端,没有浏览器地址栏,数据走的是自定义协议或者加密的 HTTP 请求。这种情况浏览器抓包就完全失效了,必须上系统级抓包工具,比如在 Mac 上用 Charles 监听本机流量,或者在客户端侧做流量转发。难度明显上一档,通常需要配合逆向分析。
判断自己面对的是哪一类,最土但最有效的办法:打开 PHD,按 F12 看 Network 面板,点一次分页,看有没有新的请求冒出来。有,第一类;没有但页面数据变了,第二类;连浏览器都打不开,第三类。
1.2 抓取之前必须钉死的三件事
第一件,授权边界。PHD 里的数据是不是你有权限访问的,导出来之后能不能二次使用,这是前提。只抓你账号能看到的数据、只抓业务上确实需要的那部分字段,别因为技术上能全量拉就把整库拖下来。很多内部系统的日志审计是完整的,异常流量一眼就能看出来。
第二件,数据边界和更新频率。你要的是全量快照还是每日增量?是按单据号拉还是按时间窗口拉?这个决定后面表结构怎么设计。我的经验是,只要目标系统有“更新时间”这个字段,一律按更新时间做增量,第一天全量,之后每天只拉窗口内的变动数据,能把请求量压到原来的百分之几。
第三件,频率和并发。内部系统通常没有为高频访问做优化,你开 20 个线程猛冲,轻则把自己的账号拖进限流名单,重则影响其他同事正常使用。我一般把并发压到 2 到 3,请求间隔加 0.5 到 1 秒的随机抖动,跑一整晚也没人来找我。
注意:先和系统负责人打个招呼,说明你的取数用途和大致频率,比事后解释省事得多。很多团队其实愿意给你开一个只读账号或者直接提供接口。
2. 三条抓取路线怎么选,别一上来就写代码
路线选错,后面全是无用功。我见过太多人一上来就打开 PyCharm 写爬虫,写了两天发现目标系统其实有个现成的导出接口,或者发现数据根本在客户端里,白干。
2.1 接口直连、浏览器渲染、RPA 三者的成本对照
| 路线 | 适用场景 | 上手成本 | 运行稳定性 | 维护成本 |
|---|---|---|---|---|
| 接口直连 | 数据走 XHR/JSON,参数可还原 | 中 | 高,速度最快 | 接口改版时需改代码 |
| 浏览器渲染 | 数据靠前端 JS 计算、图表化 | 中高 | 中,资源占用大 | 页面结构变化即失效 |
| RPA 拖拽 | 无代码基础、页面元素稳定 | 低 | 中,受分辨率影响 | 元素定位变化需重录 |
接口直连的核心优势是快和稳,一次请求拿一页数据,几百页几分钟跑完。浏览器渲染(Playwright、Selenium 这类)的优势是“所见即所得”,页面能看到什么就能拿到什么,代价是慢,一个页面渲染要两三秒,而且吃内存。RPA 的优势是零代码,业务同事自己就能维护,代价是脆弱——目标系统改个按钮位置,流程就可能断。
2.2 我自己的判断顺序和一次典型踩坑
我的判断顺序固定是三步:先翻接口,翻不到再考虑渲染,渲染也不顺才上 RPA。
具体做法是,打开目标页面,Network 面板筛选 XHR 和 Fetch,把分页、筛选、导出这几个动作各点一遍,观察请求。如果能看到结构清晰的 JSON 返回,那就是最好的结果,直接复制请求为 cURL,粘到 Postman 里跑通,再翻译成代码。这一步五分钟就能做完,性价比极高。
有一次我碰到一个 PHD 的报表页,Network 里干干净净,只有静态资源请求,数据看上去是凭空出现的。折腾半天才发现,数据是通过一个 POST 请求拿的,但请求头里带了一个前端计算的签名,且这个签名依赖一个从接口 A 拿到的随机串。这时候顺序就变成了:先调接口 A 拿种子,再本地算签名,再调接口 B 拿数据。这种链路我用一张纸画下来,把依赖关系标清楚,写代码时基本不会乱。
还有一次更坑的,页面用了 iframe 嵌套,主页面 Network 里看不到子 iframe 的请求,需要在 Network 面板里把过滤条件改成 All,或者直接在 iframe 对应的 DevTools 上下文里看。这种细节没人会告诉你,只能自己撞一次。
心得:抓包时先把浏览器插件全关掉,广告拦截、密码管理器、各种脚本注入插件都会污染请求列表,让你多花半小时排除干扰。
3. 抓包定位数据源:从页面点到接口的完整实操
抓包是整个流程里技术含量最高、也最需要耐心的一环。目标就一个:把页面上的“点击动作”翻译成“一个可以独立重放的 HTTP 请求”。
3.1 用浏览器开发者工具快速锁定 XHR 接口
打开 PHD 页面,按 F12,切到 Network 面板,勾选 Preserve log(保留日志),这样页面跳转时请求记录不会丢。然后在类型筛选里只留 Fetch/XHR,把图片、CSS、JS 这些噪音全部过滤掉。
接着做动作:点一次“下一页”。看新冒出来的请求,点进去看 Response,如果返回的 JSON 里包含你页面上看到的那些行数据,恭喜,接口找到了。这时候切到 Headers 标签,重点看四样东西:请求 URL、请求方法(GET 还是 POST)、请求体里的参数、请求头里的鉴权字段。
请求头里最关键的通常是 Cookie、Authorization、X-Requested-With、Referer 这几个。有些系统的接口会校验 Referer,你在代码里不带这个头就会被拒绝,返回一个看起来毫无头绪的错误码。这类问题在浏览器里永远不会出现,因为浏览器自动带了。
另外一个技巧:右键那个请求,选择 Copy as cURL,把命令粘到终端跑一次,如果返回同样的数据,说明这个请求是可以脱离浏览器独立复现的。这一步验证通过,后面写代码就只是翻译工作。
3.2 在 Mac 上用 Charles 抓本机流量
有些场景浏览器抓不到:客户端程序、Electron 打包的桌面应用、或者需要看 HTTPS 明文但浏览器证书锁定绕不过去的情况。这时候 Charles 就派上用场了。以下步骤是我在 Mac 上实际跑通的流程。
第一步,安装 Charles,启动后进入 Proxy Settings,确认监听端口是 8888(默认)。同时勾选 macOS Proxy,让它接管系统层面的 HTTP/HTTPS 流量。这一步的作用是把你本机所有网络请求先引到 Charles 里看一眼,再转发出去。
第二步,安装根证书。菜单里选 Help,然后 Install Charles Root Certificate,系统会弹出钥匙串访问。找到那张 Charles 证书,双击,展开“信任”,把“使用此证书时”改成“始终信任”。这步不做,HTTPS 请求在 Charles 里只会显示成一堆乱码。
第三步,开启 SSL 监听。在 Proxy Settings 里切到 SSL Proxying Settings,勾选 Enable SSL Proxying,然后 Add 一条规则,Host 填你要抓的域名,Port 填 443。想省事就把 Host 和 Port 都留空,代表全抓,代价是列表会很乱,不建议。
第四步,回到目标程序操作一次,Charles 左侧会出现域名列表,逐层展开就能看到具体请求。找到目标接口后,右键 Copy cURL Request,和浏览器抓包一样,拿到就能复现。
注意:抓包结束后,记得把系统层面的流量接管关掉,并把证书从钥匙串里移除。长期挂着一个中间人证书,对日常使用不是好习惯。
3.3 参数与签名逆向的常见套路
拿到请求不代表能稳定复现,很多系统的参数是有讲究的。
分页参数:常见的有 page/pageSize、offset/limit、cursor 三种。前两种好办,cursor 型要小心,它通常是一个不透明字符串,必须用上一页返回的 cursor 去请求下一页,跳页是不行的。
时间戳和随机数:像 ts、nonce、_t 这类参数,大部分只是防止缓存,随便填个当前时间戳就能过。但如果有服务端校验,就必须按它的规则生成。
签名参数:名字通常叫 sign、signature、token。逆向方法很朴素:在前端 JS 里全局搜索这个参数名,找到生成它的函数,看它把哪些字段按什么顺序拼接、用了什么哈希算法、盐值是什么。多数是 MD5 或者 SHA256 拼接,加一个固定盐。找到之后用 Python 的 hashlib 复刻一遍,跑一次比对,一致就说明还原成功。
Cookie 刷新:有些系统的登录态是滑动过期的,长时间跑脚本会因为 Cookie 失效中断。稳妥做法是把 Cookie 单独存一份配置文件,脚本启动时读取,发现返回 401 或者跳登录页就告警,人工更新一次 Cookie 再继续。我一般不硬去逆向登录流程,收益低、风险高。
4. 用 Python 把采集脚本写扎实
接口摸清楚之后,写代码就是个体力活。但我见过太多脚本“能跑一次”和“能跑一年”之间的差距,全在细节里。
4.1 环境准备与依赖选择
python3 -m venv venv source venv/bin/activate pip install requests pandas sqlalchemy openpyxl tenacityrequests 负责发请求,pandas 负责清洗和落 Excel,sqlalchemy 管数据库写入,tenacity 做重试。如果你走浏览器渲染那条路,再加一个 playwright,装完记得跑playwright install chromium下载浏览器内核。
我不用 Scrapy 这类重框架,原因是内部系统采集量不大,几十万行数据用 requests 加循环完全够,框架反而增加调试成本。什么时候该上框架?大概是你需要管理几十个不同站点、需要分布式调度的时候,那时候框架的价值才体现出来。
4.2 会话保持、分页翻页与断点续抓
核心结构其实很简单:建一个 Session,把所有请求头塞进去,循环翻页,每页拿到数据就立刻落盘。关键是三件事要做对。
会话保持:用requests.Session(),它能自动携带 Cookie,省得你手动拼。请求头一定要带 User-Agent、Referer、Accept,这三个缺一个都可能被拒。
分页终止条件:不要靠“总页数”来判断,很多接口不返回总数。稳妥的判断是:返回列表为空,或者返回条数小于 pageSize,就停。同时设一个最大页数上限,防止接口异常导致死循环。
断点续抓:这是长任务的保命设计。我一般把当前页码写到一个状态文件里,脚本启动时先读,从上次的位置继续。这样网络断了、机器重启了,也不用从头再来。
import json, time, random, requests STATE_FILE = "phd_state.json" def load_state(): try: with open(STATE_FILE, "r", encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return {"page": 1} def save_state(state): with open(STATE_FILE, "w", encoding="utf-8") as f: json.dump(state, f) def fetch_page(session, page, page_size=100): payload = {"pageIndex": page, "pageSize": page_size} resp = session.post( "https://phd.internal.example/api/report/list", json=payload, timeout=20, ) resp.raise_for_status() return resp.json().get("data", {}).get("rows", []) def main(): session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0", "Referer": "https://phd.internal.example/report", "Accept": "application/json", }) state = load_state() page = state["page"] while page <= 2000: rows = fetch_page(session, page) if not rows: break write_to_db(rows) # 落盘函数见下一节 state["page"] = page + 1 save_state(state) page += 1 time.sleep(0.5 + random.random()) print("完成,最后页:", page) if __name__ == "__main__": main()这段代码里,time.sleep(0.5 + random.random())那一行别省。固定间隔的请求序列在服务端日志里像机器,加上随机抖动之后,流量曲线自然很多。
4.3 数据落库与字段清洗
落盘我分两层:原始层和清洗层。原始层把接口返回的 JSON 原样存一份,字段名保持原样,哪怕它是f_01这种短码。清洗层再按业务含义重命名、转类型、去重。
为什么留原始层?因为你对字段含义的理解一定会有反复。第一版映射错了很常见,如果只存了清洗后的结果,想纠正就得重新跑一遍采集。原始层存着,重新映射只是几秒钟的 SQL 或者 pandas 操作。
类型转换上最容易出问题的是日期和数字。接口里的金额经常是字符串,日期经常是2024/1/5这种不带前导零的格式,直接入库会变成文本。我一般统一处理成 ISO 格式的字符串或者标准时间戳,数字统一转 Decimal,避免浮点误差在对账时坑你。
去重就用业务主键,比如单据号加行号。把主键设成唯一索引,写入时用 upsert,重复跑也不会产生脏数据。这一点对增量采集特别重要,窗口重叠一点也不会导致数据翻倍。
4.4 定时调度与增量更新
采集脚本跑通之后,下一步是让它自己跑。Mac 上我优先用 crontab,简单可靠:
crontab -e # 每天凌晨 2 点跑一次 0 2 * * * cd /Users/me/phd_job && ./venv/bin/python collect.py >> run.log 2>&1增量逻辑的核心是维护一个水位线:上次拉到的最新更新时间。每次启动时读取水位线,只请求更新时间大于它的数据,跑完把最大更新时间写回去。窗口可以往前重叠几分钟,防止边界数据丢失。
日志一定要打,而且要有信息量。我的日志格式是:开始时间、水位线、本次拉取的页数、条数、耗时、异常。出问题时翻日志十秒钟就能定位。别只打一个 print,也别打一堆没用的调试信息。
5. 影刀 RPA 这条路,适合不想写代码的场景
不是每个人都有精力维护代码,也不是所有 PHD 页面都值得写脚本。RPA 的价值在于让业务人员自己搞定取数。
5.1 用影刀抓取页面数据的实操思路
影刀的流程本质上是把你的手部动作录下来,然后在浏览器里重放。做 PHD 取数,流程一般长这样:打开浏览器访问 PHD 地址,输入账号密码点登录,进入目标报表页,设置筛选条件,点查询,然后循环——读取当前页表格数据,写入数据表格,点下一页,判断是否还有下一页。
元素捕获是关键。影刀里捕获元素的时候,优先选择稳定属性,比如 id 或者固定的类名,别用那种带随机数字的 class。表格数据读取用“获取相似元素”功能,把整个表格的行批量读出来,比一行一行抓快得多。
举一个类似的场景:有朋友用影刀去抓 1688 商品页上的运费信息做比价。思路跟抓 PHD 是一样的——打开商品页,定位到运费那个文本节点,读取内容,写入 Excel,翻到下一个商品。难点在于商品页结构不一致,运费可能显示“包邮”“¥8.00”“需咨询客服”三种形态,所以读取之后必须加一层文本清洗和分支判断,不能直接写进单元格。
这个例子说明一件事:RPA 的优势不在于技术多先进,而在于业务人员能看懂、能改。页面改版了,业务同事重新录一次元素就行,不用等开发排期。
5.2 RPA 和代码脚本的边界在哪
我的划分原则很简单:数据量大、频率高、需要长期稳定运行,走代码;数据量小、一次性、页面结构经常变,走 RPA。
RPA 有三个绕不过去的短板。一是运行时要占用一个完整的桌面会话,机器得开着、屏幕不能锁,很多公司在服务器上跑 RPA 都是靠虚拟机常驻。二是速度慢,一个页面渲染加读取两三秒,几百页就是小半天。三是脆,目标系统调整一个按钮的样式,元素就找不到了。
还有一个容易被忽略的点:账号。RPA 用真人账号操作,取数行为在日志里跟真人操作很像,不容易被识别成异常。这既是优点也要自律——别用 RPA 去高频刷接口,本质上还是在给系统加压。
提示:不管用哪种方式,先把采集结果和页面上人工看到的数据抽样比对十几行。字段错位、日期时区、金额单位这几种错误,肉眼看不出来,但会让下游报表全错。
6. 常见问题与排查技巧实录
采集脚本出问题,大部分情况是那几类。下面这套排查顺序是我用了很多次、基本能在十分钟内定位的流程。
6.1 抓不到数据时的排查顺序
按顺序做,别跳步。
第一,把浏览器里 Copy as cURL 的命令原封不动在终端跑一次。能出数据,说明是代码问题;也出不来,说明是请求本身过期了,重新抓一次。
第二,检查请求头。最常见的三个坑:缺 Referer、缺 Cookie、User-Agent 被服务端识别成脚本。把浏览器里的完整请求头复制过来,再逐个删减,定位到是哪一个在起作用。
第三,检查参数。特别是分页参数名,pageIndex 和 pageNo 是两个不同的东西,写错了接口不报错,只是返回第一页,你会以为翻页失效。
第四,检查返回结构。有的接口成功时返回{"code":0,"data":{...}},失败时返回{"code":401},但 HTTP 状态码还是 200。所以不能只看 status_code,必须解析业务码。
第五,打开调试日志,把请求体和响应体前 500 个字符打出来看一眼。这一步能解决绝大多数“莫名其妙”的问题。
6.2 被限速、被拦截、遇到验证码怎么处理
被限速的信号通常是响应变慢、返回 429、或者返回一个空列表但页面明明有数据。第一反应应该是降速,不是换 IP。把并发降到 1,间隔加到 2 秒以上,观察一小时。
如果确实需要更高频率,优先走正规渠道:跟系统负责人申请只读接口或者数据导出权限。内部系统的数据,走申请比走技术对抗省事得多,也更安全。
遇到验证码或者登录态失效,我的原则是停下来告警,不硬刚。自动识别验证码这类做法在很多场景下是不被允许的,而且成本高、稳定性差。正确做法是脚本检测到登录页就发通知,人工介入一次,把新的 Cookie 更新进去。
还有一个原则必须说清楚:只抓自己有权限访问的数据,遵守目标系统的使用条款和访问规则,不要尝试绕过权限控制去拿不属于你的数据。这条线不能碰。
6.3 常见坑位速查表
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 返回 200 但列表为空 | 参数名写错或分页越界 | 打印请求体,和浏览器请求逐字段比对 |
| 前几页正常,后面全空 | 触发了限流或需要游标翻页 | 降速,改用 cursor 方式翻页 |
| 中文变成乱码 | 编码识别错误 | 显式指定 resp.encoding,或直接用 resp.json() |
| 金额字段末尾多两位 | 接口返回的是分而不是元 | 除以 100,落库统一单位 |
| 日期差一天 | 时区处理不一致 | 统一转 UTC 再转本地,或者全程按字符串处理 |
| 定时任务不执行 | crontab 环境变量缺失 | 在脚本里写死绝对路径,或先 source 环境 |
| RPA 元素找不到 | 页面改版或分辨率变化 | 重新捕获元素,固定窗口尺寸 |
| 数据库主键冲突 | 重复采集没做 upsert | 建唯一索引,用 ON CONFLICT 更新 |
7. 两年跑下来攒下的几条实在经验
采集这件事,写完第一版从来不是终点,让它安静地跑上一年才是。我第一个 PHD 采集脚本上线两周就因为对方接口加了一个新参数而中断,当时没有告警,等我发现时数据已经断了五天,只能补跑。从那之后我给自己立了几条规矩。
第一条,任何采集任务都必须有失败告警。最简单的做法是脚本结束时往一个企微或钉钉机器人发一条消息,包含本次成功条数;如果连续两次条数为 0,直接 @我。这一条规则帮我拦下了至少五六次无声的故障。
第二条,保留原始数据,而且保留至少三个月。字段映射改过好几次,每次都是靠原始层回补的。磁盘成本相比重跑一遍采集的时间成本,完全可以忽略。
第三条,把采集和入库解耦。采集脚本只负责把数据落到一个中间表或者文件,入库和清洗交给单独的步骤。这样接口出问题时,已经采下来的数据不会受影响,清洗逻辑改了也不用重新请求。
第四条,文档写给自己看。每个脚本头部写清楚:目标系统、接口地址、关键参数含义、水位线存在哪、告警发到哪、上次因为什么改过。三个月后你再打开这个文件,有这几行字能省半小时。
最后分享一个小技巧,关于水位线的存储位置。我一开始把水位线存在本地文件里,换机器就丢。后来改成存在目标库里单独一张任务状态表,采集脚本、清洗脚本、报表任务都从这张表读状态,整个链路的状态一目了然,出问题时查一张表就够了。这个改动看起来很小,但它把原来散落在各处的状态收敛到了一个点上,维护成本降了一大截。