Skip to content

Extraer texto de PDF en Python

La extracción de texto de PDF es una de las tareas más comunes en los pipelines de procesamiento de documentos: desde la construcción de índices de búsqueda y la alimentación de sistemas RAG hasta la minería de datos y los flujos de trabajo de cumplimiento normativo. Esta guía cubre todo lo que necesitas para extraer texto de PDFs en Python, JavaScript y Rust usando PDF Oxide, incluyendo extracción de texto plano, posicionamiento a nivel de carácter, spans con estilo, OCR para documentos escaneados, manejo de archivos cifrados y ajuste de rendimiento para pipelines por lotes.

Extrae texto de cualquier PDF en tres líneas:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("document.pdf")
text = doc.extract_text(0)  # page 0
print(text)

WASM

import { WasmPdfDocument } from "pdf-oxide-wasm";

const bytes = new Uint8Array(buffer);
const doc = new WasmPdfDocument(bytes);
const text = doc.extractText(0); // page 0
console.log(text);
doc.free();

Rust

use pdf_oxide::PdfDocument;

let mut doc = PdfDocument::open("document.pdf")?;
let text = doc.extract_text(0)?;
println!("{}", text);

Go

package main

import (
    "fmt"
    "log"
    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
    doc, err := pdfoxide.Open("document.pdf")
    if err != nil { log.Fatal(err) }
    defer doc.Close()

    text, err := doc.ExtractText(0) // page 0
    if err != nil { log.Fatal(err) }
    fmt.Println(text)
}

C#

using PdfOxide;

using var doc = PdfDocument.Open("document.pdf");
var text = doc.ExtractText(0); // page 0
Console.WriteLine(text);

Java

import fyi.oxide.pdf.PdfDocument;
import java.nio.file.Path;

try (PdfDocument doc = PdfDocument.open(Path.of("document.pdf"))) {
    String text = doc.extractText(0); // page 0
    System.out.println(text);
}

Kotlin

import fyi.oxide.pdf.PdfDocument
import java.nio.file.Path

PdfDocument.open(Path.of("document.pdf")).use { doc ->
    val text = doc.extractText(0) // page 0
    println(text)
}

Scala

import fyi.oxide.pdf.PdfDocument
import scala.util.Using

Using.resource(PdfDocument.open("document.pdf")) { doc =>
  val text = doc.extractText(0) // page 0
  println(text)
}

Clojure

(require '[pdf-oxide.core :as pdf])

(with-open [doc (pdf/open "document.pdf")]
  (println (pdf/extract-text doc 0))) ; page 0

PHP

use PdfOxide\PdfDocument;

$doc  = PdfDocument::open('document.pdf');
$text = $doc->extractText(0); // page 0
echo $text;
$doc->close();

Ruby

require 'pdf_oxide'

PdfOxide::PdfDocument.open('document.pdf') do |doc|
  text = doc.extract_text(0) # page 0
  puts text
end

C++

#include <pdf_oxide/pdf_oxide.hpp>
#include <iostream>

auto doc  = pdf_oxide::Document::open("document.pdf");
auto text = doc.extract_text(0); // page 0
std::cout << text << '\n';

Swift

import PdfOxide

let doc  = try Document.open("document.pdf")
let text = try doc.extractText(0) // page 0
print(text)

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc  = PdfDocument.open('document.pdf');
final text = doc.extractText(0); // page 0
print(text);
doc.close();

R

library(pdfoxide)

doc  <- pdf_open("document.pdf")
text <- pdf_extract_text(doc, 0) # page 0
cat(text)

Julia

using PdfOxide

doc  = open_document("document.pdf")
text = extract_text(doc, 0) # page 0
println(text)

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("document.pdf");
const text = try doc.extractText(a, 0); // page 0
std.debug.print("{s}\n", .{text});

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"document.pdf" error:&err];
NSString *text = [doc extractText:0 error:&err]; // page 0
NSLog(@"%@", text);

Elixir

{:ok, doc}  = PdfOxide.open("document.pdf")
{:ok, text} = PdfOxide.extract_text(doc, 0) # page 0
IO.puts(text)

PDF Oxide extrae texto a una media de 0,8 ms por página — 5 veces más rápido que PyMuPDF, 15 veces más rápido que pypdf — con una tasa de éxito del 100 % en 3.830 PDFs de prueba.

Por qué la extracción de texto de PDF es difícil

