Python自动化抓取与解析巨潮资讯网年报PDF,构建金融文本分析数据源
2026/9/2 6:39:39 网站建设 项目流程

简介:本资源是一款面向金融数据分析人员、量化研究员及财经专业学生的Python自动化工具,专为解决巨潮资讯网上市公司年报PDF难以批量处理的问题而设计,支持年报抓取、下载及PDF→TXT格式转换,便于后续词频统计与文本挖掘。压缩包共13个文件(2.14MB),含4个核心Python脚本(年报链接抓取、PDF转码、文本分析等)、4个XML配置文件(IDEA项目结构与检查规则)、1个覆盖2004–2023年年报链接的Excel数据源、1个requirements.txt依赖清单及README说明文档,结构清晰、模块解耦,便于二次开发与参数调优。目前已有201人学习下载。用户可直接运行脚本完成从网页定位、PDF下载到纯文本提取的全流程,获得标准化TXT文本,并基于此开展关键词提取、情感分析或行业对比研究;配套xlsx链接库与分步脚本设计,显著降低NLP入门门槛,兼顾实用性与教学参考价值。

1. 项目概述:从数据源到分析起点的自动化桥梁

做金融数据分析或者文本挖掘的朋友,经常会遇到一个基础但繁琐的痛点:如何高效、批量地获取上市公司的官方年报(PDF格式),并将其转换为纯净的、可供程序直接分析的TXT文本?手动一个个去网站下载,再用工具转换,效率低下且容易出错。这个项目就是为了解决这个“最后一公里”的问题而诞生的。它瞄准的是国内权威的上市公司信息披露平台——巨潮资讯网,通过Python脚本实现年报的自动抓取、下载,并完成PDF到TXT的格式转换,最终输出结构化的文本文件,为后续的词频分析、情感分析、主题建模等NLP任务铺平道路。简单来说,这就是一个专为金融文本分析研究者、量化投资爱好者或者学生党打造的“数据清洗流水线”入口工具。

2. 核心需求与设计思路拆解

2.1 需求场景深度剖析

这个工具的核心用户画像非常清晰:需要进行上市公司年报文本分析的研究人员。他们的需求链条可以拆解为以下几步:

  1. 数据获取:需要获取特定公司、特定年份(或多个年份)的年报。手动在巨潮资讯网搜索、筛选、下载极其耗时。
  2. 格式转换:下载的年报多为PDF格式,而主流的文本分析库(如Jieba, SnowNLP, Sklearn)无法直接处理PDF,需要先转换为纯文本。
  3. 文本预处理基础:转换后的文本往往包含大量噪音,如页眉、页脚、页码、表格乱码、无关广告信息等,需要初步清洗,得到相对干净的正文内容。
  4. 批量化与自动化:上述过程需要对成百上千份年报进行操作,因此自动化脚本是刚性需求。

本工具精准地覆盖了前三个核心环节,并将它们串联成一个自动化流程。它不试图做一个大而全的金融数据平台,而是聚焦于解决“获取可分析文本”这一具体、高频的痛点。

2.2 技术方案选型与考量

为什么用Python?这是由整个任务链条的技术生态决定的。

  • 爬虫与网络请求:Python的requests库简单强大,BeautifulSouplxml用于解析网页结构,是处理巨潮资讯网这类动态加载(实际多为后端渲染,相对友好)网站的成熟方案。相比其他语言,Python在这方面的库丰富且易上手。
  • PDF解析:这是关键且最容易出问题的环节。我们选择了pdfplumber库,而不是常见的PyPDF2pdfminer。原因在于pdfplumber在提取文本时能更好地保持原始布局和顺序,对于包含简单表格的年报,提取效果更准确,乱码和顺序错乱的概率更低。虽然速度可能稍慢,但对于准确性要求高的文本分析,这是值得的。
  • 流程自动化与健壮性:需要处理网络超时、PDF损坏、页面结构变动等异常。Python的try-except机制和日志记录模块logging可以很好地构建一个健壮的脚本,确保长时间批量运行时,单次失败不影响整体任务。
  • 后续分析衔接:产出TXT文件后,可以直接用Python的pandas进行数据管理,用jieba进行分词,无缝对接后续分析流程,形成闭环。

