Skip to content

Текстовый поиск

PDF Oxide предоставляет полнотекстовый поиск по PDF-документам с поддержкой регулярных выражений, регистронезависимого сопоставления, режима целых слов и ограничивающих прямоугольников для каждого совпадения. Результаты поиска включают номер страницы, совпавший текст и точные координаты каждого совпадения — это позволяет легко строить рабочие процессы с поиском и выделением текста.

Для многостраничных запросов с пользовательскими параметрами используйте TextSearcher::search(), а для типовых задач — удобные методы Pdf (search(), search_page(), highlight_matches()).

Быстрый пример

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("report.pdf")
results = doc.search("conclusion", case_insensitive=True)
for r in results:
    print(f"Page {r['page']}: '{r['text']}' at ({r['x']:.1f}, {r['y']:.1f})")

Node.js

const { PdfDocument } = require("pdf-oxide");

const doc = new PdfDocument("report.pdf");
const results = doc.searchAll("conclusion", { caseSensitive: false });
for (const r of results) {
  console.log(`Page ${r.page}: '${r.text}' at (${r.x.toFixed(1)}, ${r.y.toFixed(1)})`);
}
doc.close();

Go

import pdfoxide "github.com/yfedoseev/pdf_oxide/go"

doc, _ := pdfoxide.Open("report.pdf")
defer doc.Close()
results, _ := doc.SearchAll("conclusion", false)
for _, r := range results {
    fmt.Printf("Page %d: '%s' at (%.1f, %.1f)\n", r.Page, r.Text, r.X, r.Y)
}

C#

using PdfOxide.Core;

using var doc = PdfDocument.Open("report.pdf");
var results = doc.SearchAll("conclusion");
foreach (var r in results)
{
    Console.WriteLine($"Page {r.Page}: '{r.Text}' at ({r.X:F1}, {r.Y:F1})");
}

WASM

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

Rust

use pdf_oxide::api::Pdf;

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

Java

import fyi.oxide.pdf.PdfDocument;
import fyi.oxide.pdf.search.SearchMatch;
import java.nio.file.Path;
import java.util.List;

try (PdfDocument doc = PdfDocument.open(Path.of("report.pdf"))) {
    List<SearchMatch> results = doc.search("conclusion", true, false, 0);
    for (SearchMatch m : results) {
        System.out.printf("Page %d: '%s' at (%.1f, %.1f)%n",
            m.pageIndex(), m.text(), m.bbox().x0(), m.bbox().y0());
    }
}

Kotlin

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

PdfDocument.open(Path.of("report.pdf")).use { doc ->
    val results = doc.search("conclusion", true, false, 0)
    for (m in results) {
        println("Page ${m.pageIndex()}: '${m.text()}' at (${m.bbox().x0()}, ${m.bbox().y0()})")
    }
}

Scala

import fyi.oxide.pdf.{PdfDocument, searchSeq}
import scala.util.Using

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

Clojure

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

(with-open [doc (pdf/open "report.pdf")]
  (doseq [m (pdf/search doc "conclusion")]
    (printf "Page %d: '%s' at (%.1f, %.1f)%n"
            (.pageIndex m) (.text m) (.x0 (.bbox m)) (.y0 (.bbox m)))))

Ruby

require 'pdf_oxide'

PdfOxide::PdfDocument.open('report.pdf') do |doc|
  doc.search('conclusion', case_sensitive: false).each do |r|
    bbox = r[:bbox]
    printf("Page %d: '%s' at (%.1f, %.1f)\n", r[:page], r[:text], bbox[:x], bbox[:y])
  end
end

C++

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

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

Swift

import PdfOxide

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

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

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

R

library(pdfoxide)

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

Julia

using PdfOxide

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

Zig

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

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

Objective-C

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

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

Elixir

{:ok, doc} = PdfOxide.open("report.pdf")
{:ok, results} = PdfOxide.search_all(doc, "conclusion", false)

for r <- results do
  IO.puts("Page #{r.page}: '#{r.text}' at (#{r.bbox.x}, #{r.bbox.y})")
