上次我们使用Zend Framework查看和保存元数据并将其保存到PDF文档中。在尝试使用Zend Lucene对其进行索引之前,下一步是从文档本身中提取数据。在这里我应该指出,我们不能从每个PDF文档中完美地提取数据,我们当然不能从PDF中提取任何图像或表格到任何可识别的文本中。提取文本存在一个小问题,因为我们实质上是在查看压缩数据。文本不会保存到文档中,而是使用字体呈现到文档中。因此,我们需要做的是将这些数据提取为Lucene可以标记化的某种格式。因为我们只是从文档中获取文本作为搜索索引,所以我们可以采取一些捷径以便从文档中获取尽可能多的文本数据。所有这些数据可能都不是完全可读的,并且我们肯定会丢失所有格式和图像,但是出于我们实际使用它的目的,我们实际上并不需要它。我们的想法是,我们可以检索尽可能多的相关内容和可索引内容,以供Zend Lucene标记化。另外,也无法从加密的PDF文档中提取数据。
我们首先需要设置一些项目,以便我们可以简单地使用PDF提取服务来为我们完成艰苦的工作。这确实意味着对Zend Framework的理解要比最后要求的要多。我们要做的是向Zend_Loader_Autoloader注册一个名称空间。这将使我们能够创建可以保留在整洁的文件夹结构中的类,并在需要时自动将其包括在内。如果还没有,请_initAutoload()在Bootstrap.php文件中创建一个称为或类似函数。然后输入以下代码(为清楚起见,此处包括整个类)。您可能已经在Zend Framework项目中完成了此操作,因此,在这种情况下,您可以跳过此步骤。
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap { protected function _initAutoload() { $autoloader = Zend_Loader_Autoloader::getInstance(); $autoloader->registerNamespace(array('App_')); } }
这是要注册一个名为App的文件夹,该文件夹位于我们的库文件夹中,作为Zend Framework自动加载功能的一部分。创建一个名为App_Search_Helper_PdfParser的类,并将其放入文件夹\ library \ App \ Search \ Helper \中,如下所示:
--application --library ----App ------Search --------Helper ----------PdfParser.php ----Zend
现在,我们可以实例化该对象,而不必担心是否包含该对象,Zend Framework自动加载器将通过查看类名并为我们包含它,简单地在文件的正确位置查找该文件。我们将在应用程序的其余部分中使用此文件夹结构,并在添加类时以此为基础。
我们现在需要做的是创建将在我们的PDF文档上运行的代码并挑选出文本。我必须承认,我自己并没有完全写这篇文章,这是几个小时从示例和应用程序中挑选一些零碎代码的结果,以便我可以做我想做的事情。我已经用许多不同的PDF文档示例(从不同资源中抽取了大约50个)测试了此代码,因此它应该能够从大多数PDF类型中提取数据。这段代码本质上的作用是将文档分成多个不同的部分,然后尝试解压缩每个具有FlateDecode过滤器类型的部分。如果解压缩有效(即,我们有一些数据),则将其添加到字符串中并继续,并在文档末尾将其返回一次。我还在此代码中添加了一些字符串操作,该操作将去除我们不需要的任何奇数字符或空格。这是完整的类,这里又有很多代码,因此我对其进行了注释以使其更加清晰。
另外,由于使用了gzuncompress,您将需要服务器上存在一个zip库,此库才能正常运行。
class App_Search_Helper_PdfParser { /** * Convert a PDF into text. * * @param string $filename The filename to extract the data from. * @return string The extracted text from the PDF */ public function pdf2txt($data) { /** * Split apart the PDF document into sections. We will address each * section separately. */ $a_obj = $this->getDataArray($data, "obj", "endobj"); $j = 0; /** * Attempt to extract each part of the PDF document into a "filter" * element and a "data" element. This can then be used to decode the * data. */ foreach ($a_obj as $obj) { $a_filter = $this->getDataArray($obj, "<<", ">>"); if (is_array($a_filter) && isset($a_filter[0])) { $a_chunks[$j]["filter"] = $a_filter[0]; $a_data = $this->getDataArray($obj, "stream", "endstream"); if (is_array($a_data) && isset($a_data[0])) { $a_chunks[$j]["data"] = trim(substr($a_data[0], strlen("stream"), strlen($a_data[0]) - strlen("stream") - strlen("endstream"))); } $j++; } } $result_data = NULL; // 解码块 foreach ($a_chunks as $chunk) { // 查看每个块,通过查看过滤器的内容来决定是否可以对其进行解码 if (isset($chunk["data"])) { // 查看过滤器以找出使用了哪种编码 if (strpos($chunk["filter"], "FlateDecode") !== false) { // 使用gzuncompress但抑制错误消息。 $data [email protected] gzuncompress($chunk["data"]); if (trim($data) != "") { // 如果我们得到数据,则尝试提取它。 $result_data .= ' ' . $this->ps2txt($data); } } } } /** * Make sure we don't have large blocks of white space before and after * our string. Also extract alphanumerical information to reduce * redundant data. */ $result_data = trim(preg_replace('/([^a-z0-9 ])/i', ' ', $result_data)); // 返回从文档中提取的数据。 if ($result_data == "") { return NULL; } else { return $result_data; } } /** * Strip out the text from a small chunk of data. * * @param string $ps_data The chunk of data to convert. * @return string The string extracted from the data. */ public function ps2txt($ps_data) { // 停止此函数从非数据字符串返回假信息。 if (ord($ps_data[0]) < 10) { return $ps_data; } if (substr($ps_data, 0, 8 ) == '/CIDInit') { return ''; } $result = ""; $a_data = $this->getDataArray($ps_data, "[", "]"); // 提取数据。 if (is_array($a_data)) { foreach ($a_data as $ps_text) { $a_text = $this->getDataArray($ps_text, "(", ")"); if (is_array($a_text)) { foreach ($a_text as $text) { $result .= substr($text, 1, strlen($text) - 2); } } } } // 没发现任何东西,尝试另一种提取数据的方式 if (trim($result) == "") { // 数据可能只是原始格式(在[]标签之外) $a_text = $this->getDataArray($ps_data, "(", ")"); if (is_array($a_text)) { foreach ($a_text as $text) { $result .= substr($text, 1, strlen($text) - 2); } } } // 删除所有遗留的流浪字符。 $result = preg_replace('/\b([^a|i])\b/i', ' ', $result); return trim($result); } /** * Convert a section of data into an array, separated by the start and end words. * * @param string $data The data. * @param string $start_word The start of each section of data. * @param string $end_word The end of each section of data. * @return array The array of data. */ public function getDataArray($data, $start_word, $end_word) { $start = 0; $end = 0; $a_result = array(); while ($start !== false && $end !== false) { $start = strpos($data, $start_word, $end); $end = strpos($data, $end_word, $start); if ($end !== false && $start !== false) { // 数据在开始和结束之间 $a_result[] = substr($data, $start, $end - $start + strlen($end_word)); } } return $a_result; } }
要在您的应用程序中使用此txt()方法,只需实例化该对象并调用pdf2方法,并将呈现的PDF字符串作为参数传递。我决定让Zend_Pdf对象将数据传输到类中,而不是让该对象第二次打开文件(在第一次打开以检查PDF数据之后)。以下代码显示了如何使用Zend_Pdf加载PDF并将呈现的字符串传递给pdf2txt()方法。
$pdf = Zend_Pdf::load($pdfPath); $pdfParse = new App_Search_Helper_PdfParser(); $contents = $pdfParse->pdf2txt($pdf->render());
在此过程之后,我们应该剩下的是可以在搜索索引中使用的文本块。
在下一篇文章中,我将把元数据和内容检索结合在一起,并使用它们使用Zend Lucene索引我们的PDF文档。再次,我将在最后一期中提供该项目的所有源代码,如果需要,请继续关注。
Zend Lucene和PDF文档第1部分:PDF元数据
Zend Lucene和PDF文档第2部分:PDF数据提取
Zend Lucene和PDF文档第3部分:为文档建立索引
Zend Lucene和PDF文档第4部分:搜索
Zend Lucene和PDF文档第5部分:结论