Xberg PHP 绑定实战:用 `extract` API 通过 Bytes 字节流提取 PDF 文档内容

原创2026-10-08 11:39:471,838 阅读
文章标签:后端AI 应用NLP

Xberg PHP 绑定实战:用 extract API 通过 Bytes 字节流提取 PDF 文档内容

Xberg 是一个以 Rust 为核心的多语言文档智能引擎,PHP 绑定为其提供了 PHP 8.2+ 的类型安全接口。本文围绕仓库中的 contract 契约文档 api_extract_bytes_input.md 展开,完整讲解「Bytes 字节流输入」这条 extract 调用链:如何构造 ExtractInput、如何以内存字节而非文件路径方式喂给提取引擎、如何读取 mimeType 与 content 等结果字段,并对比 URI 输入与批量提取(extractBatch)两种扩展形态。读完本文,你将能够在 PHP 项目中直接对上传的二进制文档(如 PDF、图片、Office 文件)执行无落盘的内容提取,并理解其底层契约与测试验证方式。

一、Bytes 输入提取是什么,为什么需要它

Xberg 的提取入口接收 ExtractInput 作为唯一输入描述。从仓库中的契约文档 api_extract_bytes_input.md 可以看到,最核心的调用只有两行:

use Xberg\Xberg;
use Xberg\ExtractInput;
$input = \Xberg\ExtractInput::from_json(json_encode(["bytes" => "pdf/fake_memo.pdf", "filename" => "fake_memo.pdf", "kind" => "bytes"]));
$result = Xberg::extract($input, null);

kind 字段取值为 "bytes",表示本次提取的输入是内存中的字节流而非磁盘路径或远程 URI。这在 Web 场景中非常实用:用户上传文件后,PHP 拿到的是 $_FILES 里的临时文件内容(file_get_contents 即可读出),无需先落盘再交给提取引擎,既省去了临时文件管理,也避免了对服务器文件系统的依赖。

对应的契约 fixture fixtures/contract/api_extract_bytes_input.json 给出了这个调用的完整「输入 + 断言」定义:

  • call 为 extract;
  • input.kind 为 "bytes",input.bytes 是一段完整的 PDF 字节数组(即测试文档 fake_memo.pdf),input.filename 为 "fake_memo.pdf";
  • 断言要求 results[0].mime_type 等于 "application/pdf"、results[0].content 长度不小于 10,且内容中包含 "May 5, 2023" 或 "Mallori"。

也就是说,只要把二进制 PDF 以字节形式传入,引擎就能自动识别格式、抽出正文文本并返回 MIME 类型——格式探测、解析、抽取全部在内部完成。

二、ExtractInput 的字段解析与构造方式

从契约文档与 fixture 可以归纳出 Bytes 输入下 ExtractInput 的核心字段:

字段 取值 含义与说明
kind "bytes" 输入形态标记,告知引擎按字节流处理;另见 "uri"(见下文第四节)
bytes 字节数组 文档的原始二进制内容。测试 harness 中以 JSON 数组形式给出,实际 PHP 场景中可用 file_get_contents() 获得字符串后交由绑定层编码
filename 字符串 逻辑文件名(如 fake_memo.pdf),用于辅助格式识别与结果元数据
mimeType 字符串(可选) 显式声明 MIME 类型,如 "application/pdf";不提供时引擎自动探测

补充 mimeType 的写法在 extract/extract_bytes_input.md 中给出,注意其命名空间形式 \Xberg\ExtractInput::from_json(...):

use Xberg\Xberg;
use Xberg\ExtractInput;
$input = \Xberg\ExtractInput::from_json(json_encode(["bytes" => "pdf/fake_memo.pdf", "filename" => "fake_memo.pdf", "kind" => "bytes", "mimeType" => "application/pdf"]));
$result = Xberg::extract($input, null);
var_dump($result->getResults()[0]->mimeType);
var_dump($result->getResults()[0]->content);

这里有两个值得注意的细节:

  1. from_json 是绑定层提供的结构化构造入口:PHP 侧用 json_encode 将关联数组转成 JSON 字符串,再交给 from_json 反序列化为类型安全的 ExtractInput 对象,避免手写繁琐的属性赋值。这是契约 fixture 与 e2e 测试统一使用的构造方式。
  2. Xberg::extract 的第二参数传 null 表示使用默认配置:在快速验证场景中可以直接传 null;需要精细化控制时则传入 ExtractionConfig 对象(见第四节批量示例)。

fixture extract/extract_bytes_input.json 与 contract 版本略有差异:它额外携带 "mime_type": "application/pdf" 顶层字段,断言改为 not_error、mime_type 精确匹配与 content 长度不小于 50。这说明「带上明确的 MIME 声明」同样是一条被 e2e 覆盖的合法路径,可减少格式探测的不确定性。