PDF es un formato visual, no un formato de texto. A diferencia de HTML o Markdown, un archivo PDF no almacena «párrafos» ni «frases» — almacena caracteres individuales posicionados en coordenadas específicas de una página. Para extraer texto legible se necesita:

  • Decodificación de fuentes — Las fuentes PDF mapean códigos de caracteres a glifos usando tablas de codificación (WinAnsi, MacRoman, Unicode CMap, Type 1, TrueType, CIDFont). El código de carácter 0x41 puede significar «A» en una fuente y «α» en otra.
  • Análisis del flujo de texto — Los operadores de texto como Tj, TJ, ', " colocan caracteres en la página. Los ajustes de kerning en los arrays TJ desplazan caracteres en fracciones de punto. Los espacios ausentes deben inferirse a partir de los huecos entre posiciones de caracteres.
  • Reconstrucción del diseño — Los caracteres de una página no tienen un orden de lectura explícito. Los diseños de dos columnas, encabezados, pies de página, tablas y barras laterales deben analizarse espacialmente para producir un flujo de texto lineal.
  • Casos extremos de codificación — El texto CJK (chino, japonés, coreano) usa codificación CIDFont/CMap con miles de glifos. El árabe y el hebreo requieren reordenamiento de derecha a izquierda. Las ligaduras (fi, fl, ffi) deben descomponerse.
  • Subconjuntos incrustados — Muchos PDFs incrustan solo los glifos que usan, con vectores de codificación personalizados. Una fuente puede mapear el índice de glifo 1→«T», 2→«h», 3→«e» sin codificación estándar.

Por eso diferentes bibliotecas PDF producen salidas de texto distintas para el mismo archivo, y por eso algunas fallan completamente con documentos complejos. PDF Oxide maneja todos estos casos con un parser basado en Rust probado en 3.830 PDFs reales con una tasa de éxito del 100 %.

Instalación

Python (PyPI):

pip install pdf_oxide

Wheels precompilados para Linux (x86_64, aarch64), macOS (Intel y Apple Silicon) y Windows (x86_64). Python 3.8+. Sin dependencias del sistema — el núcleo Rust está compilado en el wheel, así que no es necesario instalar Poppler, MuPDF ni ninguna biblioteca C.

JavaScript (npm):

npm install pdf-oxide-wasm

Funciona en Node.js 18+ y navegadores modernos. El binario WASM está incluido en el paquete.

Rust (Cargo):

cargo add pdf_oxide

Requiere Rust 1.70+. Sin dependencias del sistema más allá de un toolchain Rust estándar.

Extraer todas las páginas

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("report.pdf")
full_text = []
for i in range(doc.page_count()):
    text = doc.extract_text(i)
    full_text.append(text)

print("\n".join(full_text))

WASM

const doc = new WasmPdfDocument(bytes);
const fullText = doc.extractAllText();
console.log(fullText);
doc.free();

Rust

let mut doc = PdfDocument::open("report.pdf")?;
let mut full_text = Vec::new();
for i in 0..doc.page_count()? {
    full_text.push(doc.extract_text(i)?);
}
println!("{}", full_text.join("\n"));

Go

doc, err := pdfoxide.Open("report.pdf")
if err != nil { log.Fatal(err) }
defer doc.Close()

full, err := doc.ExtractAllText()
if err != nil { log.Fatal(err) }
fmt.Println(full)

C#

using var doc = PdfDocument.Open("report.pdf");
var parts = new List<string>();
for (int i = 0; i < doc.PageCount; i++)
    parts.Add(doc.ExtractText(i));
Console.WriteLine(string.Join("\n", parts));

Java

try (PdfDocument doc = PdfDocument.open(Path.of("report.pdf"))) {
    StringBuilder all = new StringBuilder();
    for (int i = 0; i < doc.pageCount(); i++)
        all.append(doc.extractText(i));
    System.out.println(all);
}

Kotlin

PdfDocument.open(Path.of("report.pdf")).use { doc ->
    val all = (0 until doc.pageCount()).joinToString("") { doc.extractText(it) }
    println(all)
}

Scala

Using.resource(PdfDocument.open("report.pdf")) { doc =>
  val all = (0 until doc.pageCount()).map(doc.extractText).mkString
  println(all)
}

Clojure

(with-open [doc (pdf/open "report.pdf")]
  (println (apply str (map #(pdf/extract-text doc %)
                           (range (pdf/page-count doc))))))

PHP

$doc = PdfDocument::open('report.pdf');
$all = '';
for ($i = 0; $i < $doc->pageCount(); $i++) { $all .= $doc->extractText($i); }
echo $all;
$doc->close();