end

Справочник API

TextSearcher::search(doc, pattern, options) -> Vec<SearchResult>

Поиск текста на нескольких страницах PDF-документа. Паттерн компилируется как регулярное выражение, если не включён режим literal.

Параметр Тип Описание
doc &mut PdfDocument PDF-документ для поиска
pattern &str Паттерн регулярного выражения (или обычный текст при установленном literal)
options &SearchOptions Настройки поиска

Возвращает: Вектор объектов SearchResult, упорядоченных по странице и позиции.

Rust

use pdf_oxide::PdfDocument;
use pdf_oxide::search::{TextSearcher, SearchOptions};

let mut doc = PdfDocument::open("report.pdf")?;

let options = SearchOptions::new()
    .with_case_insensitive(true)
    .with_max_results(50);

let results = TextSearcher::search(&mut doc, "error|warning", &options)?;
for r in &results {
    println!("Page {}: '{}'", r.page, r.text);
}

TextSearcher::search_page(doc, page, regex, options) -> Vec<SearchResult>

Поиск текста на конкретной странице с использованием предварительно скомпилированного регулярного выражения.

Параметр Тип Описание
doc &mut PdfDocument PDF-документ
page usize Индекс страницы (с нуля)
regex &Regex Предварительно скомпилированное регулярное выражение
options &SearchOptions Настройки поиска

Возвращает: Вектор объектов SearchResult для указанной страницы.

Rust

use pdf_oxide::PdfDocument;
use pdf_oxide::search::{TextSearcher, SearchOptions};
use regex::Regex;

let mut doc = PdfDocument::open("report.pdf")?;
let regex = Regex::new(r"\d{4}-\d{2}-\d{2}")?; // Date pattern
let options = SearchOptions::default();

let results = TextSearcher::search_page(&mut doc, 0, &regex, &options)?;
for r in &results {
    println!("Date found: '{}' at ({:.1}, {:.1})", r.text, r.bbox.x, r.bbox.y);
}

SearchOptions

Конфигурация поведения текстового поиска. Использует паттерн строителя для удобства.

Поле Тип По умолчанию Описание
case_insensitive bool false Игнорировать регистр при сопоставлении
literal bool false Рассматривать паттерн как обычный текст (экранировать спецсимволы регулярных выражений)
whole_word bool false Сопоставлять только целые слова (оборачивает паттерн в \b...\b)
max_results usize 0 Максимальное число результатов (0 = без ограничений)
page_range Option<(usize, usize)> None Диапазон страниц для поиска (включительно с обеих сторон)

Методы строителя

let options = SearchOptions::new()
    .with_case_insensitive(true)
    .with_literal(true)
    .with_whole_word(true)
    .with_max_results(100)
    .with_page_range(0, 9);

Быстрый конструктор

// Quick case-insensitive search
let options = SearchOptions::case_insensitive();

SearchResult

Отдельное найденное совпадение с информацией о положении.

Поле Тип Описание
page usize Номер страницы (с нуля)
text String Совпавший текст
bbox Rect Общий ограничивающий прямоугольник совпадения
start_index usize Начальный индекс в извлечённом тексте страницы
end_index usize Конечный индекс в извлечённом тексте страницы
span_boxes Vec<Rect> Отдельные ограничивающие прямоугольники для каждого фрагмента совпадения (полезно при многострочных совпадениях)

Python: В Python API результаты поиска возвращаются в виде словарей:

{
    "page": 0,
    "text": "conclusion",
    "x": 72.0,
    "y": 650.5,
    "width": 85.3,
    "height": 12.0,
}

Удобные методы Pdf

Высокоуровневый API Pdf предоставляет сокращённые методы для распространённых операций поиска.

search(pattern) -> Vec<SearchResult>

Поиск по всему документу с настройками по умолчанию.

let mut pdf = Pdf::open("report.pdf")?;
let results = pdf.search("important")?;

search_with_options(pattern, options) -> Vec<SearchResult>

Поиск с пользовательскими настройками.

let options = SearchOptions::case_insensitive()
    .with_whole_word(true)
    .with_page_range(0, 5);
