tika读取rtf表格_使用Tika在Groovy中管理表格数据

tika读取rtf表格

我一直在为一个客户进行数据抓取项目,并在进行一些实验后向我证明使用Apache Tika可以很好地完成从PDF文件中提取文本的工作。 本周,我遇到了一个DBF格式的新数据源,事实证明Tika也可以处理该数据源。

受到这种愉快无缝体验的鼓舞,我决定是时候进一步了解Tika了。 我发现自己在尝试了解如何处理解码后的信息时跌跌撞撞。 Tika网站或《 Tika in Action 》一书都没有涵盖我所需要的内容,也找不到我最喜欢的搜索引擎来解决我所要解决的问题的任何内容。

因为Tika是开源的,所以我下载了源代码以获取一些可用的示例并开始进行实验。 这是我最终完成我需要做的事情的方式-希望有人觉得它有用。

在开始之前,我应该提到Tika是用Java编写的,而我的示例是用Groovy编写的。

问题

Tika的体系结构由几个组件组成。 特别有趣的是解析器 (用于解析输入数据并将其转换为用于XML的简单API( SAX )事件的序列)和ContentHandler接口,该接口将SAX信息传递给程序员。

从我的角度来看(作为拥有大量各种格式数据的人),Tika是那些出色的开源“平台”项目之一,该项目鼓励其他开源项目的参与者做出贡献(在这种情况下,使用解析器来解码信息),促进其共享和使用。 而且Tika处理了大量的源格式(请参阅git页面,显示其mimetypes.xml )。

在我的代码中使用Tika使得解码表格文件格式(例如DBF或XLS)变得容易。 但是,我找不到满足我需求的内容处理程序。 Tika提供的内容处理程序似乎可以分为两大类:第一类是逐字段或以文本行集的形式传递转换后的输入,其中各字段由制表符分隔。 第二个给了我一个XML / HTML文档。 这些都不符合我的需求。

例如,此LibreOffice电子表格:

Spreadsheet with names and locations

保存为XLS文件,然后由BodyContentHandler处理,看起来像这样:



   
   
Sheet1
   →   Name   →   Location
   →   Jones, Ann   →   London
   →   Smith, Bob   →   San Francisco
   →   Espinoza, Mercedes   →   Buenos Aires
   →   Khan, Imran   →   Mumbai

& C & "Times New Roman,Regular" & 12 & A
& C & "Times New Roman,Regular" & 12Page & P

其中表示制表符。

当它由ToXMLContentHandler处理时,其主体看起来像这样:



   
   
< body >< div class = "page" >< h1 > Sheet1 </ h1 >
< table >< tbody >< tr >   < td > Name </ td >   < td > Location </ td ></ tr >
< tr >     < td > Jones, Ann </ td > < td > London </ td ></ tr >
< tr >     < td > Smith, Bob </ td > < td > San Francisco </ td ></ tr >
< tr >     < td > Espinoza, Mercedes </ td > < td > Buenos Aires </ td ></ tr >
< tr >     < td > Khan, Imran </ td >     < td > Mumbai </ td ></ tr >
</ tbody ></ table >
< div class = "outside" >& amp;C & amp; "Times New Roman,Regular" & amp; 12 & amp;A </ div >
< div class = "outside" >& amp;C & amp; "Times New Roman,Regular" & amp;12Page & amp;P </ div >
</ div >
</ body >

这些都不是理想的。 我不喜欢将制表符用作分隔符(如果我的一个字段包含制表符怎么办?),也找不到将该字符设置为其他任何字符的方法。 至于XML / HTML,在文本上调用XML解析器只是为了将其分开似乎很奇怪。

当我考虑这个问题并寻找定制内容处理程序的示例时​​,我注意到了两个关键的事情:首先,特别是内容处理程序和SAX通常与Tika分开,因此我开始在其他地方寻找有关它们的信息; 其次,Tika为我提供了一个完美的自定义起点(对我来说太夸张了): ContentHandlerDecorator 。 正如ContentHandlerDecorator API页面所指出的,“子类可以通过重写一个或多个SAX事件方法来提供额外的装饰。” (有关装饰器的更多信息,请阅读Wikipedia上的装饰器模式 。)

可以使用ContentHandlerDecorator给我一个很好的方法,将我的SAX事件序列转换为所需的东西,而又不需要很多多余的假装吗?

解决方案

基本上,我想要一种生成哈希表列表的方法,其中每个哈希表对应于表格数据中的一行,其键设置为列名,值设置为适当的行和列中的单元格内容。 在Groovy中,以这种方式呈现的电子表格将声明为:



   
   
def data = [
[ Name: 'Jones, Ann' , Location: 'London' ] ,
[ Name: 'Smith, Bob' , Location: 'San Francisco' ] ,
[ Name: 'Espinoza, Mercedes' , Location: 'Buenos Aires' ] ,
[ Name: 'Khan, Imran' , Location: 'Mumbai' ]
]

我阅读了Tika各种内容处理程序的源代码,并了解到可以通过重写ContentHandlerDecorator中的三个方法来实现我的目标:

  • 对于每个HTML元素(例如<TABLE><TR><TD>) ,将在每个文档元素的开始处调用startElement()方法;
  • 所述的endElement()方法将被称为对于每个HTML元素,例如</ TABLE> </ TR>,</ TD>,在这些文档中的元素中的每一个的端部; 和
  • 每当需要写出文本时,就会调用character()方法。

