跳转到内容

Ast:语义文档树与序列化

Ast 模块是引擎的语义文档树。它将文档建模为带类型的节点层级——DocumentSectionHeadingParagraphListTableFigureCodeFormField——并附带边界框、引文锚点,以及带版本控制的 JSON 序列化。无障碍标记层会使用它生成结构树。

稳定性:实验性。 这是一个内部模型接口。它的各个类并不承载版本冻结的公开 API 保证,节点集合与节点属性也会持续演进。而序列化结构描述则会独立进行版本控制 (AstDocument::CURRENT_SCHEMA_VERSION = '1.0.0')。序列化器能够检测并拒绝不兼容的结构描述,因此即使内存中的 API 并不稳定, 持久化的 AST JSON 仍拥有稳定的契约。

Terminal window
composer require nextpdf/core:^3

此处的 AST 是面向文档逻辑结构的语义抽象,而不是面向某一种输入格式的解析语法树。AstDocument 是容器。它持有根 AstNode(必须是 NodeType::Document)、结构描述版本、来源 PDF 哈希值,以及页数。它会拒绝无效的构造(结构描述版本为空、页数小于一、根类型错误)。

AstNode 是递归节点。NodeType 枚举了各种语义种类。一个节点会包含子节点、一个可选的 BoundingBox、可选的文本内容,以及通过 NodeAttributeSchema 进行结构描述验证的属性。节点 API 面向不可变衍生而设计。withBboxAndText() 会返回一个新节点。deepClone() 会复制一个子树。NodeId 是值对象身份。CitationAnchor 将节点绑定到来源位置,便于追溯。AstNodeCollection 是一个 Countable/IteratorAggregate 集合,并支持 ofType() 过滤。

AstSerializer 是持久化边界。serialize() 会将一个 AstDocument 写成 JSON。deserialize() 会把它读回来。canDeserialize()extractSchemaVersion() 让调用方可以在解析前先检查兼容性,因此结构描述不符会被检测出来,而不是变成损坏后才发现的加载问题。AstDocument::estimateTokenCount() 之所以存在,是因为这棵树也会用于为下游受 token 数量限制的处理估算内容大小。

类别主要成员角色
AstDocumenttoJson(), nodeCount(), estimateTokenCount(), CURRENT_SCHEMA_VERSION根容器;验证根类型与结构描述
AstNodeaddChild(), children(), childCount(), totalNodeCount(), withBboxAndText(), deepClone()递归语义节点
NodeType(枚举)Document, Heading, Table, Figure, FormField, …语义节点种类
AstNodeCollectionadd(), count(), isEmpty(), ofType(), toArray()可迭代、可按类型过滤的节点集合
AstSerializerserialize(), deserialize(), canDeserialize(), extractSchemaVersion()带版本控制的 JSON 持久化
BoundingBoxtoArray(), equals()几何值对象(epsilon 比较)
NodeId / CitationAnchortoString(), equals(), toArray()节点身份与来源可追溯性锚点
NodeAttributeSchema属性验证节点属性的结构描述

运行 composer docs:generate-api-php -- --module=Ast 即可获取完整的 PHPDoc 表格。

创建一棵小型树并将其序列化。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Ast\AstNode;
use NextPDF\Ast\AstSerializer;
use NextPDF\Ast\NodeType;
$root = new AstNode(NodeType::Document);
$heading = new AstNode(NodeType::Heading);
$root->addChild($heading);
$root->addChild(new AstNode(NodeType::Paragraph));
echo "Nodes: {$root->totalNodeCount()}\n";
$json = (new AstSerializer())->serialize(/* an AstDocument wrapping $root */);

在反序列化不受信任的 JSON 之前,先检查结构描述兼容性,以防御性方式对持久化 AST 进行往返处理。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Ast\AstDocument;
use NextPDF\Ast\AstSerializer;
use Psr\Log\LoggerInterface;
final readonly class AstStore
{
public function __construct(
private AstSerializer $serializer,
private LoggerInterface $logger,
) {}
public function load(string $json): ?AstDocument
{
if (!$this->serializer->canDeserialize($json)) {
$this->logger->warning('AST JSON schema incompatible; rejected.', [
'found_schema' => $this->serializer->extractSchemaVersion($json),
'expected' => AstDocument::CURRENT_SCHEMA_VERSION,
]);
return null;
}
return $this->serializer->deserialize($json);
}
}
  • AstDocument 要求根节点必须是 NodeType::Document。以任何其他类型为根的树,都会在构造时抛出异常。
  • AstNode::withBboxAndText()deepClone() 会返回新的实例。现有的节点修改方法(addChild())会就地修改,而衍生辅助方法不会。请明确区分你调用的是哪一种。
  • 处理外部来源的 JSON 时,请务必为 deserialize() 加上 canDeserialize() 做前置校验。结构描述版本不符是一种可检测且符合预期的状况。
  • estimateTokenCount() 是用于估算下游处理规模的近似值,并非精确的 tokenizer 计数。请勿将它视为权威依据。
  • BoundingBox::equals() 是一种 epsilon 比较(默认 0.001)。精确的浮点相等并不是它的契约。

树的构造与遍历相对于节点数量是 O(n)。序列化与树的大小成线性关系。其可重现性配置文件为 bitwise。同一棵树会序列化成相同的 JSON 字节,这正是结构描述能成为稳定持久化契约的原因。默认的参考工作负载远低于 1500 ms 墙钟时间 / 64 MB 峰值预算。

AstSerializer::deserialize() 所解析的 JSON 可能来自持久化或传输。请先以 canDeserialize() 验证兼容性。当反序列化后的树中的文本内容与属性重新进入应用程序或被渲染时,请将它们视为不受信任的字符串。此模块本身不执行任何 I/O,也不内嵌任何外部数据。请参阅 /modules/core/security/ 中的引擎威胁模型。

此模块不会提出任何关于 PDF 规格的规范性声明。此语义 AST 是引擎内部的抽象。它并未实现任何必须引用具体条款的标准化文档模型。在 AST 进入无障碍标记流程的位置,其输出的 PDF/UA 与标记式 PDF 符合性,会记录并验证于 /modules/core/accessibility//modules/core/conformance/,而非此处。