Для этого мне пришлось создать свой собственный парсер. Если кто-нибудь найдет это и у вас есть дополнительные предложения или вопросы о том, как я это сделал, просто добавьте комментарий.
Решение
Я не буду загружать весь код, как его очень долго, очень грязный, и неэффективно. Я вырос как разработчик с самого начала и имел смысл вернуться назад и еще раз поднять его. Поэтому я буду использовать этот пост, чтобы объяснить, что у меня есть, указать на некоторые проблемы и решения, которые я нашел, а также высказать некоторые замечания о том, как сделать его более эффективным. Надеюсь, это облегчит вам задачу, и, надеюсь, это вдохновит меня на внесение некоторых изменений. Отказ от ответственности: Прошло несколько месяцев с тех пор, как я последний раз посмотрел на этот код, поэтому не ожидайте, что я все вспомню. Тем не менее, я был довольно хорошо документировал свой код и результаты (на этот раз), поэтому то, что я не помню, в основном незначительно.
Самое главное, что я могу вам сказать, это посмотреть на необработанный 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, чтобы найти мета в блокноте ++, вы можете сделать то же самое в PHP с помощью fgets()
и цикла while. Что-то, чего я не делал, но, вероятно, было бы хорошей идеей для реализации, заключается в том, чтобы определить, с какого конца документа начать работу. Это не универсально для всех версий PDF, но одинаковые версии, похоже, аналогично размещены. Например, в PDF 1.4 они кажутся ближе к нижней части документа, в то время как в PDF 1.6 они ближе к вершине. Опять же, вы можете проверить версию PDF из первой строки. Чтение документа с помощью PHP должно быть довольно простым в настройке, поэтому я пропущу этот бит кода. Хотя, я укажу, что это хорошая идея, чтобы выйти из цикла, как только вы нашли все метаданные, так как это очень интенсивная операция обработки, поэтому вы хотите сэкономить время, где сможете. Я также предлагаю только запускать это по группам по 10-20 файлов за раз, меньше, если больше документов. Настройка системы кеширования помогла мне немного с ошибками тайм-аута.
После того, как вы получили метаданные в строке, вам нужно немного почистить ее. Первое, что вы захотите сделать, это убедиться, что ваши метаданные хорошо обернуты в одном корневом узле, так что синтаксический анализатор XML может его прочитать. Было несколько случаев, когда их не было. Лучший/самый простой способ исправить это - добавить общую оболочку. Я бы предложил использовать наиболее распространенный доступный вам. Для меня это был тег «xmpmeta» с внутренней оберткой «rdf». Обеспечение того, чтобы все метаданные начинались одинаково, важно для навигации по документу. Возможно, это лучший способ сделать это, но это работает и не слишком неэффективно (по крайней мере сейчас, после того, как я удалил две петли).
if(strpos($xmlstr, 'xmpmeta') === FALSE) {
if(strpos($xmlstr, 'rdf:rdf') === FALSE) { $xmlstr = "<rdf>$xmlstr</rdf>"; }
$xmlstr = "<xmpmeta>$xmlstr</xmpmeta>";
}
После этого вы захотите удалить пространства имен. Я попытался использовать их, но это трудно сделать, когда 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
может быть немного отличающимся для вас. Это всего лишь пространства имен, с которыми я сталкивался. Примечание: У меня были проблемы с тем, что порядок, в котором вы удаляете узлы, важен. Я не уверен, почему, но он удалит «xmp» из «xmpMM», и я застрял бы с пространством имен «ММ». Вышеприведенный код не имеет такой проблемы, поэтому я не уверен, что это проблема, но на всякий случай, будьте осторожны. В любом случае, это не так сложно исправить, просто попробуйте PHP, а затем отмените его. REGEX удаляет объявления пространства имен по умолчанию. Я попробовал несколько разных способов, но это был единственный, который я смог найти, что последовательно работал. Вероятно, есть способ объединить эти две функции REGEX, но я полностью потерял, когда дело доходит до REGEX, и мои попытки просто оставили его сломанным. Я не уверен, почему я снова удалю пространства имен с помощью XML. Это, по-видимому, одна из моих недавних попыток немного почистить, но это от рабочего решения, так что это не повредит (по крайней мере, не функциональность). Первый бит, помимо REGEX, возможно, может быть удален и заменен XML-решением, хотя я этого не проверял. По-прежнему необходимо удалить пространства имен по умолчанию перед загрузкой строки в XML, поскольку синтаксические анализаторы XML не считают атрибут «xmlns» фактическим атрибутом. Единственная причина, по которой работает версия с именами «xmlns:$prefix
», заключается в том, что они не считаются атрибутами «xmlns», но атрибутами «xmlns:$prefix
». Тонкости.
Не будьте похожими на меня. Не пытайтесь внедрять каждую версию PDF, когда-либо создаваемую. Это НЕ МОЖЕТ быть сделано. Ну ... это, вероятно, может, но это больше хлопот, чем его ценность. К счастью для меня, это были все внутренние документы, поэтому, когда я достиг своего предела и устал от настройки, просто чтобы сломать что-то еще или потерять совместимость, которую я ранее имел, у меня просто были конвертированы эти последние несколько документов. Найдите наиболее распространенные версии и обработайте их, затем следующие наиболее распространенные и настроенные условия для них и т. Д.Как только вы дойдете до того, что у вас осталось всего несколько левых, обновите их или просто сообщите, что вы не поддерживаете эту версию. Особенно, если они старше. Нет смысла добавлять функциональность для чего-то, что только когда-либо будет использоваться только для нескольких документов. Один из самых больших, который я помню, - это ситуация, когда «xpacket» не всегда был на собственной линии. Иногда это разделяло пространство с несколькими тегами метаданных. Это вызвало «отсутствующие» данные, потому что я не начал записывать мета до тех пор, пока не будет найден «xpacket». Это похоже на простое исправление, но у него появилось много проблем, поэтому я просто отказался от этой версии и обновил ее. К счастью, это были последние 3-4 файла.
После того как вы очистили метаданные, вы готовы проанализировать его как 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. Это то, что я либо все еще работаю над совместимостью, либо отказался. Я не помню, прошло какое-то время, так как мне пришлось смотреть на это, и никто не жаловался на ошибки. Поэтому я предполагаю, что это не проблема. Еще одно замечание - это количество отклонений только для этого ОДНОГО поля. Метаданные сильно изменились, а иногда и вернулись. Поэтому вам нужно проверить каждый случай, убедиться, что никаких других отклонений не было, а затем добавить любые другие условия, которые могли произойти. Что-то, на что нужно обратить внимание, было бы сохранение отдельных парсеров на основе версии, а затем загрузка соответствующего анализатора, может сократить неэффективность. Оглядываясь назад на это сейчас, возможно, проще было бы искать документы стандартизации для каждой ревизии, но вместо этого я делал это в основном посредством проб и ошибок. Итак, хотя это работает для меня, могут быть некоторые вещи, которые я пропустил, потому что это не было проблемой ни в одном из моих документов. Другое замечание - насколько сходны теги между версиями. Я не был, и все еще не так уж и хорош с продвинутым XPATH, так что, возможно, есть лучший способ сделать это, я не знаю.
Надеюсь, это поможет. Я знаю, что это дало мне несколько идей. Если у вас есть какие-то другие вопросы, дайте мне знать.
Пожалуйста, расскажите нам больше о вашей настройке и опубликуйте фактический код. – markus
Я действительно опубликовал фактический код, его стенографию из [документации] (http://framework.zend.com/manual/en/zend.pdf.info.html), скопированного непосредственно оттуда. Какую информацию вы хотели бы узнать о настройке? – mseancole