一、灵感
在看中英文文献的时候,有时候想要这篇文献的参考文献的情况,作为人类,很容易知道一遍文章的参考文献位于什么位置,每一条参考文献的具体内容。然而对于计算机来说,提取高质量的PDF内容并非易事。
其一,仅通过文字识别OCR的方式很难准确高质量的提取参考文献的,参考博客:【python实战】获取英文文献pdf中参考文献信息 - 简书
其二,不同的文献的布局方式不同,有的单栏,有的双栏,通过OCR方式提取的参考文献可能在这篇文献有效,而在另外一篇文献中可能就无法有效提取。
二、解决办法
既然通过传统方法行不通,那就采用人工智能的方法进行pdf的识别。
文本采用端到端的PDF解析工具MinerU:https://github.com/opendatalab/MinerU/blob/master/README_zh-CN.md
- 布局检测:使用LayoutLMv3模型进行区域检测,如
图像
,表格
,标题
,文本
等; - 公式检测:使用YOLOv8进行公式检测,包含
行内公式
和行间公式
; - 公式识别:使用UniMERNet进行公式识别;
- 表格识别:使用StructEqTable进行表格识别;
- 光学字符识别:使用PaddleOCR进行文本识别;
根据说明配置好Magic-PDF,有N卡的建议根据说明开启CUDA加速,开和不开提取时间差别蛮大的 ,命令行输入:
magic-pdf pdf-command --pdf "1.pdf" --inside_model true
1.pdf为我的pdf文件名,根据自己的进行修改,开启GPU加速后1分钟左右即可出结果,此时会自动生成一个文件夹:
打开layout.pdf ,可以看到文献的布局被正确识别,而且能够自动删去页眉页脚。
无论中英文文献、单栏还是双栏都能够被正确提取。
Magic-PDF还有包括公式提取等功能,有兴趣的可以自行探索,本文用于提取参考文献内容,打开提取结果中的.md文件:
可以看到参考文献部分的布局、内容都是正确的,这是通过OCR方式很难做到的,接下来就是写一个函数从md中获取参考文献部分的内容了,主要思路如下:
-
读取文件:首先,函数读取指定文件路径的内容,并将其按行存储为一个列表
markdown_text
。 -
定义关键词:函数定义了三个关键词列表:
start_keywords
: 包含可能标识参考文献部分开始的关键词,如 "References"、"参考文献" 等。end_keywords
: 包含可能标识参考文献部分结束的关键词,如 "Notes"、"附录" 等。content_keywords
: 包含可能标识目录部分的关键词,如 "目录"、"CONTENTS" 等。
-
查找参考文献的起始位置:
- 首先检查文件中是否存在与
content_keywords
相关的目录关键词,如果存在,则从目录部分后的第50行开始查找参考文献的开始位置。 - 如果找到了
content_keywords
中的目录关键词,则继续从该位置开始查找start_keywords
中的关键词,以确定参考文献部分的开始。 - 如果没有找到目录关键词或结论关键词,则直接在整个文本中查找
start_keywords
以确定参考文献部分的开始。
- 首先检查文件中是否存在与
-
查找参考文献的结束位置:
- 从确定的开始位置继续查找,直到找到
end_keywords
中的某个关键词,来确定参考文献部分的结束位置。 - 如果没有找到结束关键词,则参考文献部分的结束位置被设置为文本的末尾。
- 从确定的开始位置继续查找,直到找到
-
提取并清理参考文献内容:
- 提取从开始位置到结束位置之间的文本内容。
- 对每一行内容进行清理,删除多余的空格,保留英文或数字字符之间的空格。
-
过滤参考文献内容:
- 仅保留以
[
或数字开头的行,这些行通常是参考文献的具体条目。 - 最后将过滤后的内容返回。
- 仅保留以
完整代码如下:
def extract_references_content(self, file_path):
"""
提取参考文献内容。
"""
with open(file_path, 'r', encoding='utf-8') as file:
markdown_text = file.readlines()
start_keywords = ['References', 'REFERENCES', 'referenCes', '参考文献', 'R EFERENCES']
end_keywords = ['Notes', '附录', '致谢', '注释', 'APPENDIX']
content_keywords = ['目录', 'CONTENTS', 'C ONTENTS']
start_pos = None
end_pos = None
# 检查目录或CONTENTS等关键词
for idx, line in enumerate(markdown_text):
if any(keyword in line for keyword in content_keywords):
start_pos = min(idx + 50, len(markdown_text))
break
# 如果检查到了目录关键词,从目录后的第50行开始查找结论关键词
if start_pos is not None:
for idx in range(start_pos, len(markdown_text)):
if any(keyword in markdown_text[idx] for keyword in start_keywords):
start_pos = idx + 1
break
# 如果没有找到目录关键词或结论关键词,则查找参考文献关键词
if start_pos is None:
for idx, line in enumerate(markdown_text):
if any(keyword in line for keyword in start_keywords):
start_pos = idx + 1
break
# 查找结束关键词
if start_pos is not None:
for idx in range(start_pos, len(markdown_text)):
if any(keyword in markdown_text[idx] for keyword in end_keywords):
end_pos = idx
break
if end_pos is None:
end_pos = len(markdown_text)
references_content = ''.join(markdown_text[start_pos:end_pos]).strip()
else:
references_content = ""
def clean_line(line):
line = line.lstrip()
new_line = []
for i in range(len(line)):
if line[i] == ' ':
if (i > 0 and i < len(line) - 1 and
re.match(r'[a-zA-Z0-9]', line[i - 1]) and
re.match(r'[a-zA-Z0-9]', line[i + 1])):
new_line.append(' ')
else:
continue
else:
new_line.append(line[i])
return ''.join(new_line)
references_content = '\n'.join(clean_line(line) for line in references_content.split('\n'))
filtered_lines = []
for line in references_content.split('\n'):
if line.startswith('[') or (line and line[0].isdigit()):
filtered_lines.append(line)
references_content = '\n'.join(filtered_lines)
return references_content