New Module: Research PDF Scraper
A complete standalone module for parsing market research PDFs (McKinsey, CB Insights, PitchBook, Gartner, etc.) into markdown with properly formatted Obsidian-style citations.
Changelog - 2026-01-28
New Module: Research PDF Scraper
A complete standalone module for parsing market research PDFs (McKinsey, CB Insights, PitchBook, Gartner, etc.) into markdown with properly formatted Obsidian-style citations.
Architecture Overview
Module Structure
src/
├── schemas/
│ └── research_pdf.py # TypedDict schemas (standalone, no dependencies)
│
├── scrapers/
│ └── research_pdf/
│ ├── __init__.py # Public exports
│ ├── extractor.py # PDF text/structure extraction (PyMuPDF)
│ ├── citation_scraper.py # Citation detection and format conversion
│ └── converter.py # Main orchestrator, markdown generation
│
└── agents/
└── citation_corrector.py # LLM-powered citation correction agent
cli/
├── parse_research_pdf.py # CLI for scraper
└── correct_pdf_citations.py # CLI for correction agent
context-v/
├── PDF-Parser-Agent-Spec.md # Full specification document
└── reminders/
└── Extended-Markdown-Citation-System-Syntax.md # Citation format specDesign Decision: Scraper vs Agent
The module distinguishes between:
Scraper (deterministic):
src/scrapers/research_pdf/- No LLM calls, pattern-based extractionAgent (LLM-driven):
src/agents/citation_corrector.py- Uses Claude for correction and recovery
This separation allows:
Fast, cost-free PDF parsing for most cases
Optional LLM enhancement when higher accuracy is needed
Clear separation of concerns
Data Flow Diagram
┌─────────────────────────────────────────────────────────────────────────────┐
│ Research PDF Scraper │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────┐ │
│ │ PDF │────▶│ extractor │────▶│ citation_scraper│────▶│converter │ │
│ │ File │ │ (fitz) │ │ (regex/NLP) │ │(markdown)│ │
│ └─────────┘ └──────────────┘ └─────────────────┘ └──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Extracted: │ │ Citations │ │ Output: │ │
│ │ - text │ │ in our │ │ - .md │ │
│ │ - structure │ │ [^n] │ │ - .json │ │
│ │ - footnotes │ │ format │ │ files │ │
│ └──────────────┘ └───────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Citation Corrector Agent (Optional) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────┐ │
│ │ Scraper │────▶│ Claude │────▶│ Find Missing │────▶│ Corrected│ │
│ │ Output │ │ Correction │ │ Citations │ │ Output │ │
│ └─────────┘ └──────────────┘ └─────────────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘Citation Format Conversion
Input Formats Detected
The scraper handles multiple citation styles:
| Format | Example | Detection Pattern |
| LaTeX inline | markets.1 2 3 followed by 1U.S. Food and Drug... | ^\d{1,3}[A-Z] |
| Superscript | text¹²³ | Unicode superscript chars |
| Bracketed | [1], [2,3], [1-5] | \[\d+\] pattern |
| Endnotes | 1. Author, "Title"... | Numbered list in refs section |
Output Format (Obsidian-style)
Inline references:
Text with claim. [^10] [^11] [^12] More text continues here.Reference definitions:
[^10]: 2018. [Chronic nicotinamide riboside supplementation...](). Published: 2018 | Updated: N/ASpacing Rules (Critical)
Per context-v/reminders/Extended-Markdown-Citation-System-Syntax.md:
One space after punctuation before citation:
text. [^1]One space between multiple citations:
[^1] [^2] [^3]One space after last citation before text:
[^3] Text continuesNo space before opening bracket on definition:
\n[^1]: ...
Key Implementation Details
1. PDF Text Extraction (extractor.py)
Uses PyMuPDF (fitz) for text extraction with structure detection:
class PDFExtractor:
def extract(self, pdf_path: str) -> Dict[str, Any]:
"""
Extract all content from a PDF.
Returns:
Dict with:
- metadata: PDF properties
- pages: List[ExtractedPage] with text per page
- full_text: Complete document text
- structure: DocumentStructure (sections, TOC, refs location)
- footnotes: Position-detected footnotes
- references_text: Text from references section
- tables: Extracted tables
- is_image_based: Whether OCR would be needed
"""2. LaTeX Footnote Detection (citation_scraper.py)
The key innovation for handling LaTeX-generated PDFs where footnotes appear inline:
def extract_latex_footnotes(self, text: str) -> List[Dict[str, Any]]:
"""
Extract footnotes from LaTeX-generated PDFs where footnotes are
embedded inline in the extracted text.
LaTeX footnotes appear as: "1U.S. Food and Drug Administration..."
where the number is immediately followed by the citation text.
"""
# Pattern: number at start of line followed by capital letter
# Examples:
# 1U.S. Food and Drug Administration, "FDA Approves..."
# 10Martens CR, et al., "Chronic nicotinamide..."
match = re.match(r'^(\d{1,3})([A-Z][A-Za-z].*)', line)3. Markdown Citation Cleaning (converter.py)
The _clean_markdown_citations() method handles the critical conversion:
def _clean_markdown_citations(self, text: str, citations: List[Dict]) -> str:
"""
Clean up markdown text by:
1. Converting inline citation refs (e.g., "text.10 11 12" -> "text. [^10] [^11] [^12] ")
2. Removing footnote definition lines from body text
"""
# Key challenges solved:
# - Distinguish decimals ($1.5 billion) from citations (text.15)
# - Handle citation clusters (10 11 12) vs single refs (15)
# - Remove footnote definitions that appear inline in LaTeX PDFs
# - Proper spacing around citationsDecimal vs Citation Detection:
def replace_end_of_sentence_refs(match):
before = match.group(1) or "" # Character before punct
punct = match.group(2) # . or , or ; or :
numbers_str = match.group(3)
after = match.string[match.end():match.end()+20]
ref_nums = re.findall(r'\d+', numbers_str)
# Check if this looks like a decimal number
if before and before.isdigit() and punct == '.':
# Check for monetary/unit context
if re.match(r'\s*(billion|million|thousand|percent|%)', after, re.IGNORECASE):
return match.group(0) # It's a decimal with units
# Single digit after period is likely decimal
if len(ref_nums) == 1 and int(ref_nums[0]) < 10:
return match.group(0) # Keep as decimal
# Convert to citation format with proper spacing
valid_refs = [n for n in ref_nums if n in known_refs]
if valid_refs:
refs_formatted = ' '.join(f'[^{n}]' for n in valid_refs)
return (before or '') + punct + ' ' + refs_formatted + ' '4. Citation Corrector Agent (citation_corrector.py)
LLM-powered correction that runs after the scraper:
class CitationCorrectorAgent:
def process_pdf(self, pdf_path: str) -> Dict[str, Any]:
"""
Process a PDF: scrape then correct citations.
Steps:
1. Run the scraper to extract citations
2. Review each citation with Claude to fix parsing errors
3. Identify missing citations from the document text
4. Output corrected citations in our standard format
"""Schema Definitions (src/schemas/research_pdf.py)
Standalone TypedDict schemas with no dependencies on the memo system:
# Citation format types
CitationFormat = Literal[
"footnotes", # Numbered footnotes at bottom of page
"endnotes", # Numbered endnotes at end of document
"bibliography", # Author-date style with bibliography
"bracketed", # [1], [2], [3] style inline
"superscript", # Superscript numbers
"mixed", # Multiple formats detected
"unknown"
]
class ParsedCitation(TypedDict, total=False):
"""A citation extracted from a PDF and converted to our standard format."""
original_ref: str # Original reference marker
original_text: str # Full original citation text
title: str # Parsed title
author: Optional[str]
date: Optional[str] # YYYY-MM-DD format
date_display: Optional[str] # Human-readable date
url: Optional[str]
our_format: str # Citation in our [^n]: format
confidence: float # 0.0-1.0 parsing confidence
class ParsedPDFData(TypedDict, total=False):
"""Complete data from a parsed research PDF."""
pdf_id: str
full_markdown: str
sections: List[ParsedPDFSection]
citations: List[ParsedCitation]
citations_markdown: str
parsing_confidence: floatCLI Usage
Scraper CLI
# Parse a single PDF
python cli/parse_research_pdf.py path/to/research.pdf
# Parse to specific output directory
python cli/parse_research_pdf.py report.pdf -o output/parsed/
# Parse multiple PDFs
python cli/parse_research_pdf.py data/research/*.pdf -o output/parsed/
# Skip table extraction (faster)
python cli/parse_research_pdf.py report.pdf --no-tablesCorrection Agent CLI
# Scrape and correct citations
python cli/correct_pdf_citations.py path/to/research.pdf
# With custom output directory
python cli/correct_pdf_citations.py report.pdf -o output/corrected/Output Structure
output/parsed/{pdf-slug}/
├── content.md # Full markdown with inline [^n] citations
├── content.json # Complete structured data
├── citations.json # Extracted citations only
├── citations.md # Citations in our format (for copy/paste)
├── metadata.json # PDF metadata and parsing stats
└── sections/ # Individual sections (if structure detected)
├── 01-executive-summary.md
├── 02-market-overview.md
└── ...
# After running correction agent:
├── citations-corrected.json # LLM-corrected citations
└── citations-corrected.md # Corrected citations markdownTest Results
Tested on Dark Matter Bio Longevity Fund PPM 2026 (132 pages):
| Metric | Before Fixes | After Fixes |
| Citations found | 1 | 37 |
| Citation refs converted | 0% | 95%+ |
| Decimal numbers preserved | No ($1.[^5]) | Yes ($1.5 billion) |
| Footnote defs removed | No | Yes |
| Proper spacing | No | Yes |
Example output (before):
tion.10 11 12
$1.[^5] billion
10Martens CR, et al., "Chronic nicotinamide..."Example output (after):
tion. [^10] [^11] [^12]
$1.5 billion
[Footnote definitions removed from body]Files Created
New Files
| File | Purpose |
src/schemas/research_pdf.py | TypedDict schemas for PDF parsing |
src/scrapers/research_pdf/__init__.py | Module exports |
src/scrapers/research_pdf/extractor.py | PyMuPDF text extraction |
src/scrapers/research_pdf/citation_scraper.py | Citation detection and parsing |
src/scrapers/research_pdf/converter.py | Main orchestrator |
src/agents/citation_corrector.py | LLM correction agent |
cli/parse_research_pdf.py | Scraper CLI |
cli/correct_pdf_citations.py | Correction agent CLI |
context-v/PDF-Parser-Agent-Spec.md | Full specification |
context-v/reminders/Extended-Markdown-Citation-System-Syntax.md | Citation format spec |
Bug Fixes During Development
1. LaTeX Footnote Detection
Problem: Position-based footnote detection failed because PyMuPDF extracts text linearly
Solution: Added
extract_latex_footnotes()for pattern-based detection of\d+[A-Z]patternsResult: Citation detection improved from 1 to 37 citations
2. Decimal vs Citation Confusion
Problem:
$1.5 billionwas converting to$1.[^5] billionSolution: Added context checking for currency symbols, unit words (billion/million), and single-digit heuristics
Result: Decimal numbers preserved correctly
3. Citation Spacing
Problem: Citations rendered as
text.[^10][^11][^12]TextSolution: Updated regex replacement to add proper spacing
Result: Proper format:
text. [^10] [^11] [^12] Text
4. Footnote Definitions in Body
Problem: Footnote text like
10Martens CR, et al...appearing in body contentSolution: Added footnote definition detection and removal in
_clean_markdown_citations()Result: Clean body text with definitions only in References section
Integration Points
With Memo Generation System
The scraper can prep research PDFs for memo generation:
# In company data file (data/{Company}.json)
{
"research_pdfs": [
"data/research/McKinsey-Report.pdf",
"data/research/Industry-Analysis.pdf"
]
}
# Parsed citations available for citation_enrichment agentStandalone Usage
from src.scrapers.research_pdf import scrape_research_pdf
result = scrape_research_pdf("path/to/research.pdf")
if result["success"]:
citations = result["data"]["citations"]
markdown = result["data"]["full_markdown"]Known Limitations
Image-based PDFs: Requires OCR (not yet implemented)
Complex tables: Table extraction is basic
Non-numeric citations: Author-date style (Smith, 2024) not fully supported
Missing citations: Some refs may not be detected if pattern doesn't match
Performance
| PDF Size | Pages | Processing Time | Citations Found |
| 2.5 MB | 132 | 7.3 seconds | 37 |
Future Enhancements
OCR fallback for image-based PDFs (Claude Vision)
Author-date citation style support
Table extraction improvements
Integration into main workflow graph
Batch processing with manifest file
Contributors
Architecture design and implementation
LaTeX footnote detection algorithm
Citation format conversion with spacing rules
LLM correction agent
Related Documentation
context-v/PDF-Parser-Agent-Spec.md- Full specificationcontext-v/reminders/Extended-Markdown-Citation-System-Syntax.md- Citation formatsrc/agents/citation_enrichment.py- How citations are used downstreamtemplates/style-guide.md- Writing standards