parse_document
The primary entry point for parsing documents. Pass it a file path, URL, bytes, or file-like object and it returns a ParseResult — no need to branch on format.
Signature
docslicer.parse_document(
source,
source_url=None,
max_chunk_size=3200,
optimal_chunk_size=1500,
min_chunk_size=700,
chunking=True,
merge_small_chunks=True,
table_representation="markdown",
exact_tokens=False,
debug=False,
extra_fields=None,
password=None,
max_workers=None,
use_browser=True,
include_headers_footers=False,
include_footnotes=True,
include_comments=False,
include_speaker_notes=True,
on_stage=None,
) -> ParseResultParameters
| Parameter | Type | Default | Description |
|---|---|---|---|
source | str | Path | bytes | IO | — | File path, URL, raw bytes, raw HTML string, or file-like object. |
source_url | str | None | None | HTML only. Base URL used to resolve relative links in the parsed output (e.g. href="/about" → https://example.com/about). Only relevant when source is a local HTML file or raw HTML string. Has no effect for PDF, DOCX, or PPTX. |
max_chunk_size | int | 3200 | Hard ceiling on chunk size in characters. No chunk will exceed this. |
optimal_chunk_size | int | 1500 | Target characters per chunk. The chunker aims to stay near this size when deciding where to split. |
min_chunk_size | int | 700 | Soft floor. Chunks below this are merged into an adjacent chunk when possible. Short chunks at section boundaries may still be smaller. |
chunking | bool | True | Build chunks from blocks. Set to False to skip chunking and return only result.blocks — faster when you only need the raw blocks. |
merge_small_chunks | bool | True | Merge chunks that fall below min_chunk_size into adjacent chunks. |
table_representation | str | "markdown" | How tables are serialised into chunk text. One of "markdown", "jsonl", or "melted". See Table representations. |
exact_tokens | bool | False | Use tiktoken (cl100k_base) for exact token counts on each chunk and the document total. Falls back to chars / 4 if tiktoken is not installed. Requires pip install 'docslicer[llm]'. |
debug | bool | False | Populate result.pipeline_steps with intermediate DataFrames from each pipeline stage. |
extra_fields | list[str] | None | None | Additional pipeline DataFrame columns to attach to each Block and Chunk under their .extra dict (e.g. ["is_bold", "font_name", "font_size"]). Unknown columns get None. |
password | str | None | None | Password for an encrypted document. Works for PDF out of the box; .docx and .pptx need pip install 'docslicer[crypto]'. Ignored for HTML. |
max_workers | int | None | None | PDF only. Process-pool width for word extraction, cell building, and OCR within this document. Defaults to auto-sizing to the machine's performance cores. Set to 1 to keep a document single-process — e.g. when parsing many documents concurrently yourself via parse_all and you want to avoid nested process pools. Accepted but unused for DOCX, PPTX, and HTML. |
use_browser | bool | True | HTML only. Renders via Playwright for full JS execution, layout coordinates, and CSS-resolved styling. Set to False to use the static BeautifulSoup extractor instead — no browser launch, ~15x faster, works without Playwright installed, but loses layout coordinates and only resolves inline-style/semantic-tag typography. Accepted but unused for PDF, DOCX, and PPTX. |
include_headers_footers | bool | False | DOCX only. Include header and footer content as blocks with block_type "header" / "footer". Accepted but unused for PDF, PPTX, and HTML. |
include_footnotes | bool | True | DOCX only. Include footnotes and endnotes as extracted content. Accepted but unused for PDF, PPTX, and HTML. |
include_comments | bool | False | DOCX only. Include reviewer comments as extracted content — these are review annotations, not document content. Accepted but unused for PDF, PPTX, and HTML. |
include_speaker_notes | bool | True | PPTX only. Include speaker notes as extracted content. Accepted but unused for PDF, DOCX, and HTML. |
on_stage | Callable[[str], None] | None | None | Optional callback invoked with a stage name (e.g. "extract_elements", "process_layouts", "extract_tables") as the pipeline progresses, for driving a progress indicator. |
Format detection
The format is resolved in this order:
- Extension —
.pdf,.docx,.pptx,.html,.htm,.xhtml - Magic bytes — PDF starts with
%PDF; DOCX/PPTX detected from ZIP contents - URL — fetched first, then the delivered content type determines the parser
- Fallback — anything else is treated as HTML
Examples
File paths
import docslicer
result = docslicer.parse_document("report.pdf")
result = docslicer.parse_document("report.docx")
result = docslicer.parse_document("slides.pptx")
result = docslicer.parse_document("page.html")
for chunk in result.chunks:
print(chunk.text)Fine as-is in a REPL or notebook. If you save this as a .py script on macOS or Windows and parse large or scanned PDFs, guard the entry point with if __name__ == "__main__": — see intra-document parallelism for why.
Legacy formats (.doc, .ppt) are not supported. DocSlicer only reads the modern Open XML formats (.docx, .pptx). If you have legacy files, convert them first using LibreOffice (~350 MB download, ~800 MB installed) — DocSlicer does not bundle it as a dependency given its size.
# Install LibreOffice (macOS)
brew install --cask libreoffice
# Convert to docx / pptx
libreoffice --headless --convert-to docx report.doc
libreoffice --headless --convert-to pptx slides.pptThen pass the converted file to parse_document as normal.
URLs
The URL is fetched once and the delivered content type determines which parser runs — a URL ending in .pdf that actually serves a PDF will parse as PDF, not HTML:
result = docslicer.parse_document("https://example.com/annual-report")
result = docslicer.parse_document("https://example.com/filing.pdf")Raw bytes
with open("contract.pdf", "rb") as f:
result = docslicer.parse_document(f)Tune chunk sizes
result = docslicer.parse_document(
"report.pdf",
max_chunk_size=2000,
optimal_chunk_size=800,
min_chunk_size=400,
)Control intra-document parallelism (PDF)
Text extraction dominates PDF parsing time, and pages are independent, so the PDF pipeline distributes them across a process pool by default. No opt-in is required, and the default is appropriate for most workloads. DOCX, PPTX, and HTML are parsed single-process; max_workers is accepted for those formats but has no effect.
PDF parsing with parse_document is parallel by default. With max_workers unspecified, DocSlicer starts a ProcessPoolExecutor for any scanned PDF, and for any native PDF over 50 pages. Pass max_workers=1 to stay single-process.
Each stage is gated by document size. A process pool has fixed costs — process spawn, an interpreter and pandas import per worker, pickling work in and results out — that outweigh the speedup on small documents. Every page-parallel stage therefore has its own page threshold, evaluated before the worker count is resolved: below the threshold the stage runs single-process regardless of max_workers.
| Stage | Goes parallel at | Rationale |
|---|---|---|
| Word extraction (native PDF) | 50+ pages | Below this the pool costs more than it saves. |
| OCR (scanned PDF) | any length | Tesseract per page dwarfs pool startup, so it always pays off. |
| Cell building (tables) | 1500+ pages | Cheap per page; only worth it on very large documents. |
How the default width is chosen. With max_workers=None, DocSlicer counts performance cores, not logical CPUs: P-cores via hw.perflevel0.physicalcpu on Apple Silicon, unique physical cores from /sys topology on Linux, GetLogicalProcessorInformationEx on Windows. Efficiency cores and SMT/hyper-threading siblings are deliberately excluded — they share execution resources, so counting them over-commits compute-bound work and can make parsing slower. Detection is best-effort and zero-dependency; every probe falls back to os.cpu_count().
On Linux the count is also capped by the scheduler affinity mask. That covers cpuset pinning — docker run --cpuset-cpus, taskset, Kubernetes' static CPU-manager policy — where the process genuinely sees fewer cores. It does not cover CFS quota, which is how docker run --cpus=2, ECS task CPU units, and Kubernetes limits.cpu throttle by default. A quota caps how much CPU time you get, but the container still reports every core on the host, so detection has nothing to key off. See below.
# Override the inferred width
result = docslicer.parse_document("report.pdf", max_workers=4)
# Force single-process
result = docslicer.parse_document("report.pdf", max_workers=1)When you should override it
The common thread: DocSlicer sizes the pool to the cores it can see, which is not always the CPU you are entitled to, and not always CPU you want it to take.
CPU-quota'd containers — ECS, Cloud Run, Kubernetes limits.cpu, docker --cpus. As above, a quota throttles CPU time without shrinking the visible core count, so on a 32-core host with a 2-CPU quota DocSlicer will happily start 16 workers. You get all the pool's costs — spawn, per-worker pandas import, 16 copies of the PDF in RAM — while the cgroup throttles you back to 2 CPUs of actual throughput. Set it to your quota:
# Match the CPU limit the orchestrator actually granted you
result = docslicer.parse_document("report.pdf", max_workers=2)Cpuset-pinned containers (--cpuset-cpus, k8s static CPU-manager policy) don't need this — affinity detection already handles them.
Serverless. On AWS Lambda vCPU scales with configured memory (roughly one vCPU at 1,769 MB), so a function below that gets a fraction of a core and cannot absorb pool overhead. Memory is the sharper constraint: each worker is a real process with its own interpreter, its own pandas import, and its own pickled copy of the PDF bytes, so peak RSS scales with worker count. On a memory-capped function that is a plausible OOM, not just a slowdown.
result = docslicer.parse_document(pdf_bytes, max_workers=1)Shared web servers. Under gunicorn/uvicorn you already run several worker processes, so a per-request pool multiplies out to requests in flight × P. Even at low traffic a compute pool competes with request handling and inflates tail latency for every other caller. Keep parsing single-process and let the server own concurrency — or move parsing to a background queue, where a larger max_workers is appropriate again.
You already parallelise at the document level. Fanning documents out yourself nests pools — N documents × P workers each. Set max_workers=1 so your outer pool owns the concurrency. DocumentParser(config, workers=N) does this for you: when you set workers and haven't chosen a max_workers, each worker's own width defaults to 1.
Daemonic workers must use max_workers=1. Python forbids daemonic processes from having children, so a process pool cannot start inside one — Celery's default prefork worker is daemonic, as is any multiprocessing.Process with daemon=True.
Unlike the BrokenProcessPool fallback below, this raises AssertionError: daemonic processes are not allowed to have children and fails the parse. It only bites once a stage crosses its threshold, so a 20-page test file passes and a 60-page or scanned document fails in production. Either pin max_workers=1, or run Celery with --pool=prefork disabled (e.g. --pool=solo/threads).
@app.task
def parse_task(path):
return docslicer.parse_document(path, max_workers=1)Unguarded scripts silently lose parallelism. On macOS and Windows, Python's spawn start method re-imports your __main__ module in every worker. If your script does work at import time, each worker crashes on import and the pool dies with BrokenProcessPool.
DocSlicer catches this and re-runs the stage single-process, so your parse still succeeds — it just quietly runs several times slower, with a warning in the logs. Guard your entry point to keep parallelism:
if __name__ == "__main__":
result = docslicer.parse_document("report.pdf")Skip chunking to get raw blocks only
result = docslicer.parse_document("report.pdf", chunking=False)
for block in result.blocks:
print(block.text)Get exact token counts
result = docslicer.parse_document("contract.pdf", exact_tokens=True)
print(result.metadata.token_count) # document total
for chunk in result.chunks:
print(chunk.token_count)Attach extra fields
result = docslicer.parse_document(
"contract.pdf",
extra_fields=["is_bold", "font_name", "font_size"],
)
for block in result.blocks:
print(block.extra["font_name"], block.text[:60])Returns
A ParseResult.
Format-specific parsers
If you need parsing to fail when the file doesn't match an expected format, use the format-specific functions directly. See Format parsers.
For batches, see parse_all.