let results = pdf.search_with_options("abstract", options)?;

search_page(page, pattern) -> Vec<SearchResult>

Поиск на одной странице с настройками по умолчанию.

let results = pdf.search_page(0, r"\d+\.\d+")?; // Find decimal numbers

highlight_matches(results, color) -> Result<()>

Создание аннотаций-выделений для результатов поиска. Каждый результат получает жёлтую (или заданного цвета) аннотацию-выделение на своей странице.

Параметр Тип Описание
results &[SearchResult] Результаты поиска для выделения
color [f32; 3] RGB-цвет (0.0–1.0 для каждого компонента)
let mut pdf = Pdf::open("report.pdf")?;
let results = pdf.search("important")?;
pdf.highlight_matches(&results, [1.0, 1.0, 0.0])?; // Yellow
pdf.save("highlighted.pdf")?;

Python API поиска

Класс Python PdfDocument предоставляет поиск напрямую.

doc.search(pattern, ...) -> list[dict]

doc.search(
    pattern: str,
    case_insensitive: bool = False,
    literal: bool = False,
    whole_word: bool = False,
    max_results: int = 0,
) -> list[dict]

doc.search_page(page, pattern, ...) -> list[dict]

doc.search_page(
    page: int,
    pattern: str,
    case_insensitive: bool = False,
    literal: bool = False,
    whole_word: bool = False,
    max_results: int = 0,
) -> list[dict]

JavaScript API поиска

Класс WasmPdfDocument предоставляет те же возможности поиска.

doc.search(pattern, ...) -> Array

doc.search(pattern, caseInsensitive?, literal?, wholeWord?, maxResults?) -> Array

doc.searchPage(pageIndex, pattern, ...) -> Array

doc.searchPage(pageIndex, pattern, caseInsensitive?, literal?, wholeWord?, maxResults?) -> Array

Пример:

const doc = new WasmPdfDocument(bytes);

// Search all pages, case-insensitive
const results = doc.search("error|warning", true);
for (const r of results) {
  console.log(`Page ${r.page}: '${r.text}'`);
}

// Search a single page with whole-word matching
const pageResults = doc.searchPage(0, "abstract", true, false, true);
doc.free();

Как сериализовать результаты поиска в JSON?

Ряд привязок предоставляет одноразовый сериализатор, преобразующий список результатов поиска по странице в JSON-массив за один переход через FFI-границу — Rust сериализует весь список, а привязка декодирует его, вместо того чтобы передавать каждое поле по одному. Именно так методы SearchPage в Go и C# декодируют результаты под капотом.

Авторитетным является сигнатура C ABI:

char *pdf_oxide_search_results_to_json(
    const FfiSearchResults *results,
    int32_t *error_code);

Функция принимает непрозрачный дескриптор результатов, возвращённый pdf_document_search_page(...), и возвращает выделенную через malloc UTF-8 JSON-строку (освободить её следует через pdf_free_string). Каждый элемент содержит page, text и ограничивающий прямоугольник совпадения (x, y, width, height).

Swift — обёртка объединяет поиск и сериализацию в один вызов searchResultsToJson(_:_:caseSensitive:):

import PdfOxide

let doc = try PdfDocument(path: "report.pdf")

// Search page 0 for "conclusion" and get the matches as a JSON string
let json = try doc.searchResultsToJson(0, "conclusion", caseSensitive: false)
print(json)
// [{"page":0,"text":"conclusion","x":72.0,"y":650.5,"width":85.3,"height":12.0}, ...]

Go / C#. Эти привязки вызывают pdf_oxide_search_results_to_json внутри и возвращают уже декодированные нативные записи, поэтому вызывать сериализатор напрямую не нужно. Используйте doc.SearchPage(...) (Go: doc.SearchPage(page, text, caseSensitive); C#: doc.SearchPage(pageIndex, text, caseSensitive)) и получайте строго типизированные результаты. Чтобы получить JSON в этих языках, сериализуйте возвращённые записи стандартной библиотекой (encoding/json / System.Text.Json).

