Home / Guides / Text extraction
Text extraction

Extract a 10-K section across multiple years

Pull the same 10-K item from five years of annual reports, then measure how much the text changed and which themes grew or faded.

Companies rarely rewrite their 10-K from scratch. Most of the text carries over, and the interesting part is what changed: a new risk, a dropped paragraph, a theme that suddenly shows up ten times. This tutorial pulls Item 1A (risk factors) from five years of Apple 10-Ks and compares them.

It uses two endpoints:

  • /v1/filings to list a company's 10-K filings
  • /v1/extractor to pull one item from each filing as plain text

The script

import os
import re
import requests

BASE = "https://secfeedapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SECFEED_KEY']}"}

# 1. the last five 10-K filings for the company
r = requests.get(f"{BASE}/v1/filings", params={"query": 'ticker:AAPL AND formType:"10-K"', "size": 5},
                 headers=HEADERS, timeout=60)
filings = r.json()["filings"]

# 2. the same item from each one
years = {}
for f in reversed(filings):  # oldest first
    r = requests.get(f"{BASE}/v1/extractor", params={"url": f["linkToFilingDetails"], "item": "1A", "type": "text"},
                     headers=HEADERS, timeout=120)
    r.raise_for_status()
    years[f["filedAt"][:4]] = r.text

def paragraphs(text):
    return {p.strip() for p in text.split("\n") if len(p.strip()) > 80}

# 3. size and change versus the year before
prev = None
print("filed  words   paragraphs  new  dropped")
for year, text in years.items():
    paras = paragraphs(text)
    new = len(paras - prev) if prev is not None else 0
    dropped = len(prev - paras) if prev is not None else 0
    print(f"{year}   {len(text.split()):>6,}  {len(paras):>9}  {new:>4}  {dropped:>7}")
    prev = paras

# 4. how often a few themes come up each year
terms = ["tariff", "artificial intelligence", "pandemic", "interest rate"]
print("\n" + "term".ljust(24) + "".join(y.rjust(6) for y in years))
for t in terms:
    print(t.ljust(24) + "".join(str(len(re.findall(t, txt, re.I))).rjust(6) for txt in years.values()))

Output

filed  words   paragraphs  new  dropped
2021    9,659        109     0        0
2022   10,221        112    35       32
2023    9,794        105    36       43
2024    9,913        108    35       32
2025    9,782         99    59       68

term                      2021  2022  2023  2024  2025
tariff                       5     5     2     2    10
artificial intelligence      0     0     1     3     6
pandemic                    14    14     4     2     2
interest rate                2     2     2     2     2

Reading the result

  • Size is stable, content is not. Item 1A stays between about 9,700 and 10,200 words every year, yet each year roughly a third of the paragraphs are new or reworded. Length alone hides that churn.
  • 2025 was the biggest rewrite. 59 new paragraphs and 68 dropped ones, versus about 35 in earlier years. That is the year to read closely.
  • Themes move. "Pandemic" falls from 14 mentions to 2 after 2022, "artificial intelligence" goes from 0 to 6, and "tariff" jumps from 2 to 10 in 2025.

A paragraph counts as "new" if its exact text did not appear the year before, so a single edited word makes a paragraph both new and dropped. For a softer comparison, use difflib.SequenceMatcher on paragraph pairs, or compare embeddings.

Variations

  • Another item. Change item to 7 for MD&A, 1 for the business description or 1C for cybersecurity (filed since fiscal 2023, so expect fewer years).
  • Another company or a peer group. Loop over tickers and keep years per ticker. Filing search returns up to 200 rows per call.
  • More history. Raise size to go further back. The extractor works on 10-Ks back to the early 2000s, though older filings use plain text documents and less consistent headings.
  • Amendments. formType:"10-K" excludes 10-K/A. Amended 10-Ks often contain only Part III, so they rarely matter for Item 1A.

Next steps

More text extraction guides

Run this guide with your own key

500 free API calls a month. Every example on this page works on the free plan.

Get free API key →