尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Python爬虫批量下载巨潮资讯年报:接口分析与脚本实战
简介这是一份面向Python爬虫初学者与金融数据爱好者的实战型资源聚焦于自动化抓取巨潮资讯网披露的上市公司年报。资源以zip压缩包形式提供共4个文件包含1个Python脚本与3个txt文本整体约4KB体量轻巧便于快速阅读与二次修改。其中脚本负责请求页面、解析链接并下载报告txt文件则承担股票代码、日期参数及使用说明等辅助职责结构简洁适合作为爬虫入门练手项目。目前已有3196人学习下载热度可观。通过阅读代码与说明读者可掌握HTTP请求、HTML解析、文件保存与异常处理等关键环节理解如何遍历报告列表、匹配目标链接并落地存储同时体会遵守robots协议与尊重数据版权的重要性。对于希望用Python自动化收集财务报告、提升爬虫实战能力的开发者而言这份资源提供了清晰可复用的参考范例。1. 巨潮资讯年报批量下载从手工点击到脚本流水线每年三四月上市公司年报密集披露做投研、做数据、做审计的朋友都会经历同一件事打开巨潮资讯网输入股票代码筛选“年度报告”一页页翻一个个点下载下来的文件名还全是乱码似的编号。几十家公司还能忍几百家就是纯体力活。Python 爬虫批量下载年报这件事核心不是“能不能爬”而是“怎么爬得稳、爬得全、爬下来的 PDF 还能对得上号”。巨潮资讯的公告检索接口是公开的 JSON 接口返回结构清晰比解析 HTML 页面靠谱得多。这篇笔记面向有 Python 基础、想把手动下载变成可复现流水线的从业者从接口分析、请求构造、PDF 落盘到断点续传把每个环节的参数和坑都摊开讲。读完你能拿到一套能直接改股票代码就跑的脚本骨架也能明白哪些地方一改就翻车。2. 拆解巨潮公告接口为什么不能直接爬页面2.1 页面渲染与接口返回的差别巨潮资讯的公告列表页看起来是一个带分页的表格但如果你用 requests 直接请求页面 URL拿到的 HTML 里往往没有公告数据。原因是列表数据由前端 JavaScript 异步加载真正的数据源是一个 POST 接口。常见做法是打开浏览器开发者工具的 Network 面板筛选 XHR/Fetch 请求翻一页列表就能看到那个返回 JSON 的请求。它的请求地址通常形如http://www.cninfo.com.cn/new/hisAnnouncement/query请求体是表单格式返回结构里announcements字段是一个数组每个元素包含announcementTitle、adjunctUrl、secCode、secName、announcementTime等字段。直接请求这个接口比用 Selenium 渲染页面再解析 DOM 要快一个数量级也更稳定。这里有一个选型理由Selenium 或 Playwright 虽然能模拟点击但资源消耗大、速度慢而且页面结构一改就全废。接口请求只要参数对返回的就是结构化数据解析成本极低。我一般会优先找接口找不到再考虑无头浏览器。巨潮这个接口多年未大改参数体系相对固定适合写成脚本长期用。2.2 请求参数逐个说明接口的请求体里几个关键参数决定了你拿到什么数据参数名含义常用取值pageNum页码从 1 开始递增pageSize每页条数30 或 50太大可能被截断column板块szse代表深市sse代表沪市tabName栏目名fulltext表示全文检索searchkey搜索关键词空字符串表示不限制secid证券内部 ID可留空用stock参数替代stock股票代码,组织代码格式如000001,gssz0000001category公告分类category_ndbg_szsh对应年度报告seDate日期范围格式2024-01-01~2024-12-31其中category是筛选年报的关键。巨潮的分类编码里category_ndbg_szsh表示年度报告category_bndbg_szsh表示半年度报告category_qybg_szsh表示季度报告。如果你不传这个参数返回的是所有公告类型还得自己用标题关键词过滤容易漏也容易混入摘要。stock参数的格式比较讲究是“股票代码,组织代码”的拼接组织代码可以从接口返回的orgId字段拿到也可以先用股票代码搜索一次从结果里提取。如果只传股票代码不传组织代码部分情况下接口也能返回但为了稳定建议拼全。2.3 用 Python 发第一个请求下面是最小可运行代码请求某只股票在指定日期范围内的年报列表import requests import json url http://www.cninfo.com.cn/new/hisAnnouncement/query headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Content-Type: application/x-www-form-urlencoded; charsetUTF-8, X-Requested-With: XMLHttpRequest, Referer: http://www.cninfo.com.cn/new/commonUrl?urldisclosure/list/notice } payload { pageNum: 1, pageSize: 30, column: szse, tabName: fulltext, searchkey: , secid: , stock: 000001,gssz0000001, # 股票代码,组织代码 category: category_ndbg_szsh, # 年度报告分类 seDate: 2024-01-01~2024-12-31, sortName: , sortType: , isHLtitle: true } resp requests.post(url, headersheaders, datapayload, timeout15) data resp.json() for item in data.get(announcements, []): print(item[secCode], item[secName], item[announcementTitle], item[adjunctUrl])这段代码的逻辑是构造表单数据 POST 到接口拿到 JSON 后遍历announcements数组打印股票代码、名称、公告标题和附件相对路径。adjunctUrl是 PDF 的下载路径片段前面需要拼上http://static.cninfo.com.cn/才能访问。timeout15是必须的巨潮接口偶尔响应慢不设超时脚本会卡死。headers里的Referer和X-Requested-With建议带上虽然部分情况下不带也能返回但带上更接近浏览器行为降低被限流的概率。3. 批量下载与文件命名把 PDF 稳稳落到本地3.1 分页遍历与总量控制接口返回的 JSON 里有一个totalAnnouncement字段表示符合条件的公告总数。用这个总数除以pageSize就能算出总页数。但要注意totalAnnouncement有时会略大于实际可翻页的数量所以更稳妥的做法是循环请求直到announcements为空或页码超过一个上限。下面是一个分页拉取全部年报的函数import time def fetch_all_announcements(stock_code, org_id, se_date, categorycategory_ndbg_szsh): url http://www.cninfo.com.cn/new/hisAnnouncement/query headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Content-Type: application/x-www-form-urlencoded; charsetUTF-8, X-Requested-With: XMLHttpRequest, Referer: http://www.cninfo.com.cn/new/commonUrl?urldisclosure/list/notice } all_items [] page 1 while True: payload { pageNum: page, pageSize: 30, column: szse, tabName: fulltext, searchkey: , secid: , stock: f{stock_code},{org_id}, category: category, seDate: se_date, sortName: , sortType: , isHLtitle: true } try: resp requests.post(url, headersheaders, datapayload, timeout15) data resp.json() except Exception as e: print(f第 {page} 页请求失败: {e}) break items data.get(announcements) if not items: break all_items.extend(items) print(f已拉取第 {page} 页累计 {len(all_items)} 条) page 1 time.sleep(0.5) # 控制频率避免触发限流 return all_items这里time.sleep(0.5)是血泪经验。巨潮接口对高频请求有软限制连续快速请求几十页后可能返回空数据或 403。加 0.5 秒间隔几百页也就多花几分钟但稳定性大幅提升。如果请求失败不要立刻重试先 break 出来记录页码下次从该页继续。3.2 PDF 下载与文件名清洗拿到adjunctUrl后拼上http://static.cninfo.com.cn/就是完整下载地址。文件名建议用“股票代码_股票名称_公告标题_日期.pdf”的格式方便后续检索。但公告标题里常含有/、\、:、*、?、、、、|这些在 Windows 下非法的字符必须替换掉。下面是一个下载函数import os import re def download_pdf(item, save_dir./annual_reports): os.makedirs(save_dir, exist_okTrue) sec_code item.get(secCode, unknown) sec_name item.get(secName, unknown) title item.get(announcementTitle, untitled) adj_url item.get(adjunctUrl, ) if not adj_url: return None pdf_url fhttp://static.cninfo.com.cn/{adj_url} # 清洗文件名中的非法字符 safe_title re.sub(r[\\/:*?|], _, title) filename f{sec_code}_{sec_name}_{safe_title}.pdf filepath os.path.join(save_dir, filename) if os.path.exists(filepath): print(f已存在跳过: {filename}) return filepath try: r requests.get(pdf_url, headers{User-Agent: Mozilla/5.0}, timeout30, streamTrue) with open(filepath, wb) as f: for chunk in r.iter_content(chunk_size8192): f.write(chunk) print(f下载完成: {filename}) return filepath except Exception as e: print(f下载失败 {filename}: {e}) return NonestreamTrue配合iter_content是下载大文件的标准做法避免一次性把整个 PDF 读进内存。os.path.exists判断是断点续传的简易实现已经下载过的文件直接跳过脚本中断后重跑不会重复下载。文件名清洗用正则替换把所有非法字符统一换成下划线简单有效。注意adjunctUrl有时以/开头有时不带拼接时统一处理成http://static.cninfo.com.cn/加路径即可。3.3 组织代码的获取与股票列表维护stock参数里的组织代码不是股票代码本身而是巨潮内部的orgId。获取方式有两种一是先用股票代码调一次搜索接口从返回结果里取orgId二是维护一张股票代码到组织代码的映射表。对于批量任务建议先写一个小脚本把所有目标股票的组织代码拉下来存成 JSON后续下载直接读。下面是从搜索结果提取orgId的片段def get_org_id(stock_code): url http://www.cninfo.com.cn/new/information/topSearch/query headers {User-Agent: Mozilla/5.0, Content-Type: application/x-www-form-urlencoded} payload {keyWord: stock_code, maxNum: 10} resp requests.post(url, headersheaders, datapayload, timeout10) for item in resp.json(): if item.get(code) stock_code: return item.get(orgId) return None这个搜索接口返回的每条记录里code是股票代码orgId是组织代码。拿到后拼成f{stock_code},{org_id}传给下载接口。如果股票列表有几百只建议把这一步的结果缓存到本地文件避免每次下载都重新查。4. 避坑与排查下载年报时最容易翻车的五个地方4.1 请求返回空数组但页面明明有数据现象脚本请求接口announcements返回空列表但浏览器里能看到公告。原因通常是category参数不对或者seDate格式写错。巨潮的日期格式必须是YYYY-MM-DD~YYYY-MM-DD中间用波浪号连接两边不能有空格。另外沪市和深市的column参数不同深市用szse沪市用sse如果股票是沪市的却传了szse也会返回空。解决方法是先用浏览器抓一次真实请求把参数原样抄下来再逐个替换变量。4.2 下载的 PDF 打不开或只有几 KB现象文件下载下来了但打开提示损坏或者大小只有几 KB。原因一般是adjunctUrl拼接错误或者请求 PDF 时被重定向到了登录页。巨潮的静态资源域名是static.cninfo.com.cn如果拼成了www.cninfo.com.cn会返回一个 HTML 页面而不是 PDF。解决方法是打印出完整 URL在浏览器里直接访问验证。另外部分公告的 PDF 需要登录才能下载但年报通常不需要如果遇到 302 跳转检查headers里是否缺少User-Agent。4.3 文件名乱码或保存失败现象下载下来的文件名是一串乱码或者保存时报错OSError: [Errno 22] Invalid argument。原因是公告标题里含有 Windows 不允许的字符或者标题过长超过了文件系统限制。解决方法是下载前用正则替换非法字符并且截断文件名到 200 个字符以内。另外如果股票名称里有生僻字某些文件系统可能不支持建议统一用 UTF-8 编码保存并在文件名里保留股票代码作为主键。4.4 请求频率过高被限流现象脚本跑了几十页后接口开始返回 403 或空数据过一段时间又恢复。原因是请求间隔太短触发了巨潮的限流机制。解决方法是在每次请求后加time.sleep(0.5)到1秒的延迟并且不要并发请求。如果股票数量多可以分批次跑比如每跑 50 只股票就休息几分钟。另外pageSize不要设得太大30 到 50 比较稳妥设成 100 容易触发限制。4.5 断点续传与重复下载现象脚本跑到一半中断重跑时已经下载过的文件又下了一遍浪费时间。原因是没有做已存在文件的判断。解决方法是在下载函数里加os.path.exists检查文件存在就跳过。更进一步可以把已下载的股票代码和公告 ID 记录到一个 JSON 文件里每次启动时先读取避免重复请求接口。公告 ID 可以用announcementId字段它是每条公告的唯一标识。5. 进阶技巧把下载脚本变成可复用的数据管道5.1 用配置文件管理股票列表和日期范围硬编码股票代码和日期范围不利于复用。我一般会建一个config.json把股票列表、日期范围、保存目录、请求间隔都放进去{ stocks: [ {code: 000001, org_id: gssz0000001}, {code: 600000, org_id: gssh0600000} ], se_date: 2024-01-01~2024-12-31, category: category_ndbg_szsh, save_dir: ./annual_reports, sleep_seconds: 0.5 }脚本启动时读取这个文件遍历stocks列表逐个调用下载函数。这样换一批股票只需要改配置不用动代码。org_id可以提前用搜索接口批量拉好也可以让脚本在运行时自动获取并缓存。5.2 下载日志与失败重试批量任务一定要有日志否则出了问题不知道哪只股票漏了。建议用 Python 的logging模块把每次请求的页码、返回条数、下载成功与否都记到文件里。对于下载失败的条目单独存一个failed.json脚本结束时打印失败列表下次可以只重试这些。下面是一个简单的日志配置import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(download.log, encodingutf-8), logging.StreamHandler() ] )FileHandler把日志写到文件StreamHandler同时输出到控制台。encodingutf-8避免中文日志乱码。每次下载成功或失败都调logging.info或logging.error排查时直接翻日志文件比在终端里翻滚动记录高效得多。5.3 验证下载完整性的两个方法下载完成后怎么确认 PDF 是完整的第一个方法是检查文件大小年报 PDF 通常几百 KB 到几 MB如果某个文件只有几 KB大概率是下载失败或下到了错误页面。第二个方法是检查 PDF 文件头正常的 PDF 文件前几个字节是%PDF-可以用 Python 快速校验def is_valid_pdf(filepath): try: with open(filepath, rb) as f: header f.read(5) return header b%PDF- except Exception: return False遍历下载目录对每个文件调一次这个函数把不合法的文件列出来重新下载。这个检查花不了几秒钟但能避免后续用数据时才发现文件损坏。我一般会在批量下载结束后跑一遍这个校验把无效文件删掉并记录到失败列表。5.4 把下载和解析串起来下载只是第一步年报 PDF 里的财务数据才是最终目标。如果后续要解析 PDF 提取表格建议在下载时就按“股票代码/年份”的目录结构存放比如./annual_reports/000001/2024/000001_平安银行_2024年年度报告.pdf。这样解析脚本可以按目录遍历不用再从文件名里拆股票代码和年份。目录结构在下载函数里加两行就能实现year item.get(announcementTime, )[:4] stock_dir os.path.join(save_dir, sec_code, year) os.makedirs(stock_dir, exist_okTrue) filepath os.path.join(stock_dir, filename)announcementTime是时间戳转成日期后取前四位就是年份。按股票代码和年份分目录后续做数据对齐时非常省事。这个习惯是我踩过坑之后养成的早期把所有 PDF 堆在一个目录里几千个文件后找一只股票的年报要翻半天后来改成按目录分层效率完全不一样。5.5 一个容易忽略的细节公告标题里的年份有些公司的年报标题写的是“2024年年度报告”有些写的是“2023年年度报告”但披露日期是 2024 年。如果你用seDate筛选 2024 年的公告拿到的是 2024 年披露的所有年报其中可能包含 2023 年度的报告。所以文件名里的年份应该从公告标题里提取而不是从披露日期里取。用正则从标题里匹配20\d{2}更准确import re match re.search(r20\d{2}, title) year match.group() if match else unknown这个细节在批量下载时特别重要否则你会把 2023 年年报和 2024 年年报混在一起后续做同比分析时数据全乱。我一般会在下载日志里同时记录披露日期和标题年份方便核对。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

