老项目改造这事儿,我估计每一个干过几年“运维开发”或者“边缘系统支撑”的人都躲不开。系统上线七八年,业务代码都换了几拨人维护,原来定好的接口文档早不知道丢哪儿去了,你要是跑过去跟业务方说要加个 API,对方第一反应就是“这系统还能跑,你别给我整出故障来”。可业务又天天喊着要数据打通、要自动同步、要减少人工操作。这时候我一般就先干一件看上去有点“笨”的事——打开浏览器,F12,盯着那个老系统页面上的 HTML 发呆,然后告诉自己:这玩意儿就是 API,HTML 就是它的响应体。
这套思路我用了不少年,从早期的企业内部 OA,到这几年做自动化测试和爬虫采集,凡是遇到“没有 API 的老系统”,我基本都是靠“把页面 HTML 当 API”这条路趟过去。这篇就完整梳理一下我的做法,包括思路拆解、实操步骤、踩坑记录,希望能帮到正在跟老系统斗智斗勇的朋友。
1. 真正的困境:无 API 的老系统到底怎么自动化
1.1 现实场景:为什么老系统没有 API
先说一个我印象特别深的项目。某个内部管理系统,是 2014 年前后外包给第三方做的,前端还是老掉牙的 JSP + jQuery 那套,后端一堆 Servlet。系统跑在 Windows Server 上,数据库藏在机房,压根没有任何对外接口。业务方每个月底都要从里面导出报表,再手动整理到 Excel,一次少说俩小时。
当我说“能不能让这个系统自己把数据推送出来”的时候,外包公司早就联系不上了,原班人马散得差不多,文档里只留着数据库账号和几个管理后台入口。这种场景在传统企业里太普遍了,系统还能用,但它的技术债已经重到没人敢动。加 API 意味着要动源码、要部署新版本、要走变更流程,业务等不起,风险也扛不住。
这种时候,我通常会盘点一下手里的牌。老系统虽然没有 API,但它一定有界面,哪怕界面再丑、再老。只要它是个跑在浏览器里的系统,就一定有 HTTP 请求,一定有 HTML 输出。HTTP 请求就是接口,HTML 就是接口的返回值。这个思路听着有点野,但它就是我一直用的核心方法论。
1.2 三条技术路线怎么选
遇到无 API 老系统,市面上常见的自动化方案大致有三条路:
- 传统 RPA 工具,比如按键精灵、UiPath、影刀这类,靠屏幕识别和鼠标键盘模拟去操作界面。
- 浏览器自动化框架,比如 Selenium、Playwright、Puppeteer,控制真实浏览器去点按钮、填表单。
- HTTP 请求模拟,也就是直接抓包,把页面里的关键请求提取出来,用代码重新发一遍,再解析返回的 HTML。
这三条路我都试过,各有各的问题。RPA 工具上手快,但太脆了,屏幕分辨率换一下、页面滚轮位置不对、弹窗延迟几百毫秒,脚本就傻了,维护成本非常高。浏览器自动化比 RPA 稳一些,因为它是基于 DOM 元素定位的,不像 RPA 那样靠图像,但跑起来特别吃资源,几十个流程同时跑的时候,服务器根本扛不住。而且它本质上还是在“操作浏览器”,速度上限摆在那里。
所以大部分场景下,我的首选是第三条路——直接抓包模拟 HTTP 请求。页面里的 HTML 我当成接口响应来解析,页面里的表单提交和按钮点击,我找到背后那个 POST 请求来模拟。这样速度最快,资源消耗最小,而且可以直接在服务器上跑,不依赖桌面环境。只有遇到那种前端逻辑特别重的系统,比如大量 JS 动态渲染、加了复杂的加密参数,我才会退回到浏览器自动化这条路线。
2. 核心思路:把页面 HTML 当成 API 来“消费”
2.1 为什么这个思路能走通
在很多人眼里,API 是个很“正式”的东西,得有文档、有鉴权、有固定的 JSON 格式。但在 HTTP 协议的世界里,浏览器访问页面和程序调用 API,本质上没有任何区别。你给服务器发一个 GET 请求,服务器返回一段 HTML,这就是一次完整的接口调用。HTML 是一个标签嵌套的结构化文本,跟 JSON 一样,都是“有结构的文本”,只是结构规范不同而已。
我打个比方你就明白了。API 是一个清楚地挂了招牌的餐厅,你照着菜单点菜就行;HTML 页面是一个食材直接摆在案板上的后厨,虽然没人给你菜单,但你只要看清每样食材放在哪儿,一样能做出你要的菜。关键不在于形式上是否叫“API”,而在于你能不能稳定地拿到这段 HTML,并且稳定地从里面提取出你要的内容。
这么一想,老系统的每一个页面都是现成的接口:
- 列表页就是一个查询接口,响应体是表格 HTML。
- 表单页提交的 POST 请求就是一个写操作接口,参数藏在 Form Data 里。
- 详情页就是一个单条数据查询接口,你需要的信息全在 HTML 的标签里。
我最初做这个事的时候也担心自己是不是在搞歪门邪道,但后来想明白了:浏览器里所有你能看到的东西,都是客户端通过 HTTP 请求从服务器拿到的,你只是把这个过程从“人看”变成了“代码读”,数据链路完全一致,服务器的行为也不会有什么不同。
2.2 技术选型与工具组合
确定了“把 HTML 当 API”之后,工具选型就变得非常明确了。我的常用组合是这样一套:
Python 作为主力语言,因为它的生态太全了,解析 HTML、处理 Cookie、定时任务、重启守护,全都能在一个语言里搞定。如果你团队里已经熟悉 Node.js,用 Node 也行,但我个人还是觉得 Python 在数据清洗、文件导出这些环节上更顺手。
HTTP 客户端方面,我一般直接用 requests 库,简单直接,够用了。遇到需要保持会话的,用 Session 对象,可以自动管理 Cookie。凡是涉及文件上传的,requests 处理 multipart/form-data 也方便。我不太建议在模拟请求这一步用 Playwright 或 Selenium,它们解决的是浏览器端的问题,不是 HTTP 层面的问题,用它们去“模拟接口”纯属杀鸡用牛刀。
解析 HTML 有两个选择:BeautifulSoup 和 lxml。BeautifulSoup 简单易用,写 CSS 选择器很直观;lxml 在规模大的时候性能更好。我自己的习惯是先用 BeautifulSoup 快速验证解析逻辑,如果数据量大到十万行级别了,再把性能瓶颈的地方换成 lxml 或者直接用 xpath。还有一个我特别常用的库是 pandas,解析出来的结构化数据直接落 DataFrame,最后导出 Excel 或者 CSV 非常方便。
至于登录认证、验证码这些问题,我在第 3 部分会展开讲,这里先卖个关子。总之选型的核心原则是:能用纯 HTTP 解决的,就不上浏览器;能用小工具解决的,就不上重框架。
2.3 整体架构设计
从宏观上看,我把这套自动化系统设计成四个模块:抓取模块、解析模块、控制模块、输出模块。
抓取模块负责一切 HTTP 通信,包括登录、携带 Cookie、发送查询请求、下载附件,它拿到的原始材料就是一段 HTML 或者一个文件。解析模块负责从 HTML 里抽取数据,把页面上的表格、列表、详情信息变成干净的结构化数据,比如列表、字典、DataFrame。控制模块负责编排整个流程,什么时候上班开始跑、什么时候重试、失败了怎么处理、要不要发告警通知。输出模块负责把结果送出去,可以是写入数据库、导出 Excel、推送到企业微信机器人,也可以是调用其他系统的接口。
这种模块化设计的好处非常明显。老系统的页面经常变,但最常变的只有 HTML 结构,不会变的是 HTTP 请求路径和参数。我把解析模块单独拎出来,页面变更的时候只需要改解析逻辑,抓取、控制、输出这些模块都不用动。这对后续维护来说太重要了,因为你永远不知道外包团队会在哪一天悄悄改一个 class 名字。
3. 实操过程:从登录到数据提取的完整实现
3.1 环境准备与工具安装
正式动手之前,先准备环境。Python 我建议直接上 3.10 以上的版本,反正新项目没有历史包袱,能用新的就用新的。你需要安装的库并不多,核心就是下面几行:
pip install requests beautifulsoup4 lxml pandas这几个库的职责分别是:
- requests:发起 HTTP 请求,维持会话。
- beautifulsoup4 + lxml:解析 HTML,提取数据。
- pandas:处理数据,导出 Excel。
如果你系统里有验证码识别需求,可能还会用到 ddddocr 或者对接第三方打码平台,但那不是必修课,我后面单独说。安装好之后,先把目标系统的登录页打开,F12 切到 Network 面板,准备开始分析。
3.2 登录与会话保持
老系统最常见的登录方式有三种:表单用户名密码登录、Cookie 包含会话标识、有时候还会夹带一个验证码。我先说最简单的表单登录怎么搞。
你需要在浏览器里手动登录一次,然后在 Network 面板里找到那个提交登录信息的 POST 请求。点开它,看 Payload,通常会看到 username、password 之类的字段。有的老系统会做 MD5 加密,还有的会直接明文传输,这就看当年外包团队的良心了。拿到请求格式之后,用 requests 的 Session 模拟,代码大概长这样:
import requests session = requests.Session() login_url = "http://old-system.example.com/login" login_data = { "username": "your_account", "password": "your_password", # 有些系统还有 csrf_token、captcha 等字段 } resp = session.post(login_url, data=login_data) print(resp.status_code) print(resp.url) # 如果登录成功,一般会 302 跳转到首页用 Session 对象最大的好处是,登录成功后服务器返回的 Session Cookie 会被自动保存,之后你用这个 Session 去请求任何页面,都带着登录状态,不用自己手工维护 Cookie。
如果你在登录请求里发现了验证码字段,那就得多做一步。最简单的方案是让程序读取验证码图片,然后接入 ddddocr 自动识别。老系统的验证码普遍很简单,基本都是 4 位数字字母,ddddocr 识别率非常高。再不行就用第三方打码平台,几秒钟就返回结果。实在不行的,就把验证码图片弹出来让值班同事人工输入一次,然后手动把 Cookie 导进去,不过这种方案只适合低频操作,不推荐用于长期运行的自动化流程。
3.3 页面分析:怎么找到那条真正干活的请求
登录解决之后,下一步就是分析业务页面。很多人在这一步就懵了,因为一个页面打开后,Network 面板里动辄几十个请求,有的下载 JS 文件,有的加载图片,有的拉取数据,到底哪个才是“真正的接口”?
我的经验是抓大放小。先用“Fetch/XHR”这个筛选器,把所有 CSS、JS、图片请求过滤掉,只留下动态请求。然后看响应内容,凡是在 Preview 或 Response 标签里能看到你要的数据的请求,就是目标请求。比如你要查询报表,页面上展示了一个表格,那么在 Fetch/XHR 里找到那个返回 HTML 表格代码的请求,十有八九就是数据接口。
这里有个特别关键的点:老系统通常不走 AJAX,而是表单 POST 提交之后直接刷新整个页面。这种情况下,你不在 Fetch/XHR 里找,而是直接在请求列表里找那个method="POST"、路径包含 query、search、report 之类的请求。它的响应就是整个页面的 HTML,你需要的表格数据就嵌在里面。
找到了接口 URL 和参数之后,先用 requests 模拟一遍。注意有些参数是动态的,比如时间戳、页码、随机数。我在实操中一般会把这些参数值先硬编码,确认整个链路能通,然后再写代码去动态生成它们。这样能大大降低调试难度。
3.4 数据解析与翻页处理
请求通了,拿到了 HTML,下一步就是从中把数据抠出来。用 BeautifulSoup 解析的思路很直接:
from bs4 import BeautifulSoup html = resp.text soup = BeautifulSoup(html, "lxml") table = soup.find("table", id="dataList") rows = table.find_all("tr") data = [] for row in rows[1:]: # 跳过表头 cells = row.find_all("td") data.append([cell.get_text(strip=True) for cell in cells])这个环节最考验细心。你要先去浏览器里打开目标页面,仔细看那个表格的 HTML 结构,确认表格的 id、class、标签层级。然后看每一行里到底有几个单元格,哪些列是你要的数据,哪些列是操作按钮(比如“编辑”“删除”这种,直接跳过)。还有一点容易被忽略:单元格里可能还嵌套着 span、a、input,用get_text()会把它们都里面的文字拼在一起,这时候你可能需要用.find("span", class_="realValue")来精确取值。
翻页逻辑也一样,去浏览器里点一下“下一页”,看 URL 参数变化,找到page=2这种规律,写个循环就行了。老系统的分页参数千奇百怪,有的是 pageNo、pageIndex,有的是 startRow、limit,还有的直接用 POST 传当前页。反正你只要找到了那一个请求,参数怎么变都是能看出来的事。
3.5 异常处理与重试机制
整个流程跑通之后,千万别急着庆祝,真正让自动化“自动化”的,是异常处理和重试机制。公司网络抖动、服务器偶发 500、登录态超时,这些事都会让你的脚本在中途挂掉。我一般会在代码里加三层保护。
第一层是请求重试。对同一个请求,如果返回状态码不是 200,或者返回的 HTML 里缺少预期的关键元素(比如找不到那个表格 id),就自动重新请求,最多重试 3 次。第二层是登录态检查。每次发起核心请求前,先判断响应里有没有登录页的特征词,比如返回了“登录”或者跳转到 login 页面,说明会话过期了,就重新执行一次登录逻辑。第三层是全局异常兜底,用 try-except 把整个流程包起来,记录日志并发送告警。
比如我用一个简单的重试装饰器:
import time import logging def retry(times=3, delay=2): def decorator(func): def wrapper(*args, **kwargs): for i in range(times): try: return func(*args, **kwargs) except Exception as e: logging.warning(f"Attempt {i+1} failed: {e}") time.sleep(delay) logging.error("Max retries reached, giving up.") raise return wrapper return decorator这套机制看上去很基础,但真到了生产环境,它能帮你省下无数爬起来处理脚本故障的深夜。我后面会在第 4 部分继续聊这些坑的具体表现,这里先留个印象。
4. 翻车实录:常见问题与排查技巧
4.1 页面改版导致的选择器失效
这是所有 HTML 自动化方案里最大、也最不可避免的坑。老系统的前端代码如果有人偶尔维护,改个按钮的 class、加个 div 包裹层,都是一瞬间的事,但你的解析代码立刻就废了。
我自己的应对策略是“减少对 class 和 id 的依赖”,尽量用稳定结构定位。比如表格结构里,我更倾向于用table tr td这种标签层级,而不是依赖某个具体的 class 名。遇到必须用某类名的地方,我会把它抽成一个配置项,放在一个单独的常量文件里,页面改了只改一个地方。
更重要的预防手段是“数据完整性校验”。在解析完数据之后,加一个简单的断言,比如判断拿到的列表长度是否大于 0、关键字段是否非空。如果校验失败,就认为页面结构变了,停止后续操作并报警,而不是继续把一堆空数据写入数据库。这能避免很多“数据悄悄丢了”的恶性事故。
4.2 验证码、滑块和风控拦截
前面说了验证码,这里重点说说另外两种更烦人的情况。第一种是滑块验证,老系统里较少见,但一旦出现就特别棘手,因为它的轨迹模拟比较难写。很多滑块只需要模拟“先慢后快”的拖拽就行,用 Playwright 能实现,但维护起来是真的麻烦。第二种是 IP 层面的风控拦截,比如你频繁请求时,系统可能直接封 IP。
我的做法是控制请求频率。所有的核心抓取逻辑里都加上time.sleep(),随机间隔 1 到 3 秒,让请求节奏尽量接近真人。如果目标系统只对内网开放,流量又不大,一般来说不会触发风控。如果担心封 IP,就在请求头里伪装成正常浏览器的 User-Agent,把 Accept-Language、Accept-Encoding 这些头尽量补全。
4.3 Session 过期与会话并发问题
老系统的登录态有效期普遍很短,短的可能 20 分钟就过期。如果你的自动化流程要跑很久,比如处理几百页的数据,那中间必然遇到“会话过期”的问题。判断方式我在 3.5 里说过,就是检查响应里是否包含登录页特征。解决方式就是在检测到过期时,自动重新走一遍登录流程,然后再继续之前的任务。
另一个容易踩的坑是会话并发。老系统经常不支持同一个账号同时多会话登录,你这边脚本挂着,那边同事用同一个账号也在登录,后登录的人可能把前一个登录态踢掉,导致脚本突然失效。我的建议是给自动化程序专门申请一个独立的账号,并跟业务方说明这个账号是“机器人专用”,不要手工使用。这不仅是技术问题,更是运维规范问题。
4.4 数据量大时的性能问题
用最原始的方式解析 HTML,性能其实不差。BeautifulSoup 解析一个几千行表格的页面,耗时基本在几百毫秒的级别。真正拖慢整体速度的往往是网络 I/O,页面下载秒级、解析毫秒级,瓶颈不在代码。
但如果数据量特别大,比如一个报表有 10 万条数据,而系统分页只允许每页 20 条,你要翻 5000 页,那就需要认真考虑性能了。我的做法是采用“多线程 + 队列”模型:一个线程负责翻页拿 HTML,解析好的数据丢进队列,另外一个线程从队列取数据批量写入数据库。这样可以显著提升吞吐量。要注意的是,并发数不要开太高,5 到 8 个线程就够用了,开太高容易触发风控,系统也扛不住。
5. 一些经验体会和后续扩展方向
5.1 实操血泪心得
这一路折腾下来,我最大的体会是:把页面 HTML 当 API 用,技术含量不在“请求+解析”这套动作本身,而在你能不能把它做得足够稳、足够隐蔽、足够可维护。稳是指遇到网络抖动、页面小改时依然能跑,不悄悄产出错误数据;隐蔽是指频率控制得当,不给目标系统增加无谓的压力;可维护是指页面真变了,你能在 10 分钟内定位到是哪一行解析器挂了,而不是翻半天日志。
另外建议大家在正式写代码之前,把目标系统的操作流程完整地在浏览器里过三遍。第一遍是熟悉业务逻辑,第二遍是盯着 Network 面板找请求,第三遍是验证每个请求的参数变化规律。这三遍看起来浪费时间,但比边写边调试高效得多。我在早期就是因为没做这一步,写完了登录和抓取逻辑才发现漏了一个关键的动态参数,结果把整个流程推翻重来。
5.2 “伪 API”化封装:把 HTML 接口转成标准服务
如果你只在脚本里自己用这套逻辑,那它始终是个“脚本”。但要是业务部门要求开放接口给其他系统调用,怎么办?我的做法是在上面这套抓取解析逻辑外面,再包一层 REST API,把自己变成一个“伪 API 服务”。
具体来说,就是把抓取、解析、格式化这些步骤封装成一个 Python Web 服务,用 Flask 或者 FastAPI 提供标准接口,返回 JSON 数据。这样一来,外部系统看着像在调用一个现代 API,实际上你的服务在背后默默访问着那个老掉牙的 HTML 页面。这种模式的好处太多了:隔离变化、复用逻辑、还能做鉴权和限流。这也是“把 HTML 当 API”这个思路的进阶版,非常推荐有长期自动化需求的朋友尝试。
根据我个人经验,这种方案再往后走,你还能顺手沉淀一批公共解析组件出来。比如针对常见表格的解析器、针对分页逻辑的通用抓取器、动态 Cookie 刷新器,这些组件在不同项目之间是可以复用的。存得多了以后,再做新老系统的自动化时,基本就是拿现成的零件拼装一下,很快就能交差。