iTextSharp 将包装的单元格内容提取到新行中 - 您如何识别给定的包装数据现在属于哪一列?
iTextSharp extracts wrapped cell contents into new lines - how do you identify to which column a given wrapped piece of data belongs now?
我正在使用 iTextSharp 从 pdf 中提取数据。
我偶然发现了以下问题,如下图所示:
我创建了一个示例 excel 文件来进行说明。这是它的样子:
我使用许多可用的免费在线转换器之一将其转换为 pdf,它生成的 pdf 看起来像(当我生成 pdf 时,我没有将样式应用到 excel) :
现在,使用 iTextSharp
从 pdf 中提取数据,returns 我将以下字符串作为提取的数据:
如您所见,包裹的单元格数据生成新行,其中每个包裹的数据由一个白色分隔 space。
问题:现在如何识别给定的包装数据属于哪一列?如果仅 iTextSharp
保留与列一样多的白色 spaces...
在我的示例中 - 如何确定 111 属于哪个列?
更新一:
只要一个字段有多个单词(即包含白色 spaces),就会出现类似的问题。例如,考虑上面示例的第一行:
说它看起来像
---A--- ---B--- ---C--- ---D---
aaaaaaa bb b cccc
iText 将再次为此生成提取:
aaaaaaa bb b cccc
同样的问题,必须确定每列的边界。
更新二:
我正在使用的真实 pdf 文件示例:
这是 pdf 数据的样子。
这不是真正的答案,但我需要一个地方来展示一些可能有助于您理解的东西。
首先 "conversion" 从 Excel、Word、PowerPoint、HTML 或其他任何格式转换为 PDF 几乎总是 破坏性的 更改. 破坏性 部分非常重要,它的发生是因为您从一个程序中获取数据,该程序非常了解该数据代表的内容 (Excel),并且您正在将其转换为以非常通用的通用格式 (PDF) 绘制命令,它只关心数据的外观,而不关心数据本身。除非数据是 "tagged"(现在几乎从来没有),否则绘图命令没有 context。没有段落,没有句子,没有列,行,tables 等等。字面上只是 在 x,y
和 在a,b
画这个字。
其次,假设您 Excel 文件具有以下数据,并且由于某种原因,制作 PDF 时最后一列比其他列窄:
Column A | Column B | Column
C
Data #1 Data #2 Data
#3
你和我有 context 所以我们 知道 第二和第四 行 是实际上只是第一和第三 行 的延续。但由于 iText 在提取过程中没有任何 context,它不会那样想,它会看到 四行文本 。事实上,由于它没有上下文,它甚至看不到 列 ,只是行本身。
第三,虽然这是一件非常小的事情,但您需要了解您不会在 PDF 中绘制空格。想象下面的三列 table:
Column A | Column B | Column C
Yes
如果您从 PDF 中提取该数据,您将获得以下数据:
Column A | Column B | Column C
Yes
在 PDF 中,单词 "Yes" 将仅绘制在您和我认为位于第三列下方的某个 x
坐标处,并且不会有一堆空格前面。
正如我在开头所说,这不是一个很好的答案,但希望它能向您解释您要解决的问题。如果您的 PDF 被标记,那么它将具有上下文,您可以在提取期间使用该上下文。然而,上下文并不是通用的,因此通常不只是一个神奇的 "insert context" 复选框。 Excel 实际上确实有一个复选框(如果我没记错的话)在导出期间制作带标签的 PDF,它最终使用 table 的 HTML-like 标签创建带标签的 PDF。非常原始,但它会起作用。但是,由您来解析此上下文。
除了 Chris 的通用答案外,iText(Sharp) 内容解析的一些背景...
iText(Sharp) 在 namespace iTextSharp.text.pdf.parser
/ package com.itextpdf.text.pdf.parser
中提供了 内容提取框架 。此 franework 读取页面内容,跟踪当前图形状态,并将有关内容片段的信息转发给 IExtRenderListener
或 IRenderListener
/ ExtRenderListener
或 RenderListener
用户(即 you) 提供。特别是它不会将结构解释为该信息。
此渲染监听器可能是一种文本提取策略(ITextExtractionStrategy
/ TextExtractionStrategy
),即一种特殊的渲染监听器,主要用于在不格式化的情况下提取纯文本流或布局信息。对于这种特殊情况,iText(Sharp) 还提供了两个示例实现,SimpleTextExtractionStrategy
和 LocationTextExtractionStrategy
.
对于您的任务,您需要一个更复杂的渲染侦听器,它可以是
- 导出带坐标的文本(Chris in one of his answers 提供了一个扩展的
LocationTextExtractionStrategy
,它可以额外提供文本块的位置和边界框)允许您在额外的代码中分析表格结构;或
- 对表格数据本身进行分析。
我没有后一种变体的示例,因为一般识别和解析 tables 本身就是一个完整的项目。您可能想查看 Tabula 项目以获取灵感;这个项目出奇地擅长 table 提取任务。
PS:如果您更愿意尝试从内容的纯字符串表示中提取结构化内容,但仍会尝试反映原始布局,您可以尝试类似 this answer,LocationTextExtractionStrategy
的变体,工作方式类似于 pdftotext -layout
工具;此处仅显示要应用于 LocationTextExtractionStrategy
的更改。
PPS:从非常具体的 PDF table 中提取数据可能要容易得多;例如,查看 ,它表明经过一些 PDF 分析后,创建给定 table 的特定方式可能会产生一个简单的自定义渲染侦听器,用于提取 table 数据。这对于 table 跨越许多页的单个 PDF 很有意义,就像在那个答案的情况下一样,或者如果您有许多由同一软件完全相同地创建的 PDF,这也很有意义。
这就是为什么我在对您的问题的评论中要求提供代表性示例文件的原因
关于您的意见
Still with the pdf example above, both with an implementation from scratch of ITextExtractionStrategy and with extending LocationExtractionStrategy, I see that each RenderText is called at the following chunks: Fi, el, d, A, Fi, el, d... and so on. Can this be changed?
您作为单独的 RenderText
调用获得的文本块不会因意外或 iText 的某些随机决定而分开。就是页面内容中单独绘制的字符串!
在您的示例中 "Fi"、"el"、"d" 和 "A" 进入不同的 RenderText
调用,因为内容流包含的操作首先"Fi"被绘制,然后"el",然后"d",然后"A"。
乍一看这听起来很奇怪。造成此类字词损坏的一个常见原因是 PDF 不 使用字体中的字距调整信息;因此,要应用字距调整,PDF 生成软件必须在字符之间插入微小的向前或向后跳跃,这些字符之间的距离应该比没有字距调整时更远或更近。因此,单词经常在字距调整对之间被撕裂。
所以这不能改变,你会得到那些碎片,把它们放在一起是文本提取策略的工作。
顺便说一句,有更糟糕的 PDF,一些 PDF 生成器分别定位每个字形,最重要的是主要构建 GUI 但可以作为一项功能自动将 GUI 画布导出为 PDF 的生成器。
I would expect that in entering the realm of "adding my own implementation" I would have control over how to determine what is a "chunk" of text.
您可以...好吧,您必须决定哪些传入的片段属于一起,哪些不属于。例如。具有相同 y 坐标的字形是否形成一条线?或者它们是否在不同的列中形成单独的行,而恰好彼此相邻。
所以是的,您决定将哪些字形解释为单个单词或单个 table 单元格的内容,但您的输入由实际 PDF 内容流中使用的字形组组成。
Not only that, in none of the interface's methods I can "spot" how/where it deals with non-text data/images - so I could intercede with the spacing issue (RenderImage is not called)
RenderImage
将调用嵌入式 位图 图像、JPEG 等。如果您想了解 矢量图形 ,您的策略还必须实施 IExtRenderListener
,它提供方法 ModifyPath
、RenderPath
和 ClipPath
。
在此处留下另一种提取数据的策略 - 这并不能解决空间 treated/can 被处理的问题,但可以通过指定要提取的几何区域来让您更好地控制提取文字来自。摘自 here.
public static System.util.RectangleJ GetRectangle(float distanceInPixelsFromLeft, float distanceInPixelsFromBottom, float width, float height)
{
return new System.util.RectangleJ(
distanceInPixelsFromLeft,
distanceInPixelsFromBottom,
width,
height);
}
public static void Strategy2()
{
// In this example, I'll declare a pageNumber integer variable to
// only capture text from the page I'm interested in
int pageNumber = 1;
var text = new StringBuilder();
List<Tuple<string, int>> result = new List<Tuple<string, int>>();
// The PdfReader object implements IDisposable.Dispose, so you can
// wrap it in the using keyword to automatically dispose of it
using (var pdfReader = new PdfReader("D:/Example.pdf"))
{
float distanceInPixelsFromLeft = 20;
//float distanceInPixelsFromBottom = 730;
float width = 300;
float height = 10;
for (int i = 800; i >= 0; i -= 10)
{
var rect = GetRectangle(distanceInPixelsFromLeft, i, width, height);
var filters = new RenderFilter[1];
filters[0] = new RegionTextRenderFilter(rect);
ITextExtractionStrategy strategy =
new FilteredTextRenderListener(
new LocationTextExtractionStrategy(),
filters);
var currentText = PdfTextExtractor.GetTextFromPage(
pdfReader,
pageNumber,
strategy);
currentText =
Encoding.UTF8.GetString(Encoding.Convert(
Encoding.Default,
Encoding.UTF8,
Encoding.Default.GetBytes(currentText)));
//text.Append(currentText);
result.Add(new Tuple<string, int>(currentText, currentText.Length));
}
}
// You'll do something else with it, here I write it to a console window
//Console.WriteLine(text.ToString());
foreach (var line in result.Distinct().Where(r => !string.IsNullOrWhiteSpace(r.Item1)))
{
Console.WriteLine("Text: [{0}], Length: {1}", line.Item1, line.Item2);
}
//Console.WriteLine("", string.Join("\r\n", result.Distinct().Where(r => !string.IsNullOrWhiteSpace(r.Item1))));
输出:
PS.: 我们还剩下如何处理spaces/non文本数据的问题。
我正在使用 iTextSharp 从 pdf 中提取数据。 我偶然发现了以下问题,如下图所示:
我创建了一个示例 excel 文件来进行说明。这是它的样子:
我使用许多可用的免费在线转换器之一将其转换为 pdf,它生成的 pdf 看起来像(当我生成 pdf 时,我没有将样式应用到 excel) :
现在,使用 iTextSharp
从 pdf 中提取数据,returns 我将以下字符串作为提取的数据:
如您所见,包裹的单元格数据生成新行,其中每个包裹的数据由一个白色分隔 space。
问题:现在如何识别给定的包装数据属于哪一列?如果仅 iTextSharp
保留与列一样多的白色 spaces...
在我的示例中 - 如何确定 111 属于哪个列?
更新一:
只要一个字段有多个单词(即包含白色 spaces),就会出现类似的问题。例如,考虑上面示例的第一行:
说它看起来像
---A--- ---B--- ---C--- ---D---
aaaaaaa bb b cccc
iText 将再次为此生成提取:
aaaaaaa bb b cccc
同样的问题,必须确定每列的边界。
更新二:
我正在使用的真实 pdf 文件示例:
这不是真正的答案,但我需要一个地方来展示一些可能有助于您理解的东西。
首先 "conversion" 从 Excel、Word、PowerPoint、HTML 或其他任何格式转换为 PDF 几乎总是 破坏性的 更改. 破坏性 部分非常重要,它的发生是因为您从一个程序中获取数据,该程序非常了解该数据代表的内容 (Excel),并且您正在将其转换为以非常通用的通用格式 (PDF) 绘制命令,它只关心数据的外观,而不关心数据本身。除非数据是 "tagged"(现在几乎从来没有),否则绘图命令没有 context。没有段落,没有句子,没有列,行,tables 等等。字面上只是 在 x,y
和 在a,b
画这个字。
其次,假设您 Excel 文件具有以下数据,并且由于某种原因,制作 PDF 时最后一列比其他列窄:
Column A | Column B | Column
C
Data #1 Data #2 Data
#3
你和我有 context 所以我们 知道 第二和第四 行 是实际上只是第一和第三 行 的延续。但由于 iText 在提取过程中没有任何 context,它不会那样想,它会看到 四行文本 。事实上,由于它没有上下文,它甚至看不到 列 ,只是行本身。
第三,虽然这是一件非常小的事情,但您需要了解您不会在 PDF 中绘制空格。想象下面的三列 table:
Column A | Column B | Column C
Yes
如果您从 PDF 中提取该数据,您将获得以下数据:
Column A | Column B | Column C
Yes
在 PDF 中,单词 "Yes" 将仅绘制在您和我认为位于第三列下方的某个 x
坐标处,并且不会有一堆空格前面。
正如我在开头所说,这不是一个很好的答案,但希望它能向您解释您要解决的问题。如果您的 PDF 被标记,那么它将具有上下文,您可以在提取期间使用该上下文。然而,上下文并不是通用的,因此通常不只是一个神奇的 "insert context" 复选框。 Excel 实际上确实有一个复选框(如果我没记错的话)在导出期间制作带标签的 PDF,它最终使用 table 的 HTML-like 标签创建带标签的 PDF。非常原始,但它会起作用。但是,由您来解析此上下文。
除了 Chris 的通用答案外,iText(Sharp) 内容解析的一些背景...
iText(Sharp) 在 namespace iTextSharp.text.pdf.parser
/ package com.itextpdf.text.pdf.parser
中提供了 内容提取框架 。此 franework 读取页面内容,跟踪当前图形状态,并将有关内容片段的信息转发给 IExtRenderListener
或 IRenderListener
/ ExtRenderListener
或 RenderListener
用户(即 you) 提供。特别是它不会将结构解释为该信息。
此渲染监听器可能是一种文本提取策略(ITextExtractionStrategy
/ TextExtractionStrategy
),即一种特殊的渲染监听器,主要用于在不格式化的情况下提取纯文本流或布局信息。对于这种特殊情况,iText(Sharp) 还提供了两个示例实现,SimpleTextExtractionStrategy
和 LocationTextExtractionStrategy
.
对于您的任务,您需要一个更复杂的渲染侦听器,它可以是
- 导出带坐标的文本(Chris in one of his answers 提供了一个扩展的
LocationTextExtractionStrategy
,它可以额外提供文本块的位置和边界框)允许您在额外的代码中分析表格结构;或 - 对表格数据本身进行分析。
我没有后一种变体的示例,因为一般识别和解析 tables 本身就是一个完整的项目。您可能想查看 Tabula 项目以获取灵感;这个项目出奇地擅长 table 提取任务。
PS:如果您更愿意尝试从内容的纯字符串表示中提取结构化内容,但仍会尝试反映原始布局,您可以尝试类似 this answer,LocationTextExtractionStrategy
的变体,工作方式类似于 pdftotext -layout
工具;此处仅显示要应用于 LocationTextExtractionStrategy
的更改。
PPS:从非常具体的 PDF table 中提取数据可能要容易得多;例如,查看
这就是为什么我在对您的问题的评论中要求提供代表性示例文件的原因
关于您的意见
Still with the pdf example above, both with an implementation from scratch of ITextExtractionStrategy and with extending LocationExtractionStrategy, I see that each RenderText is called at the following chunks: Fi, el, d, A, Fi, el, d... and so on. Can this be changed?
您作为单独的 RenderText
调用获得的文本块不会因意外或 iText 的某些随机决定而分开。就是页面内容中单独绘制的字符串!
在您的示例中 "Fi"、"el"、"d" 和 "A" 进入不同的 RenderText
调用,因为内容流包含的操作首先"Fi"被绘制,然后"el",然后"d",然后"A"。
乍一看这听起来很奇怪。造成此类字词损坏的一个常见原因是 PDF 不 使用字体中的字距调整信息;因此,要应用字距调整,PDF 生成软件必须在字符之间插入微小的向前或向后跳跃,这些字符之间的距离应该比没有字距调整时更远或更近。因此,单词经常在字距调整对之间被撕裂。
所以这不能改变,你会得到那些碎片,把它们放在一起是文本提取策略的工作。
顺便说一句,有更糟糕的 PDF,一些 PDF 生成器分别定位每个字形,最重要的是主要构建 GUI 但可以作为一项功能自动将 GUI 画布导出为 PDF 的生成器。
I would expect that in entering the realm of "adding my own implementation" I would have control over how to determine what is a "chunk" of text.
您可以...好吧,您必须决定哪些传入的片段属于一起,哪些不属于。例如。具有相同 y 坐标的字形是否形成一条线?或者它们是否在不同的列中形成单独的行,而恰好彼此相邻。
所以是的,您决定将哪些字形解释为单个单词或单个 table 单元格的内容,但您的输入由实际 PDF 内容流中使用的字形组组成。
Not only that, in none of the interface's methods I can "spot" how/where it deals with non-text data/images - so I could intercede with the spacing issue (RenderImage is not called)
RenderImage
将调用嵌入式 位图 图像、JPEG 等。如果您想了解 矢量图形 ,您的策略还必须实施 IExtRenderListener
,它提供方法 ModifyPath
、RenderPath
和 ClipPath
。
在此处留下另一种提取数据的策略 - 这并不能解决空间 treated/can 被处理的问题,但可以通过指定要提取的几何区域来让您更好地控制提取文字来自。摘自 here.
public static System.util.RectangleJ GetRectangle(float distanceInPixelsFromLeft, float distanceInPixelsFromBottom, float width, float height)
{
return new System.util.RectangleJ(
distanceInPixelsFromLeft,
distanceInPixelsFromBottom,
width,
height);
}
public static void Strategy2()
{
// In this example, I'll declare a pageNumber integer variable to
// only capture text from the page I'm interested in
int pageNumber = 1;
var text = new StringBuilder();
List<Tuple<string, int>> result = new List<Tuple<string, int>>();
// The PdfReader object implements IDisposable.Dispose, so you can
// wrap it in the using keyword to automatically dispose of it
using (var pdfReader = new PdfReader("D:/Example.pdf"))
{
float distanceInPixelsFromLeft = 20;
//float distanceInPixelsFromBottom = 730;
float width = 300;
float height = 10;
for (int i = 800; i >= 0; i -= 10)
{
var rect = GetRectangle(distanceInPixelsFromLeft, i, width, height);
var filters = new RenderFilter[1];
filters[0] = new RegionTextRenderFilter(rect);
ITextExtractionStrategy strategy =
new FilteredTextRenderListener(
new LocationTextExtractionStrategy(),
filters);
var currentText = PdfTextExtractor.GetTextFromPage(
pdfReader,
pageNumber,
strategy);
currentText =
Encoding.UTF8.GetString(Encoding.Convert(
Encoding.Default,
Encoding.UTF8,
Encoding.Default.GetBytes(currentText)));
//text.Append(currentText);
result.Add(new Tuple<string, int>(currentText, currentText.Length));
}
}
// You'll do something else with it, here I write it to a console window
//Console.WriteLine(text.ToString());
foreach (var line in result.Distinct().Where(r => !string.IsNullOrWhiteSpace(r.Item1)))
{
Console.WriteLine("Text: [{0}], Length: {1}", line.Item1, line.Item2);
}
//Console.WriteLine("", string.Join("\r\n", result.Distinct().Where(r => !string.IsNullOrWhiteSpace(r.Item1))));
输出:
PS.: 我们还剩下如何处理spaces/non文本数据的问题。