MODULE 4  ยท  RAG Architecture

Document Parsing: Getting Clean Text From Messy Formats

Your boss hands you a 200-page PDF. 'Build a chatbot that answers questions about this.' Step one: extract the text. Harder than it sounds.

๐Ÿ“… Mar 2026
โฑ 10 min read
๐ŸŽฏ Episode 23 of 98
Document ParsingPDFData ExtractionRAG
In this episode

Your boss hands you a 200-page PDF. "Build a chatbot that answers questions about this."

You open the PDF. It has two-column layouts, tables, headers, footers, page numbers, images with captions, footnotes, and a watermark on every page. Some pages are scanned images. The table of contents has dotted leaders. Page 47 is rotated sideways.

Welcome to document parsing โ€” the unglamorous, critical first step of every RAG pipeline. Garbage in, garbage out. If your parser extracts junk, your embeddings encode junk, and your LLM generates confident-sounding junk.

This is the episode nobody wants to write and everybody needs to read.

Why Parsing Is Hard

Text in a PDF isn't stored as you see it. There's no concept of "paragraph" in a PDF โ€” it's a list of instructions like:

BT

/F1 12 Tf % Set font to F1, 12pt

100 700 Td % Move to position (100, 700)

(Hello World) Tj % Draw "Hello World"

ET

The PDF tells the viewer where to draw each character. It doesn't store sentences, paragraphs, or reading order. Multi-column layouts? The PDF doesn't know they're columns โ€” it just has text drawn at specific coordinates.

HTML is better (it has

tags and structure) but still messy โ€” ads, navigation, cookie banners, JavaScript-rendered content.

Scanned documents? You're looking at an image. There is no text. You need OCR (Optical Character Recognition) just to get started.

โšก

PDF Extraction: The Options

Option 1: PyPDF (Simple, Fast, Fragile)

python

from pypdf import PdfReader

snippet
code
reader = PdfReader("document.pdf")

for page in reader.pages:

example
code
text = page.extract_text()
    print(text)

How it works: Reads the PDF's internal text streams. No AI, no OCR โ€” just extraction.

Good at: Simple PDFs with single-column, left-to-right text. Bad at: Multi-column layouts (text gets interleaved), tables (cells become jumbled), scanned PDFs (returns nothing).

Option 2: pdfplumber (Better Layout Awareness)

python

import pdfplumber

with pdfplumber.open("document.pdf") as pdf:

example
code
for page in pdf.pages:
        text = page.extract_text()
example
code
# Extract tables separately!
        tables = page.extract_tables()
        for table in tables:
            for row in table:
                print(row)

Advantage: Table extraction. pdfplumber understands cell boundaries and can extract structured table data. It also handles multi-column layouts better by analyzing character positions.

Option 3: Unstructured.io (Multi-Format, Smart)

python

from unstructured.partition.pdf import partition_pdf

snippet
code
elements = partition_pdf(
"document.pdf",
    strategy="hi_res",        # Use layout detection model
    infer_table_structure=True,
    extract_images_in_pdf=True,

)

for element in elements:

example
code
print(f"Type: {element.category}")  # Title, NarrativeText, Table, Image
    print(f"Text: {element.text[:100]}")
    print("---")

How it works: Uses an AI layout detection model (detectron2) to identify titles, paragraphs, tables, images, and their reading order. It's not just extracting text โ€” it's understanding the document's structure.

Output types:

Title โ€” headers and section titles

NarrativeText โ€” body paragraphs

Table โ€” extracted as HTML or markdown tables

Image โ€” extracted images with optional captioning

ListItem โ€” bullet points and numbered lists

