尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python 爬虫实践:自动采集软件状态页事故记录并归档

Python 爬虫实践:自动采集软件状态页事故记录并归档 做了几年平台工程之后我养成了一个算不上好但非常有用的习惯翻状态页。不管是自己的系统还是外部依赖只要群里有人说“线上抖了”“有故障”“某个服务又挂了”我的第一反应不是去翻聊天记录而是把软件状态页从当前事件一路往前倒腾把时间线、影响范围、恢复时间全部捋清楚。直到有一次做年度可靠性复盘我面对的是几十个状态页链接、几百条事故记录靠人肉复制粘贴整理到怀疑人生。也就是从那天起我决定用 Python 爬虫把这些软件状态页的公开事故记录全部采集下来做一套自动化的事故归档。这篇文章想把整套思路和落地过程写下来。它不是一个“万能采集脚本”而是一套可以复用的采集框架从目标分析、字段设计、请求策略、解析归一到 SQLite/CSV 双通道归档再到增量更新、限流处理和定时运行。如果你也是做运维、开发、SRE或者恰好负责公司内部的基础设施稳定性这篇文章应该能帮你省掉大量重复劳动。你不需要很深的爬虫功底只要能写基础 Python 语法照着做就能跑起来。1. 给软件状态页做事故归档到底在解决什么问题1.1 手动整理事故记录的三个痛点状态页这东西平时没人看一出事就变成“抢手货”。但真正让运维头疼的往往不是事故本身而是事故之后的信息归档。我经历过三个阶段每个阶段都有非常具体的痛点。第一个痛点是“找得到但拼不齐”。状态页上记录的信息是结构化的但散落在不同页面、不同栏目里。今天这个事故从 10:20 开始第一次更新在 10:35恢复通告在 11:47影响组件写了两个这些信息分布在事件详情页、时间轴和状态组件列表里。如果只靠人眼很容易漏掉某个中间状态。第二个痛点是“事后复盘没有依据”。公司做季度可靠性报告时要统计“这个季度总共发生了多少次 P1/P2 事故”“平均恢复耗时多少”“哪些组件最容易出问题”。如果事故记录还躺在各自的系统里没有一份统一归档口径对不上数据就更没法看。第三个痛点是“变更记录容易被冲掉”。不少状态页只展示最近两三个月的公开事件再往前就要翻页或者干脆不展示。如果遇到监管审计、客户追问或者需要追溯半年前某次问题的具体时间线依赖第三方页面是不靠谱的。只有采集到本地归档成自己的数据资产才能随用随查。1.2 事故归档需要保存哪些字段很多人一想到“采集”第一反应是“把网页文字复制下来”。如果只是做信息收集那确实够了。但要做事故归档必须把数据结构化否则后面做统计、做对比、做趋势分析时还是要返工。以常见的软件状态页为例一条事故记录至少包含这些维度事故的唯一标识、标题、当前状态、影响级别、开始时间、解决时间、更新时间以及一组事件更新时间线。时间线是归档里最值钱的部分它记录了事故从“已确认”“正在调查”“部分故障”到“已解决”的每个节点配合时间戳就能还原出一份完整的事故演进过程。除了这些结构化字段我建议把原始正文也存档。为什么因为状态页的更新文本里经常藏着关键信息比如“Redis 集群发生主从切换”“某个云服务商的网络出现丢包”“我们已回滚至上一版本”。这类描述比状态标签本身更有分析价值。所以归档设计一定要同时保存“结构化字段”和“原始富文本”两者缺一不可。1.3 为什么选择 Python 做这件事选择 Python 做采集有两个理由生态成熟心智负担低。爬虫生态里最常用的 requests 负责 HTTP 请求BeautifulSoup 负责解析 HTMLpandas 负责清洗和转换SQLite/SQLAlchemy 负责存储这几样组合起来不到两百行代码就能解决需求。更重要的是维护成本可控。事故归档不是高频接口每天跑几次就够了完全不需要上重型框架。你用 Java、Go 也能写但 Python 的优势在于写起来快、改起来快而且社区里几乎所有爬虫相关的问题都有现成答案。团队里哪怕有刚入门的新人也能很快接手这个脚本。不过我也要泼一盆冷水这个方案适合“页面结构相对稳定、以公开信息为主”的软件状态页。如果你面对的是需要登录才能看、数据藏在复杂异步接口里的系统那么 Python 爬虫依然可行但复杂度会上升不少后面我讲 HTML 回退方案时会提到边界在哪里。2. 环境准备与目标状态页分析2.1 把 Python 环境搭起来不管你是 macOS、Windows 还是 Linux建议都用虚拟环境隔离依赖避免把系统 Python 搞乱。我本地的做法很简单先确认 Python 版本在 3.10 以上然后执行以下命令。python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install requests beautifulsoup4 lxml pandas sqlalchemy我加装了 pandas 和 sqlalchemy是因为后期做数据分析和落库时确实省事。如果你只想跑最简版本requests 和 beautifulsoup4 两个包就够了。安装时报错是最常见的事。一个是 pip 下载慢可以临时切换镜像源另一个是某些 Linux 环境缺少编译依赖安装 lxml 时可能失败。这类问题用一句话就能定位先看报错里的最后一行缺什么补什么通常跑一遍pip install lxml就能解决。2.2 先摸清楚状态页的数据出口一个良心状态页通常会提供公开的 JSON API。以大量软件团队在用的 Statuspage 形态为例它的 API 端点长这样GET https://status.example.com/api/v2/incidents.json用 curl 探一下能看到返回结果里是一个incidents数组每条记录包含 id、name、status、impact、created_at、updated_at、resolved_at以及 incident_updates 数组。incident_updates 数组里是每次更新的正文和时间戳。这几乎就是我们做归档需要的全部字段。拿到这个结构之后我建议你不要急着写代码先花十分钟做两件事。第一用浏览器打开目标状态页人工核对 API 返回的 incident 数量是否和页面上显示的事件数量一致第二看看历史事件是全部返回还是只返回最近 N 条。很多 API 接口存在默认条数限制如果只拿当前页归档就会缺历史数据。碰到这种情况再看 API 文档是否支持分页参数比如page2或per_page100。2.3 抓取合规与礼貌策略说句实在话状态页的公开事故数据抓取在合规性风险上通常是比较低的因为这类信息的定位就是向公众公开。但“低风险”不等于“瞎抓”我给自己定了几条底线。第一条只采集公开数据。需要登录、需要买授权、需要绕过访问控制的内容一律不碰。第二条先看 robots.txt。https://status.example.com/robots.txt如果明确禁止爬取某个路径我就避开那个路径。第三条控制请求频率。归档任务一天跑几次就足够完全没有必要每秒钟刷接口这既会给对方服务器造成压力也容易把自己的 IP 送进封禁名单。我还会在请求头里写一个说明身份的 User-Agent比如incident-archiver/1.0 (https://example.com/archiver)。这不仅是礼貌问题真出了问题对方也能通过 UA 联系到你反而不容易误伤。3. 采集与归档系统的核心实现3.1 请求与解析先走 JSON API再留一个 HTML 兜底实际开发时我建议你把“请求”和“解析”分成两层。请求层只负责拿数据解析层只负责转成统一结构。先看请求层用 requests 的一个持久 Session 来做。import requests session requests.Session() session.headers.update({ User-Agent: incident-archiver/1.0 (https://example.com/archiver) }) def fetch_json(url): resp session.get(url, timeout10) resp.raise_for_status() return resp.json()这里有个细节为什么用 Session 而不是全局 requests.get因为 Session 会自动复用底层 TCP 连接当我们有多次请求时效率和稳定性都会更好。timeout10 也很有必要避免某个状态页卡住时整个脚本跟着挂起。解析层就要看数据形态了。如果 API 可用我定义了一个parse_incident函数把 API 返回的 incident 字典转成归档需要的扁平结构。def parse_incident(item): updates item.get(incident_updates, []) return { id: item.get(id), name: item.get(name), status: item.get(status), impact: item.get(impact), created_at: item.get(created_at), resolved_at: item.get(resolved_at), updated_at: item.get(updated_at), updates: [ { update_id: u.get(id), status: u.get(status), body: u.get(body), created_at: u.get(created_at), } for u in updates ], raw_json: json.dumps(item, ensure_asciiFalse) }如果你遇到的状态页没有 JSON API页面是纯 HTML 的也不必慌。HTML 解析的核心思路是打开页面定位事故条目的容器再提取标题、时间、状态和正文。比如用 BeautifulSoup 做 CSS 选择器提取一份事件列表代码大概是下面这种骨架。from bs4 import BeautifulSoup soup BeautifulSoup(resp.text, lxml) for node in soup.select(.incident-item): title node.select_one(.incident-title) time node.select_one(.incident-time) status node.select_one(.incident-status) if title and time and status: # 继续提取和归档这里最需要灵活处理的是选择器。不同状态页的 class 命名规则完全不同有的是incident有的是timeline-row有的是status-event。我的习惯是先打开浏览器开发者工具找到对应节点的 class 和层级关系再把这个选择器写进代码。不要指望一套选择器通吃所有页面保持解析层和请求层分离遇到新站点时只需要替换解析函数即可。3.2 把时间线和影响级别规整成统一结构事故归档里最容易被忽略、又最重要的部分就是时间线。一条事故可能经历“已确认–正在调查–已修复–监控中–已解决”五个状态每个状态都有独立的更新时间。把这些节点串起来才能准确计算“某个故障持续了多久”。我在统一结构里用updates数组保存全部时间线节点并且在归档时单独抽一张表存。为什么单独存因为后续很可能要写 SQL 查询比如“找出所有在 30 分钟内从已确认到已解决的故障”或者“统计平均每次故障中间态的数量”。如果把这些节点塞在事故表的一个长文本字段里查询起来会非常痛苦。影响级别也要规整。Statuspage 体系里通常有 critical、major、minor、none 四级有的系统还会叫“严重/主要/轻微”。归档时我会直接统一成英文枚举方便聚合统计。如果你本地想看中文可以再留一个 display_name 字段做映射。一句话总结入库的数据要标准化展示的数据要可定制。3.3 数据落库SQLite 与 CSV 双通道存储方案我最常用的是“双通道”一份 CSV 给人类快速查看一份 SQLite 给程序做查询。CSV 用 pandas 写一行代码就能导出。SQLite 用原生 sqlite3 或者 SQLAlchemy 都行看你是不是已经在项目里用了 ORM。我用 SQLite 建了两张表CREATE TABLE incidents ( id TEXT PRIMARY KEY, name TEXT, status TEXT, impact TEXT, created_at TEXT, updated_at TEXT, resolved_at TEXT, raw_json TEXT ); CREATE TABLE incident_updates ( id TEXT PRIMARY KEY, incident_id TEXT, status TEXT, body TEXT, created_at TEXT );如果不想写原生 SQL用 SQLAlchemy 定义模型也很直接。这里给一个最小可跑的 ORM 版本适合那些本来就在用 SQLAlchemy 管理数据库的读者。from sqlalchemy import create_engine, Column, String, Text from sqlalchemy.orm import declarative_base, sessionmaker Base declarative_base() class Incident(Base): __tablename__ incidents id Column(String, primary_keyTrue) name Column(String) status Column(String) impact Column(String) created_at Column(String) updated_at Column(String) resolved_at Column(String) raw_json Column(Text) engine create_engine(sqlite:///incidents.db) Base.metadata.create_all(engine) Session sessionmaker(bindengine)关键点在于写入必须是幂等的同一个 incident id 重复写入时不要产生重复数据。最简单的方式就是 upsertSQLite 里可以写INSERT OR REPLACE INTO incidents ...。这样每次全量拉取之后数据都保持一致状态不会重复也不会缺失。4. 实战执行从抓取到归档的现场记录4.1 第一轮运行跑起来容易跑稳很难我把脚本的入口函数设计成一个大循环拉取 API逐条解析写入 SQLite同时把 DataFrame 导出成 CSV。第一次跑时我遇到了三个问题都是很典型但很容易忽视的。第一个是编码问题。解析 JSON 时如果正文里有中文或者特殊符号写入 CSV 时不用encodingutf-8打开文件就会变成乱码。我的做法是df.to_csv(incidents.csv, indexFalse, encodingutf-8-sig)这个 BOM 版本对 Excel 用户更友好。第二个是字段缺失。不是所有事故都有resolved_at部分未解决事件该字段是 null也不是每条 update 都有status有时正文为空。如果不加保护写入数据库时会报 Not Null 约束错误。代码里统一用.get()拿字段先给默认值再写库。第三个是接口抖动。公共状态页偶尔也会 502或者明明有数据偏偏超时。解决这个问题的办法是加指数退避重试第一次失败等 2 秒第二次等 4 秒最多重试 5 次连续失败就跳过这个任务并记录日志。这种做法听起来简单但对于采集任务来说能省下非常多的半夜排查时间。4.2 API 不可用时的 HTML 回退方案有一次我接的目标站点没有开放 JSON API只有渲染好的 HTML 状态页。当时我没有直接改写整个采集流程而是在请求层加了一个“出口探测”模块先请求 API 路径如果返回 404 或非 JSON 内容就自动切成 HTML 解析模式。HTML 回退方案的关键点在于“选择器可配置”。我把页面解析器抽象成一个小函数传入一组配置参数比如标题选择器、时间选择器、状态选择器。这样后续遇到新状态页时我不是改代码而是改配置文件。配置形如HTML_CONFIG { card_selector: .incident-card, title_selector: .incident-title, created_at_selector: .timestamp, status_selector: .badge, }这个方案的缺点很明显页面结构一改配置就要跟着改。但就我的经验来说状态页的改版频率远低于业务网站一般装上之后稳定运行几个月是没问题的。4.3 增量更新与幂等写入归档不可能是只跑一次的事。一个合格的归档任务应该做到跑第二次时只新增新事件或者补充已存在事件的更新节点而不是把所有数据推倒重来。我在这里设计了一个轻量级的增量策略。首先从 API 拉全量事故列表然后对每条事故检查数据库如果当前 id 不存在直接插入如果存在就对比updated_at。如果远程记录比本地记录版本新就执行更新并把它下面的 incident_updates 也同步替换。这样做的好处是逻辑简单。坏处是每次都拉全量列表对服务端来说请求量稍大。如果目标 API 支持按时间范围过滤比如只拉最近 24 小时的事件那我会进一步优化成增量拉取减少服务端压力。无论是哪种方式数据库入库操作都必须保证幂等这是整个归档系统稳定运行的基石。5. 常见问题与排查经验5.1 被限流、403、429 之后的应对话术采集状态页最常碰到的错误码是 429 Too Many Requests 和 403 Forbidden。429 是对方明确告诉你“请求太频繁了”403 可能是因为 UA 被拒绝或者路径需要鉴权。我的应对方案按顺序分三步。第一步降低请求频率把任务间隔从每 10 分钟一次改成每小时一次甚至每天两次。第二步给请求加随机延时比如每次请求前time.sleep(random.uniform(1, 3))避免固定节奏被识别。第三步如果还是被限流就检查自己是不是带了非常规的 Header有些服务会针对无 UA、ip 归属异常等特征拦截。有一点需要特别强调状态页归档本身不是实时监控系统完全没有必要高频轮询。很多状态页自己有 Webhook 或 RSS 订阅如果只是想要“第一时间知道故障”更合理的方案是接它的通知渠道而不是用爬虫怼着页面打。5.2 字段为空、日期格式乱的脏数据怎么处理状态页的体积有大有小偏偏字段格式还不统一。有的时间格式是 ISO 8601例如2024-11-20T10:20:00Z有的则返回2024-11-20 10:20。为了后续统计分析不出乱子我在入库之前统一做一轮清洗。时间字段的清洗用 pandas 很方便import pandas as pd df[created_at] pd.to_datetime(df[created_at], utcTrue, errorscoerce) df[resolved_at] pd.to_datetime(df[resolved_at], utcTrue, errorscoerce)errorscoerce的作用是把解析不了的时间变成 NaT而不是直接让程序崩溃。处理完之后再把 NaT 值转成空字符串或者 None 写库。5.3 定时任务与失败重试归档脚本写完之后还差最后一步让它定时运行。我是在服务器上用 cron 做的排一个每小时整点执行的计划任务。0 * * * * cd /data/incident-archiver /data/incident-archiver/venv/bin/python main.py logs/run.log 21Windows 环境下用“任务计划程序”也是一样的效果。关键是在脚本入口写一个总控逻辑异常捕获范围要尽可能大保证单次任务失败时留有足够日志。不要把整个脚本写成“没日志、没异常处理”的裸奔代码否则半夜跑挂了你根本不知道。5.4 还能扩展出什么这套归档系统跑通之后可以往下游扩展很多能力。最基本的扩展是告警通知如果新抓取到的事件影响级别是 critical立刻推送到群机器人或者邮件。更深一层的扩展是可靠性数据看板把 SQLite 里的事件数据导入数据分析工具按月份、组件、影响级别做多维统计让管事的人一眼看到隐患。再往后甚至可以做“事故模式识别”把历史事故正文做关键词聚合看哪类故障反复出现。我个人最深的一个体会是不要一开始就追求大而全的方案先把“能跑、能存、能查”这个最小闭环做出来然后再根据真实使用反馈去加功能。很多项目死在第一步都是因为过早引入复杂架构结果连最小可用版本都迟迟跑不起来。最后分享一个小技巧把每天的归档结果生成一个摘要文件类似“今日新增 2 条事故其中 1 条影响级别为 major平均恢复耗时 35 分钟”作为团队日报的一部分。这个功能实现起来成本极低但对团队感知稳定性变化的效果出奇地好。归档不再只是给自己看的数据而是真正变成推动改进的依据。
返回列表