我需要实现这些方法以:

  • 决定何时保存发送到character()的文本;
  • 使用startElement()监视<TD>并打开文本保存;
  • 使用endElement()监视</ TD>并关闭文本保存;
  • 使用startElement()监视<TR>并创建一个空列表以保存行数据;
  • 使用endElement()监视</ TR>并将行数据列表转换为映射并将该映射添加到行映射列表; 而且,哦,是的,
  • 使用endElement()将第一行视为标题而不是数据,并将其保存到标题列表而不是行数据。

简单! 让我们写一些代码!

首先,在Groovy中,我创建了一个ContentHandlerDecorator ,如下所示:



   
   
def handler = new ContentHandlerDecorator ( ) {

    // we know we need a row counter and a list of row maps

    int rowCount = 0
    def rowMapList = [ ]

    // we also know we need a list of column names and row values

    def columnNameList = [ ]
    def rowValueList

    // we know we want to give the user access to rowMapList [ ]

    public def getRowMapList ( ) {
        rowMapList
    }

    // we may as well offer a String version of rowMapList [ ]

    @ Override
    public String toString ( ) {
        rowMapList.toString ( )
    }

    // rest of the code goes in here
}

接下来,我需要保存感兴趣的文本:



   
   
    boolean inDataElement = false
    StringBuffer dataElement

    @ Override
    public void characters ( char [ ] ch, int start, int length ) {
        if ( inDataElement )
            dataElement.append ( ch, start, length )
    }

我使用了StringBuffer ,它本质上是一个可变字符串。 它似乎与character()的参数非常匹配。 该代码遵循上一个代码块中的注释“其余的代码在这里”。

接下来,我需要处理数据元素和行的结尾:



   
   
    @ Override
    void endElement ( String uri, String localName, String name ) {
        switch ( name ) {
        case 'td' :
            inDataElement = false
            if ( rowCount == 0 )
                columnNameList.add ( dataElement.toString ( ) )
            else
                rowValueList.add ( dataElement.toString ( ) )
            break
        case 'tr' :
            if ( rowCount > 0 )
                rowMapList.
                    add ( [ columnNameList, rowValueList ] .
                        transpose ( ) .
                        collectEntries { e - >
                              [ ( e [ 0 ] ) : ( e [ 1 ] .trim ( ) ) ]
                        } )
            rowCount++
            break
        default: break
        }
    }

该代码段在上一个代码段之后。 它包含了很好的课程,以及一些不错的Groovy知识,因此值得详细介绍一下:

  • 在测试SAX事件序列的工作方式时,我确定我只需要查看start和end元素的name参数。
  • 我正在使用Groovy的功能来在字符串值上切换(){…}以拾取</ TR></ TD>元素。
  • 当我遇到</ TD>时 ,我知道刚刚处理的数据元素如果在第一条记录上,将进入列名列表,否则将进入行值列表。
  • 遇到</ TR>时 ,我知道如果我位于第二行或后续行中,则需要将列名列表和行值列表转换为行映射。
    • 我使用transpose()在列表中插入列名/行值对。
    • 然后,我使用collectEntries()将成对的列名和行值转换为映射条目。
  • 而且我记得要更新行数!

最后,我将其设置为处理数据元素和行的开头:



   
   
    @ Override
    void startElement ( String uri, String localName, String name,
            org.xml.sax.Attributes atts ) {
        switch ( name ) {
        case 'tr' :
            if ( rowCount > 0 )
                rowValueList = [ ]
            break
        case 'td' :
            inDataElement = true
            dataElement = new StringBuffer ( )
            break
        default: break
        }
    }

这会寻找<TR>来(重新)初始化行值列表,并且寻找<TD>注意它正在处理有用的数据元素,是时候将传递给帮助器的字符保存在新的字符串缓冲区中了。 再一次,此代码紧跟先前的代码。

而已! 要使用此代码,请运行以下命令:



   
   
import org.apache.tika. *
import org.apache.tika.parser. *
import org.apache.tika.metadata. *
import org.apache.tika.sax. *

// define the file we’re reading and get an input stream
//   based on that file

def file = new File ( 'test.xls' )
def fis = new FileInputStream ( file )

// define the metadata and parser

def metadata = new Metadata ( )
def parser = new AutoDetectParser ( )

// define the content handler ( copy the code created above )

def handler = new ContentHandlerDecorator ( ) {
        //
} ;

// parse the input file

parser.parse ( fis, handler, metadata )

// here we visit all the elements in the row map list
// and print out each row map, one per line

handler.rowMapList.each { map - >
        println map
}

fis.close ( )

获得此输出:



   
   
[ Name:Jones, Ann, Location:London ]
[ Name:Smith, Bob, Location:San Francisco ]
[ Name:Espinoza, Mercedes, Location:Buenos Aires ]
[ Name:Khan, Imran, Location:Mumbai ]

正是医生命令的!

因此,现在可以代替handler.rowMapList.each {map->…}闭包来研究每个单元格的值。 例如:



   
   
if ( map.Name == ‘Smith, Bob’ )
        map.Location = ‘Nairobi’

当我想将鲍勃·史密斯从伦敦搬到内罗毕时,或


new Employee ( name: map.Name, location: map.Location ) .save ( ) 

当我想将数据插入我的Grails数据库中时。

正如我所发现的,Tika Parser非常适合将事情分开,但是ContentHandler接口是以有用的方式将其重新组合的关键。

翻译自: https://opensource.com/article/17/8/tika-groovy

tika读取rtf表格

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值