整个设计思路遵循“单一职责”和“管道模式”:每个模块(搜索、下载、转换)只做一件事,通过参数将它们连接起来,使得代码结构清晰,也便于后期维护和扩展(例如,增加其他资讯网站的数据源)。

3. 核心模块解析与实操要点

3.1 巨潮资讯网年报搜索与链接抓取

巨潮资讯网的搜索接口通常是基于查询参数的。我们需要模拟浏览器行为,构建合法的请求来获取年报列表。

关键步骤与代码解析:

  1. 构建查询请求:分析巨潮网搜索页面的网络请求。通常需要构造包含以下关键参数的URL或表单数据:

    • stock:股票代码,如000001
    • searchkey:公司简称或全称
    • plate:指定公告类型,如年报半年报
    • seDate:公告日期范围 通过浏览器的开发者工具(F12)的“网络(Network)”标签,查看搜索时产生的XHR或Fetch请求,可以找到真实的API接口。
  2. 解析响应数据:巨潮网返回的数据通常是JSON格式,里面包含了公告列表。我们需要从中提取出每个公告的标题、链接(通常是相对路径或包含唯一ID的链接)、以及公告日期。

    import requests from bs4 import BeautifulSoup import json import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s') def search_annual_reports(stock_code, year): """ 根据股票代码和年份搜索年报 """ base_url = "http://www.cninfo.com.cn/new/hisAnnouncement/query" headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'http://www.cninfo.com.cn/new/commonUrl/pageOfSearch?url=disclosure/list/search', } # 注意:参数需要根据实际接口调整,这里是一个示例 params = { 'stock': f'{stock_code},', 'tabName': 'fulltext', 'pageSize': 30, 'pageNum': 1, 'seDate': f'{year}-01-01~{year}-12-31', # 年份范围 'searchkey': '年报', # 搜索关键词 } try: resp = requests.post(base_url, headers=headers, params=params, timeout=10) resp.raise_for_status() # 检查请求是否成功 data = resp.json() announcements = data.get('announcements', []) report_list = [] for ann in announcements: # 筛选出标题中包含“年报”且不是“摘要”的PDF公告 title = ann.get('announcementTitle') if '年报' in title and '摘要' not in title and 'PDF' in title: report_list.append({ 'title': title, 'url': 'http://static.cninfo.com.cn/' + ann.get('adjunctUrl'), # 拼接完整下载链接 'date': ann.get('announcementTime') }) logging.info(f"找到{len(report_list)}份{year}年年报。") return report_list except requests.exceptions.RequestException as e: logging.error(f"搜索请求失败: {e}") return [] except json.JSONDecodeError as e: logging.error(f"解析JSON响应失败: {e}") return []

    注意:巨潮资讯网的接口和参数可能会发生变化,上述代码中的URL和参数结构仅为示例。在实际操作前,必须使用开发者工具对当前网站进行抓包分析,以获取最新的接口信息。这是爬虫项目最需要维护的部分。

3.2 年报PDF文件下载

获取到PDF链接后,下载环节相对直接,但需要考虑文件命名和避免重复下载。

