
简介本资源是一款面向金融数据分析人员、量化研究员及高校财经类专业学生的Python自动化工具专为解决巨潮资讯网上市公司年报PDF难以批量处理、不便词频分析的痛点而设计。工具实现从网页抓取年报链接、自动下载PDF、提取文本并转为TXT全流程显著提升年报文本挖掘效率。压缩包共13个文件2.14MB含4个核心Python脚本年报链接抓取、PDF转码、文本分析等、4个XML配置文件IDE与版本管理相关、1个覆盖2004–2023年年报链接的Excel数据源、1个requirements.txt依赖清单及README说明文档结构清晰、模块解耦便于二次开发与参数调优。目前已有201人学习下载使用者可直接运行脚本获取标准化TXT文本结合关键词过滤与清洗功能开展财务语义分析、风险词识别或行业对比研究是连接监管数据源与NLP分析落地的关键中间件。1. 项目缘起从数据需求到自动化工具的诞生作为一名长期关注金融市场和公司研究的从业者我经常需要批量分析上市公司的年报。巨潮资讯网作为国内权威的上市公司信息披露平台是获取这些PDF格式年报的官方渠道。然而手动下载几十甚至上百份年报再逐一打开PDF复制粘贴文本或者用OCR工具转换这个过程不仅耗时费力而且极易出错尤其是在需要构建文本分析语料库时格式混乱和编码问题简直是噩梦。最近我需要为一个词频分析和主题挖掘的项目准备数据源目标是对某个行业过去五年的所有年报进行文本分析。面对这个需求手动操作显然不现实。于是我决定用Python写一个小工具实现从巨潮资讯网自动抓取指定公司的年报PDF、下载到本地并批量将其内容转换为纯净的TXT文本文件。这样一来后续无论是用jieba分词、TF-IDF统计还是训练LDA主题模型都有了规整、统一格式的数据基础。这个工具的核心价值在于它将一个多步骤、重复性的手动劳动变成了一个一键执行的自动化流程。对于金融分析、学术研究、文本挖掘爱好者来说能节省大量数据预处理的时间让你更专注于核心的分析工作本身。下面我就把这个工具的完整实现思路、代码细节以及我踩过的坑毫无保留地分享出来。2. 核心工具链选型与设计思路在动手写代码之前合理的工具选型和整体设计至关重要。这决定了工具的稳定性、效率以及后续的可维护性。我的核心需求很明确模拟查询 - 解析列表 - 下载PDF - 转换文本。2.1 网络请求与页面解析Requests BeautifulSoup对于抓取网页内容Requests库是Python社区公认的标杆它简单易用功能强大能够轻松处理GET/POST请求、Headers设置、Cookie管理等。巨潮资讯网的搜索列表页和详情页都是标准的HTML用Requests获取页面源码再合适不过。拿到HTML源码后我们需要从中提取出年报的标题、链接和发布时间等信息。这里我选择了BeautifulSoup。它是一个灵活的HTML/XML解析库可以通过标签名、CSS选择器等方式精准定位到我们需要的数据节点。相比于正则表达式BeautifulSoup的语法更直观容错性也更好面对稍有不规范的网页结构时更能从容应对。为什么不直接用SeleniumSelenium模拟浏览器操作能处理JavaScript渲染的页面但它的开销大、速度慢。我仔细查看了巨潮资讯网的搜索页面发现其年报列表是通过后端直接生成的静态HTML没有复杂的JS动态加载。因此轻量级的RequestsBeautifulSoup组合是最高效的选择。2.2 PDF文本提取PyMuPDF (fitz)这是整个工具链中最关键、也最容易出问题的一环。PDF文本提取库的选择直接关系到最终TXT文件的质量。我对比了几个主流方案PyPDF2 / pdfplumber这两个库对简单PDF支持不错但面对中文PDF尤其是扫描版或复杂排版的PDF时提取出的文本经常出现乱序、丢失或乱码。pdfminer.six功能强大解析精度高但API相对复杂配置起来比较繁琐。PyMuPDF (fitz)这是最终的选择。它基于强大的MuPDF引擎对中文PDF的支持非常好提取文本的准确率和顺序保持都令人满意。它的API也很清晰几行代码就能获取所有页面的文本。更重要的是它的速度极快处理大批量PDF时优势明显。安装时需要注意包名是PyMuPDF但导入时使用import fitz。2.3 文件与路径管理os, pathlib我们需要在本地创建有组织的文件夹结构来存放下载的PDF和转换后的TXT。os模块提供了基础的目录操作而pathlibPython 3.4提供了更面向对象、更清晰的路径操作方式能让代码更简洁易读。我会用它们来检查目录是否存在、创建新目录、构建完整的文件保存路径。2.4 整体工作流程设计工具的逻辑流程可以概括为以下四步我将围绕这个流程来构建代码输入与配置用户输入目标公司名称或股票代码和需要抓取的年份范围。列表抓取与过滤工具自动访问巨潮资讯网搜索页面提交查询解析返回的HTML过滤出符合条件如“年报”、“年度报告”的PDF链接列表。PDF批量下载遍历链接列表将PDF文件下载到本地指定的“pdfs”文件夹中并以“公司名_年份.pdf”的格式命名。格式批量转换遍历“pdfs”文件夹中的所有PDF文件使用PyMuPDF逐一提取文本进行必要的清洗如去除过多空白符、页眉页脚然后保存到“txts”文件夹中形成与PDF同名的TXT文件。3. 实战代码拆解从搜索到下载接下来我们进入具体的代码实现环节。我会分模块详细解释并提供完整的、可运行的代码片段。3.1 环境准备与依赖安装首先确保你的Python环境建议3.7及以上已经就绪。然后通过pip安装我们所需的库pip install requests beautifulsoup4 PyMuPDF如果安装PyMuPDF遇到问题可以尝试使用清华镜像源pip install PyMuPDF -i https://pypi.tuna.tsinghua.edu.cn/simple3.2 构建网络请求与解析函数我们需要一个函数来获取搜索页面并解析出结果。这里的关键在于模拟浏览器的请求头User-Agent并正确构建查询参数。import requests from bs4 import BeautifulSoup import time import re def search_annual_reports(company_name, years): 在巨潮资讯网搜索指定公司、指定年份的年报 :param company_name: 公司名称或股票代码如‘贵州茅台’或‘600519’ :param years: 年份列表如 [2022, 2021, 2020] :return: 返回一个列表每个元素是包含‘title’, ‘pdf_url’, ‘publish_date’的字典 base_url http://www.cninfo.com.cn/new/fulltextSearch/full 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 } report_list [] for year in years: # 构建查询参数。巨潮资讯网的搜索接口参数较多这里是最核心的几个。 # ‘searchkey’是关键词我们使用公司名年份‘年报’来精确搜索。 # ‘sdate’和‘edate’是时间范围我们设为该年的第一天和最后一天。 params { searchkey: f{company_name} {year} 年报, sdate: f{year}-01-01, edate: f{year}-12-31, isfulltext: false, # 设为false搜索标题和全文true则只搜全文这里false即可 sortName: pubdate, # 按发布日期排序 sortType: desc, # 降序最新的在前 } try: resp requests.get(base_url, headersheaders, paramsparams, timeout10) resp.raise_for_status() # 如果状态码不是200抛出异常 resp.encoding utf-8 # 显式设置编码避免乱码 soup BeautifulSoup(resp.text, html.parser) # 解析搜索结果列表。具体CSS选择器需要根据巨潮资讯网的实际页面结构调整。 # 通常每个公告项在一个类名为‘el-table__row’或类似的tr/div标签里。 # 这里是一个示例你需要用浏览器的开发者工具F12查看实际结构。 items soup.select(tr.el-table__row) for item in items: title_tag item.select_one(.title a) # 标题和链接通常在a标签里 date_tag item.select_one(.time) # 发布日期 if not title_tag or not date_tag: continue title title_tag.get_text(stripTrue) link title_tag.get(href) publish_date date_tag.get_text(stripTrue) # 关键过滤只保留标题中含有‘年报’或‘年度报告’的PDF链接 if (年报 in title or 年度报告 in title) and link.endswith(.pdf): # 链接可能是相对路径需要补全为绝对URL if link.startswith(/): pdf_url http://www.cninfo.com.cn link else: pdf_url link report_list.append({ title: title, pdf_url: pdf_url, publish_date: publish_date, year: year }) except requests.exceptions.RequestException as e: print(f搜索{company_name} {year}年年报时发生网络错误: {e}) continue except Exception as e: print(f解析{company_name} {year}年年报页面时发生错误: {e}) continue # 礼貌性延时避免请求过快给服务器带来压力 time.sleep(1) return report_list注意巨潮资讯网的网页结构可能会发生变化。上面的CSS选择器如tr.el-table__row,.title a是我根据历史页面结构写的示例。在实际运行前你必须打开巨潮资讯网按F12进入开发者工具使用元素选择器查看当前搜索结果的HTML结构并更新代码中的选择器。这是爬虫项目中最常见也最重要的一步。3.3 实现PDF下载功能有了报告链接列表下一步就是下载。我们需要处理网络异常、创建本地目录并合理命名文件。import os from pathlib import Path def download_pdfs(report_list, company_name, base_dir./data): 下载PDF文件到本地 :param report_list: search_annual_reports函数返回的报告列表 :param company_name: 公司名称用于创建文件夹和文件名 :param base_dir: 基础存储目录 :return: 下载成功的本地文件路径列表 # 创建基础目录和PDF子目录 pdf_dir Path(base_dir) / company_name / pdfs pdf_dir.mkdir(parentsTrue, exist_okTrue) downloaded_files [] 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 } for report in report_list: pdf_url report[pdf_url] # 从标题或URL中提取一个更干净的文件名 # 例如将‘贵州茅台2022年年度报告.pdf’作为文件名 safe_title re.sub(r[\\/*?:|], _, report[title]) # 去除非法文件名字符 filename f{company_name}_{report[year]}_{safe_title}.pdf filepath pdf_dir / filename # 如果文件已存在跳过下载实现断点续传 if filepath.exists(): print(f文件已存在跳过: {filename}) downloaded_files.append(str(filepath)) continue try: print(f正在下载: {filename}) resp requests.get(pdf_url, headersheaders, streamTrue, timeout30) resp.raise_for_status() # 以二进制写入模式保存文件 with open(filepath, wb) as f: for chunk in resp.iter_content(chunk_size8192): f.write(chunk) print(f下载成功: {filename}) downloaded_files.append(str(filepath)) time.sleep(0.5) # 下载间隔避免请求过快 except requests.exceptions.RequestException as e: print(f下载失败 {pdf_url}: {e}) except Exception as e: print(f保存文件时出错 {filename}: {e}) return downloaded_files这个函数做了几件重要的事一是用pathlib创建了有层级的目录如./data/贵州茅台/pdfs/二是对文件名进行了安全处理防止非法字符导致保存失败三是实现了简单的“断点续传”逻辑如果文件已存在就跳过这在重新运行脚本时非常有用四是使用streamTrue分块下载大文件更节省内存。4. 核心攻坚PDF到TXT的高质量转换下载完PDF只是第一步将它们高质量地转换为纯文本才是数据分析的前提。这一步的挑战在于PDF本身不是为文本提取而设计的格式它更像是一张“图片的容器”文本信息可能以各种复杂的方式嵌入。4.1 使用PyMuPDF进行文本提取下面这个函数是转换过程的核心。它利用PyMuPDF打开PDF逐页提取文本并进行初步的清理。import fitz # PyMuPDF def convert_pdf_to_txt(pdf_path, txt_dir): 将单个PDF文件转换为TXT文件 :param pdf_path: PDF文件的完整路径 :param txt_dir: 存放TXT文件的目录Path对象 :return: 转换后的TXT文件路径失败则返回None txt_dir.mkdir(parentsTrue, exist_okTrue) txt_filename Path(pdf_path).stem .txt # 保持与PDF同名 txt_path txt_dir / txt_filename try: doc fitz.open(pdf_path) full_text for page_num in range(len(doc)): page doc.load_page(page_num) # get_text(“text”) 参数确保提取纯文本保留基本布局 text page.get_text(text) full_text text \n # 每页文本后加换行符 doc.close() # 基础文本清洗 cleaned_text clean_extracted_text(full_text) # 保存到TXT文件 with open(txt_path, w, encodingutf-8) as f: f.write(cleaned_text) print(f转换成功: {txt_filename}) return str(txt_path) except Exception as e: print(f转换失败 {pdf_path}: {e}) return None def clean_extracted_text(text): 清洗提取出的原始文本 :param text: 从PDF提取的原始文本 :return: 清洗后的文本 # 1. 合并因PDF排版导致的错误换行一个单词或中文词被分成两行 # 简单策略如果上一行以连字符结尾则与下一行合并。更复杂的需要根据实际情况调整。 lines text.split(\n) cleaned_lines [] i 0 while i len(lines): current_line lines[i].rstrip() # 去除行尾空格 if i 1 len(lines) and current_line.endswith(-): # 如果以连字符结尾去掉连字符并与下一行合并 next_line lines[i1].lstrip() cleaned_lines.append(current_line[:-1] next_line) i 2 # 跳过下一行因为它已被合并 else: cleaned_lines.append(current_line) i 1 # 2. 将合并后的行重新连接 joined_text \n.join(cleaned_lines) # 3. 去除过多的空白字符多个空格、制表符、换行符替换为单个 # 正则表达式\s 匹配任何空白字符空格、制表符、换行等 normalized_text re.sub(r\s, , joined_text) # 4. 去除常见的页眉页脚关键词需要根据年报特点自定义 # 例如很多年报每页都有“公司年度报告”或页码 # 这里只是一个示例实际需要观察你的PDF提取结果来添加 header_footer_keywords [第.*页, 共.*页, 年度报告, 公司代码\d] for keyword in header_footer_keywords: normalized_text re.sub(keyword, , normalized_text) # 5. 再次去除因替换产生的多余空格 final_text re.sub(r\s, , normalized_text).strip() return final_textclean_extracted_text函数是提升文本质量的关键。PDF提取的文本常有两个“顽疾”一是错误的换行比如一个英文单词“ex-ample”被拆在两行或者一个中文词语被意外切断二是无处不在的页眉、页脚、页码这些信息对于内容分析是噪音。这个函数通过简单的规则如合并以连字符结尾的行和正则表达式过滤能显著提升文本的整洁度。4.2 批量转换与主流程整合最后我们创建一个主函数将搜索、下载、转换三个步骤串联起来并添加批量处理PDF文件夹的功能。def main(): 主函数整合整个流程 # 用户输入 company input(请输入公司名称或股票代码例如贵州茅台 或 600519: ).strip() year_input input(请输入要抓取的年份范围例如2020-2022 或 2022: ).strip() # 解析年份范围 if - in year_input: start_year, end_year map(int, year_input.split(-)) years list(range(start_year, end_year 1)) else: years [int(year_input)] base_data_dir ./data print(f开始抓取 {company} {years} 年的年报...) # 步骤1: 搜索报告 reports search_annual_reports(company, years) if not reports: print(未搜索到符合条件的年报。请检查公司名称/代码和年份。) return print(f找到 {len(reports)} 份年报。) # 步骤2: 下载PDF pdf_paths download_pdfs(reports, company, base_data_dir) print(f成功下载 {len(pdf_paths)} 份PDF文件。) # 步骤3: 批量转换PDF为TXT pdf_dir Path(base_data_dir) / company / pdfs txt_dir Path(base_data_dir) / company / txts successful_conversions 0 for pdf_file in pdf_dir.glob(*.pdf): result convert_pdf_to_txt(str(pdf_file), txt_dir) if result: successful_conversions 1 print(f所有流程完成。成功将 {successful_conversions}/{len(pdf_paths)} 份PDF转换为TXT。) print(fPDF文件保存在: {pdf_dir}) print(fTXT文件保存在: {txt_dir}) if __name__ __main__: main()这个main函数提供了一个简单的命令行交互界面。用户输入公司名和年份后工具就会自动执行全套流程。所有文件会被有条理地存放在./data/公司名/目录下的pdfs和txts子文件夹中。5. 避坑指南与进阶优化在实际运行中你几乎一定会遇到下面这些问题。我把我的踩坑经验和解决方案总结如下希望能帮你少走弯路。5.1 网络请求失败与反爬策略巨潮资讯网对频繁访问有一定限制。如果你的IP短时间内请求过多可能会遇到连接超时或请求被拒绝。症状requests抛出ConnectionError,Timeout异常或者返回的状态码是403、429。解决方案增加延时在每次请求无论是搜索还是下载后使用time.sleep()添加一个随机延时例如1到3秒这是最基本的礼貌。使用会话Sessionrequests.Session()可以复用TCP连接并在会话级别保持一些头信息比单次请求更高效、更稳定。轮换User-Agent准备一个User-Agent列表每次请求随机选择一个模拟不同浏览器。设置代理IP如果请求量非常大可以考虑使用可靠的代理IP池。再次强调严禁讨论任何违规的网络访问方式处理异常代码中必须用try...except包裹网络请求并做好日志记录确保一个文件的失败不会导致整个程序崩溃。5.2 PDF文本提取乱码与乱序这是转换环节最头疼的问题尤其是对于扫描版PDF或排版复杂的PDF。症状提取出的TXT文件全是乱码或者段落顺序完全错乱中英文混杂不堪。解决方案与排查确认PDF类型先用Adobe Acrobat或福昕PDF阅读器打开试试能不能用鼠标选中文字。如果不能那很可能是扫描图片生成的PDF需要OCR。我们的工具PyMuPDF处理不了这种你需要换用pytesseractTesseract OCR的Python封装结合pdf2image先将PDF转图片再识别复杂度陡增。检查字体嵌入有些PDF虽然可以选词但字体没有完全嵌入或编码特殊。PyMuPDF通常能处理好如果不行可以尝试pdfminer.six它对编码的处理有时更鲁棒。调整提取参数page.get_text(“text”)中的“text”参数是一个不错的选择。你也可以尝试“blocks”或“dict”等输出格式它们能提供文本块的位置信息你可以根据坐标x0, y0, x1, y1手动排序这对于多栏排版的PDF是必须的。这需要你编写额外的排序逻辑通常按左上角y坐标然后x坐标排序。复杂的清洗规则clean_extracted_text函数只是一个起点。对于特定的年报格式你可能需要编写更复杂的规则来过滤表格线、特定页眉页脚如“董事会报告”章节标题重复出现、无关水印等。这没有通用解需要针对你的数据样本进行观察和调整。5.3 文件命名与存储混乱下载了几百个文件后如果命名混乱后续管理将是灾难。最佳实践结构化目录坚持使用公司/年份/文件类型pdfs, txts这样的目录树。这比把所有文件堆在一个文件夹里清晰得多。有意义的文件名文件名应包含公司、年份、报告类型等核心信息例如万科A_2022_年度报告.txt。避免使用1.pdf,2.pdf这样的命名。处理非法字符在生成文件名前务必用正则表达式过滤掉Windows/Linux文件名中不允许的字符\ / : * ? “ |。避免覆盖在保存文件前检查是否已存在。我们的download_pdfs函数已经做了这个检查。5.4 程序健壮性与日志一个需要长时间运行的工具必须足够健壮。添加详细日志不要只用print。使用Python内置的logging模块将信息INFO、警告WARNING、错误ERROR记录到文件方便事后排查。你可以清晰地看到“在下载XXX文件时超时”、“在转换YYY文件时编码错误”。实现断点续传在下载和转换函数中检查目标文件是否已存在。如果存在且完整可以通过文件大小简单判断就跳过该文件。这样即使程序中途因网络或错误中断重新运行时也能从断点继续而不是从头开始。资源管理使用with语句确保文件句柄和PDF文档对象被正确关闭防止资源泄漏。6. 从文本到洞察词频分析简单示例工具产出了干净的TXT文本我们的最终目的是分析。这里给出一个最简单的词频统计示例使用jieba库进行中文分词并使用collections.Counter进行统计。import jieba from collections import Counter import os def analyze_word_frequency(txt_dir, top_n50): 对指定目录下的所有TXT文件进行词频分析 :param txt_dir: 存放TXT文件的目录路径 :param top_n: 返回出现频率最高的前N个词 all_text # 读取目录下所有TXT文件 for filename in os.listdir(txt_dir): if filename.endswith(.txt): filepath os.path.join(txt_dir, filename) try: with open(filepath, r, encodingutf-8) as f: all_text f.read() except Exception as e: print(f读取文件失败 {filename}: {e}) if not all_text: print(未找到有效的TXT文件。) return # 使用jieba进行中文分词 # 可以加载自定义词典来合并专业词汇如“净资产收益率” # jieba.load_userdict(my_dict.txt) words jieba.lcut(all_text) # 过滤掉单字和常见的停用词如“的”、“了”、“在” stopwords [的, 了, 在, 是, 和, 与, 及, 等, 对, 我, 我们, 公司, 年度, 报告] filtered_words [word for word in words if len(word) 1 and word not in stopwords and word.strip()] # 统计词频 word_counts Counter(filtered_words) # 输出前N个高频词 print(f\n出现频率最高的前{top_n}个词语) for word, count in word_counts.most_common(top_n): print(f{word}: {count}) # 使用示例 if __name__ __main__: # 假设你的TXT文件在 ./data/贵州茅台/txts 目录下 txt_directory ./data/贵州茅台/txts analyze_word_frequency(txt_directory, top_n30)这只是一个起点。基于这些干净的文本数据你可以做更多深入的分析情感分析判断年报管理层讨论部分的情绪是积极还是消极。主题建模LDA发现年报中隐含的主题例如“绿色发展”、“数字化转型”、“风险提示”。关键信息抽取利用正则表达式或更高级的NLP模型抽取“营业收入”、“净利润”等关键财务数据。年度对比计算不同年份年报的词汇多样性、文本相似度等指标观察公司关注点的变化。这个工具的价值就在于它为你扫清了数据获取和预处理的最大障碍让你能快速拥有一个高质量、可扩展的上市公司文本数据库。剩下的就是发挥你的分析智慧了。本文还有配套的精品资源点击获取