
简介这是一份基于Python的企查查爬虫实现方案面向计算机科学、人工智能、通信工程、自动化、电子信息等专业学生及毕业设计、课程设计场景解决企业公开信息批量采集与结构化整理问题同时适合有一定Python基础、希望进阶爬虫实战的开发者。资源包共2个文件以Python脚本和Markdown说明文档为主爬虫源码包含请求发送、页面解析、数据提取与结果保存的完整逻辑配套文档说明环境配置与使用流程压缩包整体仅3KB轻量易读。目前已有2057人学习代码经测试运行成功可支撑毕设演示、作业提交或二次开发拓展。读者可获得可直接运行的爬虫工程以及理解页面结构解析、数据清洗、异常处理等要点的注释与说明便于快速上手也可在此基础上增加新功能或结合企查查平台规则调整采集策略用于学习与项目演练。1. 企查查爬虫爬「完整的公司数据」前先搞清数据是怎么到你屏幕上的把企查查上的公司数据完整爬下来需要几步搜一圈现成脚本大多数人第一次跑都会撞上同一个现象requests 发出去了状态码乖乖返回 200解析 JSON 却抛异常把响应体打出来才发现是一段登录页 HTML。企查查的前端是动态渲染的页面上看到的字段大多是浏览器加载完成后由后台 JSON 接口二次填充的这些接口藏在加密 JS、Cookie 校验和频率限制后面地址栏那个 URL 直接 GET 是拿不到数据的。理解这件事是整条爬取路线的第一步。这篇文章从抓包定位真实接口开始走一遍「搜索列表 → 公司详情 → 完整字段落地」的最小可复现路径再把验证码、字段缺失、登录态失效这类高概率翻车点单独铺开最后落到增量更新和工程化习惯。适合会用 requests、想落地一套真实可用采集脚本的读者全是细节没有空话。2. 先看清企查查的数据接口形态动态渲染、加密参数与登录态动手写代码之前先花半小时把浏览器开发者工具打开。这半小时能省掉后面一整天的逆向时间。企查查这类平台前端结构经常改版拿一周前的选择器套今天的页面很可能一条数据都解不出来。2.1 用浏览器开发者工具定位真实数据来源很多人一上来就对着 Elements 面板抄 CSS 选择器这是第一个坑。Elements 里看到的是最终渲染结果不是服务器原始返回。正确做法是把面板切到 Network网络勾选 XHR/Fetch 筛选项然后在搜索框输入一家公司名并回车。此时请求列表里会出现一批异步请求你要找的不是返回 HTML 的那个而是返回 JSON 的接口。判断依据很简单点击响应预览看到creditCode、legalPerson、regStatus这组字段名基本就是真正取数接口。找到后右键该请求选择 Copy → Copy as cURL粘贴到本地文本文件里。这段 cURL 完整保留了 URL、请求方法、请求头和 Cookie是后面构造 Python 请求最可靠的对照物。我一般会再把它粘到命令行实际跑一次确认还能返回数据才开始写代码。这里我要多说一句接口地址和参数名会随前端改版变化代码里的 URL 只作为样例每次开工前以抓包结果为准这句话值得写在项目 README 的第一行。提示接口地址和参数名会随前端改版变化。正式开工前重新抓一次包把 URL、请求方法、请求头、请求体四件套更新到配置里而不是沿用旧版本。2.2 加密参数与签名不是所有参数都要逆向细看抓到的请求 URL可能会发现几个看不懂的参数比如token、sig、timestamp。它们的来源不外乎两种服务端下发到页面里的或是前端 JS 运行时计算的。遇到这些参数先别急着上逆向。我的经验是分场景选方案如果只用 requests 裸调接口签名参数必须过了逆向这一关需要定位到生成签名的 JS 函数再做逻辑还原成本最高如果改用 Playwright 驱动真实浏览器签名参数由页面自己算好你只需要等待请求完成读取渲染后的结果或者拦截响应拿到 JSON。中小规模采集任务选后者代码量少维护成本也低。还要注意一个反爬细节部分敏感字段在 HTML 里会被打散。手机号、邮箱这类信息页面会拆成多个字符节点中间插入隐藏标签直接用.text提取拿到的往往是碎片。正确做法是在解析时先把同属一个字段的节点收集起来再拼接或者优先找有没有对应的 JSON 接口。网上很多「爬到的数据是残缺的」反馈其实都卡在这一步。2.3 Cookie 与登录态给脚本续上「身份」企查查的搜索接口在未登录状态下能返回基础列表但详情页里的股东、变更记录、主要人员等多数字段要登录后才看得到。如果目标只是列表数据可以暂时不处理登录但只要目标是「完整公司数据」登录态就是绕不开的前提。我的做法是手动登录一次把登录态保存成文件后续所有抓取直接加载它。保存有两种方案用 Playwright 的storage_state把 cookie 和 localStorage 整体导出后续加载创建一个新上下文或者用浏览器扩展导出 cookie 成 JSON再手动塞进 requests 的Cookie头。Playwright 方案在脚本里闭环不用来回切工具我一般选这个。登录态会过期Cookie 的有效期从几小时到几天不等。如果脚本里没有探活机制凌晨三点爬起来看日志会发现第一个请求就撞上了登录墙后面全是无效数据。所以正式开工前先加一个「探活请求」随机请求一个需要登录的接口判断响应里有没有登录用户标识字段。没有就停下来提示重新登录。这个动作很小却能把整个任务的失败模式从「静默跑了一夜」变成「明确报错」。另一个高发翻车点是指纹不一致。比如第一次用 Chrome 的 User-Agent 发了请求后面某个函数里又用了默认的 Python-requests UA这个差异很容易被风控识别。我的习惯是在项目里维护一个统一的 HEADERS 常量所有请求都从它复制避免各写一份。Referer 同样要固定企查查部分接口对 Referer 做了校验缺失或来源异常时返回空列表。3. 用 requests 实现基础版搜索公司列表与字段落地原理清楚了接下来写能跑的东西。这一章先实现一个搜索列表采集它能解决第一阶段 80% 的字段需求也是后面详情页采集的入口。3.1 最小可用代码搜索接口与参数说明下面这份代码以常见搜索接口为例子实际请求地址以你抓包结果为准。import time import requests # 以实际抓包为准这里只做示意 SEARCH_API https://www.qcc.com/api/search/company HEADERS { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ), Referer: https://www.qcc.com/, Accept: application/json, text/plain, */*, } def search_companies(keyword, pages1, sessionNone): 按关键词搜索公司返回公司摘要列表。 s session or requests.Session() s.headers.update(HEADERS) result [] for page in range(1, pages 1): params {key: keyword, page: page} try: resp s.get(SEARCH_API, paramsparams, timeout10) resp.raise_for_status() data resp.json() except Exception as exc: print(f第 {page} 页失败: {exc}) break items data.get(result, {}).get(list, []) if not items: print(f第 {page} 页没有数据停止翻页) break for item in items: result.append({ 公司名称: item.get(name), 统一社会信用代码: item.get(creditCode), 法定代表人: item.get(legalPerson), 成立日期: item.get(startDate), 登记状态: item.get(regStatus), 详情链接: item.get(detailUrl), }) time.sleep(2.5) return result这段代码里有四个值得说明的参数session用 requests.Session 而不是裸的 requests.get是为了把 UA、Referer、Cookie 统一管理避免每个请求反复传 headers。timeout10不设超时遇到慢接口时线程会一直挂着批量抓取时会越积越多。time.sleep(2.5)最简单的频率控制。你可以把它换成random.uniform(2.5, 4.5)让间隔有一点随机扰动。改成 0.1 的话大概率几十个请求后就会见到验证码。异常处理先打成 print正式工程里换成日志模块第 6 章会给出统一的装饰器方案。3.2 解析 JSON字段筛选、编码与空值处理接口返回的 JSON 结构大致长这样实际字段名以抓包为准{ result: { list: [ { name: 某科技有限公司, creditCode: 91110000XXXXXXXXXX, legalPerson: 张三, startDate: 2020-01-01, regStatus: 存续, detailUrl: https://www.qcc.com/firm/xxx.html } ] } }字段映射时我一般会做三个处理第一所有字段都走.get()拿不到就返回None避免 KeyError 中断任务第二把信用代码字段作为业务主键后续入库去重都依赖它第三遇到嵌套结构比如股东在列表接口里是{ name: 李四, ratio: 10% }先json.dumps存字符串等建好数据库再拆表。落盘第一步我建议用 CSV方便在 Excel 里快速检查字段完整性。一个容易忽视的细节文件编码要用utf-8-sig而不是utf-8否则 Excel 打开 CSV 时中文字段名会乱码。import csv def save_companies(items, pathcompanies.csv): 把公司列表写入 CSV自动处理表头。 if not items: print(没有数据不生成文件) return fields list(items[0].keys()) with open(path, w, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnamesfields) writer.writeheader() writer.writerows(items)写完 CSV 后主动读出来打印前 5 行确认字段和中文正常再继续。这一步看起来多余却能提前发现字段错位。3.3 频率控制与异常重试把反爬当成延迟处理在爬虫这件事上我一直坚持一个观念控制频率比追求速度重要。被封了不是最可怕的最麻烦的是封了之后可能几小时内都不让你恢复整个任务节奏全被打乱。与其事后找后悔药不如事前把节奏放慢。处理异常时先把错误分个类再决定动作响应特征判定方向建议动作HTTP 200 JSON正常响应解析入库HTTP 200 HTML被反爬拦截或未登录停止当前关键词翻页HTTP 302登录态失效走重新登录流程HTTP 4xx参数错误或被限制打印响应体确认遇到偶发超时重试 3 次间隔递增 1 秒、3 秒、8 秒连续失败就跳过当前页不要无限重试。遇到 JSON 解析失败立刻停止当前关键词的翻页因为大概率已经被反爬拦截继续翻只会拉高风险。4. 从摘要到完整工商数据详情页、动态渲染与关联数据入库列表数据解决了接下来是重头戏完整工商数据。这一章把详情页采集、登录态管理和数据库设计串起来。4.1 列表数据与详情数据是两套东西搜索接口返回的列表一般只有十几个核心字段。你要的「完整数据」——注册资本实缴、股东及出资比例、主要人员、变更记录、分支机构、对外投资——都在详情页或独立的子接口里。很多初学教程只讲列表采集导致有人以为自己「爬完了」交出去的核心字段却远远不够。详情页的数据来源通常分两类一类直接嵌在返回的 HTML 里另一类是页面加载时又发起的 JSON 接口。写解析之前先打开 Network 把详情页的数据来源过一遍再用代码去对号入座。选择器最好用有语义的 class 或 id不要写一整条从div到span的绝对路径前端一改版就全部失效。4.2 用 Playwright 渲染详情页登录态保存与等待策略对 HTML 部分用 BeautifulSoup 就够对异步加载的部分直接用 Playwright 把整个页面渲染完再提取更省事。先从手动登录开始把登录态存下来from playwright.sync_api import sync_playwright # 有头模式手动登录生成 storage_state.json with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context() page context.new_page() page.goto(https://www.qcc.com/, timeout30000) input(请在弹出的浏览器里扫码登录完成后回到终端按回车...) context.storage_state(pathstorage_state.json) browser.close()这段代码里的input()是给人留的操作窗口浏览器停在登录页你扫码完成后回终端回车登录态就写进文件。后面抓取时重新加载这份登录态from playwright.sync_api import sync_playwright def fetch_detail_html(detail_url, storage_statestorage_state.json): 渲染详情页返回最终 HTML。 with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context( storage_statestorage_state, user_agent( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ), ) page context.new_page() page.goto(detail_url, timeout30000) page.wait_for_load_state(networkidle) page.wait_for_selector(text统一社会信用代码, timeout15000) html page.content() browser.close() return html这里有三个细节值得注意headlessTrue是无头模式如果发现目标对无头识别严格可以把 headless 设成 False用性能换可用性批量不大时是划算的。wait_for_selector的等待条件最好选一个只有登录后才能看到的文本比如「统一社会信用代码」等错元素会出现拿到半截 HTML 的情况。networkidle不是万能的页面如果有轮询请求会一直等。遇到这种情况改用wait_for_function判断关键字段已出现。拿到 HTML 后用 BeautifulSoup 做字段抽取from bs4 import BeautifulSoup def parse_detail(html): 从详情页 HTML 中抽取公司基本信息。 soup BeautifulSoup(html, html.parser) result {} name_el soup.select_one(h1#companyTopName) if name_el: result[公司名称] name_el.text.strip() container soup.select_one(.company-info-wrapper) if container: for row in container.select(.info-item): label row.select_one(.info-label) value row.select_one(.info-value) if label and value: result[label.text.strip()] value.text.strip() return result选择器要按目标网站的实际 HTML 结构调整但解析思路是通用的找到字段容器循环取出「标签—值」对。对股东这类需要点开列表的字段可以考虑拦截页面加载时一并请求的 JSON 接口直接拿结构化数据比模拟点击更可靠。4.3 数据库建表与关联数据的外键设计字段多起来后CSV 就撑不住了。项目初期直接切 SQLite三张表起步company公司主表shareholder股东表change_record变更记录表。CREATE TABLE company ( id INTEGER PRIMARY KEY AUTOINCREMENT, credit_code TEXT UNIQUE, name TEXT, legal_person TEXT, status TEXT, detail_url TEXT, created_at TEXT DEFAULT (datetime(now)) ); CREATE TABLE shareholder ( id INTEGER PRIMARY KEY AUTOINCREMENT, company_id INTEGER NOT NULL, shareholder_name TEXT, share_ratio TEXT, subscribed_amount TEXT, FOREIGN KEY (company_id) REFERENCES company(id) ); CREATE TABLE change_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, company_id INTEGER NOT NULL, item TEXT, before_value TEXT, after_value TEXT, change_date TEXT, FOREIGN KEY (company_id) REFERENCES company(id) );用credit_code做 UNIQUE 的好处是同一家公司重复抓取时可以先查后插存在则跳过或走更新逻辑不存在才插入。这一条规则是后面增量爬虫的基础。入库函数用ON CONFLICT处理主键冲突import sqlite3 def upsert_company(db_path, data): 按信用代码插入或更新公司主表。 conn sqlite3.connect(db_path) sql INSERT INTO company (credit_code, name, legal_person, status, detail_url) VALUES (:credit_code, :name, :legal_person, :status, :detail_url) ON CONFLICT(credit_code) DO UPDATE SET legal_personexcluded.legal_person, statusexcluded.status conn.execute(sql, data) conn.commit() conn.close()股东和变更记录按company_id外键关联查询时一次 join 就能拿到一家企业的完整画像。5. 企查查爬虫避坑指南验证码、字段缺失与登录态失效的排查路线这一章是我和身边同事反复踩过的坑按「现象 → 原因 → 解决」组织哪条命中了直接翻到对应小节。5.1 坑一返回 200 却解析不出 JSON响应体是一段 HTML现象requests 请求后status_code是 200但resp.json()抛JSONDecodeError打印resp.text是一段带html标签的页面里面经常出现「访问过于频繁」「安全验证」字样。原因反爬层有意让拦截页也返回 200调用方只看状态码会误以为成功实际上请求根本没到达数据接口。这类伪装响应常伴随 Cookie 失效或请求头缺失。解决解析 JSON 之前先对响应文本做特征判断检测到 HTML 标签和验证关键词就直接中断当前关键词抓取并把状态码、响应长度、文本摘要写进日志方便事后定位。5.2 坑二滑块验证码突然出现后续请求全军覆没现象连续跑三四十个请求后某个接口不再返回 JSON变成带滑块验证码的 HTML再继续请求列表开始大量出现空数据。原因单位时间内请求次数超过阈值或者请求头的指纹组合不一致服务器把当前身份标记为高风险。解决立即停脚本等 510 分钟再继续不要硬冲。把固定 sleep 改成random.uniform(2.5, 4.5)所有请求保持一致的 User-Agent 和 Referer。如果验证码出现得越来越早说明间隔还不够大继续往上加。注意被反爬拦下后停下来是最有效的操作。连续刷新只会让限制时间更长。5.3 坑三同一家公司两次抓取字段不一致关键字段时有时无现象同一天对同一家公司抓两次详情第一次有注册资本第二次该字段缺失手工刷新页面数据又是完整的。原因详情页部分字段是异步加载的Playwright 等到了页面主框架但某个具体字段的接口还在返回途中脚本已经把 HTML 抓走了。解决等待条件从「选择器出现」升级为「字段文本非空」。用page.wait_for_function判断目标元素的innerText.length 0超时后记录 warning不写入空值。事后用「字段空值率」检查哪些数据还需要补跑。5.4 坑四昨天能跑今天全是登录墙Cookie 静默过期现象前一天晚上还正常的爬虫第二天启动后所有详情页都跳转到登录首页列表接口返回的也是登录引导。原因cookie 过期或登录态失效。企查查的登录态有效期不长隔天运行是过期的高发时间点。解决爬虫入口加一个登录态探活函数先请求一个登录后才可见的接口确认返回里包含登录用户标识再进入主任务。探活失败就输出明确的重新登录提示而不是继续跑无效请求。5.5 坑五字段对不上注册资本带了一堆前后缀符号现象CSV 里注册资本一会儿是「100万」一会儿是「100万元人民币」变更记录里还带着「从 50 万元变更为 100 万元」这类前后缀。原因同一个含义的数据在列表接口、详情接口、变更记录接口里用了不同展示格式页面又做了一层格式化处理直接入库自然不干净。解决制定统一的字段清洗规则对货币字段做「数字 单位」拆分建立字段别名映射表把不同接口采集到的字段统一映射到标准字段名。清洗逻辑集中在一个函数里不要散落在各个解析代码中。5.6 从现象定位根因的通用排查顺序看到一个异常响应时先别急着改代码。按下面顺序把现场信息凑齐再决定动哪里保存本次请求的完整响应体和响应头而不是只看状态码判断返回内容是 JSON、伪装成 200 的 HTML还是 302 跳转用浏览器无痕模式手动执行同一个操作确认是账号问题还是代码问题如果浏览器正常而脚本异常对比请求头、Cookie、请求参数三个差异点。2025-01-01 03:12:44 ERROR page 5 fetch failed: JSONDecodeError, body starts with !DOCTYPE 2025-01-01 03:12:49 WARN keyword stopped, too many retries这条顺序能覆盖 80% 的爬虫异常。剩下 20% 大概率是前端改版重新抓一次包就能解决。6. 把爬虫工程化增量更新、限速装饰器与合规红线数据抓回来不是终点能持续更新才是。这一章补上工程化三件事增量更新、统一限速、合规边界。6.1 增量抓取的三种策略与选择策略适用场景实现成本注意点全量重抓数据量小、一次能抓完低请求量大反爬风险高时间窗增量有明确变更时间字段中依赖接口支持按时间筛选断点续抓大规模抓取、需中断恢复中维护单独的进度表数据量在几千条以内全量重抓最省心数据量大了之后用进度表记录每家企业的抓取时间每天只重抓最近有变更的企业其余时间平稳轮询。CREATE TABLE crawl_progress ( credit_code TEXT PRIMARY KEY, last_crawl_time TEXT, last_status TEXT );每次入库前先查这张表决定新增还是更新这是把「爬虫」变成「采集系统」的分水岭。项目目录可以这样组织源码和文档一起维护qcc-crawler/ ├── config.py ├── storage_state.json ├── db.py ├── spiders/ │ ├── search.py │ └── detail.py └── docs/ ├── 接口字段说明.md └── 运行指南.md6.2 用装饰器统一加限速与重试把限速逻辑抽成装饰器让每类请求都默认带节流不用在函数体里到处写 time.sleepimport time import functools import random def throttle(min_interval2.0): 给函数加最小调用间隔随机抖动避免规整。 last_called [] def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): now time.time() if last_called: gap min_interval - (now - last_called[0]) if gap 0: time.sleep(gap random.uniform(0, 1)) last_called.clear() last_called.append(time.time()) return func(*args, **kwargs) return wrapper return decorator用法很简单throttle(min_interval3.0) def fetch_company_detail(detail_url): ...这样每次调用会自动间隔至少 3 秒又带随机扰动比把 sleep 写在函数内部更容易维护测试时还能临时把间隔调成 0。6.3 合规红线与工程习惯最后必须说清楚一件事技术能让你爬到数据不代表你有权随意使用这些数据。我自己的习惯是先确认是否有合法获取这批数据的授权再用于学习或明确允许的内部分析场景不用爬虫恶意突破付费墙去抓本应购买的字段也不把大批量数据打包公开传播。采集频率保持在合理范围不给目标平台造成明显压力这是基本的工程素养。最后说一个我自己的教训有次为了赶进度把请求间隔压到 0.5 秒结果抓了两千条数据后整个入口被限制项目停滞了三天。从那以后「控制节奏」成了我写采集代码的默认习惯慢一点反而能跑得久一点。希望帮到你。本文还有配套的精品资源点击获取