Ruby

PdfOxide::PdfDocument.open('report.pdf') do |doc|
  all = (0...doc.page_count).map { |i| doc.extract_text(i) }.join
  puts all
end

C++

auto doc = pdf_oxide::Document::open("report.pdf");
auto all = doc.extract_all_text();
std::cout << all << '\n';

Swift

let doc = try Document.open("report.pdf")
let all = try doc.extractAllText()
print(all)

Dart

final doc = PdfDocument.open('report.pdf');
final all = doc.extractAllText();
print(all);
doc.close();

R

doc <- pdf_open("report.pdf")
all <- pdf_extract_all_text(doc)
cat(all)

Julia

doc = open_document("report.pdf")
all = extract_all_text(doc)
println(all)

Zig

var doc = try pdf_oxide.Document.open("report.pdf");
const all = try doc.extractAllText(a);
std.debug.print("{s}\n", .{all});

Objective-C

POXDocument *doc = [POXDocument openPath:@"report.pdf" error:&err];
NSString *all = [doc extractAllTextWithError:&err];
NSLog(@"%@", all);

Elixir

{:ok, doc} = PdfOxide.open("report.pdf")
{:ok, n}   = PdfOxide.page_count(doc)
all = 0..(n - 1)
      |> Enum.map(fn i -> {:ok, t} = PdfOxide.extract_text(doc, i); t end)
      |> Enum.join()
IO.puts(all)

Extraer texto con posiciones de carácter

Obtén coordenadas exactas, nombres de fuente y tamaños para cada carácter:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("paper.pdf")
chars = doc.extract_chars(0)

for ch in chars[:20]:
    print(f"'{ch.char}' at ({ch.x:.1f}, {ch.y:.1f}) "
          f"font={ch.font_name} size={ch.font_size:.1f}")

WASM

const doc = new WasmPdfDocument(bytes);
const chars = doc.extractChars(0);
for (const ch of chars.slice(0, 20)) {
    console.log(`'${ch.char}' at (${ch.x.toFixed(1)}, ${ch.y.toFixed(1)}) font=${ch.fontName} size=${ch.fontSize.toFixed(1)}`);
}
doc.free();

Rust

let mut doc = PdfDocument::open("paper.pdf")?;
let chars = doc.extract_chars(0)?;
for ch in chars.iter().take(20) {
    println!("'{}' at ({:.1}, {:.1}) font={} size={:.1}",
        ch.char, ch.x, ch.y, ch.font_name, ch.font_size);
}

Go

doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()

chars, _ := doc.ExtractChars(0)
for _, ch := range chars[:20] {
    fmt.Printf("%q at (%.1f, %.1f) font=%s size=%.1f\n",
        ch.Char, ch.X, ch.Y, ch.FontName, ch.FontSize)
}

C#

using var doc = PdfDocument.Open("paper.pdf");
var chars = doc.ExtractChars(0);
foreach (var ch in chars.Take(20))
    Console.WriteLine($"'{ch.Char}' at ({ch.X:F1}, {ch.Y:F1}) font={ch.FontName} size={ch.FontSize:F1}");

Java

import fyi.oxide.pdf.text.TextChar;

try (PdfDocument doc = PdfDocument.open(Path.of("paper.pdf"))) {
    for (TextChar ch : doc.page(0).chars().subList(0, 20)) {
        System.out.printf("'%s' at (%.1f, %.1f)%n",
            ch.asString(), ch.bbox().x0(), ch.bbox().y0());
    }
}

Kotlin

PdfDocument.open(Path.of("paper.pdf")).use { doc ->
    doc.page(0).chars().take(20).forEach { ch ->
        println("'${ch.asString()}' at (${ch.bbox().x0()}, ${ch.bbox().y0()})")
    }
}

Scala

import fyi.oxide.pdf.charsSeq

Using.resource(PdfDocument.open("paper.pdf")) { doc =>
  doc.page(0).charsSeq.take(20).foreach { ch =>
    println(f"'${ch.asString}' at (${ch.bbox.x0}%.1f, ${ch.bbox.y0}%.1f)")
  }
}

Clojure

(with-open [doc (pdf/open "paper.pdf")]
  (doseq [ch (take 20 (pdf/chars (pdf/page doc 0)))]
    (let [b (.bbox ch)]
      (println (format "'%s' at (%.1f, %.1f)"
                       (.asString ch) (.x0 b) (.y0 b))))))

C++

