介绍
在大型语言模型(LLM)和其广泛应用的时代,从简单的文本摘要和翻译到基于情绪和财务报告主题预测股票表现,文本数据的重要性。
有许多类型的文档共享这种无结构信息,包括网页文章、博客文章、手写信件和诗歌等。然而,这些文本数据中有一大部分以PDF格式存储和传输。具体来说,每年有超过20亿个PDF在Outlook中被打开,而每天有7300万个新的PDF文件保存在Google Drive和电子邮件中。
因此,开发一种更系统的方式来处理这些文档并从中提取信息,将使我们能够实现自动化流程,并更好地理解和利用这庞大的文本数据。而在这个任务中,Python无疑是我们最好的朋友。
然而,在我们开始处理之前,我们需要明确一下现在存在的不同类型的PDF,更具体地说,最常见的三种类型:
1. 程序生成的PDF:这些PDF是在计算机上使用W3C技术(如HTML、CSS和JavaScript)或其他软件(如Adobe Acrobat)创建的。这种类型的文件可以包含各种组件,如图片、文本和链接,这些都是可搜索和易于编辑的。
2. 传统扫描文档:这些PDF是通过扫描仪或移动应用程序从非电子介质创建的。这些文件实际上只是存储在PDF文件中的一组图像。换句话说,这些图像中出现的元素(如文本或链接)无法选择或搜索。实质上,PDF文件仅充当这些图像的容器。
3. 带OCR的扫描文档:在这种情况下,扫描文档后使用光学字符识别(OCR)软件来识别文件中每个图像中的文本,并将其转换为可搜索和可编辑的文本。然后,软件在图像上添加一个带有实际文本的图层,这样在浏览文件时可以将其作为独立组件选择。
尽管如今越来越多的机器都安装了可以从扫描文档中识别文本的OCR系统,但仍然存在包含整页图像的文档。当你阅读一篇精彩的文章并尝试选择一句话时,可能会发现你选中了整个页面。这可能是具体的OCR机器的限制或完全没有OCR的结果。因此,为了不让这些文章中的信息被忽略,我尝试创建了一个过程,同时考虑了这些情况,并从我们宝贵且信息丰富的PDF中获取了最多的信息。
理论方法
在考虑到所有这些不同类型的PDF文件和它们的各种组成部分之后,对PDF的布局进行初步分析非常重要,以确定每个组件所需的适当工具。更具体地说,根据此分析的结果,我们将应用适当的方法从PDF中提取文本,无论是在包含元数据的语料库块中呈现的文本,还是在图像内的文本,还是在表中的结构化文本。在没有OCR的扫描文档中,能够识别和提取图像中的文本的方法将完成所有的繁重工作。此过程的输出将是一个Python字典,其中包含提取的每页PDF文件的信息。该字典中的每个键将表示文档的页码,其对应的值将是一个包含以下5个嵌套列表的列表:
1. 每个语料库文本块的提取文本
2. 每个文本块的文本格式,包括字体系列和大小
3. 从页面上提取的图像文本
4. 以结构化格式提取的表格中的文本
5. 页面的完整文本内容
这样我们可以实现对提取的文本根据源组件进行更合理的分离,有助于我们更轻松地检索通常出现在特定组件中的信息(例如,标志图像中的公司名称)。此外,从文本中提取的元数据,如字体家族和大小,可以用于轻松识别文本标题或重要的高亮文本,这将帮助我们进一步将文本分割或进行多个不同块的后处理。最后,以使LLM能够理解的方式保留结构化表信息将极大增强对提取数据内关系的推理质量。然后,这些结果可以组合为每个页面上出现的所有文本信息的输出。
你可以在下面的图像中看到这种方法的流程图。
安装所有必要的库
在我们开始这个项目之前,我们需要安装必要的库。我们假设你的机器上已安装了Python 3.10或更高版本。然后让我们安装以下库:
PyPDF2:用于从存储库路径读取PDF文件。
pip install PyPDF2
Pdfminer:执行布局分析并从PDF中提取文本和格式。(该库的.six版本支持Python 3)
pip install pdfminer.six
Pdfplumber:用于识别PDF页面中的表格并从中提取信息的工具。
pip install pdfplumber
Pdf2image:将裁剪后的PDF图像转换为PNG图像。
pip install pdf2image
PIL:读取PNG图像。
pip install Pillow
Pytesseract: 使用OCR技术从图像中提取文本
安装Pytesseract稍微有些棘手,因为首先需要安装Google Tesseract OCR,它是一种基于LSTM模型的OCR机器,用于识别行识别和字符模式。
如果你是Mac用户,可以通过终端使用Brew安装此软件,并且你就可以开始使用了。
brew install tesseract
然后,在下载和安装软件后,你需要将其可执行路径添加到计算机的环境变量中。或者,你可以使用以下代码直接运行以下命令来在Python脚本中包含它们的路径:
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'r'C:\Program Files\Tesseract-OCR\tesseract.exe'
然后你可以安装Python库。
pip install pytesseract
最后,我们将在脚本的开头导入所有的库。
# To read the PDF
import PyPDF2
# To analyze the PDF layout and extract text
from pdfminer.high_level import extract_pages, extract_text
from pdfminer.layout import LTTextContainer, LTChar, LTRect, LTFigure
# To extract text from tables in PDF
import pdfplumber
# To extract the images from the PDFs
from PIL import Image
from pdf2image import convert_from_path
# To perform OCR to extract text from images
import pytesseract
# To remove the additional created files
import os
现在我们已经准备好了。让我们继续进行有趣的部分。
使用Python进行文档布局分析
对于初步分析,我们使用了PDFMiner Python库将文档对象中的文本分离成多个页面对象,然后对每个页面的布局进行分解和检查。PDF文件本身缺乏结构化信息,例如人眼可见的段落、句子或单词。相反,它们只能理解文本中的单个字符以及它们在页面上的位置。因此,PDFMiner试图将页面的内容重构为由单个字符及其在文件中的位置组成的形式。然后,通过比较这些字符与其他字符之间的距离,将其组成适当的单词、句子、行和段落等文本内容。为了实现这一点,该库:
使用高级函数extract_pages()从PDF文件中分离出各个页面,并将其转换为LTPage对象。
然后针对每个LTPage对象,从上到下遍历每个元素,并尝试将其识别为适当的组件,可能是:
1. LTFigure,表示页面中呈现为图形或作为另一个PDF文档嵌入的图像的区域。
2. LTTextContainer,表示矩形区域中的一组文本行,进一步分析后转为LTTextLine对象的列表。每个LTTextLine对象代表了一组LTChar对象,其中存储了单个字符的文本以及其元数据。
3. LTRect,表示可以用于包围图像、图形或在LTPage对象中创建表格的二维矩形。
因此,基于对页面的重构以及将页面元素分类为LTFigure(包含页面中的图像或图形)、LTTextContainer(表示页面的文本信息)或LTRect(表明存在表格的强有力迹象),我们可以应用适当的函数来更好地提取信息。
for pagenum, page in enumerate(extract_pages(pdf_path)):
# Iterate the elements that composed a page
for element in page:
# Check if the element is a text element
if isinstance(element, LTTextContainer):
# Function to extract text from the text block
pass
# Function to extract text format
pass
# Check the elements for images
if isinstance(element, LTFigure):
# Function to convert PDF to Image
pass
# Function to extract text with OCR
pass
# Check the elements for tables
if isinstance(element, LTRect):
# Function to extract table
pass
# Function to convert table content into a string
pass
现在我们了解了分析过程的部分,让我们创建从每个组件中提取文本所需的函数。
定义从PDF中提取文本的函数
从此处开始,从文本容器中提取文本非常简单。
# Create a function to extract text
def text_extraction(element):
# Extracting the text from the in-line text element
line_text = element.get_text()
# Find the formats of the text
# Initialize the list with all the formats that appeared in the line of text
line_formats = []
for text_line in element:
if isinstance(text_line, LTTextContainer):
# Iterating through each character in the line of text
for character in text_line:
if isinstance(character, LTChar):
# Append the font name of the character
line_formats.append(character.fontname)
# Append the font size of the character
line_formats.append(character.size)
# Find the unique font sizes and names in the line
format_per_line = list(set(line_formats))
# Return a tuple with the text in each line along with its format
return (line_text, format_per_line)
因此,要从文本容器中提取文本,我们只需使用LTTextContainer元素的get_text()方法。该方法检索出特定语料框中构成单词的所有字符,并将输出存储在文本数据列表中。此列表中的每个元素代表容器中包含的原始文本信息。
现在,为了识别文本的格式,我们通过迭代LTTextContainer对象来逐个访问该语料库的每一行文本。在每次迭代中,都会创建一个新的LTTextLine对象,表示该语料库块中的一行文本。然后,我们检查嵌套的行元素是否包含文本。如果有文本,我们访问每个单独的字符元素作为LTChar,其中包含该字符的所有元数据。从这些元数据中,我们提取两种格式,并将它们存储在一个单独的列表中,与所检查的文本对应位置上:
1. 字符的字体系列,包括字符是否为粗体或斜体格式
2. 字符的字体大小
通常,特定文本块中的字符往往具有一致的格式,除非其中有一些以粗体突出显示。为了方便进一步的分析,我们捕获文本中所有字符的唯一格式值,并将它们存储在相应的列表中。
定义从图像中提取文本的函数
如何处理 PDF 中的图像文本?
首先,我们需要明确这一点,存储在 PDF 中的图像元素与文件本身并没有不同的格式,比如 JPEG 或 PNG。因此,为了在它们上应用 OCR 软件,我们需要先将它们从文件中分离出来,然后将其转换为图像格式。
# Create a function to crop the image elements from PDFs
def crop_image(element, pageObj):
# Get the coordinates to crop the image from the PDF
[image_left, image_top, image_right, image_bottom] = [element.x0,element.y0,element.x1,element.y1]
# Crop the page using coordinates (left, bottom, right, top)
pageObj.mediabox.lower_left = (image_left, image_bottom)
pageObj.mediabox.upper_right = (image_right, image_top)
# Save the cropped page to a new PDF
cropped_pdf_writer = PyPDF2.PdfWriter()
cropped_pdf_writer.add_page(pageObj)
# Save the cropped PDF to a new file
with open('cropped_image.pdf', 'wb') as cropped_pdf_file:
cropped_pdf_writer.write(cropped_pdf_file)
# Create a function to convert the PDF to images
def convert_to_images(input_file,):
images = convert_from_path(input_file)
image = images[0]
output_file = "PDF_image.png"
image.save(output_file, "PNG")
# Create a function to read text from images
def image_to_text(image_path):
# Read the image
img = Image.open(image_path)
# Extract the text from the image
text = pytesseract.image_to_string(img)
return text
为了实现这一点,我们遵循以下流程:
1. 我们从PDFMiner检测到的LTFigure对象中使用元数据来裁剪图像框,并利用页面布局中的坐标。然后我们使用PyPDF2库将其保存为新的PDF文件到我们的目录中。
2. 然后,我们使用pdf2image库的convert_from_file()函数将目录中的所有PDF文件转换为图像列表,并以PNG格式保存它们。
3. 最后,现在我们有了图像文件,我们在脚本中使用PIL模块的Image包读取它们,并使用pytesseract的image_to_string()函数来使用tesseract OCR引擎从图像中提取文本。
结果,这个流程返回图像中的文本,我们将其保存在输出字典的第三个列表中。该列表包含从检查页面上提取的图像中提取的文本信息。
定义从表格中提取文本的函数
在这个部分,我们将从PDF页面上提取一个更具有逻辑结构的表格文本。这是一个比从语料库中提取文本更复杂的任务,因为我们需要考虑信息的粒度以及表格中呈现的数据点之间的关系。
虽然有几个用于从PDF中提取表格数据的库,Tabula-py是其中最知名的之一,但我们发现其功能存在一定的局限性。
在我们看来,最明显的局限性来自于该库在识别表格的不同行时使用了换行特殊字符\n的方式。这在大多数情况下效果还不错,但当一个单元格的文本被换行成2行或更多行时,它无法正确捕捉到,导致额外添加了不必要的空行并丢失了提取单元格的上下文。
你可以看到下面的示例,当我们尝试使用tabula-py从表格中提取数据时:
然后,提取的信息会以Pandas DataFrame的形式输出,而不是字符串。在大多数情况下,这可能是一种理想的格式,但对于考虑文本的变换器来说,这些结果需要在输入模型之前进行转换。
为了解决这个任务,我们使用了pdfplumber库,原因有很多。首先,它是基于我们用于初步分析的pdfminer.six构建的,意味着它包含了类似的对象。此外,它的表格检测方法是基于线元素及其交点来构建包含文本的单元格以及表格本身。这样,当我们识别到一个单元格所属的表格后,我们可以仅提取单元格内的内容,而不需要考虑需要渲染多少行。然后,当我们有了一个表格的内容后,我们会将其格式化为类似表格的字符串,并存储在适当的列表中。
# Extracting tables from the page
def extract_table(pdf_path, page_num, table_num):
# Open the pdf file
pdf = pdfplumber.open(pdf_path)
# Find the examined page
table_page = pdf.pages[page_num]
# Extract the appropriate table
table = table_page.extract_tables()[table_num]
return table
# Convert table into the appropriate format
def table_converter(table):
table_string = ''
# Iterate through each row of the table
for row_num in range(len(table)):
row = table[row_num]
# Remove the line breaker from the wrapped texts
cleaned_row = [item.replace('\n', ' ') if item is not None and '\n' in item else 'None' if item is None else item for item in row]
# Convert the table into a string
table_string+=('|'+'|'.join(cleaned_row)+'|'+'\n')
# Removing the last line break
table_string = table_string[:-1]
return table_string
为了实现这个目标,我们创建了两个函数,extract_table()函数用于提取表格的内容为一个嵌套列表,table_converter()函数用于将这些列表的内容连接成类似表格的字符串。
在extract_table()函数中:
1. 我们打开PDF文件。
2. 我们导航到PDF文件的指定页面。
3. 从pdfplumber找到的表格列表中,我们选择所需的表格。
4. 我们提取表格的内容,并将其输出为一个嵌套列表,表示表格的每一行。
在table_converter()函数中:
1. 我们迭代每个嵌套列表,并清除其中任何来自换行的不需要文本。
2. 我们通过使用|符号将每个元素连接起来,以创建表格单元的结构。
3. 最后,我们在结尾处添加一个换行符以转到下一行。
这将导致一个文本字符串,以不丢失表格中呈现的数据的细粒度方式呈现表格的内容。
综合起来
现在我们已经准备好了代码的所有组件,让我们将它们全部加在一起,形成一个完全功能的代码。
# Find the PDF path
pdf_path = 'OFFER 3.pdf'
# create a PDF file object
pdfFileObj = open(pdf_path, 'rb')
# create a PDF reader object
pdfReaded = PyPDF2.PdfReader(pdfFileObj)
# Create the dictionary to extract text from each image
text_per_page = {}
# We extract the pages from the PDF
for pagenum, page in enumerate(extract_pages(pdf_path)):
# Initialize the variables needed for the text extraction from the page
pageObj = pdfReaded.pages[pagenum]
page_text = []
line_format = []
text_from_images = []
text_from_tables = []
page_content = []
# Initialize the number of the examined tables
table_num = 0
first_element= True
table_extraction_flag= False
# Open the pdf file
pdf = pdfplumber.open(pdf_path)
# Find the examined page
page_tables = pdf.pages[pagenum]
# Find the number of tables on the page
tables = page_tables.find_tables()
# Find all the elements
page_elements = [(element.y1, element) for element in page._objs]
# Sort all the elements as they appear in the page
page_elements.sort(key=lambda a: a[0], reverse=True)
# Find the elements that composed a page
for i,component in enumerate(page_elements):
# Extract the position of the top side of the element in the PDF
pos= component[0]
# Extract the element of the page layout
element = component[1]
# Check if the element is a text element
if isinstance(element, LTTextContainer):
# Check if the text appeared in a table
if table_extraction_flag == False:
# Use the function to extract the text and format for each text element
(line_text, format_per_line) = text_extraction(element)
# Append the text of each line to the page text
page_text.append(line_text)
# Append the format for each line containing text
line_format.append(format_per_line)
page_content.append(line_text)
else:
# Omit the text that appeared in a table
pass
# Check the elements for images
if isinstance(element, LTFigure):
# Crop the image from the PDF
crop_image(element, pageObj)
# Convert the cropped pdf to an image
convert_to_images('cropped_image.pdf')
# Extract the text from the image
image_text = image_to_text('PDF_image.png')
text_from_images.append(image_text)
page_content.append(image_text)
# Add a placeholder in the text and format lists
page_text.append('image')
line_format.append('image')
# Check the elements for tables
if isinstance(element, LTRect):
# If the first rectangular element
if first_element == True and (table_num+1) <= len(tables):
# Find the bounding box of the table
lower_side = page.bbox[3] - tables[table_num].bbox[3]
upper_side = element.y1
# Extract the information from the table
table = extract_table(pdf_path, pagenum, table_num)
# Convert the table information in structured string format
table_string = table_converter(table)
# Append the table string into a list
text_from_tables.append(table_string)
page_content.append(table_string)
# Set the flag as True to avoid the content again
table_extraction_flag = True
# Make it another element
first_element = False
# Add a placeholder in the text and format lists
page_text.append('table')
line_format.append('table')
# Check if we already extracted the tables from the page
if element.y0 >= lower_side and element.y1 <= upper_side:
pass
elif not isinstance(page_elements[i+1][1], LTRect):
table_extraction_flag = False
first_element = True
table_num+=1
# Create the key of the dictionary
dctkey = 'Page_'+str(pagenum)
# Add the list of list as the value of the page key
text_per_page[dctkey]= [page_text, line_format, text_from_images,text_from_tables, page_content]
# Closing the pdf file object
pdfFileObj.close()
# Deleting the additional files created
os.remove('cropped_image.pdf')
os.remove('PDF_image.png')
# Display the content of the page
result = ''.join(text_per_page['Page_0'][4])
print(result)
以上脚本将:
导入所需的库。
使用pyPDF2库打开PDF文件。
提取PDF的每个页面,并迭代以下步骤。
检查页面上是否有任何表格,并使用pdfplumner创建它们的列表。
找到页面上嵌套的所有元素,并按照它们在布局中出现的顺序进行排序。
然后针对每个元素:
检查它是否是文本容器,并且不出现在表格元素中。然后使用text_extraction()函数提取文本及其格式,否则跳过此文本。
检查它是否为图像,并使用crop_image()函数从PDF中裁剪图像组件,使用convert_to_images()将其转换为图像文件,并使用image_to_text()函数提取其中的文本。
检查它是否为矩形元素。在这种情况下,我们检查第一个矩形是否属于页面的表格,如果是,则继续以下步骤:
1. 找到表的边界框,以防止再次使用text_extraction()函数提取其文本。
2. 提取表的内容并将其转换为字符串。
3. 然后添加一个布尔参数以阐明我们是从表中提取文本。
4. 此过程在最后一个落入表边界框的LTRect之后,并且布局中的下一个元素不是矩形对象时结束。 (组成表的其他对象将被跳过)
处理的输出将在每次迭代中存储在5个列表中,命名为:
1. page_text:包含来自PDF中文本容器的文本(当文本是从其他元素提取的时将放置占位符)
2. line_format:包含上述提取的文本的格式(当文本是从其他元素提取的时将放置占位符)
3. text_from_images:包含从页面上提取的图像中提取的文本
4. text_from_tables:包含具有表内容的类似表的字符串
5. page_content:包含在页面上呈现的所有文本的元素列表
所有列表将存储在一个字典中,该字典将以每次检查的页面编号作为键。
然后,我们将关闭PDF文件。
然后,我们将删除在过程中创建的所有额外文件。
最后,我们可以通过将page_content列表的元素连接起来来显示页面的内容。
结论
这是我认为使用许多库的最佳特性并使此过程对各种类型的PDF和我们可能遇到的元素具有韧性的一种方法。然而,PDFMiner会处理大部分重活。此外,关于文本格式的信息可以帮助我们识别潜在的标题,将文本分成不同的逻辑部分,而不仅仅是每页的内容,并帮助我们识别重要性更大的文本。