Option 4: Docling (IBM's Document Parser)

python

from docling.document_converter import DocumentConverter

snippet
code
converter = DocumentConverter()
result = converter.convert("document.pdf")

Get markdown output

snippet
code
markdown = result.document.export_to_markdown()
print(markdown)

How it works: IBM's open-source document parser. Uses transformer models for layout detection. Outputs clean markdown with proper headers, tables, and structure.

Comparison

Tool Tables Scanned PDFs Multi-column Speed Quality

PyPDFโŒโŒโŒโšก FastestBasic
pdfplumberโœ…โŒPartialFastGood
Unstructuredโœ…โœ… (with OCR)โœ…SlowVery good
Doclingโœ…โœ…โœ…MediumVery good
LlamaParseโœ…โœ…โœ…Slow (API)Excellent

๐Ÿ”ง

HTML to Text

Web pages are structured but noisy. You want the article content, not the nav bar, sidebar, ads, and cookie consent popup.

Option 1: BeautifulSoup (Manual)

python

from bs4 import BeautifulSoup

import requests

snippet
code
html = requests.get("https://example.com/article").text
soup = BeautifulSoup(html, "html.parser")

Remove noise

for tag in soup(["script", "style", "nav", "footer", "header", "aside"]):

example
code
tag.decompose()
snippet
code
text = soup.get_text(separator="\n", strip=True)

Option 2: Trafilatura (Article Extraction)

python

import trafilatura

snippet
code
url = "https://example.com/article"
downloaded = trafilatura.fetch_url(url)
text = trafilatura.extract(downloaded, include_tables=True, include_links=True)

Trafilatura is specifically designed to extract main content from web pages. It strips navigation, ads, and boilerplate automatically. It's what you want 90% of the time.

Option 3: Jina Reader API

bash

Prefix any URL with r.jina.ai

curl https://r.jina.ai/https://example.com/article

Returns clean markdown. No parsing needed. Free tier available.

๐Ÿ“Š

OCR: When the Document Is an Image

Scanned PDFs, photos of whiteboards, handwritten notes โ€” these are images, not text.

Tesseract (Free, Open Source)

python

import pytesseract

from PIL import Image

Simple OCR

snippet
code
text = pytesseract.image_to_string(Image.open("scanned_page.png"))

With language specification

snippet
code
text = pytesseract.image_to_string(
Image.open("scanned_page.png"),
    lang="eng+hin"  # English + Hindi

)

EasyOCR (Better for Multiple Languages)

python

import easyocr

snippet
code
reader = easyocr.Reader(['en', 'hi'])  # English + Hindi
results = reader.readtext("scanned_page.png")
for (bbox, text, confidence) in results:
print(f"[{confidence:.2f}] {text}")

Vision LLMs (The Nuclear Option)

For complex documents where traditional OCR fails:

python

from openai import OpenAI

import base64

snippet
code
client = OpenAI()

with open("complex_page.png", "rb") as f:

example
code
image_data = base64.b64encode(f.read()).decode()
snippet
code
response = client.chat.completions.create(
model="gpt-4o",
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "Extract all text from this document image. Preserve structure, tables, and formatting as markdown."},
            {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_data}"}}
        ]
    }]

)

snippet
code
text = response.choices[0].message.content

When to use vision LLMs: Complex layouts, mixed handwriting/print, documents in unusual languages, or when traditional OCR output is too noisy to be useful. Expensive but accurate.

Table Extraction: The Hardest Problem

Tables are where every parser struggles. A simple table:

NameRoleExperience AbhishekEngineer12 years PriyaDesigner8 years

What PyPDF sees: "Name Role Experience Abhishek Engineer 12 years Priya Designer 8 years"

All structure is lost. "12 years" could belong to any column.

Approaches

pdfplumber: Detects cell boundaries using ruled lines. Works well for bordered tables.

python

import pdfplumber

with pdfplumber.open("report.pdf") as pdf:

example
code
page = pdf.pages[0]
    tables = page.extract_tables()
example
code
for table in tables:
        headers = table[0]
        for row in table[1:]:
            record = dict(zip(headers, row))
            print(record)

Camelot: Specialized PDF table extractor:

python

import camelot

snippet
code
tables = camelot.read_pdf("report.pdf", pages="all")

for table in tables:

example
code
df = table.df  # Pandas DataFrame!
    print(df)

Unstructured with hi_res: AI-based table detection:

python

snippet
code
elements = partition_pdf("report.pdf", strategy="hi_res", infer_table_structure=True)
tables = [el for el in elements if el.category == "Table"]

