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电子表格:
保存为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接口是以有用的方式将其重新组合的关键。
tika读取rtf表格