1. 项目背景与核心价值
股票数据爬取是量化投资、金融分析的基础工作之一。东方财富网作为国内领先的财经门户,其上市公司业绩报表数据具有以下特点:
- 数据维度完整(营收、净利润、每股收益等关键指标)
- 更新及时(财报季实时更新)
- 历史数据可追溯(部分数据可查询多年历史)
这个爬虫项目要解决的核心痛点是:
- 人工收集效率低下(需逐个公司页面查看)
- 数据格式不统一(网页展示与结构化存储需求矛盾)
- 持续更新维护困难(财报季需要重复操作)
2. 技术方案设计
2.1 整体架构
mermaid复制graph TD
A[东方财富网] -->|requests| B(HTML下载)
B -->|BeautifulSoup| C[数据解析]
C -->|pandas| D[数据清洗]
D -->|SQLAlchemy| E[MySQL存储]
2.2 技术选型说明
- 爬虫框架:requests + BeautifulSoup组合
- 相比Scrapy更轻量级
- 适合中等规模数据抓取(单次约3000+上市公司)
- 数据存储:MySQL 8.0
- 支持事务处理(避免数据部分写入)
- 便于后续SQL分析查询
- ORM工具:SQLAlchemy
- 提供连接池管理
- 自动处理数据类型转换
3. 核心实现细节
3.1 网页结构分析
目标页面示例:http://data.eastmoney.com/bbsj/202206/yjbb.html
关键特征:
- 数据通过异步加载(XHR请求)
- 分页参数:
pageNum和pageSize - 数据接口返回JSON格式
3.2 反爬应对策略
- 请求头伪装:
python复制headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'http://data.eastmoney.com/bbsj/'
}
- 请求频率控制:
python复制import random
time.sleep(random.uniform(0.5, 1.5))
3.3 数据解析关键代码
python复制def parse_json(response):
data = response.json()
items = data['result']['data']
for item in items:
yield {
'stock_code': item['SECURITY_CODE'],
'company_name': item['SECURITY_NAME_ABBR'],
'report_date': item['REPORT_DATE'],
'revenue': float(item['TOTAL_OPERATE_INCOME']),
'net_profit': float(item['PARENT_NETPROFIT'])
}
4. 数据库设计
4.1 表结构
sql复制CREATE TABLE `stock_reports` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`stock_code` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL,
`company_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`report_date` date NOT NULL,
`revenue` decimal(15,2) DEFAULT NULL,
`net_profit` decimal(15,2) DEFAULT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_stock_date` (`stock_code`,`report_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4.2 批量写入优化
使用SQLAlchemy的bulk_insert_mappings方法:
python复制from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)
session = Session()
try:
session.bulk_insert_mappings(StockReport, data_list)
session.commit()
except:
session.rollback()
raise
finally:
session.close()
5. 完整代码实现
python复制import requests
import time
import random
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from models import StockReport
# 数据库配置
DB_URI = 'mysql+pymysql://user:password@localhost:3306/stock_data'
# 爬取参数
BASE_URL = 'http://datacenter.eastmoney.com/api/data/get'
PARAMS = {
'type': 'RPT_LICO_FN_CPD',
'sty': 'ALL',
'p': '1',
'ps': '50',
'st': 'REPORT_DATE',
'sr': '-1',
'filter': '(REPORTDATE=^2022-06-30^)'
}
def get_data(page):
params = PARAMS.copy()
params['p'] = str(page)
try:
response = requests.get(
BASE_URL,
params=params,
headers=headers,
timeout=10
)
return response.json() if response.status_code == 200 else None
except Exception as e:
print(f"Request failed: {e}")
return None
def main():
engine = create_engine(DB_URI)
Session = sessionmaker(bind=engine)
page = 1
while True:
print(f"Processing page {page}")
data = get_data(page)
if not data or not data.get('result', {}).get('data'):
break
session = Session()
try:
items = []
for item in data['result']['data']:
items.append({
'stock_code': item['SECURITY_CODE'],
'company_name': item['SECURITY_NAME_ABBR'],
'report_date': item['REPORT_DATE'],
'revenue': float(item['TOTAL_OPERATE_INCOME']),
'net_profit': float(item['PARENT_NETPROFIT'])
})
session.bulk_insert_mappings(StockReport, items)
session.commit()
except Exception as e:
session.rollback()
print(f"Database error: {e}")
finally:
session.close()
page += 1
time.sleep(random.uniform(1, 2))
if __name__ == '__main__':
main()
6. 运维与监控
6.1 日志记录配置
python复制import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('stock_spider.log'),
logging.StreamHandler()
]
)
6.2 异常处理增强
python复制class DataQualityError(Exception):
"""自定义数据质量异常"""
pass
def validate_item(item):
if not item['stock_code'].isdigit():
raise DataQualityError(f"Invalid stock code: {item['stock_code']}")
# 其他验证规则...
7. 性能优化建议
- 异步请求优化:
python复制import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response:
return await response.json()
async def main_async():
async with aiohttp.ClientSession() as session:
tasks = []
for page in range(1, 11):
url = build_url(page)
tasks.append(fetch(session, url))
results = await asyncio.gather(*tasks)
# 处理结果...
- 分布式扩展方案:
- 使用Redis作为任务队列
- 部署多个爬虫worker节点
- 通过Scrapy-Redis实现分布式调度
8. 数据应用示例
8.1 财务指标分析
sql复制-- 营收同比增长TOP10
SELECT
stock_code,
company_name,
report_date,
revenue,
(revenue - LAG(revenue) OVER (PARTITION BY stock_code ORDER BY report_date)) /
LAG(revenue) OVER (PARTITION BY stock_code ORDER BY report_date) AS growth_rate
FROM stock_reports
ORDER BY growth_rate DESC
LIMIT 10;
8.2 数据可视化
python复制import matplotlib.pyplot as plt
import pandas as pd
df = pd.read_sql("""
SELECT * FROM stock_reports
WHERE report_date = '2022-06-30'
""", engine)
plt.figure(figsize=(12, 6))
df['revenue'].hist(bins=50)
plt.title('Revenue Distribution')
plt.xlabel('Revenue (100 million)')
plt.ylabel('Company Count')
plt.show()
9. 法律合规要点
-
严格遵守robots.txt协议
- 东方财富网robots.txt部分限制:
code复制User-agent: * Disallow: /api/ -
数据使用建议:
- 仅用于个人学习研究
- 不进行商业用途
- 控制请求频率(建议≤1请求/秒)
- 数据存储安全:
- 数据库访问权限控制
- 敏感配置信息加密处理
- 定期数据备份机制
10. 常见问题排查
10.1 数据获取失败
可能原因:
- 接口参数变更(需重新分析网络请求)
- IP访问限制(建议使用代理轮询)
解决方案:
python复制# 代理设置示例
proxies = {
'http': 'http://proxy.example.com:8080',
'https': 'http://proxy.example.com:8080'
}
response = requests.get(url, proxies=proxies)
10.2 数据库写入冲突
错误现象:
code复制IntegrityError: (1062, "Duplicate entry '600000-2022-06-30'")
解决方案:
python复制# 使用ON DUPLICATE KEY UPDATE语法
insert_stmt = insert(StockReport).values(item)
on_duplicate_stmt = insert_stmt.on_duplicate_key_update(
revenue=insert_stmt.inserted.revenue,
net_profit=insert_stmt.inserted.net_profit
)
session.execute(on_duplicate_stmt)
11. 项目扩展方向
-
多数据源整合:
- 对接新浪财经、雪球等平台
- 建立统一数据仓库
-
自动化预警系统:
- 设置财务指标阈值
- 邮件/短信通知异常波动
-
机器学习应用:
- 基于历史数据的业绩预测
- 财务造假风险识别模型
-
前端展示系统:
- Flask/Django开发Web界面
- 交互式数据看板
提示:在实际开发中,建议使用配置文件管理数据库连接、请求参数等设置,避免硬编码。同时可以考虑使用Airflow等工具实现定时自动抓取。