for table in tables:

example
code
print(table.metadata.text_as_html)  # HTML table structure
๐Ÿ’ก

Multimodal Parsing: The Future

The latest approach: skip text extraction entirely. Feed the raw document image to a vision model and let it understand the content.

python

Using LlamaParse (API)

from llama_parse import LlamaParse

snippet
code
parser = LlamaParse(
result_type="markdown",
    use_vendor_multimodal_model=True,
    vendor_multimodal_model_name="openai-gpt-4o",

)

snippet
code
documents = parser.load_data("complex_report.pdf")

for doc in documents:

example
code
print(doc.text)  # Clean markdown with tables, structure preserved

Why this works: Vision models can "see" the layout. They understand that a two-column layout should be read column by column. They can read tables by looking at the grid structure. They can handle mixed content (text + charts + diagrams) in one pass.

The trade-off: Slow and expensive. Processing a 50-page PDF through GPT-4o costs several dollars and takes minutes. But the quality is often unmatched.

๐Ÿ”ฌ

The Parsing Decision Tree

What format is your document?

โ”‚

โ”œโ”€โ”€ PDF

โ”‚ โ”œโ”€โ”€ Simple, text-based โ†’ PyPDF or pdfplumber

โ”‚ โ”œโ”€โ”€ Has tables โ†’ pdfplumber or Camelot

โ”‚ โ”œโ”€โ”€ Complex layout โ†’ Unstructured (hi_res) or Docling

โ”‚ โ”œโ”€โ”€ Scanned/image PDF โ†’ OCR first, then extract

โ”‚ โ””โ”€โ”€ Critical accuracy needed โ†’ Vision LLM (GPT-4o)

โ”‚

โ”œโ”€โ”€ HTML/Web page

โ”‚ โ”œโ”€โ”€ Article content โ†’ Trafilatura

โ”‚ โ”œโ”€โ”€ Full page needed โ†’ BeautifulSoup

โ”‚ โ””โ”€โ”€ Quick and clean โ†’ Jina Reader API

โ”‚

โ”œโ”€โ”€ Office docs (Word, Excel, PPT)

โ”‚ โ”œโ”€โ”€ Word โ†’ python-docx or Unstructured

โ”‚ โ”œโ”€โ”€ Excel โ†’ openpyxl or pandas

โ”‚ โ””โ”€โ”€ PowerPoint โ†’ python-pptx

โ”‚

โ””โ”€โ”€ Images (screenshots, photos)

example
code
โ”œโ”€โ”€ Printed text โ†’ Tesseract or EasyOCR
    โ”œโ”€โ”€ Handwriting โ†’ Vision LLM
    โ””โ”€โ”€ Complex (charts, diagrams) โ†’ Vision LLM

๐Ÿ›ก๏ธ

Practical Takeaways

Parsing quality determines RAG quality โ€” invest time here before optimizing anything downstream

PyPDF for simple PDFs, Unstructured for complex ones โ€” know when to upgrade

Tables need special handling โ€” pdfplumber or Camelot for structured extraction

Vision LLMs are the nuclear option โ€” expensive but handle anything

Always inspect parsed output โ€” sample 10-20 documents and manually check extraction quality

Trafilatura for web content โ€” it strips boilerplate better than manual BeautifulSoup

๐Ÿ“ฆ

What's Next?

Episode 24: Embedding Models โ€” You've been using embedding models as a black box. Time to open them up. nomic-embed, text-embedding-3, BGE โ€” which to pick, what dimensions mean, and the benchmarks that actually matter.

โ† Previous

Ep 22: RAG Pipeline End-to-End

Next โ†’

Ep 24: Embedding Models

Next: Episode 24 โ€” Embedding Models

'Just use OpenAI's embedding model.' That's like saying 'just use a Toyota' without knowing if you need a city car or a truck.

This is part of a 98-episode series covering AI engineering from tokens to production deployment.

โ† Previous Ep 22: RAG Pipeline End-to-End: From Document to Answer