ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

信用中国行政处罚数据爬虫实战:从抓包到落库的完整流程

信用中国行政处罚数据爬虫实战:从抓包到落库的完整流程 接到同事需求的时候他手里拿着一张 Excel里面躺着三百多家供应商的名字每家都要核查有没有行政处罚记录。手动打开信用中国一家家搜再复制粘贴以每家三分钟算一个下午就没了。而且这种活儿不是一次性的下个季度还要复查一遍。我就想干脆写个爬虫把“信用中国”里的行政处罚公示数据批量拉下来结构化落库以后每次只要把名单丢进去跑一遍就行。信用中国creditchina.gov.cn是官方信用信息公示平台里面的行政处罚公示数据属于公开信息字段相对规范适合做批量采集和二次加工。但和普通电商商品页不一样这类政府公示网站经历过多次改版网上能找到的旧教程很多已经失效主要原因在于前端接口加了动态签名参数、部分页面走异步渲染直接用 requests 请求静态 HTML 的方式很容易碰壁。这篇文章我会从需求场景说起完整走一遍抓包定位、参数逆向、列表爬取、详情补全、增量更新和合规使用的过程把我实际踩过的坑也一并列出来给准备做同类数据采集的读者一个可以直接参考的模板。1. 处罚数据的需求场景以及为什么偏偏要爬这个站很多人一听到“爬行政处罚”就默认是搞黑产的实际不是。我接手这个需求后梳理了一下真正的应用场景集中在企业风险控制、招投标辅助判断和深度舆情分析这几个方向。这类数据的特点是权威、公开、更新有规律而且带有明确的行政相对人、处罚机关、决定日期、处罚结果等结构化字段非常适合做成自动化监控。1.1 三个最常见的真实业务场景供应商准入复核是目前最普遍的需求。一家大型企业选供应商候选名单几十上百家如果靠人工去信用中国逐条搜索效率太低。更现实的是很多企业的采购流程要求在短时间内完成多家供应商合规性筛查这个时候自动化采集的价值就体现出来了把供应商名单整理成 Excel脚本逐家查询命中处罚就把记录和链接输出到结果表没有命中的标记为“干净”。存量客户风险预警是另一个典型场景。融资租赁、商业保理、银行信贷这类机构客户不是查一次就结束而是需要持续跟踪。行政处罚记录往往是企业信用恶化的早期信号比如一家企业突然因为环保问题被罚后续可能影响经营和还款能力。我们的做法是每天跑一次增量采集新增的处罚记录通过机器人推送到企业微信群由风控人员判断要不要调整授信策略。招投标辅助判断对工程类、咨询类企业很有用。很多招标文件要求投标人“近三年内无重大违法记录”这里的“重大违法记录”通常指较大数额罚款、责令停产停业、吊销许可证等。把投标方名单批量跑一遍按处罚类型和金额筛选出疑似不满足条件的单位能在评标前就发现问题避免中标后被人质疑。1.2 “公开数据”不等于“好爬的数据”理想状态下政府公示数据应该有一个标准的开放接口像数据库一样按字段查询。但实际操作中你会发现这些网站的前端展示逻辑往往比商业网站更“复古而复杂”。信用中国经过多次改版页面结构、接口地址、签名参数都换过好几轮网上早年那种“直接 GET 某个 URL 拿 JSON”的教程基本失效了。我总结这类网站的共同难点有三个。第一动态签名 token。为了防止爬虫批量调用前端 JS 会动态生成一个 token 参数每次请求都不一样直接构造请求的话缺了这个参数就会被拦截或者返回空数据。第二异步渲染。列表页初始 HTML 里可能根本没有数据数据是页面加载后通过 XHR 请求拿到的必须找到真正的数据接口而不是去解析页面源码。第三接口字段不稳定。同一个字段在不同时期、不同接口里可能叫法不同给后续的数据清洗带来不少工作量。这几个难点合在一起注定了爬取信用中国的行政处罚数据不是“复制粘贴几行 requests 代码”就能搞定的需要一套完整的分析流程。这也是我写这篇文章的原因——把你需要掌握的方法论和避坑经验都讲清楚。2. 别急着写代码先把网页到接口的数据链路摸清楚很多新手拿到一个网站就急着写解析规则结果解析半天发现页面里没有数据。正确的做法是先打开浏览器开发者工具把请求链路看清楚搞清楚数据到底是从哪个接口、以什么格式返回的。这一步花二十分钟后面省两天。2.1 四步锁定真正的数据接口第一步打开信用中国的行政处罚公示页面按 F12 进入开发者工具切到 Network网络面板勾选 XHR/Fetch 过滤只显示异步请求。保持开发者工具开启状态在搜索框输入一个测试关键词比如“某某建设有限公司”然后点击查询。第二步观察 Network 面板中新产生的请求。正常情况下列表页面会发出一个或几个 XHR 请求其中那个返回内容里包含“处罚机关”“决定文书号”等字段的请求就是我们要找的数据接口。判断方法很简单点击请求后在 Preview 或 Response 页签里查看响应内容如果能看到列表数据就锁定它。第三步查看这个请求的细节。General 部分能看到请求 URL 和请求方式一般是 POSTRequest Headers 部分能看到请求头Request Payload 或 Form Data 部分能看到请求参数。这里要重点记录请求头里有没有自定义参数、请求体里有哪些字段、哪些字段的值看起来像动态生成的。第四步也是最关键的一步验证这个接口能否直接访问。用 Postman 或者写几行 Python 代码把抓到的 URL、Headers、Payload 原样复现一遍看能不能返回同样的数据。如果返回正常恭喜你已经拿到了核心数据通道如果返回异常或者提示参数错误说明请求里有动态签名参数需要进入下一节的处理流程。我在做的过程中发现信用中国这类公示站点的列表接口通常返回 JSON 格式数据里面会包含 total总记录数、records当前页记录等字段。如果返回的是 HTML 片段也不要慌用 parsel 或 BeautifulSoup 解析这个 HTML 片段即可原理是一样的。2.2 动态签名参数是怎么生成的这是整个爬虫里最耗时间的环节。所谓的动态签名参数本质上就是前端 JS 用固定算法把请求参数、时间戳、随机数拼在一起算出一个 token请求时带上这个 token 表示“我是正常的浏览器”。要还原这个逻辑核心手段是搜索前端源代码。在开发者工具的 Sources源代码面板里打开页面相关的 JS 文件用 CtrlShiftF 全局搜索你看到的 token 参数名。比如请求体里有个字段叫 h5token就在 JS 里搜 h5token看它的值是怎么赋值的。现在的前端代码虽然压缩过但变量名和字符串常量一般不会被完全替换搜索字段名往往能直接定位到生成逻辑。我见过的签名参数生成方式中最常见的是“拼接字符串后做 MD5”。比如把时间戳、关键词、页码、固定盐值拼成一个字符串然后md5()加密生成 token。这里需要注意三点拼接顺序、分隔符类型、盐值藏在哪个变量里。只要这三点还原对了Python 里用hashlib.md5()就能复现。如果前端代码做了重度混淆直接阅读 JS 成本太高一个更快的替代方案是使用 DrissionPage 或 Playwright 这类自动化浏览器工具让浏览器自己去执行 JS 生成参数Python 程序只需要负责操作页面、翻页、提取数据。代价是效率比纯接口模式慢一些但稳定性很高而且不依赖对签名算法的理解。我的建议是先用浏览器自动化把流程跑通确认数据结构和业务口径没问题再决定要不要花时间去逆向签名参数。如果采集量不大、频率不高浏览器自动化模式完全够用。2.3 动手前先确认三件合规的事爬虫不是能不能写的问题而是怎么用的问题。行政处罚公示数据是官方主动公开的信息采集这类数据本身没有原罪但有三条线我在实际项目中始终坚持。第一采集范围只限定于公开可访问的数据。信用中国的行政处罚公示页不需要登录直接打开就能看这意味着它是面向全社会的公开信息。如果网站把某些数据放在登录后才能访问的区域那就要谨慎了绕过一个登录机制去抓取非公开数据是明确的风险行为不做。第二严格遵守访问频率。不管网站有没有明确限制我都会把请求间隔控制在 1 到 3 秒之间单 IP 的并发数不超过 2。这个节奏虽然慢但对目标网站的服务端压力小也能有效降低触发验证码和 IP 封禁的概率。数据采集本质上是个长期工程追求一时的速度往往得不偿失。第三数据使用不得违背个保和隐私原则。企业工商类和行政处罚类信息属于公开公示信息但数据落到自己手里后不能随意对外传播更不能进行恶意拼接、人格攻击式披露。企业内部用于风控决策是可以的但要在数据交付时注明数据来源和采集时间避免被认定为“来源不明数据”。3. 核心实现把列表接口和详情页的数据稳定落库明确了接口和合规边界后就可以写代码了。我的实现结构分四层会话层负责维持请求头、token、数据获取层列表和详情、解析清洗层、存储层。每层职责单一后面任何一层改版只需要改对应模块。3.1 依赖选型和请求会话维护Python 环境下我用httpx替代requests原因是它支持 HTTP/2、连接池管理和请求重试的配置更灵活。解析用parsel合并了 CSS、XPath 和正则提取比单纯用 BeautifulSoup 效率更高。存储选 SQLite单机跑、字段不复杂SQLite 完全够用后面要上生产环境再切 MySQL 也不难。请求会话的关键是保持统一的 headers、维持 cookies、自动处理 token。代码骨架如下import time import random import httpx from parsel import Selector HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Referer: https://www.creditchina.gov.cn/xypd/xzcf/index.html, Accept: application/json, text/plain, */*, } class CreditChinaClient: def __init__(self): self.client httpx.Client(headersHEADERS, timeout15, follow_redirectsTrue) def get_token(self, keyword: str, page: int, page_size: int) - str: # 这里根据实际逆向结果实现核心是复现前端JS的签名算法 import hashlib ts str(int(time.time())) raw f{keyword}|{page}|{page_size}|{ts}|your_salt_here return hashlib.md5(raw.encode()).hexdigest()注意代码里盐值部分我用了占位符“your_salt_here”实际项目里一定要替换成自己抓包逆向出来的真实盐值。不同时期的信用中国前端版本签名算法和字段名都可能不同抄代码没有意义掌握逆向思路才是关键。3.2 列表接口的请求构造与分页列表接口的核心参数一般包括关键词、页码、每页条数、查询状态、搜索范围等。我从接口里观察到的典型结构如下字段名以你实际抓包为准def fetch_list(self, keyword: str, page: int 1, page_size: int 10): token self.get_token(keyword, page, page_size) payload { keyword: keyword, page: page, pageSize: page_size, searchState: 2, entityType: 2, template: xzcf, token: token, } headers {h5token: token} resp self.client.post(LIST_API_URL, jsonpayload, headersheaders) resp.raise_for_status() data resp.json() # 假设结构为 data.records 和 data.total return data.get(data, {}).get(records, []), data.get(data, {}).get(total, 0)这段代码只是示意。实际字段名、token 位置、是放在 header 还是 body都要以你自己的抓包结果为准。但结构框架是通用的动态部分集中在一个方法里生成后续站点改版时改那个方法就行不用动整个爬虫。分页策略上有一个非常容易踩的坑不要一味地为了减少请求数把 pageSize 调得很大。我实测过pageSize 超过 50 以后响应时间明显变长偶尔还会被服务端判定为异常请求。我最终的配置是 pageSize 固定 10配合 1.5 到 2.5 秒随机间隔翻页。这样单页数据量小解析快也不容易触发风控。3.3 详情数据补全与字段清洗列表接口返回的数据通常是摘要性质包含企业名称、处罚机关、决定文书号、处罚日期、处罚结果这几项。但完整的处罚事由、罚款金额、没收违法所得等字段往往在详情页里。详情页有两种拿法一种是根据列表里返回的详情 ID 调详情接口另一种是直接请求详情页链接从 HTML 里提取数据。我用的是第二种因为详情页的 HTML 结构相对稳定而且直接拿链接方便后续溯源。解析过程如下def parse_detail(html: str) - dict: sel Selector(texthtml) fields { case_no: sel.xpath(//td[contains(text(),决定文书号)]/following-sibling::td[1]/text()).get(), punish_reason: sel.xpath(//td[contains(text(),处罚事由)]/following-sibling::td[1]/text()).get(), punish_basis: sel.xpath(//td[contains(text(),处罚依据)]/following-sibling::td[1]/text()).get(), punish_result: sel.xpath(//td[contains(text(),处罚结果)]/following-sibling::td[1]/text()).get(), punish_date: sel.xpath(//td[contains(text(),处罚决定日期)]/following-sibling::td[1]/text()).get(), punish_authority: sel.xpath(//td[contains(text(),处罚机关)]/following-sibling::td[1]/text()).get(), } for k, v in fields.items(): if v: fields[k] v.strip() return fields注意这种后代选择器在解析表格型详情页时很实用但前提是页面里文本节点的格式是“字段名值”或“字段名”紧跟一个单元格。如果字段名和值之间还有其他节点就需要根据实际情况调整 XPath。另外处罚日期经常出现“2023年05月18日”这种中文格式入库前要统一转成 ISO 格式2023-05-18方便后续排序和查询。3.4 任务调度重试、限速与去重落库爬虫长时间跑网络抖动、接口偶发 500、解析时某个字段缺失都是常态。没有重试机制和去重逻辑任务跑一半就断了后面往往要人工重建非常浪费时间。我在这个项目里做了三件事一是请求重试。统计失败次数连续失败超过 5 次就停止任务并发送告警而不是无限重试。重试使用指数退避策略第一次等 2 秒第二次 4 秒第三次 8 秒最多重试 3 次。二是全局限速。整个爬虫使用单线程加随机延时每完成一次请求后time.sleep(random.uniform(1.2, 3.0))。不要同时开多线程跑信用中国这类站点对并发非常敏感多线程带来的收益远低于封 IP 带来的损失。三是以业务主键去重。处罚记录的唯一性由“企业名称”和“决定文书号”共同决定。我把这两列建成联合唯一索引插入时使用INSERT OR IGNORE新数据自动跳过已存在的记录天然实现增量采集。CREATE TABLE IF NOT EXISTS punish_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, company_name TEXT NOT NULL, credit_code TEXT, case_no TEXT, punish_authority TEXT, punish_result TEXT, punish_reason TEXT, punish_amount REAL, punish_date TEXT, detail_url TEXT, created_at TEXT DEFAULT (datetime(now, localtime)), UNIQUE(company_name, case_no) );落库前的最后一道工序是清洗处罚金额。行政处罚结果文本里经常混着中文大写和阿拉伯数字比如“罚款人民币伍拾万元整”或“罚款人民币 500000 元”。我用正则统一提取数字部分再转成数值型字段punish_amount便于后续做金额排序和统计分析。4. 容易被卡住的高频坑验证码、翻页截断与数据漂移写完代码不代表就能稳定跑通。我在这个项目里前后迭代了三周卡住我的不是接口逻辑而是几个隐藏很深的坑。这些坑如果没人提醒你可能要自己踩一遍才能发现。4.1 触发验证码的真实阈值与应对节奏信用中国在短时间高频请求下会弹出滑块验证我第一次跑就是吃了这个亏。当时为了图快把间隔调到 0.3 秒结果大概跑到第 80 个请求页面开始返回验证码提示整个 IP 被临时限制后续请求全部失败。后来我统计了一下单个 IP 在大约每分钟 20 次请求以内是相对安全的超过这个阈值触发概率显著上升。所以我把节奏固定在每次请求后随机等待 2 到 3 秒这样每分钟大概 20 到 30 次请求跑几个小时没有问题。如果不幸触发验证码不要试图硬破。最稳妥的做法是停止当前任务等待 10 到 15 分钟让限制解除然后降低请求频率重新跑。如果项目对时效性要求高可以准备一个合规的代理池把请求分散到多个 IP 上但代理质量直接影响数据完整性免费代理的响应延迟过大会拖垮整个采集效率不推荐。4.2 接口翻页的隐性上限看起来能翻到200页实际只有前2000条这是最坑的一个问题单靠看接口返回的 total 字段根本发现不了。我最初的方案是一个关键词直接翻页总共 2350 条记录按 pageSize10 算应该有 235 页但脚本翻到第 200 页时返回的数据突然变成了空数组而且 total 字段还显示 2350。排查了很久才发现接口对单个查询条件的结果集有隐性上限超过前 2000 条之后就不再返回数据但统计总数还是按全量计算的。解决思路是拆分查询维度让每个查询的结果集小于上限。具体做法有几种按处罚日期区间分段查询把“2020年至2024年”切成“2020年1月-6月”“2020年7月-12月”这样的小区间或者按处罚机关所在地进行分类查询用关键词加上地区维度缩小结果集。分段之后每个段落的记录数降下来了翻页就不会撞到天花板。这个坑在爬大型企业集团时尤其明显。一个知名房地产公司的处罚记录可能上千条按企业名精确查仍然超上限就必须用日期分段。我最终的做法就是把关键词和日期范围做成笛卡尔积逐段采集再统一去重合并。4.3 数据漂移别用接口返回的ID做唯一键我发现列表接口里每条记录都有一个类似 ID 的字段一开始想直接用这个字段做去重和增量更新的依据。跑了一轮增量后发现同一家企业的同一条行政处罚在两次接口返回中 ID 居然变了。原因可能是后端每次查询生成的临时序列不同或者接口对不同查询维度生成了不同的聚合 Key。这直接导致两轮采集之间产生了大量重复数据。解决方法是抛弃接口 ID改用业务主键“企业名称 决定文书号”做唯一约束。决定文书号是处罚决定书的编号同一家企业被处罚一次只会有一个文书号在业务上是稳定的。哪怕处罚内容后续被更正文书号一般不会变。顺带说一个字段清洗的经验处罚金额有时候不是数字而是“壹拾万元整”这种中文大写。我写了一个简单的转换函数把中文大写数字转成阿拉伯数字再把“万元”换算成元。这种细节很容易被忽略但做风险评分时金额字段是核心指标转换不干净会直接导致统计口径错误。5. 数据到手之后评分字段提取、增量更新与落地边界采集只是第一步爬下来的数据怎么变成业务能用的资产才是真正拉开差距的地方。我把自己的处理方式分成三层字段标准化、增量监控、按规矩使用。5.1 把“处罚内容”字符串变成可统计的字段原始处罚结果是一段文本比如“罚款人民币10万元”没法直接参与计算。我通过规则和关键词匹配做了一层轻量级的字段提取不需要上 NLP准确率已经足够PUNISH_TYPE_KEYWORDS { 警告: warning, 罚款: fine, 没收违法所得: confiscation, 责令停产停业: stop_production, 吊销许可证: revoke_license, 限制从业: restrict_practice, } def extract_punish_types(text: str) - list: types [] for kw, code in PUNISH_TYPE_KEYWORDS.items(): if kw in text: types.append(code) return types def extract_amount(text: str) - float | None: # 匹配“罚款XXX元/万元”模式 match re.search(r罚款[人民币]?\s*([\d.])\s*(元|万元)?, text) if not match: return None value float(match.group(1)) if match.group(2) 万元: value * 10000 return value这两段代码的思路是把非结构化的处罚结果转成结构化的标签和数值。用它跑一遍全量数据可以很直观地统计出某个区域的处罚类型分布、平均罚款金额、重点监管领域等直接服务于风控决策。5.2 增量更新和告警推送的工程做法行政处罚数据不是一次性拉完就结束的企业每天都在新增记录爬虫需要按增量节奏持续运行。我的做法是每天凌晨跑一次全量关键词列表每条记录插入前先查库存在就不处理不存在就插入并触发告警。告警推送我接的是企业微信群机器人。核心逻辑是新记录入库后把企业名称、处罚机关、处罚日期、处罚结果拼成一段文本通过 webhook POST 到群里。这样风控同事早上到公司打开群消息就能看到昨天有哪些存量客户或者供应商新增了处罚记录不用主动去查。如果后续数据量增大、需要做更复杂的关联分析我建议把 SQLite 里的数据同步到 ClickHouse 或 Elasticsearch。但在数据量低于百万级之前SQLite 配合索引完全够用没必要为了技术炫耀而引入重组件。5.3 使用边界能爬、能存但别乱用最后必须认真说一句行政处罚公示数据虽然是公开数据但采集和使用的边界不能含糊。我把合规红线列成三条第一采集频率和方式不对目标网站造成实质性负担不试图绕过登录、验证码或访问控制机制去获取非公开数据第二采集到的数据用于企业内部风控和合规审查不向第三方批量转卖原始数据第三如果结果表里涉及自然人的处罚信息严格控制访问权限避免在公开场合展示防止个人隐私被不当扩散。我曾经看到过一些机构把行政处罚数据做成公开榜单还附带法人的个人信息这种做法非常危险。数据能够被采集到不代表可以被任意使用这一点从项目立项开始就要想清楚。回到这个爬虫本身我在实际维护中的感受是这类政府公示网站改版频率不低签名参数、接口字段、页面模板都可能调整爬虫维护的重心不是“跑得快”而是“改得动”。我的代码把签名生成、请求构造、解析规则、存储逻辑拆成了独立模块站方一改版我只需要定位到变化的那一层改动量通常控制在一两个函数以内整个任务就能继续运转。另外一个心得是处罚数据采集的稳定性远没有想象中的“一次性全量拉取”方案来得靠谱。早期我追求一天内把几年的历史数据全搞定结果频繁触发限制、频繁断点续跑效率反而更低。后来改成每天按日期段小批量补数据每天任务总量可控跑完自动校验当天抓取数量与接口返回总数是否一致不一致就重试漏掉的段落整个系统变得非常省心。如果你的需求也是持续监控企业处罚动态建议直接采用这种“细水长流”的策略而不是一次压榨到位。
返回列表