我最终不得不为此创建自己的解析器。如果有人发现这一点并对我的做法有任何进一步的建议或问题,请添加评论。
解决方案
我不会上传整个代码,因为它真的很长、很乱而且效率低下。自从最初的帖子以来,我作为一名开发人员已经成长了一些,并且一直打算回去再试一次。所以我会用这篇文章来解释我有什么,指出我发现的一些问题和解决方案,并就如何提高效率发表一些评论。希望这会让你更容易,并希望这会激励我做出一些改变。免责声明:自从我上次查看此代码以来已经有几个月了,所以不要指望我会记住所有内容。但是,我非常擅长(一次)记录我的代码和发现,所以我不记得的大部分都是次要的。
我可以告诉您的最重要的事情是查看原始 XML、做笔记并比较您的一些文件。Adobe 在创建元数据语法时显然无法下定决心,因此您最终将不得不为所有不同的修订添加多个检查(稍后我将给出一个示例)。实际上在文档中查找元数据非常容易。Adobe 为您提供了一组很好的开始/结束标签,因此您只需遍历文档直到找到它们。这是我正在解析的一个 PDF 中的一个经过清理和概括的示例。
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 4.2.1-c043 52.372728, 2009/01/18-15:08:04 ">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:format>application/pdf</dc:format>
<dc:title>
<rdf:Alt>
<rdf:li xml:lang="x-default">Title of Document</rdf:li>
</rdf:Alt>
</dc:title>
<dc:creator>
<rdf:Seq>
<rdf:li>Creator of Document (Not author)</rdf:li>
</rdf:Seq>
</dc:creator>
<dc:description>
<rdf:Alt>
<rdf:li xml:lang="x-default">Short description</rdf:li>
</rdf:Alt>
</dc:description>
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/">
<xmp:CreateDate>2004-01-27T16:36:09Z</xmp:CreateDate>
<xmp:CreatorTool>FrameMaker 7.0</xmp:CreatorTool>
<xmp:ModifyDate>2012-02-20T15:55:19Z</xmp:ModifyDate>
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
<pdf:Producer>Acrobat Distiller 9.4.5 (Windows)</pdf:Producer>
</rdf:Description>
<rdf:Description rdf:about=""
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/">
<xmpMM:DocumentID>uuid:4eae0fcf-f493-4773-9473-f81c7491e8aa</xmpMM:DocumentID>
<xmpMM:InstanceID>uuid:98209926-ba98-4ac7-a5f7-050050048f5d</xmpMM:InstanceID>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
查看原始 XML 数据的最佳方法是下载 notepad++(尽管您可以使用任何类似记事本的程序)并在其中打开 PDF。您将首先看到的是 PDF 版本,在这种情况下为“%PDF-1.4”,然后是许多看起来令人困惑的字符。忽略这一点,但请注意 PDF 版本。请注意上面示例中的“xpacket”标签,这是您每次想要查找元数据时都需要查找的标签。只需 Ctrl+F 即可找到“xmpmeta”,第一次出现的应该是您的元数据。警告:不要尝试使用受密码保护的文档。一切都被混淆了,包括元数据,这也意味着 PHP 也无法读取它。我相信有一个选项可以允许读取受密码保护的 PDF 中的元数据,但我无法确定,也不知道它是否真的适用于 PHP。
就像您可以 Ctrl+F 在 notepad++ 中查找元数据一样,您也可以在 PHP 中使用fgets()
和一个while循环。我没有做但可能是一个好主意的事情是确定从文档的哪一端开始。这在所有 PDF 版本之间并不通用,但相同版本的位置似乎相似。例如,在 PDF 1.4 中,它们似乎都更靠近文档的底部,而在 PDF 1.6 中,它们都更靠近顶部。同样,您可以从第一行检查 PDF 版本。用 PHP 阅读文档应该很容易设置,所以我将跳过这段代码。不过,我会指出,一旦找到整个元数据就退出循环是个好主意,因为这是一个处理非常密集的操作,因此您需要尽可能节省时间。我还建议一次只在 10-20 个文件组上运行它,如果文件较大,则更少。
在字符串中获取元数据后,您需要对其进行一些清理。您要做的第一件事是确保将元数据很好地包装在单个根节点中,以便 XML 解析器可以读取它。有几个例子他们不是。解决此问题的最佳/最简单方法是添加一个通用包装器。我建议您使用最常见的一种。对我来说,那是带有内部“rdf”包装器的“xmpmeta”标签。确保每个元数据开始相同对于导航文档很重要。可能有更好的方法来做到这一点,但这很有效,而且效率并不低(至少现在,在我删除了两个循环之后)。
if(strpos($xmlstr, 'xmpmeta') === FALSE) {
if(strpos($xmlstr, 'rdf:rdf') === FALSE) { $xmlstr = "<rdf>$xmlstr</rdf>"; }
$xmlstr = "<xmpmeta>$xmlstr</xmpmeta>";
}
之后,您将要删除命名空间。我尝试使用它们,但是当 URL 在每个实现中不断变化并且您不确定自己拥有哪些 URL 时,这样做有点困难。此外,它已经开始运行缓慢,添加所有额外的 XML 解析只会让情况变得更糟。删除它们要简单得多。
$nodesToRemove = array('rdf', 'pdf', 'xap', 'xapMM', 'xmp', 'xmpMM', 'dc', 'x');
foreach($nodesToRemove as $remove) { $xmlstr = str_replace("$remove:", '', $xmlstr); }
$xmlstr = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $xmlstr);
$xmlstr = preg_replace("/xmlns[^=]*='[^']*'/i", '', $xmlstr);
$dom = new DOMDocument();
$dom->loadXML($xmlstr);
$sxe = simplexml_import_dom($dom);
$root = $dom->documentElement;
$namespaces = $sxe->getDocNamespaces(TRUE);
foreach($namespaces as $prefix => $uri) {
$root->removeAttributeNS($uri, $prefix);
$root->removeAttribute("xmlns:$prefix");
}
if($root->hasChildNodes()) {
foreach($root->childNodes as $element) {
if ($element->nodeType != XML_TEXT_NODE) {
$this->_removeNS($element, $namespaces);
}
}
}
对$nodesToRemove
你来说可能有点不同。这些只是我遇到的所有命名空间。笔记:我遇到了删除节点的顺序很重要的问题。我不知道为什么,但它会从“xmpMM”中删除“xmp”,我会被困在“MM”命名空间中。上面的代码似乎没有这个问题,所以我不确定它是否仍然是一个问题,但以防万一,要小心。无论哪种方式,它都不太难修复,只需让 PHP 对其进行排序然后反转它。REGEX 删除默认命名空间声明。我尝试了许多不同的方法来解决这个问题,但这是我能找到的唯一一种始终有效的方法。可能有一种方法可以结合这两个 REGEX 函数,但是当谈到 REGEX 时我完全迷失了,我的尝试只是让它坏了。我不确定为什么我要使用 XML 再次删除命名空间。这似乎是我最近尝试清理一下的尝试之一,但是这是来自一个可行的解决方案,所以它不会受到伤害(至少不是功能)。除了 REGEX 之外,第一个位可能会被删除并替换为 XML 解决方案,尽管我尚未对此进行验证。在将字符串加载到 XML 之前,仍然需要删除默认名称空间,因为 XML 解析器不认为“xmlns”属性是实际属性。命名空间版本的唯一原因“ 在将字符串加载到 XML 之前,仍然需要删除默认名称空间,因为 XML 解析器不认为“xmlns”属性是实际属性。命名空间版本的唯一原因“ 在将字符串加载到 XML 之前,仍然需要删除默认名称空间,因为 XML 解析器不认为“xmlns”属性是实际属性。命名空间版本的唯一原因“xmlns:$prefix
" 起作用是因为它们不被视为 "xmlns" 属性,而是 " xmlns:$prefix
" 属性。微妙之处。
不要像我一样。不要尝试实现曾经创建的每个版本的 PDF。这是做不到的。嗯......它可能可以,但它比它的价值更麻烦。对我来说幸运的是,这些都是内部文档,所以当我达到我的极限并且厌倦了调整它只是为了破坏其他东西,或者失去我以前拥有的兼容性时,我只是转换了最后几个文档。找到最常见的版本并处理它们,然后找到下一个最常见的版本并为它们设置条件,依此类推。一旦你到了只剩下几个的地步,更新它们,或者只是宣布你不支持这个版本。特别是如果他们年纪大了。为只用于少数文档的东西添加功能是没有意义的。我能记得的一件大事是“
清理元数据后,您就可以将其解析为 XML。例如,这是我获取描述的方式。
function getDescription($xml) {
$return = 'Error: Metadata could not be retrieved';//Return value if metadata can not be parsed
$sxe = new SimpleXMLElement($xml);
$xpath = array(
'//description/Alt/li',
'//Description/Alt/li',
'//xmpmeta/RDF/*[last()]',
//'//Description/description',
);
foreach($xpath as $pattern) {
$temp = $sxe->xpath($pattern);
if( ! empty($temp)) {
$return = isset($temp[0]->description) ? $temp[0]->description : $temp[0];
break;
}
}
//Return value if description was not found in metadata
return empty($return) ? 'Error: Metadata "description" could not be retrieved' : strval($return);
}
有几点需要注意。第一个是 XPATH 的数组。这些是我之前谈到的多重条件。您可能还注意到注释掉了 XPATH。那是我仍在为兼容性工作或已经放弃的一个。我不记得了,自从我不得不看这个以来已经有一段时间了,而且没有人抱怨错误。所以我假设这不是问题。需要注意的另一件事是仅此 ONE 字段的偏差量。元数据发生了很大变化,有时还会恢复。因此,您必须检查每种情况,确保没有其他偏差,然后添加可能发生的任何其他情况。需要研究的是根据版本保存单独的解析器,然后加载正确的解析器,这可能会降低效率。现在回想起来,也许更简单的方法是查找每个修订版的标准化文档,但我最终主要是通过反复试验来完成这项工作。因此,虽然这对我有用,但我可能错过了一些事情,因为这在我的任何文档中都不是问题。需要注意的另一件事是修订之间的标签有多相似。我不是,而且对于高级 XPATH 仍然不是那么好,所以也许有更好的方法来做到这一点,我不知道。需要注意的另一件事是修订之间的标签有多相似。我不是,而且对于高级 XPATH 仍然不是那么好,所以也许有更好的方法来做到这一点,我不知道。需要注意的另一件事是修订之间的标签有多相似。我不是,而且对于高级 XPATH 仍然不是那么好,所以也许有更好的方法来做到这一点,我不知道。
我希望这会有所帮助。我知道它给了我一些想法。如果您有任何其他具体问题,请告诉我。