auto doc   = pdf_oxide::Document::open("paper.pdf");
auto chars = doc.extract_chars(0);
int shown = 0;
for (const auto& ch : chars) {
    if (shown++ >= 20) break;
    std::printf("U+%04X at (%.1f, %.1f) font=%s size=%.1f\n",
        ch.character, ch.bbox.x, ch.bbox.y,
        ch.font_name.c_str(), ch.font_size);
}

Swift

let doc   = try Document.open("paper.pdf")
let chars = try doc.extractChars(0)
for ch in chars.prefix(20) {
    let s = String(UnicodeScalar(ch.character) ?? " ")
    print("'\(s)' at (\(ch.bbox.x), \(ch.bbox.y)) font=\(ch.fontName) size=\(ch.fontSize)")
}

Dart

final doc   = PdfDocument.open('paper.pdf');
final chars = doc.extractChars(0);
for (final ch in chars.take(20)) {
  final s = String.fromCharCode(ch.character);
  print("'$s' at (${ch.bbox.x}, ${ch.bbox.y}) "
      "font=${ch.fontName} size=${ch.fontSize}");
}
doc.close();

R

doc   <- pdf_open("paper.pdf")
chars <- pdf_extract_chars(doc, 0)
for (ch in head(chars, 20)) {
  cat(sprintf("'%s' at (%.1f, %.1f) font=%s size=%.1f\n",
              intToUtf8(ch$character), ch$bbox$x, ch$bbox$y,
              ch$font_name, ch$font_size))
}

Julia

doc   = open_document("paper.pdf")
chars = extract_chars(doc, 0)
for ch in chars[1:min(20, end)]
    println("'$(Char(ch.character))' at ($(ch.bbox.x), $(ch.bbox.y)) ",
            "font=$(ch.font_name) size=$(ch.font_size)")
end

Zig

var doc = try pdf_oxide.Document.open("paper.pdf");
const chars = try doc.extractChars(a, 0);
defer pdf_oxide.Document.freeChars(a, chars);
for (chars[0..@min(20, chars.len)]) |ch| {
    std.debug.print("U+{X:0>4} at ({d:.1}, {d:.1}) font={s} size={d:.1}\n",
        .{ ch.character, ch.bbox.x, ch.bbox.y, ch.fontName, ch.fontSize });
}

Objective-C

POXDocument *doc = [POXDocument openPath:@"paper.pdf" error:&err];
NSArray<POXChar*> *chars = [doc extractChars:0 error:&err];
for (POXChar *ch in [chars subarrayWithRange:NSMakeRange(0, MIN(20, chars.count))]) {
    NSLog(@"U+%04X at (%.1f, %.1f) font=%@ size=%.1f",
        ch.character, ch.bbox.x, ch.bbox.y, ch.fontName, ch.fontSize);
}

Elixir

{:ok, doc}   = PdfOxide.open("paper.pdf")
{:ok, chars} = PdfOxide.extract_chars(doc, 0)
chars
|> Enum.take(20)
|> Enum.each(fn ch ->
  IO.puts("'#{<<ch.character::utf8>>}' at (#{ch.bbox.x}, #{ch.bbox.y}) " <>
          "font=#{ch.font_name} size=#{ch.font_size}")
end)

Cada carácter incluye:

Campo Tipo Descripción
char str El carácter Unicode
x, y float Posición en puntos
font_size float Tamaño de fuente en puntos
font_name str Nombre de fuente PostScript
bbox tuple Caja delimitadora (x0, y0, x1, y1)

La extracción a nivel de carácter es útil para reconstruir tablas, detectar encabezados por tamaño de fuente o construir cajas delimitadoras alrededor de regiones de texto. Por ejemplo, puedes agrupar caracteres en líneas por su coordenada y y detectar límites de columna por los huecos en las posiciones x.

Extraer spans de texto con estilo

Agrupa caracteres consecutivos por fuente y tamaño:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("paper.pdf")
spans = doc.extract_spans(0)

for span in spans:
    print(f"'{span.text}' font={span.font_name} size={span.font_size:.1f}")

WASM

const doc = new WasmPdfDocument(bytes);
const spans = doc.extractSpans(0);
for (const span of spans) {
    console.log(`'${span.text}' font=${span.fontName} size=${span.fontSize.toFixed(1)}`);
}
doc.free();

Rust

let mut doc = PdfDocument::open("paper.pdf")?;
let spans = doc.extract_spans(0)?;
for span in &spans {
    println!("'{}' font={} size={:.1}", span.text, span.font_name, span.font_size);
}

