简介这是一份面向Python初学者与数据采集实践者的天眼查企业信息自动化爬取工具解决普通用户无法直接获取VIP专属字段如企业邮箱、联系电话等的痛点适用于商业尽调、竞品分析、销售线索挖掘等场景。资源包仅2个文件1个Python脚本1个说明文档总大小3KB轻量易部署其中主程序实现登录态复用、关键词搜索、详情页解析及关键字段提取文档则提供Cookie配置指引与常见问题排错提示如验证码识别、空结果排查。已有620人学习下载内容聚焦实战无需额外依赖安装仅需替换第26行Cookie即可运行附带针对性调试建议如手动过验证码后再执行显著降低入门门槛与调试成本。 做企业数据采集的应该都懂天眼查这种平台单个公司数据看着不复杂但要批量拉下来还挺折腾的。尤其邮箱、电话这些字段网页上大多要点开VIP才能看完整手动一个个复制既不现实效率也低得离谱。最近我把这套基于Python的天眼查爬虫完整整理了一遍从搜索关键词到公司详情全链路打通项目直接下载就能跑公司基本信息、经营状态、股东、主要人员这些常规字段都能拿到连默认折叠、需要VIP才能看的邮箱和电话也能通过接口响应直接取到。文章里我会把整体架构、核心代码、踩坑记录都讲清楚适合有一定Python基础、正在做企业数据采集或竞品分析的朋友参考。先说清楚一件事爬虫本质上就是在模拟浏览器行为对任何目标站点动手之前都要仔细阅读对方的服务条款和robots协议控制好请求频率只把数据用于合法合规的学习和研究用途。这套项目我实现时也严格遵循了这个原则下面进入正题。1. 整体设计思路与方案选型1.1 为什么锁定天眼查市面上做企业信息查询的平台不少天眼查、企查查、爱企查、启信宝这些我都接触过。这套爬虫选择天眼查作为目标主要是三个原因。第一个是数据字段非常全。一个公司详情页里工商信息、股权结构、主要人员、对外投资、司法风险、经营状况这些维度基本全覆盖做一个公司画像采集项目几乎不需要再从中找其他数据源补字段。第二个是页面结构相对规整尤其是它内部接口返回的JSON数据字段名规范、层级清晰解析起来比从HTML里抠文本要省事太多。第三个其实也是大家最关心的很多核心字段比如联系电话、电子邮箱普通用户不登录看不到登录了也提示要VIP权限但是这些数据在接口响应里往往已经全量返回了页面端只是做了权限控制来引导用户开通会员。爬虫直接拿接口数据就绕开了页面的展示限制。从技术选型上说Python这个生态做爬虫确实是最顺手的。requests处理HTTP请求、json解析响应、sqlite3做本地存储标准库加少量第三方库就能完成整个项目不用搞复杂框架。新手能用老手改起来也快。项目里我没用Scrapy这类重型框架核心逻辑全部用requests实现原因在下一节展开。1.2 一次请求 vs 页面解析接口优先原则写爬虫通常会遇到两条路一条是直接请求目标页面拿HTML源码后用BeautifulSoup或XPath去解析DOM节点另一条是找到页面背后的数据接口直接请求JSON数据。以我个人的经验能走接口就绝不解析页面。天眼查这类前后端分离的站点页面数据基本上都是通过接口动态加载的HTML只是壳。你打开一个公司详情页看到的工商信息、联系方式、股东信息其实是浏览器依次请求了好几个不同的接口才渲染出来的。如果直接解析HTML需要先处理大量的节点嵌套还要处理动态渲染带来的空元素问题效率低维护成本也高。直接请求JSON接口一个字段对应一个key取数据就是data[result][baseInfo][phone]这种操作一清二楚。而且接口还有一个页面没有的优势返回的数据结构稳定。页面改版可能只是调CSS样式但接口字段一旦定了基本不会乱动。就算接口调整了返回的错误信息也比HTML结构变化容易排查得多。这套爬虫定位就是长期稳定跑所以接口优先是必须的。1.3 项目目录与模块规划整个项目我按功能拆成了几个模块各管一摊便于维护和二次开发。目录结构大概是这样的tianyancha_spider/ ├── config.py # 配置文件 ├── spider.py # 爬虫核心逻辑 ├── parser.py # 数据解析和清洗 ├── storage.py # 数据存储 ├── utils.py # 通用工具函数 ├── main.py # 程序入口 └── requirements.txt # 依赖清单config.py统一管理请求头、Cookie、超时时间、请求间隔等参数改配置不动代码。spider.py负责发起HTTP请求构造搜索和详情页请求参数包含重试和异常处理。parser.py接收接口返回的JSON按业务需要提取字段、处理缺失值、清洗格式。storage.py封装存储逻辑默认SQLite需要的话可以改成MySQL或直接导出CSV。utils.py一些通用方法比如UA随机切换、日志输出、验证码或风控提示识别等。main.py读取待查询的公司名单调度整个爬取流程。这样拆的原因很简单爬虫项目最大的痛点不是写不快而是改不动。数据字段变化、页面结构调整、存储需求变更这些改动如果都集中在一个大文件里后期谁接手都头疼。2. 环境准备与基础模块搭建2.1 运行环境和依赖安装项目运行需要Python 3.8以上版本Python环境安装这里不展开讲Windows、macOS、Linux都有对应的官方安装包装的时候记得勾选添加到系统环境变量。装好之后建议用虚拟环境隔离依赖避免污染全局环境。依赖清单在requirements.txt里核心就几个库requests2.28.0 fake-useragent1.4.0 loguru0.7.0安装方式pip install -r requirements.txt这里有个实用小建议fake-useragent能自动生成随机的浏览器User-Agent避免每个请求都用一个固定的UA这是最基础的反检测手段。loguru用来做日志输出比Python自带的logging模块好看也好用。如果是在公司内网环境需要走代理的话requests支持通过环境变量或参数指定代理但项目默认屏蔽了系统代理防止代理配置错误导致请求异常。2.2 请求会话与Header构造requests库里的requests.Session()是我强烈推荐的工具。Session对象会自动保持Cookie这意味着你在一个会话里先访问了搜索页、再访问详情页服务端收到的就是一个连贯的访问链路而不是每次都像新用户一样这在很多网站的风控逻辑里是一个加分项模拟真实浏览器行为更自然。import requests from fake_useragent import UserAgent ua UserAgent() def create_session(): session requests.Session() session.headers.update({ Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Content-Type: application/json;charsetUTF-8, User-Agent: ua.random, Referer: https://www.tianyancha.com/, Origin: https://www.tianyancha.com }) return session这里的Referer和Origin很关键。很多站点后端会校验这两个头如果通过代码直接请求不带上它们接口返回可能直接是403或风控提示。另外要特别注意天眼查的某些接口会校验请求头里的X-Requested-With或者自定义的加密参数这种一般是通过异步脚本生成的普通requests模拟会有难度。我自己实测下来搜索和详情接口的常规请求头就能通过但你自己写新功能的时候如果遇到鉴权参数就需要用浏览器开发者工具Network面板查看完整请求头再逐个补上。2.3 Cookie与登录态管理的关键点Cookie管理是这套爬虫能不能拿到完整数据的分水岭。先说结论部分字段不登录也能拿到但VIP字段比如邮箱电话通常需要带上登录后的Cookie才有完整响应。具体来说天眼查的接口对未登录用户和登录用户返回的数据权限是不一样的。不带Cookie时phone和email字段往往为空字符串或直接不返回带上登录Cookie后接口返回的JSON里就包含了完整值。这里有个细节不是说要开VIP会员而是只要你登录了账号接口就会把字段值返回给你页面展示层才会根据你的权限去控制显不显示。这个差异我第一次测的时候也很意外后来用抓包工具对比了登录和未登录的接口响应才确认。实现层面就是把浏览器里登录后的Cookie复制出来填到config.py里# config.py COOKIES 你的浏览器登录Cookie多段用;连接然后创建会话时加进去def create_session(): session requests.Session() session.headers.update({...}) # 简单处理直接用Cookie头 session.headers.update({Cookie: COOKIES}) return session注意Cookie是有时效的。天眼查的登录态一般能保持几天到几周不等取决于账号活跃度和环境变化。爬虫跑到一半突然发现返回的数据没有电话邮箱了优先检查Cookie是不是过期了。另外同一个账号频繁请求也有触发风控的风险实际生产环境建议备几个小号轮换使用但前提还是合法合规、符合平台规则。3. 核心爬虫逻辑实现3.1 公司搜索从关键词到公司ID整个爬虫的第一步是把待查询的公司名称或关键词转换成天眼查内部的公司ID。天眼查每个公司都对应一个唯一的ID叫companyId或gid后续所有详情接口都用它来定位。搜索接口的请求URL格式大致是https://www.tianyancha.com/api/search/suggest.json?key关键词pageSize10这个接口返回的是JSON数组里面包含匹配的公司列表。解析时取出每条记录里的id字段就是后续要用的公司ID。这里有一个细节搜索关键词时如果是精确的公司全名第一条结果基本就是目标公司但如果是模糊关键词返回的可能是多个候选比如输入“华”字能搜出来一堆带“华”的公司。这时候需要在代码里增加一个过滤逻辑def search_company(session, keyword): url https://www.tianyancha.com/api/search/suggest.json params {key: keyword, pageSize: 10} resp session.get(url, paramsparams, timeout10) data resp.json() companies data.get(data, []) for item in companies: # 精确匹配优先否则返回第一条 if item.get(name) keyword: return item return companies[0] if companies else None这个search_company函数是后续所有流程的入口它的返回值里最核心的就是id和name。拿到ID后再去请求公司详情接口这个ID能否正确提取直接决定后面能不能取到数据。3.2 详情页数据解析完整字段提取公司详情接口是天眼查页面数据的完整JSON来源。不同字段分布在不同的接口里我在项目里主要用了这么几个数据维度接口说明基础工商信息公司名称、法人、注册资本、成立时间、统一社会信用代码等联系方式电话、邮箱、官网、地址股东/合伙人股东名单及持股比例主要人员董监高名单经营异常/司法风险经营状态、风险数量以基础工商信息为例请求URL格式大致是https://www.tianyancha.com/api/company/baseinfo.json?idcompanyId响应JSON里的字段命名很规范result下面就是各个业务字段。我在parser.py里专门写了一个通用提取函数根据配置好的字段映射表去取值def extract_fields(data, mapping): result {} for target_key, source_path in mapping.items(): # 支持两级路径例如 result.baseInfo.regCapital keys source_path.split(.) value data try: for k in keys: value value[k] result[target_key] value except (KeyError, TypeError): result[target_key] None return result这个函数看起来简单但在数据清洗阶段帮了大忙。它会自动容错如果某个字段不存在不会让整个进程崩掉而是统一置为None。实际企业数据里缺字段的情况非常多这种防御式写法能让爬虫稳定跑完整个公司列表不会被一条脏数据卡死。3.3 邮箱/电话字段的获取思路现在来说这个项目最核心的部分也就是标题里那句“可爬需要VIP才能用的邮箱和电话”。获取这些字段的思路和技术路径前面已经铺垫了关键点不要在页面HTML里找去接口响应里找。天眼查的接口中联系方式相关的数据放在contact或baseInfo这类节点下字段名通常是phone和email。以实测经验来说登录状态下的接口响应中phone和email字段会有值。如果返回空常见原因就两个一是当前账号Cookie未生效或已过期二是该公司在工商登记信息里本身就没登记联系方式这个情况很多尤其是小微企业。实现代码和提取普通字段没有本质区别parser.py里映射表加两行就行{ phone: result.contactInfo.phone, email: result.contactInfo.email, }这里要给刚入门的朋友提个醒不要试图去分析前端的VIP弹窗逻辑或尝试绕过会员体系。一没必要因为正规JSON接口返回的数据已经够用了二有风险触碰权限校验边界既违反平台规则也可能触发更严厉的风控。我能拿到这些字段靠的就是正常登录后接口返回的公开数据这个定位需要明确。3.4 多线程抓取与限速数据量大之后单线程一个个请求确实太慢。我在项目里用了Python标准库concurrent.futures的ThreadPoolExecutor来做多线程代码不复杂from concurrent.futures import ThreadPoolExecutor, as_completed def crawl_multi(companies, max_workers5): results [] with ThreadPoolExecutor(max_workersmax_workers) as executor: future_map { executor.submit(process_one_company, comp): comp for comp in companies } for future in as_completed(future_map): comp future_map[future] try: data future.result() results.append(data) except Exception as e: logger.error(f处理 {comp} 失败: {e}) return resultsprocess_one_company就是前面搜索加详情解析的封装。线程数max_workers我建议从5开始调不要一上来就开几十个线程。天眼查的风控对频率非常敏感我之前用10个线程短时间内连续请求跑了几百条数据就触发了滑块验证得不偿失。稳比快重要能稳定跑完全部数据才是真效率。限速方面我在utils.py里写了一个简单的请求间隔控制import time import random def throttle(min_interval1.5, max_interval3.0): time.sleep(random.uniform(min_interval, max_interval))每个请求完成后随机休息1.5到3秒这个时间区间实测比较安全能有效降低连续请求的特征峰不容易被风控识别为机器行为。时间间隔也可以根据自身网络情况和目标站点规则适当调整。4. 数据落地存储与增量更新4.1 存储方案选择数据存哪里取决于你后续怎么用。这个项目默认用SQLite原因就一个字省事。它是一个单文件数据库不需要安装数据库服务Python标准库自带支持IO性能对几万条公司数据完全够用。扩展性上后续如果切换到MySQL只需要改storage.py里的连接和插入语句其他模块都不用动。建表语句我放在storage.py里import sqlite3 def get_conn(db_pathcompanies.db): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS companies ( id INTEGER PRIMARY KEY AUTOINCREMENT, company_id TEXT UNIQUE, company_name TEXT, legal_person TEXT, reg_capital TEXT, established_date TEXT, phone TEXT, email TEXT, website TEXT, address TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) return conn这里company_id字段设置了UNIQUE约束这是去重的基础。插入数据时用INSERT OR REPLACE或者ON CONFLICT语法就能做到相同公司ID重复抓取时直接更新不会产生重复记录。如果你更看重数据分享和管理也可以把数据导出成CSV项目里写好了export_csv接口一行命令就能导出全量数据。4.2 增量更新的实现策略企业数据是会变的比如法人变更、注册资本调整、联系电话修改这些都是常见情况。所以爬虫不能只跑一次就完事需要考虑增量更新。增量更新有两个层面。第一个是新公司增量就是每次跑的时候新增了哪些公司这个直接查公司ID在表里是否已存在不存在就插入。第二个是已有公司更新就是当新抓到的数据和库里数据不一致时把最新的值覆盖进去。判断是否更新的最快做法是比对updated_at字段。比如设定一个策略距离上次抓取超过7天的公司本次重新抓取并覆盖7天内的直接跳过。这个逻辑放在main.py里from datetime import datetime, timedelta def should_update(conn, company_id, max_age_days7): row conn.execute( SELECT updated_at FROM companies WHERE company_id ?, (company_id,) ).fetchone() if row is None: return True last_time datetime.fromisoformat(row[0]) return datetime.now() - last_time timedelta(daysmax_age_days)这种基于时间戳的增量策略简单可靠配合定时任务比如crontab或Windows计划任务每周跑一次就能构建一个持续更新的企业数据库。数据量达到千万级以后要考虑分表或换数据库但对绝大多数个人项目来说这个方案已经够用了。4.3 数据清洗与去重接口返回的数据是“原生态”的直接存库会埋很多坑。我在parser.py里做了几个清洗规则。第一个是电话和邮箱的格式统一。工商登记信息里的电话经常有空格、短横线、地区码比如010-12345678和010 12345678还有的带括号。统一清洗为只保留数字和短横线不然做数据匹配的时候会出各种幺蛾子。邮箱统一转小写去掉首尾空格。第二个是注册资本这类数值字段。天眼查接口里通常返回的是字符串比如1000万人民币或1000万元。如果后续要做统计分析最好统一拆成数值和单位两个字段。我会存成reg_capital和reg_capital_unit两列方便排序和聚合。第三个就是去重。除了数据库的UNIQUE约束解析层也要做一道防重。因为接口返回的JSON里有时会包含重复的嵌套字段或者同一条数据在搜索接口和详情接口各出现一次解析时以公司ID作为唯一键重复出现时以后一次为准直接覆盖前一次的结果。清洗代码示例import re def clean_phone(raw): if not raw: return None return re.sub(r[^\d-], , raw) def clean_email(raw): if not raw: return None return raw.strip().lower()这些清洗规则看着基础但实际跑数据时的脏数据比你想象的要多得多没有清洗环节后面做数据分析时会反复被恶心到。5. 反爬应对与稳定性维护5.1 常见反爬机制与应对企业信息平台是反爬的重灾区毕竟是商业数据产品对数据保护投入的资源很多。我在开发过程中遇到过的主要反爬机制有这么几类也顺手总结了我的应对策略。第一类User-Agent检测。最简单的检测方式如果请求头里UA是爬虫库的默认UA直接拒绝。应对方式就是前面提到的用fake-useragent随机切换UA必要的时候抓取浏览器真实的UA字符串硬编码到配置里。这个是最基础的防护基本没人会被这层卡住。第二类IP访问频率限制。单个IP在短时间内请求次数超过阈值就会触发封禁或验证码。应对方式是控制全局请求速率加上随机延时。我特意throttle函数放在每次HTTP请求之前调用保证任何入口都不会漏掉限速。更严格的情况下可以考虑使用代理池但代理质量参差不齐选不好反而会拖垮整个爬虫初期不建议优先上代理。第三类验证码。滑块验证、点选验证、图形验证都见过。常见的触发时机是频繁请求搜索接口、单账号高并发、数据量突然暴涨。应对策略就一条触发就立即刹车。代码里识别到验证码页面的特征比如返回的JSON里出现verify相关字段或者页面标题是验证码页面立刻停止当前线程等一段时间再继续。不要硬刚验证码识别算法成本和稳定性都不划算。小规模爬取时直接人工过一下验证码反而更高效。5.2 代理池与请求频率平衡代理池这个方案我说说我的真实使用感受。一开始我担心单IP容易封还没被验证码教做人的时候就兴冲冲接了一堆免费代理。结果免费代理的质量惨不忍睹连接超时、返回乱码、还有的是肉鸡数据安全性都成问题。折腾一圈下来速度和稳定性反而不如单IP限速跑。所以我的建议是分阶段处理数据量小的时候单IP加合理延时完全够用。一万条以内的公司数据用单IP每请求间隔2秒左右大概几个小时能跑完这个速率实测很少触发风控。数据量到十万、百万级别的时候再考虑自建代理池或者购买可靠的代理商服务用轮询策略把请求分散到多个IP上同时严格限制每个代理IP的请求频率。这里要注意代理选择上如果涉及国内合规服务优先选有资质的IDC或云厂商提供的API代理服务不要用来路不明的免费代理。请求频率这块我的经验值是单IP的QPS最好控制在1以内也就是每秒最多一个请求。对天眼查这种风控级别的平台来说再低一点更稳妥。测试的时候先跑20到50个请求观察返回是否正常再加大规模。5.3 异常重试与日志体系爬虫跑起来之后最怕的不是慢而是莫名其妙挂掉你还不知道它挂在哪。日志是排查问题最重要的抓手我在项目里用loguru把日志分成三个级别INFO记录每个公司抓取成功和耗时WARNING记录接口返回异常但不需要中断的情况ERROR记录重试多次依然失败需要人工介入的严重问题。核心的异常处理逻辑封装在spider.py的重试装饰器里import time from functools import wraps def retry(max_retries3, delay2): def decorator(func): wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: if attempt max_retries - 1: raise time.sleep(delay * (attempt 1)) return None return wrapper return decorator这个retry装饰器可以用在任何HTTP请求函数上遇到网络超时、连接重置这类瞬时错误会自动重试重试间隔按倍数增长避免每次都等同样长的时间。重试三次仍然失败就抛出异常由上层逻辑记录日志并把这条数据标记为待处理不影响整体进度。日志样例2024-01-15 10:23:45.123 | INFO | 抓取成功: 北京某某科技有限公司, 耗时 2.3s 2024-01-15 10:23:47.456 | WARNING | 接口返回异常: 上海某某贸易有限公司, 触发限流, 已自动跳过这样的日志体系每天跑完打开日志文件扫一眼基本就知道哪些公司需要补抓、哪些请求触发了风控不用盯着控制台发呆。6. 常见问题与排查技巧实录6.1 常见问题速查表实际跑这个项目的过程中我遇到过不少问题这里整理成一张速查表方便大家对照排查。现象可能原因排查方法返回403 Forbidden请求头缺失或UA不合法检查Header是否完整换UA重试返回验证码页面请求频率过高或IP被风控立即停止请求等待10-30分钟再试搜索接口返回空数据关键词格式不对或接口参数变化浏览器开发者工具抓包对比请求参数详情接口正常但phone为空账号未登录或Cookie过期重新登录账号并更新Cookie请求超时网络波动或目标站点响应慢调大timeout值增加重试机制数据库插入失败company_id重复或字段类型不匹配检查表结构处理数据格式跑了几百条后突然全被拒IP被临时封禁暂停一段时间后续降低频率这张表是我在开发迭代中不断沉淀下来的每解决一个问题就往里加一行。它最大的价值不是列出所有情况而是提示了你排查的方向先看请求头再看Cookie然后看频率最后才是接口字段变化。6.2 两个比较隐蔽的坑除了上面那些表面问题还有两个坑是比较隐蔽的不仔细排查很难发现。第一个是请求参数大小写和类型问题。天眼查搜索接口的pageSize参数类型是字符串还是整数都行但key参数如果没做URL编码含中文的公司名直接拼进URL会导致接口返回空数据或者404。用requests的params传参就能自动处理编码千万别自己拼URL字符串。第二个是接口返回JSON的结构层级变化。天眼查接口返回的数据结构在登录和未登录状态下可能有差异。未登录时result下没有contactInfo这一层直接取字段会报KeyError登录状态下才有。所以在extract_fields函数里对缺失值做了容错遇到结构差异就直接返回None。这个设计帮我避免了不少跑到一半崩溃的尴尬。除这两个之外还有一个经验性的技巧每个新请求都打印一次完整URL和状态码到日志。排查问题的时候有完整请求日志和只有返回结果效率完全不是一个级别。很多人调试时图省事不记录请求URL一旦出问题就只能盲猜浪费的时间远超写日志的时间。这套爬虫项目从设计到落地前后迭代了好几版核心经验就是三句话能用接口别抠HTML能限速别追求极速日志一定要完整。实际跑起来之后你会发现真正决定项目成败的不是某段代码多巧妙而是稳定性够不够、容错好不好、出问题能不能快速排查。把这个底子打好后续往任何方向扩展都有底气。本文还有配套的精品资源点击获取