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
0x41puede 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 arraysTJdesplazan 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
- API de Extracción de Texto — referencia completa de la API
- PDF a Markdown — conversión estructurada
- Procesamiento por Lotes — patrones de procesamiento paralelo
- OCR para PDFs Escaneados — configuración y uso del OCR
- Benchmarks de Rendimiento — metodología y resultados del benchmark