三、读取提取结果:getResults 与核心字段

Xberg::extract 返回的是包含一组结果的输出对象,通过 getResults()[0] 取得第一个文档结果。契约文档演示了最常用的两个字段:

var_dump($result->getResults()[0]->mimeType);
var_dump($result->getResults()[0]->content);
  • mimeType:识别出的文档 MIME 类型,如 application/pdf。该字段在 fixture 断言中被精确校验,是验证输入是否正确被识别的最直接信号;
  • content:抽取出的正文文本。contract fixture 断言其长度不小于 10 且包含关键内容("May 5, 2023" 或 "Mallori"),确保的不是「返回了非空字符串」,而是「抽取到了真实文档内容」。

从 packages/php/README.md 的 Quick Start 可以进一步看到结果对象携带的更多能力:$result->content(正文)、$result->metadata?->title(标题)、$result->metadata?->authors(作者)、$result->metadata?->pdf?->page_count(PDF 页数)、$result->mimeType(格式)以及 $result->tables(表格数组,含 pageNumber 与 markdown 渲染)。也就是说,Bytes 输入与 URI/路径输入的返回结构完全一致——提取引擎对输入形态透明,结果永远是统一的 ExtractedDocument。

<?php

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

$output = \Xberg\XbergApi::extract(\Xberg\ExtractInput::fromUri('document.pdf'), $config ?? \Xberg\ExtractionConfig::default());
$result = $output->getResults()[0];

echo "Extracted Content:\n";
echo "==================\n";
echo $result->content . "\n\n";

echo "Metadata:\n";
echo "=========\n";
echo "Title: " . ($result->metadata?->title ?? 'N/A') . "\n";
echo "Authors: " . (isset($result->metadata?->authors) ? implode(', ', $result->metadata?->authors) : 'N/A') . "\n";
echo "Pages: " . ($result->metadata?->pdf?->page_count ?? 'N/A') . "\n";
echo "Format: " . $result->mimeType . "\n\n";

这段示例还揭示了一个 API 形态细节:绑定同时提供 Xberg 静态门面与 XbergApi 门面两种调用方式,契约 fixture 使用 Xberg::extract($input, null),而 README 中的面向对象示例使用 \Xberg\XbergApi::extract(...),二者指向同一套提取语义,可按项目风格选用。

四、与其他输入形态对比:URI 与批量提取

Bytes 并不是唯一的输入形态。契约文档目录下并存的 api_extract_uri.md 展示了 kind = "uri" 的写法:

use Xberg\Xberg;
use Xberg\ExtractInput;
$input = \Xberg\ExtractInput::from_json(json_encode(["kind" => "uri", "uri" => "https://example.com/pdf/fake_memo.pdf"]));
$result = Xberg::extract($input, null);
var_dump($result->getResults()[0]->content);

区别一目了然:Bytes 形态传 bytes + filename,URI 形态传 uri。前者适合内存数据(上传文件、缓存内容),后者适合由引擎直接拉取的远程资源(注意该 fixture 的 side_effect 标记为 server,而 Bytes 版本的 side_effect 是 safe——不发起到外部网络的副作用)。生产代码中,用 ExtractInput::fromUri(...) 与 ExtractInput::fromBytes(file_get_contents(...), 'text/plain', 'note.txt') 这两条便捷构造器即可分别表达两种形态。

当需要一次处理多个文档时,契约文档提供了批量入口 extractBatch(见 api_extract_batch_bytes.md),其字节输入写法与单条 extract 完全同构,并显式传入 ExtractionConfig:

use Xberg\Xberg;
use Xberg\ExtractInput;
use Xberg\ExtractionConfig;
$result = Xberg::extractBatch([ExtractInput::from_json('{"bytes":"pdf/fake_memo.pdf","filename":"fake_memo.pdf","kind":"bytes"}')], \Xberg\ExtractionConfig::from_json('{}'));
var_dump($result->getResults()[0]->mimeType);
var_dump($result->getResults()[0]->content);

注意三点差异:参数由单个 ExtractInput 变为数组;第二个参数由 null 变为 \Xberg\ExtractionConfig::from_json('{}')(空配置对象,等价于默认配置的显式形式);返回对象同样通过 getResults() 取结果数组,其 summary->results 字段(README 批量示例可见)可报告处理文档总数。README 中的完整批量用法为:

<?php

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

use Xberg\Xberg;
use Xberg\ExtractInput;
use Xberg\ExtractionConfig;

$inputs = [
    ExtractInput::fromUri('document1.pdf'),
    ExtractInput::fromUri('document2.docx'),
    ExtractInput::fromBytes(file_get_contents('note.txt') ?: '', 'text/plain', 'note.txt'),
];

$config = new ExtractionConfig(
    extractTables: true,
    extractImages: false,
);

