Frontend Labs
⋯

60. Data pipelines for training: extraction, cleaning, dedup, JSONL

Most of the work in a real AI project is data, and most data work is Python. A typical offline pipeline turns raw documents (PDFs, EPUBs, HTML, logs, tickets) into clean training or retrieval records:

 raw files ──▶ extract text ──▶ clean/normalize ──▶ filter ──▶ dedup ──▶ chunk ──▶ JSONL / Parquet
 (pdf, epub,    (pypdf,          (unicode NFC,       (length,    (exact hash,  (by tokens,   one record per line,
  html, md)      ebooklib, bs4)   whitespace, PII)    language)   near-dup)     with overlap) with provenance
# labs/corpus.py: build a deduplicated, chunked JSONL corpus from text and markdown files
import hashlib
import json
import re
import sys
import unicodedata
from collections.abc import Iterable, Iterator
from pathlib import Path

EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")
WS = re.compile(r"[ \t]+")

def extract(path: Path) -> str:
    if path.suffix in {".txt", ".md"}:
        return path.read_text(encoding="utf-8", errors="replace")
    if path.suffix == ".pdf":
        from pypdf import PdfReader                    # uv add pypdf  (imported only when needed)
        return "\n".join(page.extract_text() or "" for page in PdfReader(path).pages)
    raise ValueError(f"unsupported: {path.suffix}")

def clean(text: str) -> str:
    text = unicodedata.normalize("NFC", text)          # one encoding for "é" (Security Ch 7)
    text = EMAIL.sub("<email>", text)                  # scrub obvious PII before it reaches a model
    text = "\n".join(WS.sub(" ", line).strip() for line in text.splitlines())
    return re.sub(r"\n{3,}", "\n\n", text).strip()

def chunks(text: str, max_words: int = 200, overlap: int = 40) -> Iterator[str]:
    words = text.split()
    step = max_words - overlap
    for start in range(0, max(len(words) - overlap, 1), step):
        yield " ".join(words[start : start + max_words])

def build(paths: Iterable[Path], out: Path, min_words: int = 20) -> dict[str, int]:
    seen: set[str] = set()
    stats = {"files": 0, "chunks": 0, "dupes": 0, "short": 0}
    with out.open("w", encoding="utf-8") as f:
        for path in paths:
            stats["files"] += 1
            for i, chunk in enumerate(chunks(clean(extract(path)))):
                if len(chunk.split()) < min_words:
                    stats["short"] += 1
                    continue
                key = hashlib.sha256(chunk.lower().encode()).hexdigest()
                if key in seen:
                    stats["dupes"] += 1
                    continue
                seen.add(key)
                record = {"id": key[:16], "source": str(path), "chunk": i, "text": chunk}
                f.write(json.dumps(record, ensure_ascii=False) + "\n")
                stats["chunks"] += 1
    return stats

if __name__ == "__main__":
    root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
    files = sorted(p for p in root.rglob("*") if p.suffix in {".md", ".txt", ".pdf"})
    print(build(files, Path("corpus.jsonl")))

What to notice:

  • JSONL (one JSON object per line) is the lingua franca of LLM data: streamable (Ch 13), appendable, splittable, and readable with head. Use Parquet when the data is large and columnar.
  • Provenance in every record (source, chunk). When a model or a RAG answer says something wrong, you need to find where it came from.
  • Deduplicate before splitting train/test, or duplicates leak across the split (Ch 56.3). Exact hashing catches copies; near-duplicates need MinHash/LSH (datasketch).
  • Scrub PII early and keep raw data out of the training set's directory. Be deliberate about which directories a pipeline reads: point it at an explicit list of roots, never at a home directory, because personal documents (payslips, IDs) end up in corpora this way.
  • Chunk by tokens for real models (count with the model's tokenizer, Ch 59); words are an approximation.

Run each stage as a separate, restartable step that writes its output atomically (Ch 35), so a crash at stage 4 does not redo stages 1–3.


Generated from real-life-python-guide.md · 8 Oct 2026 16:36