1. 项目背景与价值解析
股票投资分析中,上市公司的财务数据是最核心的研究基础。东方财富网作为国内领先的金融数据平台,其公布的上市公司业绩报表数据全面、更新及时,是量化投资、基本面分析的重要数据来源。但手动收集这些数据不仅效率低下,而且难以保证数据的完整性和一致性。
这个爬虫项目正是为了解决这个痛点——通过自动化手段从东方财富网抓取上市公司业绩报表数据,并结构化存储到MySQL数据库中。这种方案相比Excel手工整理有三个显著优势:
- 数据获取效率提升数十倍
- 支持历史数据自动更新维护
- 便于后续进行SQL查询分析和可视化
我在金融科技领域工作多年,经常需要处理类似的数据采集需求。下面就把这个实战项目的完整实现过程,包括关键的技术细节和踩坑经验分享给大家。
2. 技术方案设计
2.1 整体架构设计
项目采用经典的三层爬虫架构:
code复制爬取层 → 解析层 → 存储层
│ │ │
▼ ▼ ▼
Requests → BeautifulSoup → MySQL
具体技术选型如下:
- 爬取工具:Python requests库(轻量级,适合中小规模爬取)
- 解析工具:BeautifulSoup(对动态渲染要求不高的静态页面足够使用)
- 存储方案:MySQL 8.0(关系型数据库适合结构化财务数据)
- 调度方式:APScheduler定时任务(适合定期更新场景)
提示:如果目标数据量非常大(如全市场10年历史数据),建议改用Scrapy框架并增加分布式部署方案。
2.2 目标页面分析
以东方财富网的个股业绩报表页面为例(示例URL:http://data.eastmoney.com/bbsj/202306/yjbb.html),通过浏览器开发者工具分析可见:
- 数据通过服务端渲染返回,直接包含在HTML中
- 表格数据位于
<table class="dataview-body">标签内 - 分页通过URL参数控制(如
&page=2) - 关键字段包括:股票代码、名称、营业收入、净利润等20+个财务指标
3. 核心代码实现
3.1 数据库设计
创建存储表时需特别注意财务数据的精度问题:
sql复制CREATE TABLE stock_finance (
id INT AUTO_INCREMENT PRIMARY KEY,
stock_code VARCHAR(10) NOT NULL COMMENT '股票代码',
stock_name VARCHAR(50) COMMENT '股票名称',
report_date DATE COMMENT '报告期',
operating_income DECIMAL(20,2) COMMENT '营业收入(元)',
net_profit DECIMAL(20,2) COMMENT '净利润(元)',
eps DECIMAL(10,4) COMMENT '每股收益(元)',
# 其他财务指标...
crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY (stock_code, report_date) -- 防止重复存储
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 爬虫核心代码
python复制import requests
from bs4 import BeautifulSoup
import pymysql
from datetime import datetime
def crawl_eastmoney_finance():
# 数据库连接配置
db = pymysql.connect(
host='localhost',
user='your_username',
password='your_password',
database='stock_data',
charset='utf8mb4'
)
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
'Referer': 'http://data.eastmoney.com/bbsj/'
}
base_url = "http://data.eastmoney.com/bbsj/202306/yjbb.html"
try:
# 获取第一页数据
response = requests.get(base_url, headers=headers, timeout=10)
response.encoding = 'utf-8'
soup = BeautifulSoup(response.text, 'html.parser')
# 解析表格数据
table = soup.find('table', {'class': 'dataview-body'})
rows = table.find_all('tr')[1:] # 跳过表头
with db.cursor() as cursor:
for row in rows:
cols = row.find_all('td')
if len(cols) < 10: # 确保是数据行
continue
# 提取关键字段
stock_code = cols[1].text.strip()
stock_name = cols[2].text.strip()
report_date = datetime.strptime(cols[3].text.strip(), '%Y-%m-%d')
operating_income = float(cols[5].text.strip().replace(',', ''))
net_profit = float(cols[7].text.strip().replace(',', ''))
# 构造插入SQL
sql = """INSERT INTO stock_finance
(stock_code, stock_name, report_date, operating_income, net_profit)
VALUES (%s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
operating_income=VALUES(operating_income),
net_profit=VALUES(net_profit)"""
cursor.execute(sql, (stock_code, stock_name, report_date,
operating_income, net_profit))
db.commit()
except Exception as e:
print(f"爬取失败: {str(e)}")
db.rollback()
finally:
db.close()
4. 关键问题与解决方案
4.1 反爬机制应对
东方财富网主要有以下反爬措施:
-
IP限制:解决方案是:
- 控制请求频率(建议2-3秒/次)
- 使用优质代理IP池(商业方案)
- 对于小规模爬取,可以尝试降低并发
-
User-Agent验证:
python复制headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...', 'Accept-Language': 'zh-CN,zh;q=0.9' } -
动态参数:部分接口需要携带
__jsluid等动态生成的cookie。
4.2 数据清洗要点
原始数据常见问题及处理方法:
- 千分位分隔符:
"1,234.56" → 1234.56 - 缺失值处理:
"--" → NULL - 单位统一:确保所有金额单位统一为"元"
- 日期格式化:
"2023-06-30" → DATE类型
4.3 性能优化方案
当需要爬取全市场数据时:
-
分页并行处理:使用
concurrent.futures实现python复制from concurrent.futures import ThreadPoolExecutor def crawl_page(page): url = f"{base_url}?page={page}" # 爬取逻辑... with ThreadPoolExecutor(max_workers=5) as executor: executor.map(crawl_page, range(1, total_pages+1)) -
批量插入:改用
executemany提升数据库写入效率python复制sql = "INSERT INTO ... VALUES (%s, %s, %s)" cursor.executemany(sql, data_list)
5. 数据应用示例
存储到MySQL后,可以方便地进行各种分析:
sql复制-- 查询净利润同比增长TOP10
SELECT stock_name,
(net_profit - LAG(net_profit) OVER (PARTITION BY stock_code ORDER BY report_date)) /
ABS(LAG(net_profit) OVER (PARTITION BY stock_code ORDER BY report_date)) AS growth_rate
FROM stock_finance
WHERE report_date = '2023-06-30'
ORDER BY growth_rate DESC
LIMIT 10;
也可以配合Python可视化:
python复制import matplotlib.pyplot as plt
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://user:password@localhost/stock_data')
df = pd.read_sql("SELECT * FROM stock_finance WHERE report_date='2023-06-30'", engine)
plt.figure(figsize=(12,6))
df['net_profit'].hist(bins=50)
plt.title('A股上市公司净利润分布')
plt.xlabel('净利润(亿元)')
plt.ylabel('公司数量')
plt.show()
6. 法律合规提醒
金融数据爬取需特别注意:
- 严格遵守网站的robots.txt规定
- 不得用于商业用途(除非获得授权)
- 控制请求频率,避免对目标服务器造成负担
- 个人学习研究使用也应遵守相关法律法规
我在实际项目中会添加自动限速功能:
python复制import time
from random import uniform
def throttled_request(url):
time.sleep(uniform(1.0, 3.0)) # 随机延迟
return requests.get(url, headers=headers)
这个项目最让我有成就感的是,通过简单的技术方案就解决了金融数据分析中的基础数据获取难题。建议初次尝试时可以从小规模数据开始,逐步完善异常处理和性能优化。如果遇到403错误,不妨试试更换User-Agent或者添加Referer头信息,这些小技巧往往能解决大部分反爬问题。