实操要点:

  • 命名规范:建议使用{股票代码}_{年份}_{报告标题}.pdf的格式命名,便于管理。注意剔除标题中的非法文件名字符(如\/:*?"<>|)。
  • 断点续传与去重:在下载前检查本地是否已存在同名文件,可以通过比较文件大小或MD5值来判断是否需重新下载。对于大批量任务,这是一个节省时间和流量的好习惯。
  • 设置请求头:模拟浏览器,特别是User-Agent,有些网站会对没有User-Agent或类似爬虫的请求进行限制。
  • 流式下载:使用requests.get(url, stream=True)来下载大文件,可以避免一次性加载到内存,更安全可靠。
    import os from urllib.parse import unquote def download_pdf(report_info, save_dir='./reports_pdf'): """ 下载PDF文件 """ if not os.path.exists(save_dir): os.makedirs(save_dir) # 清理文件名 safe_title = "".join([c for c in report_info['title'] if c not in r'\/:*?"<>|']).replace(' ', '_') filename = f"{report_info.get('stock_code', 'unknown')}_{report_info['date'][:4]}_{safe_title}.pdf" filepath = os.path.join(save_dir, filename) # 检查文件是否已存在且完整(简单通过存在性判断,可扩展为检查大小) if os.path.exists(filepath): logging.info(f"文件已存在,跳过下载: {filename}") return filepath pdf_url = report_info['url'] try: headers = {'User-Agent': 'Mozilla/5.0'} resp = requests.get(pdf_url, headers=headers, stream=True, timeout=30) resp.raise_for_status() with open(filepath, 'wb') as f: for chunk in resp.iter_content(chunk_size=8192): if chunk: f.write(chunk) logging.info(f"下载成功: {filename}") return filepath except Exception as e: logging.error(f"下载失败 {pdf_url}: {e}") return None

3.3 PDF转TXT:核心难点与解决方案

这是工具最核心也最容易踩坑的部分。上市公司的PDF年报通常是扫描版(图片PDF)或文本版。我们的工具主要针对文本版PDF,但也要考虑对扫描版的兼容性提示。

使用pdfplumber进行文本提取:

import pdfplumber def pdf_to_txt(pdf_path, txt_save_dir='./reports_txt'): """ 将PDF文件转换为TXT文本 """ if not os.path.exists(txt_save_dir): os.makedirs(txt_save_dir) txt_filename = os.path.splitext(os.path.basename(pdf_path))[0] + '.txt' txt_path = os.path.join(txt_save_dir, txt_filename) all_text = [] try: with pdfplumber.open(pdf_path) as pdf: for page_num, page in enumerate(pdf.pages): # 提取页面文本 text = page.extract_text() if text: # 可在此处添加简单的每页标记,便于定位 all_text.append(f"--- Page {page_num+1} ---\n{text}\n") else: # 如果extract_text()返回空,可能是扫描件,记录日志 logging.warning(f"{pdf_path} 第{page_num+1}页可能为扫描图像,未提取到文本。") all_text.append(f"--- Page {page_num+1} [扫描图像,文本未提取] ---\n") # 将所有页面文本写入文件 with open(txt_path, 'w', encoding='utf-8') as f: f.writelines(all_text) logging.info(f"转换完成: {txt_path}") return txt_path except Exception as e: logging.error(f"转换PDF失败 {pdf_path}: {e}") return None

关键注意事项与心得:

  1. 编码问题:确保读写文件时使用utf-8编码,避免中文乱码。
  2. 布局保留pdfplumberextract_text()方法会尝试保持文本的阅读顺序。但对于复杂多栏排版,顺序可能错乱。可以尝试page.extract_text(layout=True)或使用page.extract_words()获取单词和坐标后自行排序,但这会复杂很多。对于年报,大多数正文部分顺序提取是可接受的。
  3. 非文本内容处理:表格、图表中的文字提取效果不佳。pdfplumber提供了page.extract_tables()来提取表格数据,但对于分析而言,通常选择忽略或仅做简单记录。我们的首要目标是获取连续的叙述性文本。
  4. 扫描件处理:如果PDF是扫描图片,上述方法将失效。解决方案是引入OCR(光学字符识别),如pytesseract库配合PILpdf2image先将PDF页面转为图片,再进行识别。但这会极大增加处理时间和复杂度,且准确性依赖图片质量。在工具设计上,建议先尝试文本提取,如果发现大量页面无文本,则记录日志并提示用户该文件可能需要OCR处理
  5. 性能考量:处理上百页的PDF时,pdfplumber可能较慢。可以考虑只提取前N页(如管理层讨论与分析部分)以加快速度,这需要根据具体分析需求调整。

4. 工具集成与完整工作流实现

将上述模块整合,并添加一些提升用户体验的功能,形成一个完整的命令行工具。

4.1 主程序流程设计

主程序逻辑应该清晰:

  1. 解析用户输入(如股票代码列表、年份范围)。
  2. 遍历股票代码,调用搜索函数获取年报列表。
  3. 遍历年报列表,下载PDF文件。
  4. 遍历下载的PDF文件,转换为TXT文本。
  5. 在整个过程中,记录详细的日志,包括成功、失败、跳过等信息。
# main.py 示例框架 import argparse import time from your_module import search_annual_reports, download_pdf, pdf_to_txt # 假设功能在单独模块 def main(): parser = argparse.ArgumentParser(description='巨潮资讯网年报抓取与转换工具') parser.add_argument('-s', '--stock', required=True, help='股票代码,多个用逗号分隔,如 000001,000002') parser.add_argument('-y', '--year', required=True, help='年份,如 2022 或 2020-2023') parser.add_argument('--pdf-dir', default='./reports_pdf', help='PDF保存目录') parser.add_argument('--txt-dir', default='./reports_txt', help='TXT保存目录') parser.add_argument('--skip-downloaded', action='store_true', help='跳过已下载的PDF') args = parser.parse_args() stock_codes = [code.strip() for code in args.stock.split(',')] # 解析年份范围 if '-' in args.year: start_year, end_year = map(int, args.year.split('-')) years = range(start_year, end_year + 1) else: years = [int(args.year)] for stock_code in stock_codes: for year in years: logging.info(f"开始处理 {stock_code} {year}...") # 1. 搜索 reports = search_annual_reports(stock_code, year) if not reports: logging.warning(f"未找到{stock_code} {year}年的年报。") continue for report in reports: report['stock_code'] = stock_code # 补充信息 # 2. 下载 pdf_path = download_pdf(report, args.pdf_dir) if not pdf_path: continue # 下载失败,跳过此报告 # 3. 转换 txt_path = pdf_to_txt(pdf_path, args.txt_dir) # 可选:添加短暂延时,避免请求过快 time.sleep(1) logging.info("所有任务处理完毕。") if __name__ == '__main__': main()

4.2 配置与依赖管理

为了便于他人使用,需要提供清晰的requirements.txt文件。

# requirements.txt requests>=2.28.0 beautifulsoup4>=4.11.0 pdfplumber>=0.9.0 lxml>=4.9.0

用户可以通过pip install -r requirements.txt一键安装所有依赖。

4.3 输出结果与后续分析接口

工具运行后,文件目录结构如下:

project/ ├── main.py ├── requirements.txt ├── reports_pdf/ │ ├── 000001_2022_年度报告.pdf │ └── 000002_2022_年度报告.pdf └── reports_txt/ ├── 000001_2022_年度报告.txt └── 000002_2022_年度报告.txt

生成的TXT文件已经去除了PDF的复杂格式,是包含换行符和分页标记的纯文本。接下来,用户就可以使用任何文本分析工具进行处理了。例如,一个简单的词频分析入门:

import jieba from collections import Counter import re def simple_word_freq(txt_path, top_n=50): with open(txt_path, 'r', encoding='utf-8') as f: text = f.read() # 移除分页标记等无关内容 text_clean = re.sub(r'--- Page \d+ ---', '', text) # 使用jieba分词 words = jieba.lcut(text_clean) # 过滤掉纯符号、单个字和停用词(这里简单过滤长度) filtered_words = [w for w in words if len(w) > 1 and not re.match(r'^\W+$', w)] word_counts = Counter(filtered_words) return word_counts.most_common(top_n) # 对单个文件进行分析 freq = simple_word_freq('./reports_txt/000001_2022_年度报告.txt') print(freq[:20]) # 打印前20个高频词

5. 常见问题、排查技巧与优化建议实录

在实际运行过程中,你肯定会遇到各种各样的问题。下面是我在开发和多次使用类似工具中积累的一些“坑”和解决方案。

5.1 网络请求与反爬虫问题

  • 问题:请求失败,返回403、404,或获取不到数据。
  • 排查
    1. 检查User-Agent:确保请求头中包含了常见的浏览器User-Agent字符串。
    2. 检查Referer:有些网站会验证Referer,需要设置为目标网站的合法来源页。
    3. 使用Session:对于需要保持会话的网站,使用requests.Session()对象。
    4. 分析接口变化:巨潮网的API接口地址和参数名可能更新。务必定期使用浏览器开发者工具重新抓包分析,这是维护爬虫的必修课。
    5. 频率限制:在循环请求中添加time.sleep(random.uniform(1, 3))来模拟人工操作间隔,避免IP被临时封锁。

5.2 PDF解析与文本提取乱码

  • 问题:提取出的TXT文本乱码、顺序错乱、或大量空白。
  • 排查与解决
    1. 确认PDF类型:用Adobe Reader等软件打开PDF,看能否正常选中文字。如果不能,则是扫描件,需要OCR方案。
    2. 尝试不同库:如果pdfplumber效果不好,可以尝试pymupdf(又名fitz),它的文本提取速度极快,有时对某些PDF格式兼容更好。
      import fitz # PyMuPDF doc = fitz.open(pdf_path) text = "" for page in doc: text += page.get_text()
    3. 调整提取参数pdfplumberextract_text()layout,x_tolerance,y_tolerance等参数,微调它们可能改善对复杂版式的提取效果。
    4. 处理加密PDF:极少情况下年报PDF可能有简单密码保护(如000000),pdfplumber打开时需要password参数。

5.3 文件管理与错误处理

  • 问题:脚本中途崩溃,或重复下载了相同文件。
  • 建议
    1. 完善的日志:使用Python的logging模块记录INFO、WARNING、ERROR等级别的日志,并输出到文件,便于事后排查。
    2. 持久化记录:可以将已成功处理的股票代码、年份、报告标题记录在一个JSON文件或轻量级数据库(如sqlite3)中。每次运行时先查询记录,避免重复工作。
    3. 异常捕获与继续:在遍历股票和报告的循环内部做好异常捕获,确保一个报告的失败不会导致整个程序停止。
      try: # 下载和转换操作 except Exception as e: logging.error(f"处理报告{report_title}时发生未知错误: {e}", exc_info=True) # exc_info打印堆栈 continue # 继续下一个报告

5.4 性能优化建议

  • 并发下载:对于大量文件,可以使用concurrent.futures.ThreadPoolExecutor实现多线程下载,显著提升I/O密集型任务的效率。但要注意对目标网站的压力,控制并发数(如3-5个线程)
  • 增量更新:设计脚本时,考虑支持“增量模式”,只下载和处理新的或之前失败的报告。
  • 缓存页面:对于搜索请求的结果,可以考虑在一定时间内(如1小时)缓存到本地,避免频繁请求相同内容。

5.5 扩展性思考

这个工具是一个很好的起点,你可以根据需求轻松扩展:

  • 多数据源:将搜索模块抽象成接口,可以适配其他信息披露网站(如上海证券交易所、深圳证券交易所官网)。
  • 结构化信息提取:在PDF转TXT后,可以使用正则表达式或更高级的NLP模型,尝试提取特定章节(如“主要财务指标”、“董事、监事、高级管理人员情况”),形成结构化数据。
  • 集成OCR:增加一个分支流程,当检测到PDF为扫描件时,自动调用Tesseract-OCR进行识别,实现全类型PDF覆盖。
  • 图形界面:使用PyQttkinter为不熟悉命令行的用户制作一个简单的GUI,方便输入参数和查看进度。

最后,我想分享一点个人体会:这类数据获取和预处理工具的价值,在于将研究者从重复、机械的劳动中解放出来,把精力集中在更有创造性的分析工作上。它的稳定性比华丽的功能更重要。因此,在开发时,务必把日志、错误处理和健壮性放在首位。一开始可能只需要处理几十份报告,但当你的分析扩展到全市场、十年维度时,一个能稳定运行数小时甚至数天的脚本,才是真正可靠的生产力工具。在运行大规模任务前,先用小样本(如2-3家公司,1-2个年份)充分测试所有环节,确保流程畅通无阻。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询