Dev.to · 5 min read

Bulk-Convert Digital PDFs to Text and Markdown for a RAG Index (and Catch the Scanned Ones)

Bulk-Convert Digital PDFs to Text and Markdown for a RAG Index (and Catch the Scanned Ones)

Most "chat with your documents" projects die on step one: getting text out of the PDFs. A handful of files is easy. A few hundred reports, manuals and specs from different sources is where you find the edge cases: encrypted files, 400-page monsters, and scans that look like documents but contain only images. This post walks through: a DIY extraction script and its traps, the same batch job with a hosted Actor and real output, chunking the result for a search or RAG index, plus cost and limits. The DIY version With pypdf the basic extraction is a few lines: import io, requests from pypdf import PdfReader def pdf_text(url): data = requests.get(url, timeout=60).content reader = PdfReader(io.BytesIO(data)) return "\n\n".join(page.extract_text() or "" for page in reader.pages) What bites you in production: Scanned PDFs return an empty string, silently. Your index now holds a "document" with no text, and nobody notices. Encrypted files raise errors or return nothing. Size and page limits. One huge file can stall a whole batch, so you need byte and page caps. Bookkeeping. You want page counts, title/author metadata and a status per file, not a pile of strings. The hosted version PDF to Text Markdown Extractor (Digital, No OCR) does this batch job with explicit statuses. Disclosure: I build this Actor at SteadyScrape. It downloads each direct PDF URL and extracts text locally. There's no OCR in this version, so scanned files are labelled image_only and incur no extraction charge. Input matching the current schema: { "pdfUrls": [ "https://www.orimi.com/pdf-test.pdf", "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", "https://www.rfc-editor.org/rfc/pdfrfc/rfc8259.txt.pdf" ], "format": "both", "maxFiles": 20, "includeMetadata": true } format is text, markdown or both. You can also upload files in the Apify Console instead of passing links. From Python (token from an environment variable): import os from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) run = client.actor("steady-scrape/pdf-text-markdown").call(run_input={ "pdfUrls": [ "https://www.orimi.com/pdf-test.pdf", "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", "https://www.rfc-editor.org/rfc/pdfrfc/rfc8259.txt.pdf", ], "format": "both", }) rows = list(client.dataset(run["defaultDatasetId"]).iterate_items()) Real output from a verified run of exactly those three files (text trimmed): [ {"source": "https://www.orimi.com/pdf-test.pdf", "status": "ok", "pageCount": 1, "chars": 367, "title": "PDF Test Page", "text": "PDF Test File\n\nCongratulations, your computer is equipped with a PDF ...", "chargeEvent": "pdf-extracted"}, {"source": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", "status": "image_only", "note": "No extractable text — PDF looks image-only/scanned. OCR is not included in v1. Not charged.", "text": null, "chargeEvent": null}, {"source": "https://www.rfc-editor.org/rfc/pdfrfc/rfc8259.txt.pdf", "status": "ok", "pageCount": 16, "chars": 23656, "text": "Internet Engineering Task Force (IETF) T. Bray, Ed.\nRequest for Comments: 8259 ...", "chargeEvent": "pdf-extracted"} ] That run charged two PDF extractions plus the run fee and the standard start event; the image-only file added no charge, and the row tells you why. Chunking for a search or RAG index Keep the ok rows, route image_only rows to an OCR queue (or a human), and split the Markdown on headings, with a size fallback: import re def chunks(md, max_chars=2000): parts = re.split(r"\n(?=#{1,3} )", md) for part in parts: for i in range(0, len(part), max_chars): yield part[i:i + max_chars] index, needs_ocr = [], [] for r in rows: if r["status"] == "ok": for n, c in enumerate(chunks(r["markdown"] or r["text"])): index.append({"source": r["source"], "chunk": n, "text": c}) elif r["status"] == "image_only": needs_ocr.append(r["source"]) print(len(index), "chunks;", len(needs_ocr), "files need OCR") Embed index with whatever model you use and store source with each chunk so answers can cite the file. The Markdown headings are heuristic: the PDF format doesn't reliably mark headings, so treat them as hints for splitting, not as a guaranteed document outline. Tables and multi-column layouts can come out in reading-order text rather than structured tables. What it costs From the Store pricing: $0.004 per successfully extracted digital PDF $0.003 once per run, only if at least one PDF was extracted Apify's standard $0.00005 Actor start event 200 successfully extracted digital PDFs in one run cost $0.80305 (200 × $0.004 + $0.003 + $0.00005). Scanned/image-only, empty, encrypted, oversized and failed files incur no extraction charge, and a run where no PDF is extracted doesn't pay the $0.003 run fee; Apify's $0.00005 start event still applies to every run. Limits No OCR. Scanned documents are flagged, not read. Direct PDF links only. It doesn't crawl a website looking for PDFs. Per-run file caps and per-file page and size caps apply. Oversized files return too_large / too_many_pages (no extraction charge). Only process documents you're allowed to use. Integrations Run it via the Apify API, on a schedule, or from Make, Zapier or n8n through Apify's integrations. AI agents can call it through the Apify MCP server, which is handy as a "read this PDF" tool. SteadyScrape (steady-scrape) builds small, pay-per-result data tools on Apify.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News