尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从SEC EDGAR批量获取美股年报:官方JSON接口的合规数据管线

从SEC EDGAR批量获取美股年报:官方JSON接口的合规数据管线 先说结论美国上市公司的年报数据最好的来源不是第三方数据平台也不是各种网盘群而是SEC自己维护的EDGAR系统。这个系统从1993年开始公开运行所有注册公司的定期报告、重大事项公告都能查到而且官方提供了机器可读的JSON接口和历史文件索引。财报季一到做基本面研究的人就需要批量拉取大量公司的年报我之前在整理行业对标库的时候正好完整踩过一遍这个流程这次就把合规渠道下的操作细节和遇到的坑都梳理出来给同样有批量下载需求的读者参考。1. 方案选型与整体设计思路1.1 为什么绕开“爬全站”的思路刚开始接触这个需求时很多人第一反应是写爬虫去抓财经网站的个股页面或者直接去搜索引擎找PDF网盘链接。这两个路径问题都很明显财经网站的反爬策略一直在升级页面结构说改就改抓下来的数据字段还可能不全网盘链接的时效性没法保证而且大量散布在非官方来源的文件本身来源可靠性就是一个隐患。真正靠谱的路径只有一条从SEC的EDGAR系统走。为什么因为年报也就是10-K、20-F、40-F这些表格是上市公司依法必须向SEC提交的法定披露文件EDGAR就是这些文件的官方存档库。文件全、格式统一、更新及时而且是官方免费开放的数据源。更重要的是EDGAR提供了非常友好的机器访问接口和公开的下载指南只要遵守它的限速规则它就是合规且稳定的批量下载渠道。我最终选定的方案是先拿ticker与CIK的映射表再用CIK去调submissions接口拿文件清单最后按需下载归档文件。整个过程都是基于官方接口不涉及任何绕过访问控制的动作。1.2 整体管线设计整套流程可以拆成四个阶段每个阶段解决一个明确的问题确定“要哪些公司”维护一份自己的目标公司清单精确到ticker和公司法定名称。打通“公司与文件的关联”通过EDGAR的submissions接口把CIK与该公司历史上提交的所有文件索引关联起来。筛选“哪些算年报”在文件索引里按form type规则过滤10-K是标准年报20-F是外国私有发行人年报40-F是加拿大发行人年报。批量拉取与归档根据筛选出的文件URL限速下载并按照“公司代码财年文件类型”的规则重命名归档。这套管线跑通之后增量更新也很简单只需要记录每个公司最后处理到哪一份文件下次重新拉取submissions接口时对比时间戳把新出现的文件补齐就行不需要每次都全量重下。2. 关键资源地址与数据格式解析2.1 三个核心资源EDGAR为开发者提供了几套相对独立的数据资源批量下载年报主要用到下面三个公司列表文件company_tickers.json 地址https://www.sec.gov/files/company_tickers.json 这个文件把上市公司的ticker、CIK、公司名称做成一个JSON数组。CIK是SEC内部给每个公司分配的唯一编号结构是10位数字前面的0可以省略也可以保留在URL中使用时通常补足10位。公司文件索引接口submissions 地址格式https://data.sec.gov/submissions/CIK0000320193.json 把CIK替换成目标公司的10位编号就能拿到该公司全部历史申报文件的索引。指数里包含文件类型、申报日期、文件编号、访问链接等信息是筛选年报的主要依据。归档文件目录Archives 地址格式https://www.sec.gov/Archives/edgar/data/320193/000032019323000106/000032019323000106-index.htm 这里是实际文件的存放目录包含申报正文、附件、XBRL等。下载年报正文时从submissions接口里拿到的主文档访问路径在实际访问时会跳转到这个目录下边的具体文件。2.2 为什么选JSON而不是FTP目录早期EDGAR提供FTP方式访问现在也在维护但FTP目录是树状展开的要按年份、季度一层层去翻效率很低。JSON接口则是直接按公司维度组织一次请求就能拿到一家公司整个历史的文件索引做增量更新特别方便。不过submissions接口的数据量大响应体常常有几MB到十几MB不能频繁请求。官方要求请求间隔不少于0.1秒我把实际间隔设置在0.5秒以上既不会触发限流一家公司也就不到一秒的事。真遇到大公司文件特别多跑几千家公司也不会有太大压力。3. 实操步骤与核心代码3.1 获取全部美股主体的ticker→CIK映射第一步把公司列表文件拉下来保存成本地JSON。因为这里用的是EDGAR官方接口请求时需要在User-Agent里标明自己的身份格式一般写成“公司名 邮箱地址”这是官方文档明确要求的合规姿势不写UA很容易被拒。import json import urllib.request # 官方推荐在UA里写清楚你的应用名与联系方式 headers { User-Agent: Research Desk researchexample.com, Accept-Encoding: gzip, deflate, } req urllib.request.Request(https://www.sec.gov/files/company_tickers.json, headersheaders) with urllib.request.urlopen(req) as resp: data json.load(resp) # 这个JSON的结构是 { 0: {cik_str, ticker, title}, 1: {...}, ... } ticker_to_cik {} for row in data.values(): cik str(row[cik_str]).zfill(10) ticker row[ticker] ticker_to_cik[ticker] cik with open(company_tickers.json, w) as f: json.dump(ticker_to_cik, f, indent2)这一步拉下来之后你就有了一张全市场的对照表。实际使用时可以按自己的持仓池或研究范围过滤只需要保留目标ticker即可后续所有请求都复用这张表不需要再重复请求。3.2 从submissions接口拿年报文件清单有了CIK后就能直接拼出每家公司的submissions地址。下面以苹果公司为例CIK是0000320193请求后解析JSONimport requests cik 0000320193 url fhttps://data.sec.gov/submissions/CIK{cik}.json headers { User-Agent: Research Desk researchexample.com } resp requests.get(url, headersheaders, timeout30) data resp.json() # 文件清单的关键字段 recent data[filings][recent] form_list recent[form] # 文件类型如 10-K, 10-Q, 8-K filing_date recent[filingDate] # 申报日期 accession_list recent[accessionNumber] # 入档编号 doc_list recent[primaryDocument] # 主文档文件名这里要注意submissions接口返回的JSON里文件清单分两段filings.recent是最新一批文件filings.files里存的是一系列历史文件的地址。对于绝大多数公司recent里的数据量已经覆盖得比较全了但老牌公司几十年的申报历史可能会很大真正要全量拉取时要把filings.files里的历史地址也循环请求一遍再合并所有结果。3.3 按“主文档修正文件”策略过滤年报拿到文件清单后关键一步是筛选哪些文件算年报。SEC的表单代码规则是这样的10-K美国本土公司年度报告最常见10-K/A10-K的修正文件如果原始申报有错误公司会提交修正版通常也需要下载因为修正后的内容才是最终版本20-F外国私有发行人年报比如在美股上市的很多中国公司提交的是20-F20-F/A20-F的修正版40-F加拿大公司年报40-F/A40-F的修正版筛选时直接用form字段做前缀匹配valid_forms (10-K, 20-F, 40-F) target_filings [] for i in range(len(form_list)): form form_list[i] if form.startswith(valid_forms): target_filings.append({ form: form, filing_date: filing_date[i], accession_number: accession_list[i], primary_document: doc_list[i], })这里有两个有争议的地方需要单独说明。第一要不要包含修正文件带/A后缀的。我的建议是默认包含但做归档时把它们单独放在一个子目录或者用文件名后缀标记清楚。原因是修正文件不一定每次都是实质性的内容修正有的是签名页补正有的是XBRL修正不下载的话遇到实质性修正时数据就会有偏差。但为了后续判断哪一版才是“最终有效版本”需要记录好申报日期通常同一年内后提交的修正版要覆盖先提交的原版。第二要不要只看主文档primaryDocument字段。submissions接口返回了primaryDocument它指向这次申报的正文主文件而不是附加的附件或XBRL文件。按主文档下载可以避免把整次申报的所有附件都拉下来省流量也省存储。但前提是你只需要年报正文不要XBRL、不要附件。如果做量化分析需要财务数据XBRL那就得另说需要在下载时把dataFiles字段里的XBRL相关文件也一并解析。3.4 限速下载与文件命名筛选出年报清单后下载时需要一个结构化规则来拼接实际文件URL。EDGAR归档路径的规则是https://www.sec.gov/Archives/edgar/data/{CIK无前导零}/{accession去除横杠}/{primaryDocument}CIK无前导零的意思是0000320193要转成320193accession number要去掉横杠。例如苹果公司某次10-K申报https://www.sec.gov/Archives/edgar/data/320193/000032019323000106/aapl-20231230.htm增量请求代码如下import time import os def download_10k(ticker, cik, filings, output_dir10k_files): cik_no_pad str(int(cik)) os.makedirs(output_dir, exist_okTrue) for f in filings: acc f[accession_number].replace(-, ) doc f[primary_document] url fhttps://www.sec.gov/Archives/edgar/data/{cik_no_pad}/{acc}/{doc} filename f{ticker}_{f[filing_date]}_{f[form]}_{doc} local_path os.path.join(output_dir, filename) if os.path.exists(local_path) and os.path.getsize(local_path) 0: continue headers {User-Agent: Research Desk researchexample.com} try: r requests.get(url, headersheaders, timeout60) r.raise_for_status() with open(local_path, wb) as out: out.write(r.content) except Exception as e: print(f[ERROR] {ticker} {f[filing_date]} {f[form]}: {e}) time.sleep(0.5) # 限速至少等待0.1秒文件名规则我调过好几次最后定成“ticker 申报日期 表单类型 主文档名”好处是同一家公司同一年度多份修正文件能很直观地看出先后顺序排序后直接用脚本对比即可。4. 常见问题与排查实录4.1 请求被拒429与403的时效性处理跑批量任务时最容易遇到的就是HTTP 429和403。429表示请求太频繁被限流403通常是User-Agent设置不符合要求。我踩过的坑第一版脚本没有设置UA跑了大概两百个请求之后就开始大量返回403。后来统一加上了带联系邮箱的UA问题就基本消失了。如果仍然出现429最简单的策略是增加sleep时间从0.1秒逐步加到1秒。同时加上指数退避遇到429时暂停时间递增重试三次三次后跳过该文件并记录日志方便回头补下。def robust_get(url, headers, max_retries3): for attempt in range(max_retries): resp requests.get(url, headersheaders, timeout30) if resp.status_code 200: return resp if resp.status_code 429: wait 2 ** (attempt 1) time.sleep(wait) continue resp.raise_for_status() raise RuntimeError(fFailed after retries: {url})大批量下载时还有一个技巧按天错峰。比如今天下载A-K开头的公司明天下载L-Z开头的把总请求量分散到多天能显著降低触发限流的概率。反正增量更新也不差这一天的时差。4.2 首次全量与增量更新的取舍如果只需要最近几年的年报比如2019年至今直接过滤filing_date即可不用全量拉。如果要做历史分析那就要处理全量数据此时要特别留意submissions接口里filings.files字段。以一家有20年申报历史的公司为例filings.recent通常返回最近几百条记录早期记录都存放在filings.files指向的JSON文件里。这些文件路径也是EDGAR服务器上的公开JSON文件需要逐个请求后合并。合并时要先做去重因为recent和历史文件可能会有重叠去重逻辑按accessionNumber唯一即可。首次全量下载时不要并发太猛。我实测过10个并发线程跑会频繁触发429改成2到3个并发并把每个请求间隔拉开后稳定性就很高了整个进度虽然不快但胜在能持续跑完。4.3 主文档文件名乱码与格式差异主文档不一定是HTM格式还有可能是HTML、TXT甚至PDF。早年部分公司的文件命名也不规范主文档字段可能指向一个过渡页而不是真正的正文。判断文件是不是“正文”可以看下载下来的文件里是否包含DOCUMENT标签SGML/HTML格式年报都有或者先看文件大小正常10-K正文至少几百KB链接文件通常只有几KB。如果发现某个文件明显异常可以直接回EDGAR的归档索引页去人工核对。归档索引地址就是前面提到的https://www.sec.gov/Archives/edgar/data/{cik_no_pad}/{accession_no_dash}/这个目录下能看到所有相关文件手动点进去比猜哪个是正文要高效得多。实际操作中异常文件的比例很低不必过度优化。5. 本地归档与自动化小技巧5.1 批量重命名与目录归档下载到本地后原始的命名方式虽然能区分但同一个公司的多年文件混在一个目录里时间一长还是不方便。我后来用了一个简单的Python脚本做归档整理先按ticker建目录再按年份分子目录把同一个财年的10-K和10-K/A放到同一个年份文件夹里。import os import shutil import re def organize_files(src_dir): for filename in os.listdir(src_dir): # 示例文件名: AAPL_2023-12-30_10-K_aapl-20231230.htm m re.match(r([A-Z])_([0-9]{4})-[0-9]{2}-[0-9]{2}_(.?)_(.), filename) if not m: continue ticker, year, form, doc m.groups() target_dir os.path.join(src_dir, ticker, year) os.makedirs(target_dir, exist_okTrue) shutil.move(os.path.join(src_dir, filename), os.path.join(target_dir, filename))批量重命名文件这件事其实就是一个小脚本的事没必要用一个通用工具来解。你需要做的只是把命名规则提前定义好后面不管是归档、搜索还是写进数据库都能按规则提取元数据。我在这个项目里用到的正则规则很简单但到了几百上千个文件时省下的手工操作时间就很可观了。5.2 后续扩展解析正文与入库存下载只是第一步。拿到大量htm格式的年报原始文件后常见的下一步就是做文本解析。简单场景下用BeautifulSoup或lxml把HTML转成纯文本再按关键词抽取管理层讨论MDA、风险因素、财务报表等章节。复杂场景下可以按SEC的Financial Report格式解析XBRL数据直接提取结构化财务数据。这套批量下载的方案在设计时就已经考虑到了后续扩展所以文件名里的ticker和日期本身就是很好的索引键入库时不需要再做额外的映射。如果你打算长期跟踪美股公司的定期报告建议把下载结果落进一个简单的SQLite表CREATE TABLE filings ( ticker TEXT, cik TEXT, form TEXT, filing_date TEXT, accession_number TEXT PRIMARY KEY, local_path TEXT );每天跑一次增量更新把新的申报记录插进去这样就能形成一个持续更新的本地年报库后面做筛选和分析都会非常方便。我在实际跑这个流程时最深的体会有两点一是合规渠道往往比各种“加速通道”更省心EDGAR官方接口的稳定性远高于任何第三方源只要遵守限速规则基本不会出问题二是文件命名和归档规则一定要在下载前设计好宁可多花半小时定规则也不要一次性下载几千个文件之后再来整理那体验真的非常糟糕。希望这篇总结能帮你少走点弯路。
返回列表