从Lucene到Elasticsearch:全文检索实战
上QQ阅读APP看书,第一时间看更新

3.3 文本内容抽取

做文件搜索项目面临的第一个难题是如何从各种各样的文件中抽取出来内容,只有抽取出来内容才能进行索引和搜索。本节先来介绍内容解析提取工具Apache Tika。

3.3.1 Tika简介

Apache Tika是一个用于文件类型检测和文件内容提取的库,该项目于2007年3月开始启动,最开始是Apache Lucene项目的子项目,2010年5月成为Apache组织的顶级项目,该项目的目标使用群体主要为搜索引擎以及其他内容索引和分析工具,编程语言为Java。Tika可以检测超过1000种不同类型的文档,比如PPT、PDF、DOC、XLS等,所有的文本类型都可以通过一个简单的接口被解析,Tika广泛应用于搜索引擎、内容分析、文本翻译、数字资产管理等诸多领域。

为什么要使用Tika?据filext.com网站统计,文件内容类型有几万种之多,并且这个数字还在与日俱增。数据有不同的格式,如文本文档、Excel表格、PDF、图像和多媒体文件等,应用程序如搜索引擎和内容管理系统需要从这些不同类型文档中容易地提取数据。Tika通过提供一个通用的API来检测并提取多种文件格式的内容服务来达到这一目的。Tika的特点如下:

● 统一解析器接口。Tika所有的第三方解析器库被封装在一个单一的解析器中,由于这个特征,用户减少了根据不同文档类型选择合适的解析器库的负担。

● 低内存占用。因为统一的解析器接口,Tika消耗的内存资源更少,也很容易嵌入各种Java应用程序中。

● 快速处理。应用中内容检测和信息提取可以预期,处理速度快。

● 灵活元数据。Tika理解所有用来描述文件的元数据模型。

● 解析器集成。Tika可以使用单一应用程序中每个文件类型的各种解析器库。

● MIME类型检测。Tika可以检测并从所有包括在MIME标准的媒体类型中提取内容。

● 语言检测。Tika包括语言识别功能,因此可以在一个多语种网站基于语言类型的文档中使用。

3.3.2 Tika下载