从NIO到Netty:Java网络编程为何选择框架而非原生API

从NIO到Netty:Java网络编程为何选择框架而非原生API

最近团队内部搞技术答辩,一个刚转岗过来的同事被问到“为什么不用原生 NIO,而是用 Netty”。他愣了一下,先说了句“因为大家都用 Netty”,接着就卡住了。其实这个题我面试别人时也常问,看上去是个框架选型问题&#xf…

📅 2026/10/13 22:29:17
SpringBoot+Vue+MyBatis助农平台:从业务设计到部署上线

SpringBoot+Vue+MyBatis助农平台:从业务设计到部署上线

搞前后端分离的助农项目,这几年来在开发者圈子里一直很常见,不是因为它技术多潮,而是这条技术链覆盖面足够典型:后端要管接口、鉴权、业务状态;前端要管页面、组件、路由;中间还要解决跨域、联调、部署上线…

📅 2026/10/13 22:29:17
TensorFlow迁移学习实战:用Inception-v3快速实现图像分类

TensorFlow迁移学习实战:用Inception-v3快速实现图像分类

图像识别这块,算是深度学习中落地最成熟的方向之一。我最近在处理一个自然图像分类的小项目,对比了好几套预训练模型,最后用TensorFlow自带的Inception-v3配合迁移学习快速跑通了。这篇文章就把我完整思路上到模型选型,下到代码细…

