Zend Lucene和PDF文档第2部分:PDF数据提取

上次我们使用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部分:结论