Útil para detectar encabezados, texto en negrita o construir salida estructurada.

Procesamiento por lotes

Procesa cientos o miles de PDFs:

from pdf_oxide import PdfDocument, PdfError
from pathlib import Path

pdf_dir = Path("documents/")
for pdf_path in pdf_dir.glob("*.pdf"):
    try:
        doc = PdfDocument(str(pdf_path))
        for i in range(doc.page_count()):
            text = doc.extract_text(i)
            # Process text...
    except PdfError as e:
        print(f"Skipped {pdf_path.name}: {e}")

A 0,8 ms por página, procesar 3.830 PDFs lleva unos 3,1 segundos. Para pipelines de producción, consulta la guía de Procesamiento por Lotes para patrones de procesamiento paralelo con multiprocessing y async I/O.

Manejo de PDFs escaneados (OCR)

Si un PDF contiene imágenes escaneadas en lugar de texto, extract_text() devuelve una salida vacía o mínima. Usa el OCR integrado de PDF Oxide:

from pdf_oxide import PdfDocument

doc = PdfDocument("scanned.pdf")
text = doc.extract_text(0)

if not text.strip():
    # Page is likely scanned — use OCR
    text = doc.extract_text_ocr(0)
    print(text)

PDF Oxide usa PaddleOCR mediante ONNX Runtime — no se requiere instalar Tesseract. Consulta la guía de OCR para selección de modelos y configuración.

Manejo de PDFs cifrados

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("protected.pdf", password="secret")
text = doc.extract_text(0)
print(text)

WASM

const doc = new WasmPdfDocument(bytes);
doc.authenticate("secret");
const text = doc.extractText(0);
console.log(text);
doc.free();

Rust

let mut doc = PdfDocument::open_with_password("protected.pdf", "secret")?;
let text = doc.extract_text(0)?;
println!("{}", text);

Go

doc, _ := pdfoxide.Open("protected.pdf")
defer doc.Close()

if _, err := doc.Authenticate("secret"); err != nil { log.Fatal(err) }
text, _ := doc.ExtractText(0)
fmt.Println(text)

C#

using var doc = PdfDocument.OpenWithPassword("protected.pdf", "secret");
Console.WriteLine(doc.ExtractText(0));

Java

try (PdfDocument doc = PdfDocument.open("protected.pdf", "secret")) {
    System.out.println(doc.extractText(0));
}

Kotlin

PdfDocument.open("protected.pdf", "secret").use { doc ->
    println(doc.extractText(0))
}

Scala

Using.resource(PdfDocument.open("protected.pdf", "secret")) { doc =>
  println(doc.extractText(0))
}

Clojure

(with-open [doc (pdf/open "protected.pdf" "secret")]
  (println (pdf/extract-text doc 0)))

Ruby

PdfOxide::PdfDocument.open('protected.pdf', password: 'secret') do |doc|
  puts doc.extract_text(0)
end

C++

auto doc = pdf_oxide::Document::open_with_password("protected.pdf", "secret");
std::cout << doc.extract_text(0) << '\n';

Swift

let doc = try Document.openWithPassword("protected.pdf", password: "secret")
print(try doc.extractText(0))

Dart

final doc = PdfDocument.openWithPassword('protected.pdf', 'secret');
print(doc.extractText(0));
doc.close();

R

doc <- pdf_open_with_password("protected.pdf", "secret")
cat(pdf_extract_text(doc, 0))

Julia

doc = open_with_password("protected.pdf", "secret")
println(extract_text(doc, 0))

Zig

var doc = try pdf_oxide.Document.openWithPassword("protected.pdf", "secret");
const text = try doc.extractText(a, 0);
std.debug.print("{s}\n", .{text});

Objective-C

POXDocument *doc = [POXDocument openWithPassword:@"protected.pdf"
                                       password:@"secret" error:&err];
NSLog(@"%@", [doc extractText:0 error:&err]);

Elixir

{:ok, doc}  = PdfOxide.open_with_password("protected.pdf", "secret")
{:ok, text} = PdfOxide.extract_text(doc, 0)
IO.puts(text)

Admite PDFs cifrados con AES-256, AES-128 y RC4. A diferencia de pdfplumber (que no puede abrir archivos cifrados) y pdfminer (que falla con AES-256), PDF Oxide maneja de forma transparente todos los métodos de cifrado estándar de PDF.

Salida como Markdown

Para salida estructurada con encabezados y formato:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("paper.pdf")
md = doc.to_markdown(0, detect_headings=True)
print(md)

