便携式文档格式(PDF)作为分发内容的主导文件格式,尤其在学术、科学、企业和法律界。其强大之处在于保持文档的原始布局和结构。然而,当涉及到数据的检索、分析或操控时,PDF文件带来了一定的挑战。这正是文本提取和结构化的重要性变得明显的地方。在本文中,我们将深入探索构建一个专门设计用于从PDF文件中提取文本的流程,并使用自然语言处理(NLP)技术进行后续处理。关键的是,这个流程纳入了命名实体识别(NER),这是一个专注于识别和分类文本中关键信息元素的NLP子领域,比如人名、组织名称、地点、时间表达、数量、货币价值等。NER在将非结构化文本转换为结构化数据中发挥了关键作用,使得信息提取和分析更加有效。我们将探索这个流程在提取和识别这些命名实体的真实复杂情景中的表现,从而证明它在实际应用中的实用性和有效性。
需求
在开始这个项目之前,你应该确保你的机器上安装了Python 3.8或更高版本。在这项工作中,我们将需要两个Python库:
在安装所需库之前,请确保你的pip、setuptools和wheel是最新的。为此,运行以下命令:
pip install -U pip setuptools wheel
要安装所述库,请运行以下命令:
pip install pdfminer.six spacy
此外,你需要下载一个Spacy模型:
python -m spacy download en_core_web_sm
文本和实体提取
首先,我们需要导入项目所需的所有必要库。
from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer
from tqdm import tqdm
import re
我们有一个顶级函数process_document,它需要一个PDF文档的路径,一个具体的页码,我们将要处理它来提取文本。
def process_document(pdf_path, page_ids=None):
extracted_pages = extract_pages(pdf_path, page_numbers=page_ids)
page2content = {}
# Process each extracted page
for extracted_page in tqdm(extracted_pages):
page_id = extracted_page.pageid
content = process_page(extracted_page)
page2content[page_id] = content
return page2content
如果你需要解析所有页面,可以将 page_ids 设置为 None。该函数返回一个字典,字典中的键表示页面编号,值是一个字符串值,包含了从页面中以正确的顺序提取的文本信息。
process_page 函数解析整个 PDF 页面,提取文本内容;它通过迭代一个按位置排序的页面高级元素的列表,并将其转换成字符串格式。
def process_page(extracted_page):
content = []
# Get a sorted list of elements based on
# their Y-coordinate in reverse order
elements = [element for element in extracted_page._objs]
elements.sort(key=lambda a: a.y1, reverse=True)
for i, element in enumerate(elements):
# Extract text if the element is a text container
# and text extraction is enabled
if isinstance(element, LTTextContainer):
line_text = extract_text_and_normalize(element)
content.append(line_text)
# Combine and clean up the extracted content
content = re.sub('\n+', '\n', ''.join(content))
return content
为了让从PDFs中提取的文本对机器和人类更易读,我们应该恰当地处理缩进。实际上,我们需要确保没有换行符切割未完成的句子。这是非常关键的,否则我们可能会给语言模型添加一些偏差,那么最终结果将会更差。
def extract_text_and_normalize(element):
# Extract text from line and split it with new lines
line_texts = element.get_text().split('\n')
norm_text = ''
for line_text in line_texts:
line_text=line_text.strip()
# empty strings after striping convert to newline character
if not line_text:
line_text = '\n'
else:
line_text = re.sub('\s+', ' ', line_text)
# if the last character is not a letter or number,
# add newline character to a line
if not re.search('[\w\d\,\-]', line_text[-1]):
line_text+='\n'
else:
line_text+=' '
# concatenate into single string
norm_text+=line_text
return norm_text
实体提取
命名实体识别(NER)已成为许多自然语言处理(NLP)应用的基础任务。它涉及到复杂的过程,即在文本中检测特定实体,并将它们归类到预定义的类别中。当你需要分析大量文本并挑选出你最感兴趣的特定信息时,NER可以很有用。它是信息提取管道的第一阶段之一,旨在从原始文本构建知识图谱。此外,NER还可以用于内容的筛选和分类。与将整个文档或文本块分类到广泛主题的标准文本分类不同,NER更深入地提取和分类个人、组织、地点以及其他预定义类别的单个实体。所以,让我们使用Spacy模型,在真实数据上实施NER管道。
在我们的案例中,我们将处理制药公司的财务报告,我们的任务是提取有关该公司顶级产品的财务信息。要下载报告,请运行以下命令:
!curl "https://s28.q4cdn.com/781576035/files/doc_financials/2022/ar/PFE-2022-Form-10K-FINAL-(without-Exhibits).pdf" > pfizer-report.pdf"https://s28.q4cdn.com/781576035/files/doc_financials/2022/ar/PFE-2022-Form-10K-FINAL-(without-Exhibits).pdf" > pfizer-report.pdf
为了简化流程,我们将放置此类信息所在的页面:
pdf_path = 'pfizer-report.pdf'
page2content = process_document(pdf_path, page_ids=[9])
首先,我们需要初始化NLP对象并排除所有不必要的管道,这将显著加快管道的工作速度:
import spacy
nlp = spacy.load("en_core_web_sm", disable=["tok2vec", "tagger", "parser", "attribute_ruler", "lemmatizer"])
让我们使用displacy来可视化Spacy模型的工作:
from spacy import displacy
text = page2content[1]
doc = nlp(text)
displacy.serve(doc, style="ent")
这是Spacy管道结果:
对于这样一个小型模型来说,这是一个相当不错的结果,不过,模型错误地将药品名称匹配为了人名。遗憾的是,我们当前使用的模型不支持识别药物;然而,这对于所有训练在固定实体类集合上的监督模型而言都是一个问题。在现实生活案例中,我们处理着大量不同的实体,能够在运行时指定它们是至关重要的。
让我们创建一个使用零样本 NER API 的自定义 Spacy 管道。
import requests
from spacy.language import Language
from spacy.tokens import Span
@Language.factory("knowledgator_ner")
class KnowledgatorNERComponent:
def __init__(self, nlp, name, API_KEY, labels_to_add, labels_to_exclude):
self.url = "https://zero-shot-ner.p.rapidapi.com/token_searcher/ner"
self.headers = {
"content-type": "application/json",
"X-RapidAPI-Key":API_KEY,
"X-RapidAPI-Host": "zero-shot-ner.p.rapidapi.com"
}
self.labels_to_add = labels_to_add
self.labels_to_exclude = set(labels_to_exclude)
def get_positions(self, doc):
start2token_id = {}
end2token_id = {}
for tok in doc:
start = tok.idx
start2token_id[start] = tok.i
end = len(tok.text) + tok.idx
end2token_id[end] = tok.i+1
return start2token_id, end2token_id
def filter_predefined_ents(self, custom_ents, predefined_ents):
filtered_ents = []
custom_ents_pos = [(ent.start, ent.end) for ent in custom_ents]
for ent in predefined_ents:
if ent.label_ in self.labels_to_exclude:
continue
overlap = False
for cust_ent_start, cust_ent_end in custom_ents_pos:
if (ent.start < cust_ent_end and ent.end > cust_ent_start):
overlap = True
break
if not overlap:
filtered_ents.append(ent)
return filtered_ents
def __call__(self, doc):
try:
payload = {"text": doc.text, "labels": self.labels_to_add}
entities = requests.post(self.url, json=payload, headers=self.headers).json()['entities']
except:
return doc
start2token_id, end2token_id = self.get_positions(doc)
spans = [] # keep the spans for later so we can merge them afterwards
for ent in entities:
# Generate Span representing the entity & set label
ent_start = ent['start']
ent_end = ent['end']
if ent_start in start2token_id:
start = start2token_id[ent_start]
else:
continue
if ent_end in end2token_id:
end = end2token_id[ent_end]
else:
continue
entity = Span(doc, start, end, label=ent['entity'].upper())
spans.append(entity)
# Replacing overlaping entities
filtered_ents = self.filter_predefined_ents(spans, doc.ents)
doc.ents = spans+filtered_ents
return doc # don't forget to return the Doc!
labels_to_add = ['drug', 'pharma company', 'disease']
labels_to_exclude = ['WORK_OF_ART', 'NORP', 'PERSON', 'ORG']
API_KEY = "your_api_key"
nlp.add_pipe("knowledgator_ner", config={"API_KEY": API_KEY, "labels_to_add": labels_to_add, "labels_to_exclude": labels_to_exclude})
你需要输入你的 API 密钥,你可以通过在 RapidAPI 上注册并订阅我们的 API 来获得。此外,你需要设置一个自定义标签列表,你需要添加的标签越多,系统处理所需的时间就越长。还可以指定你想排除的原始标签。如果原始标签可能干扰我们的零射击命名实体识别(NER)的结果,或者如果 Spacy 模型表现不佳,这会很有帮助。
让我们可视化自定义 NER 管道的结果。