天眼查爬虫实战:Python接口解析与Cookie维护全攻略

天眼查爬虫实战:Python接口解析与Cookie维护全攻略 简介面向爬虫开发学习与毕业设计场景的Python天眼查数据采集工具以单文件爬虫脚本为主体实现公司公开信息、甚至需要VIP权限才能查看的邮箱与电话等字段的抓取。配合README说明文档给出运行前在指定位置配置Cookie的提示与必要使用说明适合计算机相关专业学生用作毕设参考、课程设计或爬虫技术进阶练习。资源包共2个文件包含1个py源码和1个md说明文档整体仅3KB结构精简便于直接阅读与二次修改。已有1473人学习下载。代码经测试可运行作者为毕设答辩高分项目具备较高的参考价值读者可重点研究其请求参数构造、页面解析与VIP数据提取思路。1. 天眼查爬虫不是“一个脚本”那么简单如果只看标题很多人会以为“天眼查爬虫”是写一个requests.get然后把 HTML 里的公司名称抠出来。真去做一遍你会发现公司详情里的邮箱和电话在 VIP 条件下才显示完整页面里的字段经常被脱敏接口要带 Cookie 和 token请求一快就 403。本文用 Python 把这条链路完整走一遍从定位 JSON 接口到保存可利用的 Cookie再到一个能断点续爬的采集器。适合已经会 Python 基础语法、想用爬虫获取企业公开数据的开发者。先声明一句爬取数据请遵守相关法律法规和平台服务条款下面代码只用于学习研究。2. 摸清天眼查数据从哪来先定位接口再写请求2.1 为什么第一件事是打开 Network而不是写正则这里有一个反直觉的结论解析天眼查不需要一上来就抠 HTML。打开浏览器开发者工具随便搜索一家公司Network 面板里能抓到一堆 JSON 请求公司注册资本、法人、股东信息全部以结构化字段返回。前端页面用 JS 渲染数据先以 JSON 形式加载到浏览器内存再拼接到 DOM 上所以 HTML 里看到的内容其实是 JSON 的“二手货”。与其解析二手数据不如直接拿一手数据。很多爬虫教程教你先用requests.get拿到页面再用BeautifulSoup去find_all。这在静态网站上可行但天眼查大量使用异步渲染首屏 HTML 可能只有框架数据靠后续的 XHR 请求灌入。因此第一步要把 F12 当成主要采证工具。打开天眼查首页搜索一家公司进入详情页点击 Network 面板筛选 XHR。请求列表里会出现大量数据接口按名称可以分成几类搜索接口、公司基本信息接口、股东接口、主要人员接口、联系方式接口。这些接口的路径和参数各不相同但都共用同一组请求头这组请求头就是后面用 Python 构造请求的模板。定位接口时还有一个技巧在 Network 面板里按CtrlF搜索“电话”或“邮箱”然后查看是哪条 XHR 响应命中了关键词。很多时候页面上的脱敏字段只是在渲染时做了遮蔽但接口返回的 JSON 里已经完全包含真实值。这也是很多“VIP 邮箱电话”能被爬到的根本原因前端必须拿到完整数据才能完成展示权限控制写在页面上不等于写在接口里。2.2 用 requests 复现一次真实请求定位到接口后常见做法是把浏览器里的请求复制为 cURL再转换成 Pythonrequests代码。这里给出一个最小示例对应的就是一个简单的搜索请求import requests session requests.Session() session.headers.update({ 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, Accept: application/json, text/plain, */*, Referer: https://www.tianyancha.com/, X-Requested-With: XMLHttpRequest, Cookie: 你的登录Cookie, }) url https://www.tianyancha.com/search.json params { key: 阿里巴巴, pageNum: 1, } resp session.get(url, paramsparams, timeout10) print(resp.status_code) print(resp.text[:500])这段代码用Session对象保持会话后续所有请求都会自动携带同样的头部信息。User-Agent必须与真实浏览器一致Referer用来告诉服务器请求来自天眼查页面X-Requested-With标记这是一个 AJAX 请求Cookie则承载你的登录身份。params中的key是搜索关键词pageNum是页码。如果返回200并且text里能看到data字段说明你已经成功复现了一个接口调用。提示不要直接把 cURL 里的所有 Header 原样照搬。复制后重点检查Cookie、User-Agent、Referer、X-Requested-With这几个字段其余如Accept-Language、Sec-Fetch-*等保持默认即可减少请求头怪异导致的校验失败。2.3 返回字段的容错与字段映射天眼查详情页的数据经常会按模块拆分到不同接口里抓包时可以看到getBaseInfo、getPartners、contactInfo这样的路径。以下是一个常见的字段速查表具体路径以你当前抓包结果为准因为平台会不定期调整接口地址模块示例路径常见字段公司基本信息/company/getBaseInfo/{companyId}公司名称、法人、注册资本、统一社会信用代码股东信息/company/getPartners股东名称、出资比例、认缴出资额主要人员/company/getStaff姓名、职务联系方式/company/contactInfo电话、邮箱可能要求登录或VIP在实际解析时不要假设每次接口返回的 JSON 结构都一致。响应里可能同时存在data、result、dataList等不同包装字段字段名在不同接口之间也可能出现驼峰和下划线混用。稳妥的做法是先打印resp.json()的键名再用字典的.get()方法提取避免因为某个字段缺失导致程序崩溃。data resp.json() base data.get(data, {}) phone base.get(phone, ) or email base.get(email, ) or .get默认返回None这里用or 把空值统一成空字符串方便后续写入 CSV。为什么不用[phone]因为接口只要少一个字段程序就会抛KeyError在批量采集时会导致整个任务中断。爬虫的稳定比代码优雅更重要所以所有字段提取都要做容错。3. VIP 字段能不能爬取决于登录态怎么维护3.1 选手动 Cookie还是 Selenium Cookie天眼查的邮箱、电话在未登录或低等级状态下返回的是脱敏值。即使你找到了正确的接口看到的内容也可能是0571-****1234或者x*****.com。因此爬虫的第一优先级不是解析规则而是拿一个长期有效的登录授权。常见做法是分两种路线一种是用浏览器手动登录然后复制 Cookie 到脚本里另一种是用 Selenium 自动打开登录页人工配合扫码或短信验证再保存 Cookie 给requests使用。手动复制 Cookie 的优点是稳定、简单适合一次性采集缺点是 Cookie 会过期需要每隔一段时间重新操作一次。Selenium 自动登录虽然前期配置麻烦但可以把“登录”这个动作也脚本化后续只要人工扫码一次就能把 Cookie 持久化到文件复用时完全不需要浏览器。我一般建议先把手动取 Cookie 跑通。这样你可以确认当前账号能否看到完整邮箱和电话也方便排查接口权限问题。确认可行后再升级成 Selenium 自动保存 Cookie 的方案。3.2 把登录 Cookie 保存成 JSON再用 requests 复用这里给出一个混合方案用 Selenium 完成登录把 Cookie 保存到本地文件再用requests加载。这样既解决登录验证问题又保留requests的高并发和高效率。# login.py import time import json from selenium import webdriver driver webdriver.Chrome() driver.get(https://www.tianyancha.com/login) # 留出时间手动扫码或输入账号密码 time.sleep(30) cookies driver.get_cookies() with open(tyc_cookies.json, w, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse) print(f已保存 {len(cookies)} 个 Cookie) driver.quit()这段代码里time.sleep(30)是强制等待时间你可以根据手速调整到 20~40 秒。driver.get_cookies()返回的是一个由多个字典组成的列表每个字典包含name、value、domain、path等信息。保存成 JSON 文件的好处是结构完整后续不仅能用requests加载还能在需要时用浏览器重新打开文件调试。在采集脚本中加载 Cookie 的写法如下# crawl.py import json import requests session requests.Session() with open(tyc_cookies.json, r, encodingutf-8) as f: cookies json.load(f) for c in cookies: session.cookies.set(c[name], c[value]) session.headers.update({ User-Agent: Mozilla/5.0 ..., Referer: https://www.tianyancha.com/, })循环里只取了name和valuerequests会自动按域名和路径发送 Cookie。这里需要注意如果 Selenium 登录时用的浏览器配置没有关闭自动关闭请确保在driver.quit()之前已经写完了 Cookie 文件。另外Cookie 有时效性建议在程序里加一个“会话检查”每次请求前先访问一个轻量接口如果返回状态码是 302 或 401就提示重新执行登录脚本。3.3 token 参数与登录过期判断天眼查很多接口除了验证 Cookie还会校验X-Auth-Token这类请求头或者 URL 上的 token 参数。这类 token 通常不是登录后固定不变的而是从某个页面中提取出来的。一个常见来源是详情页 HTML 里的全局变量类似下面这种结构import re import json html session.get(https://www.tianyancha.com/company/123456, timeout10).text m re.search(rwindow\.__INITIAL_STATE__ (.*?);, html) if m: state json.loads(m.group(1)) print(state.keys())正则里的.*?用非贪婪匹配从window.__INITIAL_STATE__开始截取到第一个分号。实际运行时会发现这个全局变量内容非常长而且包含很多 HTML 转义字符直接json.loads可能失败。这时不要硬解析可以用字符串find定位token关键字再取附近的一段值能拿到可用字段就行。如何判断登录过期访问一个需要登录的详情接口如果返回的code是401或者页面 HTML 里出现了“登录”“验证码”等关键词说明登录态已失效。把这些判断逻辑写成一个函数每次采集前调用一次能省下很多无意义请求。def is_login_valid(session, url): resp session.get(url, timeout10) if resp.status_code 401: return False if 请先登录 in resp.text: return False return True这个函数返回False时直接终止爬虫并提示运行login.py。不建议在爬虫内部再嵌套一个 Selenium 去自动重新登录因为登录过程涉及滑块交互可能会打断主任务分开处理更稳定。4. 搭建一个带断点续爬的 Python 采集框架4.1 先定义数据模型再写采集代码爬虫跑通之后下一步要解决工程化问题。直接在一个循环里写requests.get和print能跑但用来批量采集完整公司数据时必须考虑任务来源、字段落地、断点续爬。下面这套结构是我平时做这类 Python 爬虫的默认骨架不依赖 Scrapy只靠标准库加requests就能跑。第一步是定义要保存的字段。这里以公司基本信息为主加入 VIP 邮箱和电话company_record { company_id: , company_name: , legal_person: , reg_capital: , founded_date: , phone: , email: , address: , status: , }字段名建议和 JSON 接口返回的字段保持一致这样映射代码可以写得很机械record[phone] data.get(phone, )。唯一需要注意的就是company_id要单独作为主键后续去重、更新、断点续爬都依赖它。4.2 从文件读取任务队列采集循环加限速和重试任务队列不需要用数据库一个文本文件足够。每行一个公司 ID爬虫启动时读取到列表再逐个处理。这里给出一段带限速和状态码区分的循环代码import time import random import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(tyc_spider) def load_tasks(filenamecompany_ids.txt): with open(filename, r, encodingutf-8) as f: return [line.strip() for line in f if line.strip()] company_ids load_tasks() for company_id in company_ids: url fhttps://www.tianyancha.com/company/{company_id} try: resp session.get(url, timeout10) if resp.status_code 200: logger.info(company_id%s 请求成功页面长度%d, company_id, len(resp.text)) # 在这里继续做解析 elif resp.status_code 403: logger.warning(company_id%s 被拦截暂停 60 秒, company_id) time.sleep(60) else: logger.warning(company_id%s 状态码%s, company_id, resp.status_code) except requests.RequestException as e: logger.error(company_id%s 请求异常%s, company_id, e) # 随机限速避免请求频率过高 time.sleep(random.uniform(1, 3))timeout10是单次请求最大等待时间放在session.get里防止某个公司页面卡住整个任务。random.uniform(1, 3)让每次请求后随机睡眠 1 到 3 秒比固定睡眠 2 秒更难被风控识别。遇到403时强制休息 60 秒这时候如果还在高速请求大概率会被封账号所以这里的等待宁长勿短。load_tasks返回一个列表实际项目里任务量大的话可以改成生成器每次 yield 一个 ID减少内存占用。如果团队规模大可以把文本文件替换成 Redis List实现多个 Python 进程共享任务队列进化为分布式爬虫。4.3 解析邮箱和电话先找 mailto再做实体反转义在页面源码里完整邮箱经常以mailto:链接形式出现电话以tel:链接出现。这是一个很稳定的线索可以直接用正则提取import re import html email_match re.findall(rmailto:([^\]), raw_html) tel_match re.findall(rtel:([^\]), raw_html) # 清洗结果 email email_match[0].strip() if email_match else tel tel_match[0].strip() if tel_match else 但很多页面并不会把完整邮箱放在mailto里而是直接展示在文本中并且用#64;代替用#46;代替点号。这时需要使用html.unescape把实体转义恢复成可读字符clean_html html.unescape(raw_html) all_emails re.findall(r[A-Za-z0-9._%-][A-Za-z0-9.-]\.[A-Za-z]{2,}, clean_html)这里的正则是一个简单的邮箱匹配模式允许英文字母、数字、点及常见符号。由于页面中包含大量 JS 变量和注释提取结果里可能混入不相关的邮箱所以建议只取第一个匹配项并且过滤掉example.com、2x这类明显无效的结果。电话匹配更复杂国内电话号码有手机号、座机号、400 电话等格式可以分步提取先找400开头再找0开头的座机统一去掉空格和横线再存入字段。4.4 落盘去重与断点续爬批量采集最怕跑到一半中断第二天重启又从第一条开始。断点续爬的常用做法是维护一个“已完成 ID”集合每次成功解析并写库后就把 ID 追加到文件中。import os FINISHED_FILE finished.txt if os.path.exists(FINISHED_FILE): with open(FINISHED_FILE, r) as f: finished set(line.strip() for line in f) else: finished set() # 循环内每个 company_id 成功解析后 with open(FINISHED_FILE, a) as f: f.write(company_id \n)这里用set做内存去重判断条件就是if company_id in finished: continue。文件追加写入不需要加锁因为单线程模式下不会出现并发写冲突。如果想扩展成多线程或分布式只要把finished换成一个 Redis Set多个进程通过SADD和SISMEMBER操作即可思路一样。数据落盘可以先写在 CSV 里方便直接打开看效果。每次启动时如果 CSV 文件存在就让DictWriter只写行不写表头避免表头重复。import csv file_exists os.path.exists(tyc_data.csv) with open(tyc_data.csv, a, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnamescompany_record.keys()) if not file_exists: writer.writeheader() writer.writerow(company_record)newline是为了避免写入 CSV 时出现空行这是 Python 写入 CSV 的一个常见坑。encodingutf-8防止中文乱码后续用 Excel 打开时如果还是乱码可以改成utf-8-sigExcel 会更容易识别。5. 排错思路与可交接的文档说明5.1 高频异常403、验证码、JSON 解析失败先看 403。这个状态码在爬虫世界里几乎等于“平台认识你但不想让你进”。常见原因是 Cookie 过期、请求头太少、请求频率太高。解决顺序是先检查浏览器里同一个接口是否正常如果正常说明不是账号问题而是脚本请求头的问题补上Referer和User-Agent。如果浏览器也不正常那就换账号或者暂停一段时间。再看验证码。响应 HTML 里出现“滑动”“验证”等关键词说明触发了风控。不要在爬虫里硬处理验证码最好的策略是停止当前任务并延时重试。验证码通常和 IP、账号行为有关连续高频请求必触发降速比换任何技术都有效。最后是 JSON 解析失败。接口偶尔会返回一段 HTML 而不是 JSON用resp.json()就会抛异常。排错时先打印resp.text[:200]如果看到“繁忙”或“频繁”字样就退避重试而不是改代码。5.2 给爬虫加结构化日志一眼定位请求状态很多爬虫跑挂了都不知道挂在哪一步。建议把所有关键信息用结构化日志记录下来import time start time.time() resp session.get(url, timeout10) elapsed time.time() - start logger.info(company_id%s status%s cost%.2fs, company_id, resp.status_code, elapsed)日志里带上company_id、状态码、耗时这比只打印 URL 有用得多。配合grep 403就能快速统计哪些公司 ID 被拦截如果连续很多条 403说明需要在循环中间加更长的睡眠时间。日志还有一个作用当你要把爬虫交给别人的时候对方不需要看代码先看日志就能了解运行状态。5.3 项目文档该写哪些内容既然标题写了“文档说明”那就要把文档也当成代码的一部分。一个可交接的天眼查爬虫项目至少要有四个文件README.md写清楚这个爬虫能爬什么、需要什么 Python 版本、怎么安装依赖docs/cookie.md用截图或步骤说明如何手动登录取 Cookie以及如何运行login.pydocs/fields.md列出每个字段的中文名、对应的 JSON 路径、是否需要 VIPdocs/troubleshooting.md记录 403、JSON 解析失败、Cookie 过期等问题的解决步骤写文档时不要只写“如何运行”要把环境变量、任务文件格式、日志位置都写清楚。比如company_ids.txt每行一个 ID这个细节如果不写别人拿到代码不知道怎么造数据。文档里还可以加一段“常见误用”提醒不要用固定 sleep 代替随机限速不要在验证码出现时自动重试。最后把电话和邮箱的解析函数单独抽成parse_contact.py这样天眼查改版后你只需要维护这一个文件。本文还有配套的精品资源点击获取