工具概述
这是一个专门为Word XML文档设计的差异分析工具,能够深入比较两个XML文档之间的结构性差异,特别针对文档克隆操作后的变化进行智能分析。
核心功能解析
1. 多维度文档结构对比
该工具采用分层对比策略,从六个关键维度全面分析文档差异:
php
// 核心对比方法
public function compare() {
$this->compareDocumentStructure(); // 文档整体结构
$this->compareParagraphCount(); // 段落数量统计
$this->compareTableStructure(); // 表格结构分析
$this->compareCommentCount(); // 批注数量对比
$this->compareTextContent(); // 文本内容差异
$this->compareClonedContent(); // 克隆内容识别
}
2. 智能文本内容分析
工具采用创新的文本合并与分割算法,确保准确识别段落级变化:
php
private function extractAndMergeText($doc) {
// 按Word XML段落结构合并文本,保留语义完整性
$paragraphs = $xpath->query('//w:p');
foreach ($paragraphs as $paragraph) {
$paragraphText = "";
$textNodes = $xpath->query('.//w:t', $paragraph);
// 合并同一段落内的所有文本节点
}
return $mergedText;
}
3. 高级克隆模式识别
工具内置智能克隆检测机制,能够识别多种克隆模式:
php
private function analyzeClonePatterns($paragraphs) {
// 支持两种克隆模式识别:
// 1. "文本内容 clone#数字" 格式
preg_match_all('/([a-zA-Z0-9]+(?:\s+[a-zA-Z0-9]+)*)\s*clone#(\d+)/', $text, $matches);
// 2. 碎片化克隆模式
preg_match_all('/([a-zA-Z]+)\s*clone#(\d+)/', $text, $matches);
}
技术架构深度解析
1. DOM与XPath协同处理
php
public function __construct($sourceFile, $targetFile) {
// 双文档DOM加载,确保一致性
$this->sourceDoc = new DOMDocument();
$this->targetDoc = new DOMDocument();
// XPath命名空间注册,支持Word XML标准
$this->xpath->registerNamespace('w',
'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
}
2. 差异检测算法
工具采用差异集计算策略:
php
// 新增内容检测
$newParagraphs = array_diff($targetParagraphs, $sourceParagraphs);
// 删除内容检测
$removedParagraphs = array_diff($sourceParagraphs, $targetParagraphs);
// 克隆内容过滤
$clonedParagraphs = array_filter($paragraphs, function($paragraph) {
return strpos($paragraph, 'clone') !== false;
});
实际应用场景
1. 文档版本控制
- 追踪文档修改历史
- 识别非授权更改
- 验证文档完整性
2. 自动化测试验证
- 验证文档生成工具输出
- 检测克隆操作正确性
- 监控批量处理结果
3. 质量保证
bash
# 命令行使用示例
php XmlComparator.php original.xml modified.xml
=== XML文档对比报告 ===
源文件: original.xml
目标文件: modified.xml
=== 1. 文档结构对比 ===
源文档body子节点数: 156
目标文档body子节点数: 312
差异: +156
输出报告解析
结构化差异统计
段落数量对比:
源文档: 45段落
目标文档: 89段落
差异: +44 (克隆操作预期内)
表格结构对比:
源文档: 3表格
目标文档: 3表格
行数一致: 验证通过
克隆内容分析
克隆模式分析:
- '项目描述' 被克隆了 5 次
- '技术规范' 被克隆了 3 次
- '验收标准' 被克隆了 2 次
技术优势
1. 精准的XML结构感知
- 理解Word XML命名空间
- 识别文档语义结构
- 保持段落边界完整性
2. 智能的内容变化识别
- 区分实质修改与格式调整
- 识别克隆操作模式
- 过滤空白和格式字符
3. 可扩展的架构设计
php
// 易于添加新的比较维度
private function compareNewFeature() {
// 实现新的比较逻辑
}
// 支持自定义分析规则
private function analyzeCustomPatterns($criteria) {
// 用户定义的模式识别
}
使用最佳实践
1. 预处理建议
php
// 确保XML格式一致性
$doc->preserveWhiteSpace = false;
$doc->formatOutput = true;
2. 结果解读指南
- 段落数量增加:通常表示成功克隆
- 文本内容变化:需要人工审查
- 结构差异:可能影响文档格式
3. 集成到工作流
bash
# 自动化脚本集成
php XmlComparator.php $SOURCE $TARGET > diff_report.txt
# 解析报告并触发后续操作
局限性及改进方向
当前限制
- 主要依赖文本内容比较
- 样式变化检测有限
- 复杂嵌套结构支持待增强
未来增强
php
// 计划中的功能扩展
private function compareStyles() {
// 样式属性对比
}
private function detectMoveOperations() {
// 内容移动识别
}
private function generatePatchFile() {
// 差异补丁生成
}
结论
这个XML对比工具为Word文档的自动化处理提供了重要的质量保证机制。通过深入分析文档结构变化,特别是对克隆操作的精准识别,它成为文档生成流水线中不可或缺的验证环节。工具的模块化设计为后续功能扩展奠定了良好基础,使其能够适应更复杂的文档处理需求。
在实际应用中,该工具已证明能够有效检测文档克隆操作的正确性,为自动化文档生成系统提供了可靠的质量监控手段。
代码如下
php
<?php
/**
* XML对比工具
* 用于比较两个Word XML文档之间的差异
*/
class XmlComparator {
private $sourceFile;
private $targetFile;
private $sourceDoc;
private $targetDoc;
private $xpath;
public function __construct($sourceFile, $targetFile) {
$this->sourceFile = $sourceFile;
$this->targetFile = $targetFile;
// 加载源文档
$this->sourceDoc = new DOMDocument();
$this->sourceDoc->preserveWhiteSpace = false;
$this->sourceDoc->formatOutput = true;
$this->sourceDoc->load($sourceFile);
// 加载目标文档
$this->targetDoc = new DOMDocument();
$this->targetDoc->preserveWhiteSpace = false;
$this->targetDoc->formatOutput = true;
$this->targetDoc->load($targetFile);
// 创建XPath对象
$this->xpath = new DOMXPath($this->sourceDoc);
$this->xpath->registerNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
}
/**
* 比较两个文档并输出差异报告
*/
public function compare() {
echo "=== XML文档对比报告 ===\n";
echo "源文件: {$this->sourceFile}\n";
echo "目标文件: {$this->targetFile}\n\n";
// 1. 比较文档结构
$this->compareDocumentStructure();
// 2. 比较段落数量
$this->compareParagraphCount();
// 3. 比较表格数量和行数
$this->compareTableStructure();
// 4. 比较批注数量
$this->compareCommentCount();
// 5. 比较文本内容
$this->compareTextContent();
// 6. 比较特定克隆内容
$this->compareClonedContent();
}
/**
* 比较文档基本结构
*/
private function compareDocumentStructure() {
echo "=== 1. 文档结构对比 ===\n";
$sourceBody = $this->sourceDoc->getElementsByTagName('body')->item(0);
$targetBody = $this->targetDoc->getElementsByTagName('body')->item(0);
$sourceChildren = $sourceBody->childNodes->length;
$targetChildren = $targetBody->childNodes->length;
echo "源文档body子节点数: {$sourceChildren}\n";
echo "目标文档body子节点数: {$targetChildren}\n";
echo "差异: " . ($targetChildren - $sourceChildren) . "\n\n";
}
/**
* 比较段落数量
*/
private function compareParagraphCount() {
echo "=== 2. 段落数量对比 ===\n";
$sourceParagraphs = $this->sourceDoc->getElementsByTagName('p')->length;
$targetParagraphs = $this->targetDoc->getElementsByTagName('p')->length;
echo "源文档段落数量: {$sourceParagraphs}\n";
echo "目标文档段落数量: {$targetParagraphs}\n";
echo "差异: " . ($targetParagraphs - $sourceParagraphs) . "\n\n";
}
/**
* 比较表格结构
*/
private function compareTableStructure() {
echo "=== 3. 表格结构对比 ===\n";
$sourceTables = $this->sourceDoc->getElementsByTagName('tbl');
$targetTables = $this->targetDoc->getElementsByTagName('tbl');
echo "源文档表格数量: {$sourceTables->length}\n";
echo "目标文档表格数量: {$targetTables->length}\n";
if ($sourceTables->length > 0 && $targetTables->length > 0) {
$sourceRows = $sourceTables->item(0)->getElementsByTagName('tr')->length;
$targetRows = $targetTables->item(0)->getElementsByTagName('tr')->length;
echo "源文档第一个表格行数: {$sourceRows}\n";
echo "目标文档第一个表格行数: {$targetRows}\n";
echo "行数差异: " . ($targetRows - $sourceRows) . "\n";
}
echo "\n";
}
/**
* 比较批注数量
*/
private function compareCommentCount() {
echo "=== 4. 批注数量对比 ===\n";
$sourceCommentRanges = $this->sourceDoc->getElementsByTagName('commentRangeStart')->length;
$targetCommentRanges = $this->targetDoc->getElementsByTagName('commentRangeStart')->length;
echo "源文档批注范围开始数: {$sourceCommentRanges}\n";
echo "目标文档批注范围开始数: {$targetCommentRanges}\n";
echo "差异: " . ($targetCommentRanges - $sourceCommentRanges) . "\n\n";
}
/**
* 比较文本内容
*/
private function compareTextContent() {
echo "=== 5. 文本内容对比 ===\n";
// 提取并合并文本内容
$sourceMergedText = $this->extractAndMergeText($this->sourceDoc);
$targetMergedText = $this->extractAndMergeText($this->targetDoc);
echo "源文档合并后文本长度: " . strlen($sourceMergedText) . " 字符\n";
echo "目标文档合并后文本长度: " . strlen($targetMergedText) . " 字符\n";
// 将合并后的文本按段落分割
$sourceParagraphs = $this->splitIntoParagraphs($sourceMergedText);
$targetParagraphs = $this->splitIntoParagraphs($targetMergedText);
echo "源文档段落数量: " . count($sourceParagraphs) . "\n";
echo "目标文档段落数量: " . count($targetParagraphs) . "\n";
// 找出新增的段落
$newParagraphs = array_diff($targetParagraphs, $sourceParagraphs);
if (!empty($newParagraphs)) {
echo "\n新增的段落内容:\n";
foreach ($newParagraphs as $paragraph) {
if (trim($paragraph) !== '') {
echo "- {$paragraph}\n";
}
}
}
// 找出删除的段落
$removedParagraphs = array_diff($sourceParagraphs, $targetParagraphs);
if (!empty($removedParagraphs)) {
echo "\n删除的段落内容:\n";
foreach ($removedParagraphs as $paragraph) {
if (trim($paragraph) !== '') {
echo "- {$paragraph}\n";
}
}
}
// 显示合并后的完整文本(可选)
if (strlen($sourceMergedText) < 1000 && strlen($targetMergedText) < 1000) {
echo "\n源文档合并后文本:\n{$sourceMergedText}\n";
echo "\n目标文档合并后文本:\n{$targetMergedText}\n";
}
echo "\n";
}
/**
* 比较克隆内容
*/
private function compareClonedContent() {
echo "=== 6. 克隆内容分析 ===\n";
// 提取并合并文本内容
$targetMergedText = $this->extractAndMergeText($this->targetDoc);
$targetParagraphs = $this->splitIntoParagraphs($targetMergedText);
// 查找包含"clone"的段落
$clonedParagraphs = array_filter($targetParagraphs, function($paragraph) {
return strpos($paragraph, 'clone') !== false;
});
if (!empty($clonedParagraphs)) {
echo "目标文档中的克隆内容:\n";
foreach ($clonedParagraphs as $paragraph) {
echo "- {$paragraph}\n";
}
} else {
echo "未找到包含'clone'的文本内容\n";
}
// 分析克隆模式
$this->analyzeClonePatterns($targetParagraphs);
echo "\n";
}
/**
* 分析克隆模式
* @param array $paragraphs 段落数组
*/
private function analyzeClonePatterns($paragraphs) {
echo "克隆模式分析:\n";
// 使用已经加载的目标文档
$xpath = new DOMXPath($this->targetDoc);
$xpath->registerNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
// 获取所有包含"clone"的段落
$paragraphNodes = $xpath->query('//w:p[.//w:t[contains(text(), "clone")]]');
$clonePatterns = [];
foreach ($paragraphNodes as $paragraph) {
$paragraphText = '';
$textNodes = $xpath->query('.//w:t', $paragraph);
foreach ($textNodes as $textNode) {
$paragraphText .= $textNode->nodeValue;
}
// 改进的正则表达式,支持包含数字的文本(如Section2)
// 匹配格式为 "文本 clone#数字" 的模式
if (preg_match_all('/([a-zA-Z0-9]+(?:\s+[a-zA-Z0-9]+)*)\s*clone#(\d+)/', $paragraphText, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$text = trim($match[1]);
$cloneNumber = $match[2];
if (!isset($clonePatterns[$text])) {
$clonePatterns[$text] = [];
}
// 只添加新的克隆号
if (!in_array($cloneNumber, $clonePatterns[$text])) {
$clonePatterns[$text][] = $cloneNumber;
}
}
}
// 单独处理可能的碎片克隆模式
if (preg_match_all('/([a-zA-Z]+)\s*clone#(\d+)/', $paragraphText, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$text = $match[1];
$cloneNumber = $match[2];
// 避免匹配单个字母,并确保不会覆盖已有的完整模式
if (strlen($text) > 1) {
// 检查是否已经存在此文本和克隆号的组合
$exists = false;
foreach ($clonePatterns as $existingText => $existingNumbers) {
if (strpos($existingText, $text) !== false && in_array($cloneNumber, $existingNumbers)) {
$exists = true;
break;
}
}
if (!$exists) {
if (!isset($clonePatterns[$text])) {
$clonePatterns[$text] = [];
}
if (!in_array($cloneNumber, $clonePatterns[$text])) {
$clonePatterns[$text][] = $cloneNumber;
}
}
}
}
}
}
// 输出克隆次数(去重)
foreach ($clonePatterns as $text => $numbers) {
$uniqueNumbers = array_unique($numbers);
$count = count($uniqueNumbers);
echo "- '{$text}' 被克隆了 {$count} 次\n";
}
if (empty($clonePatterns)) {
echo "未找到明显的克隆模式\n";
}
}
/**
* 提取文档中的所有文本
*/
private function extractAllTexts($doc) {
$texts = [];
$textNodes = $doc->getElementsByTagName('t');
foreach ($textNodes as $node) {
$texts[] = $node->nodeValue;
}
return $texts;
}
/**
* 提取并合并文档中的文本内容
* 按段落结构合并文本,保留段落间的分隔
*/
private function extractAndMergeText($doc) {
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
$mergedText = "";
$paragraphs = $xpath->query('//w:p');
foreach ($paragraphs as $paragraph) {
$paragraphText = "";
$textNodes = $xpath->query('.//w:t', $paragraph);
foreach ($textNodes as $textNode) {
$paragraphText .= $textNode->nodeValue;
}
// 如果段落有内容,添加到合并文本中
if (trim($paragraphText) !== "") {
$mergedText .= $paragraphText . "\n";
}
}
return $mergedText;
}
/**
* 将合并后的文本分割成段落
*/
private function splitIntoParagraphs($mergedText) {
// 按换行符分割文本,过滤空段落
$paragraphs = explode("\n", $mergedText);
$result = [];
foreach ($paragraphs as $paragraph) {
if (trim($paragraph) !== "") {
$result[] = trim($paragraph);
}
}
return $result;
}
/**
* 生成详细的节点差异报告
*/
public function generateDetailedDiff() {
echo "\n=== 详细节点差异分析 ===\n";
// 比较特定节点的内容
$this->compareSpecificNodes('Title text', 'titletext');
$this->compareSpecificNodes('Section2', 'section2');
$this->compareSpecificNodes('Tilte', 'tabletitle');
}
/**
* 比较特定节点的内容
*/
private function compareSpecificNodes($displayName, $searchTerm) {
echo "\n--- {$displayName} 节点对比 ---\n";
// 在源文档中查找
$sourceNodes = $this->findNodesContainingText($this->sourceDoc, $searchTerm);
echo "源文档中包含 '{$searchTerm}' 的节点数: " . count($sourceNodes) . "\n";
// 在目标文档中查找
$targetNodes = $this->findNodesContainingText($this->targetDoc, $searchTerm);
echo "目标文档中包含 '{$searchTerm}' 的节点数: " . count($targetNodes) . "\n";
// 列出目标文档中的相关内容
if (!empty($targetNodes)) {
echo "目标文档中的相关内容:\n";
foreach ($targetNodes as $node) {
echo "- {$node}\n";
}
}
}
/**
* 查找包含特定文本的节点
*/
private function findNodesContainingText($doc, $searchTerm) {
$results = [];
$textNodes = $doc->getElementsByTagName('t');
foreach ($textNodes as $node) {
$text = $node->nodeValue;
if (stripos($text, $searchTerm) !== false) {
$results[] = $text;
}
}
return $results;
}
}
/**
* 从命令行参数获取源文件和目标文件路径
* 用法: php XmlComparator.php <源文件路径> <目标文件路径>
*/
function getCommandLineArguments() {
// 获取命令行参数(不包括脚本名称)
$args = array_slice($_SERVER['argv'], 1);
// 检查参数数量
if (count($args) !== 2) {
echo "用法错误: php XmlComparator.php <源文件路径> <目标文件路径>\n";
echo "示例: php XmlComparator.php source.xml target.xml\n";
exit(1);
}
$sourceFile = $args[0];
$targetFile = $args[1];
// 验证文件是否存在
if (!file_exists($sourceFile)) {
echo "错误: 源文件 '{$sourceFile}' 不存在\n";
exit(1);
}
if (!file_exists($targetFile)) {
echo "错误: 目标文件 '{$targetFile}' 不存在\n";
exit(1);
}
// 验证文件是否为XML文件
if (pathinfo($sourceFile, PATHINFO_EXTENSION) !== 'xml') {
echo "警告: 源文件可能不是XML文件\n";
}
if (pathinfo($targetFile, PATHINFO_EXTENSION) !== 'xml') {
echo "警告: 目标文件可能不是XML文件\n";
}
return [$sourceFile, $targetFile];
}
// 主程序执行
function main() {
try {
// 获取命令行参数
list($sourceFile, $targetFile) = getCommandLineArguments();
// 初始化比较器并执行比较
$comparator = new XmlComparator($sourceFile, $targetFile);
$comparator->compare();
$comparator->generateDetailedDiff();
echo "\n=== 对比完成 ===\n";
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "\n";
exit(1);
}
}
// 执行主程序
main();