Python网络爬虫实战:从天眼查高效采集企业数据的技术解析
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 class="name">标签内。那么提取代码大致如下:
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(strip=True) # 获取标签内文本,并去除首尾空格 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(strip=True) ratio = cells[1].get_text(strip=True) 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, headers=headers) 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/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.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-age=0', }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(options=options) 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后,后续的批量数据请求可以尝试切换回轻量级的requests+Session模式。
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‘, index=False, encoding=‘utf-8-sig‘) # utf-8-sig解决Excel打开中文乱码 # 也可以保存到Excel # df.to_excel(‘tianyancha_companies.xlsx‘, index=False)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(level=logging.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_retries=3): for attempt in range(max_retries): try: response = session.get(url, timeout=10) # 设置超时 response.raise_for_status() # 如果状态码不是200,抛出HTTPError异常 return response except Timeout: logger.warning(f“请求 {url} 超时,第{attempt+1}次重试...“) 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_ascii=False, indent=2) # 在主循环中使用 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: researcher@example.com)。
5. 数据使用限制:爬取的数据应在合理使用范围内消化。不要试图爬取整个网站的所有数据,这既不现实,也不合法。明确你的数据需求边界。
个人体会:技术是一把双刃剑。爬虫能力越强,责任就越大。在整个开发和运行过程中,我始终抱着“借阅”而非“掠夺”的心态。我的代码里设置了长达5-8秒的随机延迟,并且只爬取项目必需的具体字段。在一次爬取中,因为网络波动触发了网站的异常流量警报,我立即停止了脚本,等待了数小时后再继续。这种克制,不仅是为了项目的长远进行,更是对数据源和互联网共享精神的一种尊重。最终,这个爬虫平稳运行了数周,采集到了所需的数据,而没有给对方的服务器带来可感知的负担。这才是可持续的技术应用方式。