📅 2026/10/13 22:29:17
MORE NEWS

更多资讯

📰

NPDP BoK第2版深度拆解:七大知识域与产品创新落地实践

简介:NPDP Body of Knowledge-2nd 是 PDMA 官方编写的第二版产品开发与管理知识体系指南,面向备考 NPDP 认证的产品经理、产品开发与创新从业者,也适合希望系统梳理产品管理知识框架的中高级人员。全书围绕策略、投资组合管理、产品创新过程、…

📰

陆基导弹打航母分段轨道建模:从参数方程到MATLAB实现

简介:本资源为第八届MathorCup数学建模竞赛C题优秀论文,面向备战数学建模赛事的高校学生及指导教师,聚焦陆基导弹打击航母这一军事任务,系统展示从建模到求解的完整思路。论文围绕静态轨道模型、动态轨道模型与误差命中率分析三大…

📰

Litellm实战:统一大模型API网关,解决多服务商接入与成本管控难题

做应用开发的朋友应该都有这种经历:项目里接了好几个模型服务商的API,代码里写满了不同的请求格式、不同的鉴权方式,每换一家厂商就得改写一版调用层;密钥散落在代码和环境变量里,月底对账的时候得翻聊天记录找谁用了多…

📰

Agent-Skills实战:从工具函数到技能封装,打造真正会干活的智能体

去年底我在做一个内部的智能体项目,当时团队对“agent-skills”这个词的理解还非常混乱。有人说是给模型挂一堆工具函数,有人说是一套提示词模板,还有人直接把它等同于工作流。折腾了两个月,踩了不少坑,才慢慢摸清楚&a…

📰

数字经济与实体经济深度融合:从商业模式创新到数字化转型落地路径

“行行通集团”的名字同时出现在“数字经济与实体经济深度融合创新发展典范企业”与“品牌强国数字新消费最佳商业模式创新平台”两项荣誉名单里时,我身边不少做企业和做咨询的朋友都在聊这件事。这不是一次普通的获奖,它背后释放的信号,恰恰…

📰

PSO有约束优化:粒子群算法处理工程限制条件寻优实战

简介:解决带约束非线性优化问题的Matlab源码包,面向掌握一定优化算法基础的研究人员与工程师,采用Kennedy和Eberhart提出的粒子群算法,并借助Deb可行性规则处理等式/不等式约束,同时涵盖罚函数、约束投影等辅助策略。压…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