Python / Rust. Методы Python doc.search(...) / doc.search_page(...) уже возвращают нативные записи list[dict] (сериализуйте напрямую через json.dumps), а Rust возвращает типизированный Vec<SearchResult>, сериализуемый через serde_json. Ни тому, ни другому C-ABI сериализатор не нужен.


Расширенные примеры

Поиск и выделение с пользовательским цветом

use pdf_oxide::api::Pdf;
use pdf_oxide::search::SearchOptions;

let mut pdf = Pdf::open("contract.pdf")?;

// Find all dollar amounts
let options = SearchOptions::new()
    .with_literal(false); // regex mode
let results = pdf.search_with_options(r"\$[\d,]+\.?\d*", options)?;

println!("Found {} dollar amounts", results.len());
for r in &results {
    println!("  Page {}: {}", r.page + 1, r.text);
}

// Highlight them in green
pdf.highlight_matches(&results, [0.6, 1.0, 0.6])?;
pdf.save("highlighted_amounts.pdf")?;

Поиск с ограничением диапазона страниц

from pdf_oxide import PdfDocument

doc = PdfDocument("book.pdf")

# Search only the first 10 pages
results = doc.search(
    "introduction",
    case_insensitive=True,
    whole_word=True,
    max_results=5,
)

for r in results:
    print(f"Found on page {r['page'] + 1}")

Построение поискового индекса по нескольким PDF

use pdf_oxide::PdfDocument;
use pdf_oxide::search::{TextSearcher, SearchOptions};
use std::collections::HashMap;

let files = vec!["paper_a.pdf", "paper_b.pdf", "paper_c.pdf"];
let query = "machine learning";
let options = SearchOptions::case_insensitive();

let mut index: HashMap<String, Vec<(usize, String)>> = HashMap::new();

for file in &files {
    let mut doc = PdfDocument::open(file)?;
    let results = TextSearcher::search(&mut doc, query, &options)?;

    for r in results {
        index.entry(file.to_string())
            .or_default()
            .push((r.page, r.text));
    }
}

for (file, matches) in &index {
    println!("{}: {} matches", file, matches.len());
    for (page, text) in matches {
        println!("  Page {}: '{}'", page + 1, text);
    }
}

Извлечение контекста вокруг совпадений

use pdf_oxide::PdfDocument;
use pdf_oxide::search::{TextSearcher, SearchOptions};

let mut doc = PdfDocument::open("report.pdf")?;
let options = SearchOptions::new().with_case_insensitive(true);
let results = TextSearcher::search(&mut doc, "error", &options)?;

for r in &results {
    // Extract full page text for context
    let page_text = doc.extract_text(r.page)?;

    // Show 50 chars before and after the match
    let start = r.start_index.saturating_sub(50);
    let end = (r.end_index + 50).min(page_text.len());
    let context = &page_text[start..end];

    println!("Page {} match: ...{}...", r.page + 1, context.trim());
}

Часто задаваемые вопросы

Как получить результаты поиска в виде JSON? В Swift вызовите doc.searchResultsToJson(page, term, caseSensitive:) — метод выполнит поиск по странице и сразу вернёт JSON-массив совпадений. В Python и Rust поиск возвращает нативные записи (list[dict] / Vec<SearchResult>), которые сериализуются через json.dumps / serde_json. Go и C# возвращают типизированные записи, которые сериализуются через encoding/json / System.Text.Json.

Что содержит каждое JSON-совпадение? Поле page (с нуля), совпавший text и общий ограничивающий прямоугольник: x, y, width, height (в точках PDF, начало координат — левый нижний угол).

Поиск по умолчанию использует регулярные выражения или обычный текст? Паттерны компилируются как регулярные выражения, если не включён режим literal (with_literal(true) / literal=True), который экранирует метасимволы и сопоставляет текст дословно.

Поддерживается ли регистронезависимый поиск и поиск целых слов? Да — установите case_insensitive и whole_word в SearchOptions (Rust) или передайте их как именованные аргументы (Python) / параметры (другие привязки).


Связанные страницы