WASM

const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdown(0);
console.log(md);
doc.free();

Rust

let mut doc = PdfDocument::open("paper.pdf")?;
let md = doc.to_markdown(0, true)?;
println!("{}", md);

Go

doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()

md, _ := doc.ToMarkdown(0)
fmt.Println(md)

C#

using var doc = PdfDocument.Open("paper.pdf");
Console.WriteLine(doc.ToMarkdown(0));

Java

try (PdfDocument doc = PdfDocument.open(Path.of("paper.pdf"))) {
    System.out.println(doc.toMarkdown(0));
}

Kotlin

PdfDocument.open(Path.of("paper.pdf")).use { doc ->
    println(doc.toMarkdown(0))
}

Scala

Using.resource(PdfDocument.open("paper.pdf")) { doc =>
  println(doc.toMarkdown(0))
}

Clojure

(with-open [doc (pdf/open "paper.pdf")]
  (println (pdf/to-markdown doc 0)))

PHP

$doc = PdfDocument::open('paper.pdf');
echo $doc->toMarkdown(0);
$doc->close();

Ruby

PdfOxide::PdfDocument.open('paper.pdf') do |doc|
  puts doc.to_markdown(0)
end

C++

auto doc = pdf_oxide::Document::open("paper.pdf");
std::cout << doc.to_markdown(0) << '\n';

Swift

let doc = try Document.open("paper.pdf")
print(try doc.toMarkdown(0))

Dart

final doc = PdfDocument.open('paper.pdf');
print(doc.toMarkdown(0));
doc.close();

R

doc <- pdf_open("paper.pdf")
cat(pdf_to_markdown(doc, 0))

Julia

doc = open_document("paper.pdf")
println(to_markdown(doc, 0))

Zig

var doc = try pdf_oxide.Document.open("paper.pdf");
const md = try doc.toMarkdown(a, 0);
std.debug.print("{s}\n", .{md});

Objective-C

POXDocument *doc = [POXDocument openPath:@"paper.pdf" error:&err];
NSLog(@"%@", [doc toMarkdown:0 error:&err]);

Elixir

{:ok, doc} = PdfOxide.open("paper.pdf")
{:ok, md}  = PdfOxide.to_markdown(doc, 0)
IO.puts(md)

Consulta la guía de PDF a Markdown para patrones de integración con RAG y LLM.

Buscar dentro de PDFs

Encuentra texto en todas las páginas con datos de posición:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("manual.pdf")
results = doc.search("configuration")
for r in results:
    print(f"Page {r.page}: '{r.text}' at ({r.x:.0f}, {r.y:.0f})")

WASM

const doc = new WasmPdfDocument(bytes);
const results = doc.search("configuration", false);
for (const r of results) {
    console.log(`Page ${r.page}: '${r.text}' at (${r.x.toFixed(0)}, ${r.y.toFixed(0)})`);
}
doc.free();

Rust

let mut pdf = Pdf::open("manual.pdf")?;
let results = pdf.search("configuration")?;
for r in &results {
    println!("Page {}: '{}' at ({:.0}, {:.0})", r.page, r.text, r.bbox.x, r.bbox.y);
}

Go

doc, _ := pdfoxide.Open("manual.pdf")
defer doc.Close()

results, _ := doc.SearchAll("configuration", false)
for _, r := range results {
    fmt.Printf("Page %d: %q at (%.0f, %.0f)\n", r.PageIndex, r.Text, r.X, r.Y)
}

C#

using var doc = PdfDocument.Open("manual.pdf");
foreach (var r in doc.SearchAll("configuration", caseSensitive: false))
    Console.WriteLine($"Page {r.PageIndex}: '{r.Text}' at ({r.X:F0}, {r.Y:F0})");

Java

import fyi.oxide.pdf.search.SearchMatch;

try (PdfDocument doc = PdfDocument.open(Path.of("manual.pdf"))) {
    for (SearchMatch m : doc.search("configuration")) {
        System.out.printf("Page %d: '%s' at (%.0f, %.0f)%n",
            m.pageIndex(), m.text(), m.bbox().x0(), m.bbox().y0());
    }
}

Kotlin

PdfDocument.open(Path.of("manual.pdf")).use { doc ->
    for (m in doc.search("configuration")) {
        println("Page ${m.pageIndex()}: '${m.text()}' at (${m.bbox().x0()}, ${m.bbox().y0()})")
    }
}

Scala

import fyi.oxide.pdf.searchSeq