下面通过实例来看如何使用Tika这一利器。首先到Tika官网的下载页面(https://tika.apache.org/download.html)下载相应的jar包,可以看到当前版本为1.13,单击“Mirrors for tika-app-1.13.jar”之后选择合适的镜像源下载Tika的jar包。

Tika可作为GUI工具使用。打开终端(Windows平台打开CMD命令行工具),切换到tika-app-1.13.jar所在目录,启动Tika,运行命令:

        java -jar tika-app-1.13.jar -g

启动成功以后,Tika客户端界面如图3-3所示。

图3-3 Tika启动界面

单击File菜单按钮,可以选择打开一个本地文件,也可以打开一个远程URL地址。文件打开以后,默认情况下显示的是文本的元数据信息,如图3-4所示。如果想查看更多内容,可以单击View菜单,分别查看文档的元数据信息、格式化之后的文本内容、纯文本内容、核心内容、结构化文本内容以及递归转换后的JSON文本。

图3-4 Tika解析PDF文件

3.3.3 搭建工程

这一小节介绍如何在Java程序中使用Tika,创建Java工程的步骤如下:

步骤01 打开Eclipse,新建java project,把工程命名为TikaDemo。

步骤02 在工程根目录下新建一个lib文件夹,拷贝tika-app-1.13.jar到lib目录下。

步骤03 选中tika-app-1.13.jar并右击,在打开的快捷菜单中依次选择Build Path→Add to Build Path,把jar包加入工程类路径下,然后就可以在java类中调用Tika提供的各种API。

步骤04 添加完jar包以后在工程根目录下新建一个files文件夹用于存放测试文件,这里我们在files文件夹下放了DOC、DOCX、PDF、TXT、PPTX 5种类型的文件。

步骤05 最后,新建一个名为TikaParsePdf的Java类。

上述步骤完成以后,工程目录结构如图3-5所示。

图3-5 Tika工程目录结构

3.3.4 内容抽取

在TikaParsePdf.java中编写Java代码,提取出PDF文件中的文本内容,最后打印到控制台,见代码清单3-1。

代码清单3-1 Tika提取PDF文件内容

              import java.io.File;
              import java.io.FileInputStream;
              import java.io.IOException;
              import org.apache.tika.exception.TikaException;
              import org.apache.tika.metadata.Metadata;
              import org.apache.tika.parser.ParseContext;
              import org.apache.tika.parser.pdf.PDFParser;
              import org.apache.tika.sax.BodyContentHandler;
              import org.xml.sax.SAXException;
              public class TikaParsePdf {
                public static void main(String[] args)throws IOException,
                  SAXException, TikaException {
                  // 文件路径
                  String filepath = "files/中国人工智能大会CCAI 2016圆满幕.pdf";
                  // 新建File对象
                  File pdfFile = new File(filepath);
                  // 创建内容处理器对象
                  BodyContentHandler handler = new BodyContentHandler();
                  // 创建元数据对象
                  Metadata metadata = new Metadata();
                  // 读入文件
                  FileInputStream inputStream = new FileInputStream(pdfFile);
                  // InputStream inputStream=TikaInputStream.get(pdfFile);
                  // 创建内容解析器对象
                  ParseContext parseContext = new ParseContext();
                  // 实例化PDFParser对象
                  PDFParser parser = new PDFParser();
                  // 调用parse()方法解析文件
                  parser.parse(inputStream, handler, metadata, parseContext);
                  // 遍历元数据内容
                  System.out.println("文件属性信息:");
                  for(String name : metadata.names()){
                      System.out.println(name + ":" + metadata.get(name));
                  }
                  // 打印pdf文件中的内容
                  System.out.println("pdf文件中的内容:");
                  System.out.println(handler.toString());
                }
              }

运行结果如图3-6所示。

图3-6 Tika PDF文件运行结果

上面的例子实现了如何使用Tika读取本地文件中的内容,files目录下放了一个PDF文件用于做测试,在主函数中首先创建一个File对象pdfFile,传入参数为PDF文件的路径,通过文件路径作为参数实例化一个文件对象,这样一来在程序中pdfFile就指向了PDF文件。目标是要提取的文件的文本内容,因此需要实例化BodyContentHandler对象,创建Metadata对象用于获取文件属性。之后实例化一个FileInputStream对象,将pdfFile对象作为参数传给FileInputStream类的构造方法,但是使用这种输入流不支持随机访问读取文件,如果想要更高效地处理各种类型的文件,可以使用Tika提供的TikaInputStream类。接下来创建一个解析上下文的ParseContext对象并实例化一个PDF解析器对象,然后调用PDF解析器对象的parse方法,并传入所有需要的4个参数,包含任何文件内容的InputStream对象、ContentHandler对象、metadata元数据对象和ParseContext对象。解析完成以后可以通过内容处理器对象(本实例中的handler)的toString()方法输出文件内容,文件属性名保存在元数据对象的names()方法中,返回结果是字符数组,遍历属性名数组通过元数据对象的get()方法得到属性信息。

上面的例子是如何提取PDF格式的文件内容以及元数据信息,那么该如何处理其他类型的文件呢?我们注意到,在创建解析器对象的时候使用的是PDFParser类,PDFParser类适用于解析PDF格式的文件,如果想解析其他类型的文件,就需要更改解析器接口类型。下面一一列出创建解析不同类型文件所需要的解析器对象的方法:

● 解析MS Office文档:

        OOXMLParser  parser = new OOXMLParser();

● 解析文本文件:

        TXTParser  parser = new TXTParser();

● 解析HTML文件:

        HtmlParser parser = new HtmlParser();

● 解析XML文件:

        XMLParser parser = new XMLParser();

● 解析class文件:

        ClassParser  parser = new  ClassParser();

图3-7 文档自动解析流程

读者可以把TikaParsePdf.java中的文件路径加以修改,根据文档类型采用不同的解析器接口进行实验。除了解析常见的文档类文件,Tika还可以解析图像、音频(如MP3)、视频(如MP4)等多种类型的文件。

3.3.5 自动解析

上述解析PDF的例子的流程大致如下:确定要解析PDF文件→实例化PDFParser→提取内容,如果要提取的文档类型不止一种,又该如何处理?Tika的强大之处正在于此,它可以先判断文档类型,再根据文档类型实例化解析器接口,流程如图3-7所示。

自动解析文档的分析过程如下:

(1)首先,传入一个文件到Tika,文件类型可以是任意的,Tika使用自身的类型检测机制来检测文件类型。

(2)Tika提供了一个解析器库,解析器库中包含多种类型的解析器。一旦文档类型被检测出来,下一步就可以根据已知的文档类型从解析器库中选择合适的解析器。

(3)选择合适的解析器以后就可以把文档传送到解析器中,解析完成后即可进行文档内容提取、元数据提取。

下面介绍使用Tika对象和使用Parser接口两种方法进行内容提取。使用Tika对象提取文档内容的Java代码见代码清单3-2。

代码清单3-2 使用Tika对象提取文档内容

              import java.io.File;
              import java.io.IOException;
              import org.apache.tika.Tika;
              import org.apache.tika.exception.TikaException;
              public class TikaExtraction {
                  public static void main(String[] args)
                      throws IOException, TikaException {
                      Tika tika = new Tika();
                      // 新建存放各种文件的files文件夹
                      File fileDir = new File("files");
                      // 如果文件夹路径错误,退出程序
                      if(! fileDir.exists()){
                          System.out.println("文件夹不存在,请检查!");
                          System.exit(0);
                      }
                      // 获取文件夹下的所有文件,存放在File数组中
                      File[] fileArr = fileDir.listFiles();
                      String filecontent;
                      for(File f : fileArr){
                          filecontent = tika.parseToString(f); // 自动解析
                      System.out.println("Extracted Content: " + filecontent);
                    }
                  }
              }

上述代码首先新建了一个File对象指向存放各种文档的文件夹,通过File对象的exists()方法判断文件夹路径是否正确,如果路径错误则退出程序,打印提示信息。接下来,通过listFiles()方法获取files目录下所有的文件,存放在文件数组之中。最后新建一个Tika对象,调用parseToString()方法获取文档内容,该方法的传入参数为File对象。

使用Tika对象提取文档内容的Java代码见代码清单3-3。

代码清单3-3 使用Parser接口提取文档内容

              import java.io.File;
              import java.io.FileInputStream;
              import java.io.IOException;
              import org.apache.tika.exception.TikaException;
              import org.apache.tika.metadata.Metadata;
              import org.apache.tika.parser.AutoDetectParser;
              import org.apache.tika.parser.ParseContext;
              import org.apache.tika.parser.Parser;
              import org.apache.tika.sax.BodyContentHandler;
              import org.xml.sax.SAXException;
              public class ParserExtraction {
              public static void main(String[] args)throws IOException,
                  SAXException, TikaException {
                  // 新建存放各种文件的files文件夹
                  File fileDir = new File("files");
                  // 如果文件夹路径错误,退出程序
                  if(! fileDir.exists()){
                      System.out.println("文件夹不存在,请检查!");
                      System.exit(0);
                  }
                  // 获取文件夹下的所有文件,存放在File数组中
                  File[] fileArr = fileDir.listFiles();
                  // 创建内容处理器对象
                  BodyContentHandler handler = new BodyContentHandler();
                  // 创建元数据对象
                  Metadata metadata = new Metadata();
                  FileInputStream inputStream = null;
                  Parser parser = new AutoDetectParser();
                  // 自动检测分词器
                  ParseContext context = new ParseContext();
                  for(File f : fileArr){
                      inputStream = new FileInputStream(f);
                      parser.parse(inputStream, handler, metadata, context);
                      System.out.println(f.getName()+ ":\n" + handler
                                        .toString());
                  }
              }
          }

使用Parse接口自动提取内容和前面单一的提取一种文档的区别在于实例化对象不一样,AutoDetectParser是CompositeParser的子类,它能够自动检测文件类型,并使用相应的方法把接收到的文档自动发送给最接近的解析器类。