$output = Xberg::extractBatch($inputs, $config);

echo "Processed {$output->summary->results} documents\n";

foreach ($output->getResults() as $result) {
    echo "Content: " . strlen($result->content) . " chars\n";
    echo "Tables: " . count($result->tables) . "\n";
    echo "MIME: {$result->mimeType}\n\n";
}

这里 ExtractInput::fromBytes($bytes, $mimeType, $filename) 正是「Bytes 输入」的面向对象便捷构造器——三个参数恰好对应契约字段 bytes、mimeType、filename,kind 由绑定层自动补齐为 "bytes"。

五、底层实现与契约来源:Rust 引擎 + 绑定桥

Bytes 输入之所以能在 PHP 侧保持如此精简,是因为真正的解析与抽取全部发生在 Rust 核心中。PHP 绑定通过绑定桥把 ExtractInput / ExtractionConfig 序列化后传递给共享的 Rust 实现——packages/php/README.md 明确指出「Same engine as every binding」,Rust、Python、Node.js、Go、Java、PHP、Ruby、.NET、Elixir、WASM 等 15 种绑定共享同一套实现。

从源码结构还可以印证插件的契约边界: packages/php/src/DocumentExtractor.php 定义了 DocumentExtractor 插件接口,其 extract(ExtractInput $input, ExtractionConfig $config): ExtractedDocument 与 supported_mime_types(): mixed 两个方法,说明自定义提取器/后处理器同样以 ExtractInput 作为输入描述、以 ExtractedDocument 作为返回类型——与内置引擎的输入输出模型完全对齐。

值得说明的是,契约文档位于 docs-site/src/snippets-generated/php/contract/,其文件头注明由 alef 工具自动生成(This file is auto-generated by alef),是对应 fixture JSON 的「可读 + 可执行」投影。因此这条 extract 调用链有三重保障:

  1. fixture 定义(fixtures/contract/api_extract_bytes_input.json)给出精确断言;
  2. snippet 文档(api_extract_bytes_input.md)给出可直接复制的 PHP 代码;
  3. e2e 测试(如 e2e/php/tests/ContractTest.php)在真实绑定上执行同一调用并校验断言,保证文档与实现不脱节。

六、边界情况与错误处理

字节输入虽然简单,但输入描述的正确性同样会被契约校验。仓库 fixtures/error/ 目录下与 Bytes 输入直接相关的三个错误场景值得在生产代码中提前防御:

对应地,在 PHP 中建议在调用 Xberg::extract 前先校验字节非空、文件名非空,并在显式传 mimeType 时保证其符合 type/subtype 格式;若依赖自动探测,则 filename 中的扩展名是重要辅助信号(如 fake_memo.pdf 之于 application/pdf)。

七、完整可运行示例

综合契约文档与 README,给出一个从「读取上传字节」到「输出结果」的完整闭环示例:

<?php

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

use Xberg\Xberg;
use Xberg\ExtractInput;

// 1. 从磁盘读入字节(Web 场景下等价于读取 $_FILES 上传内容)
$pdfBytes = file_get_contents('fake_memo.pdf');
if ($pdfBytes === false || $pdfBytes === '') {
    throw new RuntimeException('empty bytes input');
}

// 2. 构造 Bytes 输入:bytes + filename + kind
$input = \Xberg\ExtractInput::from_json(json_encode([
    'bytes'    => $pdfBytes,
    'filename' => 'fake_memo.pdf',
    'kind'     => 'bytes',
]));

// 3. 调用提取(第二参数传 null 使用默认配置)
$result = Xberg::extract($input, null);

// 4. 读取结果
$doc = $result->getResults()[0];
printf("MIME: %s\n", $doc->mimeType);
printf("Content: %s\n", $doc->content);

将 bytes 换成真实文件内容、filename 换成实际文件名,即可用于任意受支持格式(PDF、DOCX、XLSX、PNG、HTML 等共 107 种格式)。若需要批量或远程输入,把第 3 步替换为 Xberg::extractBatch([$input], \Xberg\ExtractionConfig::from_json('{}')) 或改传 kind => "uri" 即可。

八、小结

Bytes 输入是 Xberg PHP 绑定中最直接、最贴合 Web 上传场景的提取形态:一个 ExtractInput::from_json(...) 构造输入,一次 Xberg::extract($input, null) 完成提取,getResults()[0]->mimeType 与 ->content 拿到结果。本文从契约文档出发,依次剖析了 kind/bytes/filename/mimeType 四个字段、getResults 结果读取、URI 与批量两种扩展形态,并借助 fixtures/contract/api_extract_bytes_input.json 的断言、packages/php/README.md 的完整用法与 packages/php/src/DocumentExtractor.php 的接口定义,将一条契约 snippet 还原成了可对照源码深入验证的完整技术路径。

登录后查看全文
xberg