Using.resource(PdfDocument.open("manual.pdf")) { doc =>
  for (m <- doc.searchSeq("configuration"))
    println(f"Page ${m.pageIndex}: '${m.text}' at (${m.bbox.x0}%.0f, ${m.bbox.y0}%.0f)")
}

Clojure

(with-open [doc (pdf/open "manual.pdf")]
  (doseq [m (pdf/search doc "configuration")]
    (let [b (.bbox m)]
      (println (format "Page %d: '%s' at (%.0f, %.0f)"
                       (.pageIndex m) (.text m) (.x0 b) (.y0 b))))))

Ruby

PdfOxide::PdfDocument.open('manual.pdf') do |doc|
  doc.search('configuration').each do |r|
    puts "Page #{r[:page]}: '#{r[:text]}' at (#{r[:bbox][:x].round}, #{r[:bbox][:y].round})"
  end
end

C++

auto doc = pdf_oxide::Document::open("manual.pdf");
for (const auto& r : doc.search_all("configuration", /*case_sensitive=*/false)) {
    std::printf("Page %d: '%s' at (%.0f, %.0f)\n",
        r.page, r.text.c_str(), r.bbox.x, r.bbox.y);
}

Swift

let doc = try Document.open("manual.pdf")
for r in try doc.searchAll("configuration", false) {
    print("Page \(r.page): '\(r.text)' at (\(r.bbox.x), \(r.bbox.y))")
}

Dart

final doc = PdfDocument.open('manual.pdf');
for (final r in doc.searchAll('configuration', false)) {
  print("Page ${r.page}: '${r.text}' at (${r.bbox.x}, ${r.bbox.y})");
}
doc.close();

R

doc <- pdf_open("manual.pdf")
for (r in pdf_search_all(doc, "configuration", case_sensitive = FALSE)) {
  cat(sprintf("Page %d: '%s' at (%.0f, %.0f)\n",
              r$page, r$text, r$bbox$x, r$bbox$y))
}

Julia

doc = open_document("manual.pdf")
for r in search_all(doc, "configuration", false)
    println("Page $(r.page): '$(r.text)' at ($(r.bbox.x), $(r.bbox.y))")
end

Zig

var doc = try pdf_oxide.Document.open("manual.pdf");
const hits = try doc.searchAll(a, "configuration", false);
defer pdf_oxide.Document.freeSearchResults(a, hits);
for (hits) |r| {
    std.debug.print("Page {d}: '{s}' at ({d:.0}, {d:.0})\n",
        .{ r.page, r.text, r.bbox.x, r.bbox.y });
}

Objective-C

POXDocument *doc = [POXDocument openPath:@"manual.pdf" error:&err];
for (POXSearchResult *r in [doc searchAll:@"configuration" caseSensitive:NO error:&err]) {
    NSLog(@"Page %ld: '%@' at (%.0f, %.0f)",
        (long)r.page, r.text, r.bbox.x, r.bbox.y);
}

Elixir

{:ok, doc}  = PdfOxide.open("manual.pdf")
{:ok, hits} = PdfOxide.search_all(doc, "configuration", false)
Enum.each(hits, fn r ->
  IO.puts("Page #{r.page}: '#{r.text}' at (#{r.bbox.x}, #{r.bbox.y})")
end)

Comparación con otras bibliotecas Python para PDF

Existen varias bibliotecas Python para la extracción de texto de PDF. Así se comparan:

  • pypdf — Python puro, sin dependencias C. Fácil de instalar pero lento (12 ms por página) y falla en el 1,6 % de los PDFs por soporte limitado de fuentes y codificaciones. Sin datos de posición de caracteres. Adecuado para PDFs simples donde la velocidad no importa.
  • pdfplumber — Basado en pdfminer, ofrece extracción detallada de caracteres y tablas. Muy lento (23 ms por página) y no puede abrir PDFs cifrados. El mejor para extracción de tablas cuando necesitas datos a nivel de celda y no te importa el rendimiento.
  • PyMuPDF (fitz) — Bindings Python para la biblioteca C MuPDF. Rápido (4,6 ms por página) y fiable (99,3 % de éxito). Requiere instalación de una biblioteca C y tiene licencia AGPL. Una opción sólida si la licencia encaja con tu proyecto.
  • pypdfium2 — Bindings Python para el motor PDFium de Google. Rápido (4,1 ms por página) pero la latencia p99 es alta (42 ms) en documentos complejos. Superficie de API más limitada que PyMuPDF.
  • pdfminer.six — Python puro con análisis de diseño detallado. Muy lento y sin mantenimiento. Falla en PDFs cifrados con AES-256. En gran medida reemplazado por pdfplumber.
  • PDF Oxide — Basado en Rust con bindings Python mediante PyO3. La opción más rápida (0,8 ms por página), 100 % de éxito, gestiona todos los métodos de cifrado, incluye OCR integrado. Licencia MIT sin dependencias del sistema.

