1. 项目概述与核心价值最近在做一个市场分析项目需要批量获取一批目标公司的工商信息、股东构成和经营状况。手动去查几百家公司光是复制粘贴就能把人逼疯。这时候网络爬虫就成了我的“救命稻草”而天眼查作为国内最权威的企业信息平台之一自然成了数据源的首选。今天我就来详细拆解一下如何用Python构建一个稳定、高效且相对“友好”的天眼查企业数据爬虫。这不仅仅是写几行代码那么简单它涉及到反爬策略的对抗、数据结构的解析以及如何在合法合规的框架内最大化我们的效率。这个爬虫的核心价值在于它能将散落在天眼查网页上的结构化信息自动化地采集并整理成我们熟悉的格式比如CSV或Excel。无论是用于竞品分析、投资尽调、市场研究还是学术论文的数据收集都能极大提升效率。但必须强调我们的一切操作都应建立在尊重网站robots.txt规则、不进行恶意攻击、不将数据用于非法牟利的前提下。我们的目标是学习技术解决实际问题而不是给别人的服务器添堵。2. 环境准备与核心库选型工欲善其事必先利其器。在开始编码之前我们需要搭建一个合适的Python环境并选择趁手的工具库。这里我推荐使用Python 3.8及以上版本兼容性和稳定性都比较好。2.1 核心库安装与作用解析打开你的终端或命令行我们通过pip来安装几个核心库。别小看这几个库它们各自承担着爬虫流水线上的关键环节。pip install requests pip install beautifulsoup4 pip install lxml pip install pandasrequests: 这是我们的“网络搬运工”。它负责向天眼查的服务器发送HTTP请求比如GET请求来获取网页并把服务器的响应HTML代码带回来。它是所有基于HTTP协议交互的基石。beautifulsoup4(常简写为bs4): 你可以把它想象成一个智能的“网页解析器”。requests拿回来的是一大坨混杂着标签、样式和内容的HTML字符串人类很难直接阅读。BeautifulSoup的作用就是把这坨“乱麻”解析成一棵结构清晰的树DOM树然后允许我们像使用导航地图一样通过标签名、CSS类名、ID等属性精准地找到我们想要的数据所在的位置。lxml: 这是BeautifulSoup背后一个高效的解析器引擎。虽然bs4也支持Python内置的html.parser但lxml在解析速度和容错能力上通常更胜一筹特别是在处理复杂或略有瑕疵的HTML时。pandas: 我们的“数据整理大师”。爬取到的数据往往是零散的字典或列表pandas可以轻松地将它们组织成结构化的DataFrame并一键导出为CSV、Excel等格式方便后续进行数据分析。注意在实际操作中天眼查的反爬机制比较严格仅靠requests可能很快就会被屏蔽。因此我们往往还需要用到selenium库来模拟浏览器行为或者使用维护Cookie/Session、设置代理IP、添加随机请求头等技术。为了聚焦核心流程我们先从基础版本讲起后续再深入反爬策略。2.2 开发环境配置建议我个人的习惯是使用VSCode配合Jupyter Notebook插件进行数据爬虫的开发和调试。Notebook的单元格执行模式非常适合爬虫这种需要逐步测试请求、解析逻辑的工作。当然PyCharm或纯粹的Python脚本文件也是完全可行的。关键在于保持代码的清晰和模块化。3. 网页结构分析与数据定位策略写爬虫就像“按图索骥”我们得先有一张“图”——也就是目标网页的HTML结构。拿到结构才能知道数据藏在哪个“柜子”的哪个“抽屉”里。3.1 获取与分析页面源代码首先我们手动打开天眼查官网搜索一个示例公司比如“北京字节跳动科技有限公司”。在详情页按下浏览器F12键打开开发者工具。第一步定位数据区块。我们需要的信息如公司名、法定代表人、注册资本、成立日期等通常都包裹在特定的div标签中并且会有唯一的class名或id。在开发者工具的“元素”Elements面板使用左上角的箭头工具点击页面上你想获取的数据比如公司名称工具会自动在代码面板中高亮显示对应的HTML片段。第二步分析结构规律。你会发现天眼查的页面结构是高度模板化的。同一类信息比如“基本信息”、“股东信息”的HTML结构在不同公司页面几乎是相同的。例如公司名称很可能在一个h1标签里或者在一个class包含company-name的span里。我们的任务就是找到这些稳定的“特征”并用BeautifulSoup的查找方法将其定位出来。一个关键技巧多用class慎用id。网页中id通常是唯一的但天眼查可能对动态内容使用变化的id。而用于样式控制的class名往往更稳定。观察那些看起来像是专门为包裹数据而设计的class例如content、info、detail、table等。3.2 使用BeautifulSoup进行精准提取假设我们通过分析发现公司名称位于一个h1 classname标签内。那么提取代码大致如下from bs4 import BeautifulSoup import requests # 假设我们已经获取了网页HTML内容存储在变量 html_content 中 soup BeautifulSoup(html_content, lxml) # 使用lxml解析器 # 查找第一个h1标签且其class属性为name company_name_tag soup.find(h1, class_name) if company_name_tag: company_name company_name_tag.get_text(stripTrue) # 获取标签内文本并去除首尾空格 print(f公司名称: {company_name}) else: print(未找到公司名称标签)对于像“股东信息”这样的表格数据我们需要先定位到整个表格然后遍历它的行tr和单元格td。# 假设股东信息在一个class为shareholder-table的表格中 shareholder_table soup.find(table, class_shareholder-table) if shareholder_table: shareholders [] # 跳过表头通常第一个tr是表头 for row in shareholder_table.find_all(tr)[1:]: cells row.find_all(td) if len(cells) 2: # 假设至少有两列股东名称、出资比例 name cells[0].get_text(stripTrue) ratio cells[1].get_text(stripTrue) shareholders.append({股东: name, 出资比例: ratio}) print(shareholders)实操心得天眼查的页面结构可能会随着前端改版而调整。因此你的选择器如class_name不能写死。一个健壮的爬虫应该具备一定的容错能力比如同时尝试多个可能的class名或者在找不到目标时记录日志以便后续调整策略而不是直接崩溃。4. 核心爬取流程与反爬对抗实战有了数据定位策略我们就可以构建完整的爬取流程了。但直接上手很容易碰壁因为天眼查有完善的反爬虫机制。4.1 基础请求与反爬初步应对一个最基础的requestsGET请求看起来是这样的import requests url https://www.tianyancha.com/company/2310230 # 示例公司ID headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } response requests.get(url, headersheaders) print(response.status_code) print(response.text[:500]) # 打印前500字符看看这里我们做了第一层反爬对抗设置请求头Headers特别是User-Agent。这告诉服务器我们是一个“正常的浏览器”在访问而不是一个脚本。你可以从自己浏览器的开发者工具“网络”Network面板中复制任意一个请求的User-Agent来用。然而仅凭这个你很可能收到一个状态码为200但内容却是反爬验证页面比如要求滑动拼图或点选验证码的HTML。这说明服务器通过其他特征如Cookie、访问频率、IP地址识别出了爬虫。4.2 进阶策略会话维持与请求头伪装1. 使用Session对象requests.Session()可以自动管理Cookie在一次会话中保持登录状态或服务器设置的会话标识这比单次请求更接近真实用户。session requests.Session() session.headers.update(headers) # 为整个会话设置头部 # 首次访问可能获取必要的初始Cookie first_response session.get(https://www.tianyancha.com) # 然后用同一个session去请求目标页面 target_response session.get(url)2. 完善请求头伪装除了User-Agent还应添加一些常见的浏览器头部信息让请求看起来更“逼真”。headers { User-Agent: Mozilla/5.0 ..., Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, Connection: keep-alive, Upgrade-Insecure-Requests: 1, Sec-Fetch-Dest: document, Sec-Fetch-Mode: navigate, Sec-Fetch-Site: none, Sec-Fetch-User: ?1, Cache-Control: max-age0, }3. 控制访问频率这是最重要的道德和技术准则。疯狂地连续请求会迅速触发IP封禁。必须在请求之间加入随机延时。import time import random def delay(): time.sleep(random.uniform(2, 5)) # 随机等待2到5秒 # 在每次关键请求后调用 response session.get(url) delay()4.3 终极方案Selenium模拟浏览器当上述所有方法都失效页面必须通过JavaScript渲染才能看到数据或者验证码无法绕过时我们就需要祭出大杀器——selenium。它可以自动化控制一个真实的浏览器如Chrome所有操作都和真人一模一样。from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time # 初始化Chrome浏览器驱动需提前下载chromedriver并配置PATH options webdriver.ChromeOptions() # 可选添加参数如无头模式不显示浏览器界面 # options.add_argument(--headless) driver webdriver.Chrome(optionsoptions) try: driver.get(url) # 显式等待直到某个关键元素如公司名加载出来 company_element WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, company-name)) ) # 此时页面已完全加载可以直接获取渲染后的HTML html_content driver.page_source # 接下来就可以用BeautifulSoup解析html_content了 finally: driver.quit() # 一定要记得关闭浏览器释放资源使用Selenium的代价速度慢资源消耗大。它更适合用于登录、解决复杂验证码等“关键突破”获取到关键Cookie或Token后后续的批量数据请求可以尝试切换回轻量级的requestsSession模式。5. 数据解析、清洗与存储爬取到HTML并解析出原始文本数据后工作只完成了一半。这些数据往往夹杂着无关字符、空格、换行格式也不统一需要进行清洗和结构化。5.1 数据清洗的常见操作去除空白字符使用字符串的.strip()方法去除首尾空格、换行符。处理多余空格和换行使用.replace(‘\n’, ‘’)或正则表达式re.sub(r‘\s’, ‘ ‘, text)将多个空白字符替换为单个空格。提取关键部分例如注册资本可能是“1000万元人民币”我们可能只想提取数字“1000”。这时就需要用正则表达式re.search(r‘(\d(?:\.\d)?)’, text)来匹配。统一格式将日期字符串统一转换为datetime对象将货币统一为数字类型等。import re def clean_text(text): if not text: return None # 去除首尾空白 text text.strip() # 将多个连续空白包括换行替换为一个空格 text re.sub(r‘\s’, ‘ ‘, text) return text def extract_number(text): # 从字符串中提取第一个出现的整数或浮点数 match re.search(r‘(\d(?:\.\d)?)’, text) return float(match.group(1)) if match else None # 示例清洗 raw_data “\n\n 注册资本1000万元人民币 \n” cleaned clean_text(raw_data) # “注册资本1000万元人民币” capital_num extract_number(cleaned) # 1000.05.2 使用Pandas进行结构化存储清洗后的数据我们通常按公司为单位存储为字典然后将多个字典组成列表最后用pandas.DataFrame来处理。import pandas as pd # 假设我们爬取了三家公司信息 company_list [ { ‘公司名称‘: ‘字节跳动‘, ‘法定代表人‘: ‘张一鸣‘, ‘注册资本(万元)‘: 1000, ‘成立日期‘: ‘2012-03-09‘, ‘状态‘: ‘在业‘ }, { ‘公司名称‘: ‘阿里巴巴‘, ‘法定代表人‘: ‘张勇‘, ‘注册资本(万元)‘: 50000, ‘成立日期‘: ‘1999-09-09‘, ‘状态‘: ‘在业‘ }, # ... 更多公司 ] # 转换为DataFrame df pd.DataFrame(company_list) # 查看数据 print(df.head()) print(df.info()) # 保存到CSV文件 df.to_csv(‘tianyancha_companies.csv‘, indexFalse, encoding‘utf-8-sig‘) # utf-8-sig解决Excel打开中文乱码 # 也可以保存到Excel # df.to_excel(‘tianyancha_companies.xlsx‘, indexFalse)DataFrame提供了强大的数据清洗、筛选和分析能力。例如你可以轻松筛选出注册资本大于1000万的公司df[df[‘注册资本(万元)‘] 1000]。6. 工程化优化与错误处理一个只能爬取一两个页面的脚本是玩具一个能稳定运行、处理各种异常、管理成千上万条数据的爬虫才叫工具。这就需要工程化思维。6.1 模块化设计将代码按功能拆分成不同的模块或函数提高可读性和可维护性。config.py: 存放常量如请求头、URL模板、文件保存路径、数据库连接信息。spider.py: 核心爬取逻辑包含发送请求、解析页面的函数。utils.py: 工具函数如清洗数据、处理日期、随机延时函数。main.py: 主程序入口控制整个爬取流程读取公司列表、遍历、保存。log_config.py: 日志配置。6.2 健壮的错误处理与日志记录网络请求充满不确定性连接超时、页面404、HTML结构变化、触发反爬被禁……我们的程序必须能优雅地处理这些异常而不是崩溃。import logging import requests from requests.exceptions import RequestException, Timeout # 配置日志 logging.basicConfig(levellogging.INFO, format‘%(asctime)s - %(levelname)s - %(message)s‘, handlers[ logging.FileHandler(‘spider.log‘), logging.StreamHandler() ]) logger logging.getLogger(__name__) def safe_request(url, session, max_retries3): for attempt in range(max_retries): try: response session.get(url, timeout10) # 设置超时 response.raise_for_status() # 如果状态码不是200抛出HTTPError异常 return response except Timeout: logger.warning(f“请求 {url} 超时第{attempt1}次重试...“) time.sleep(2 ** attempt) # 指数退避策略 except RequestException as e: logger.error(f“请求 {url} 时发生错误: {e}“) if attempt max_retries - 1: return None # 重试多次后仍失败返回None time.sleep(1) return None # 在解析数据时也要加入容错 def parse_company_info(html): try: soup BeautifulSoup(html, ‘lxml‘) # ... 解析逻辑 # 如果找不到关键元素返回None或空字典 if not some_critical_element: logger.warning(“页面结构可能已变化未找到关键数据“) return {} return info_dict except Exception as e: logger.error(f“解析HTML时发生未知错误: {e}“) return {}6.3 增量爬取与断点续传对于大规模爬取不可能每次都从头开始。我们需要记录爬取状态。记录已爬取的URL或公司ID将成功爬取的公司ID写入一个文件或数据库。每次启动时先加载这个列表跳过已爬取的公司。保存中间状态可以将爬取到的数据分批比如每100条保存一次而不是全部放在内存里最后一起存。这样即使程序中途因错误或断电停止也只损失最近一批数据。import json import os STATE_FILE ‘crawler_state.json‘ def load_state(): if os.path.exists(STATE_FILE): with open(STATE_FILE, ‘r‘, encoding‘utf-8‘) as f: return json.load(f) return {‘last_company_id‘: None, ‘failed_list‘: []} def save_state(state): with open(STATE_FILE, ‘w‘, encoding‘utf-8‘) as f: json.dump(state, f, ensure_asciiFalse, indent2) # 在主循环中使用 state load_state() last_id state.get(‘last_company_id‘) for company_id in company_id_list: if last_id and company_id last_id: continue # 跳过已爬取的 # ... 爬取逻辑 if success: state[‘last_company_id‘] company_id save_state(state) # 每成功一个就保存一次状态7. 法律、伦理与最佳实践这是所有技术讨论的基石必须放在最后也是最重要的位置强调。1. 遵守robots.txt协议访问https://www.tianyancha.com/robots.txt查看网站允许或禁止爬虫访问的路径。尊重这些规则是网络爬虫最基本的礼仪。2. 控制访问频率这是最直接的善意表现。像前面提到的在请求间添加随机延时例如3-10秒避免对目标服务器造成类似DDoS攻击的负载。宁可慢一点稳一点。3. 识别并尊重版权与隐私天眼查上的企业信息其汇编成果可能享有著作权。我们爬取数据应限于个人学习、研究或企业内部合规使用。绝对禁止将大规模爬取的数据用于商业售卖、公开传播或从事不正当竞争。对于涉及个人隐私的信息如某些股东的个人信息应格外谨慎避免爬取和存储。4. 用户代理标识在请求头中设置清晰的User-Agent标明自己是一个“善意”的爬虫甚至可以包含一个联系方式邮箱以便网站管理员在认为有必要时能联系到你。例如User-Agent: MyResearchBot/1.0 (contact: researcherexample.com)。5. 数据使用限制爬取的数据应在合理使用范围内消化。不要试图爬取整个网站的所有数据这既不现实也不合法。明确你的数据需求边界。个人体会技术是一把双刃剑。爬虫能力越强责任就越大。在整个开发和运行过程中我始终抱着“借阅”而非“掠夺”的心态。我的代码里设置了长达5-8秒的随机延迟并且只爬取项目必需的具体字段。在一次爬取中因为网络波动触发了网站的异常流量警报我立即停止了脚本等待了数小时后再继续。这种克制不仅是为了项目的长远进行更是对数据源和互联网共享精神的一种尊重。最终这个爬虫平稳运行了数周采集到了所需的数据而没有给对方的服务器带来可感知的负担。这才是可持续的技术应用方式。