PDF Oxide fue creado específicamente para cubrir las carencias de las bibliotecas existentes: las limitaciones de velocidad de los parsers Python puros, las restricciones de licencia de MuPDF y los problemas de fiabilidad que hacen que las bibliotecas fallen en PDFs reales con fuentes inusuales, tablas de referencia cruzada dañadas o codificaciones no estándar.

Rendimiento: ¿qué tan rápido es PDF Oxide?

Probado en 3.830 PDFs de tres conjuntos de pruebas públicos independientes:

Biblioteca Media p99 Tasa de éxito
PDF Oxide 0,8 ms 9 ms 100 %
PyMuPDF 4,6 ms 28 ms 99,3 %
pypdfium2 4,1 ms 42 ms 99,2 %
pypdf 12,1 ms 97 ms 98,4 %
pdfplumber 23,2 ms 189 ms 98,8 %

Para un pipeline que procesa 10.000 PDFs:

  • PDF Oxide: 8 segundos
  • PyMuPDF: 46 segundos
  • pypdf: 2 minutos
  • pdfplumber: 3,9 minutos

Consulta los benchmarks completos para la metodología y los pasos de reproducción.

Problemas comunes y resolución de problemas

Salida de texto vacía

Si extract_text() devuelve una cadena vacía, la página probablemente contiene imágenes escaneadas en lugar de texto. Usa extract_text_ocr() en su lugar. Consulta OCR para PDFs Escaneados para las instrucciones de configuración.

Caracteres distorsionados o incorrectos

Esto suele indicar una fuente con un vector de codificación no estándar o un ToUnicode CMap ausente. PDF Oxide maneja la mayoría de los casos extremos de codificación, pero algunos PDFs intencionalmente ofuscados (contenido protegido por DRM) pueden producir salida incorrecta.

Espacios ausentes o palabras fusionadas

Los operadores de texto PDF colocan los caracteres individualmente. La inferencia de espacios depende del hueco entre posiciones de caracteres en relación con el ancho del espacio de la fuente. Si las palabras aparecen fusionadas, prueba con extract_chars() y aplica lógica de espaciado personalizada basada en las posiciones de los caracteres.

Salida diferente a otras bibliotecas

Las distintas bibliotecas usan heurísticas diferentes para la inferencia de espacios, el salto de línea y el orden de lectura. PDF Oxide logra una paridad de texto del 99,5 % con PyMuPDF en 3.830 PDFs. El 0,5 % de diferencia está en la normalización de espacios en blanco y el manejo de ligaduras.

Casos de uso reales

Indexación de búsqueda — Extrae texto de cada página de cada PDF en un repositorio de documentos y aliméntalo a Elasticsearch, Typesense o una base de datos vectorial para búsqueda de texto completo. La velocidad de PDF Oxide hace práctico reindexar miles de documentos bajo demanda.

Pipelines RAG (generación aumentada por recuperación) — Extrae y divide en fragmentos el texto de PDFs para generar embeddings con OpenAI, Cohere o modelos de código abierto. Usa extract_spans() para preservar la estructura de encabezados de modo que los fragmentos se alineen con las secciones del documento. Consulta la guía de PDF a Markdown para salida optimizada para LLM.

Cumplimiento normativo y auditoría — Analiza contratos, facturas y documentos regulatorios en busca de cláusulas o palabras clave específicas. Usa doc.search() para localizar términos en todas las páginas con posiciones exactas, o extrae el texto completo para detección de cláusulas basada en NLP.

Extracción de datos — Extrae datos estructurados de facturas, recibos, estados de cuenta bancarios y formularios. Combina extract_chars() para datos de posición con reglas específicas del dominio para localizar campos como «Importe total» o «Fecha de factura» y extraer valores adyacentes.

Investigación académica — Procesa miles de artículos de investigación para revisiones de literatura, extracción de citas o metaanálisis. PDF Oxide maneja toda la gama de productores de PDF (LaTeX, Word, InDesign, Quark) y codificaciones de fuentes presentes en publicaciones académicas.

Páginas relacionadas