关于pdf论文(期刊)总结模型与开发

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

本文的目的是做一个深度学习模型,可以对PDF论文、期刊(现只限英文)进行总结。在科研和生活中,假如需要长期关注某个课题组的论文;或者有大量的文献,一篇篇的去看又太耗费时间,那么可以使用这个模型,让模型对大量的论文进行初步的总结,再从中有侧重的挑取自己感兴趣或者研究方向相符的论文进行精读。

本文的相关代码和思路来自于github上的chatpaper,只不过chatpaper是将pdf的读取内容通过ChatGPT的api传递给ChatGPT,让gpt来进行总结,假如我们没有gpt账户,或者账户下没有余额,或者觉得成本过高,可以使用huggingface上开源的模型,即博主使用的这个方法。


一、PDF如何读取

这是一个比较复杂的问题,如果想要准确的对一篇文章进行一个总结,那么对文章的“摘要”部分的提取就要相对精准一些。但是文章PDF的内容和排版是不同的,不同期刊使用的格式也是千差万别。仅使用一种方法很难对“摘要”部分进行准确的提取。比如:

文章出自nature communition。这类文章没有明确的标注“Abstract”和“Introduction”,但是我们人自己去读,一眼就能知道第一段是文章的摘要部分。但是通过相关的api去读取,怎么用一个通用的方法去读就是一个问题。
再比如:
在这里插入图片描述
文章出自IEEE。这类文章有相关描述性的文字,告知读者这部分的内容是什么,那么提取的难度就会小一些。

二、具体的PDF读取方法

1.通过fitz库读取PDF标题

本方法是通过chatpaper的源码学习到的思路。即是对PDF进行分块处理。通过读取相关的关键字来将论文所需要内容提取出来。

首先是

def chat_paper_main(args):
    # 创建一个Reader对象,并调用show_info方法
    if args.sort == 'Relevance':
        sort = arxiv.SortCriterion.Relevance
    elif args.sort == 'LastUpdatedDate':
        sort = arxiv.SortCriterion.LastUpdatedDate
    else:
        sort = arxiv.SortCriterion.Relevance

这里因为我们不需要使用ChatGPT的API因此我将reader对象部分就删去了。


    if args.pdf_path:
        # 开始判断是路径还是文件:
        paper_list = []
        if args.pdf_path.endswith(".pdf"):
            paper_list.append(Paper(path=args.pdf_path))
        else:
            for root, dirs, files in os.walk(args.pdf_path):
                print("root:", root, "dirs:", dirs, 'files:', files)  # 当前目录路径
                for filename in files:
                    # 如果找到PDF文件,则将其复制到目标文件夹中
                    if filename.endswith(".pdf"):
                        paper_list.append(Paper(path=os.path.join(root, filename)))
        print("------------------paper_num: {}------------------".format(len(paper_list)))

首先是根据传入的参数是否是一个pdf文件,因为这个项目是允许传入单独的pdf,或者是总结一个文件夹下所有的pdf。因此这段代码得逻辑就非常好理解了,判断args参数的结尾字符是不是pdf,如果不是,那说明用户给的是一个文件夹路径,则读取文件夹下所有结尾是pdf的文件,并使用Paper类来处理这些文件,显然这个Paper类就是处理PDF数据的关键了。

class Paper:
    def __init__(self, path, title='', url='', abs='', authers=[]):
        # 初始化函数,根据pdf路径初始化Paper对象
        self.url = url  # 文章链接
        self.path = path  # pdf路径
        self.section_names = []  # 段落标题
        self.section_texts = {}  # 段落内容
        self.abs = abs
        self.title_page = 0
        if title == '':
            self.pdf = fitz.open(self.path)  # pdf文档
            self.title = self.get_title()
            self.parse_pdf()
        else:
            self.title = title
        self.authers = authers
        self.roman_num = ["I", "II", 'III', "IV", "V", "VI", "VII", "VIII", "IIX", "IX", "X"]
        self.digit_num = [str(d + 1) for d in range(10)]
        self.first_image = ''

打开Paper类,查看这段源码,首先title和url以及abs和authers都是空的,这些都是需要我们通过读取pdf然后去给它们一一赋值。

self.pdf = fitz.open(self.path)

这行代码即是使用fitz库区读取pdf。读出来的内容如下所示:
在这里插入图片描述
这个类在后面的方法中会用到。

self.title = self.get_title()

这行代码则是利用self里的get_title方法来读取title,以下是get_title方法的代码:


    def get_title(self):
        doc = self.pdf  # 打开pdf文件
        max_font_size = 0  # 初始化最大字体大小为0
        max_string = ""  # 初始化最大字体大小对应的字符串为空
        max_font_sizes = [0]
        for page_index, page in enumerate(doc):  # 遍历每一页
            text = page.get_text("dict")  # 获取页面上的文本信息
            blocks = text["blocks"]  # 获取文本块列表
            for block in blocks:  # 遍历每个文本块
                if block["type"] == 0 and len(block['lines']):  # 如果是文字类型
                    if len(block["lines"][0]["spans"]):
                        font_size = block["lines"][0]["spans"][0]["size"]  # 获取第一行第一段文字的字体大小
                        max_font_sizes.append(font_size)
                        if font_size > max_font_size:  # 如果字体大小大于当前最大值
                            max_font_size = font_size  # 更新最大值
                            max_string = block["lines"][0]["spans"][0]["text"]  # 更新最大值对应的字符串
        max_font_sizes.sort()
        print("max_font_sizes", max_font_sizes[-10:])
        cur_title = ''
        for page_index, page in enumerate(doc):  # 遍历每一页
            text = page.get_text("dict")  # 获取页面上的文本信息
            blocks = text["blocks"]  # 获取文本块列表
            for block in blocks:  # 遍历每个文本块
                if block["type"] == 0 and len(block['lines']):  # 如果是文字类型
                    if len(block["lines"][0]["spans"]):
                        cur_string = block["lines"][0]["spans"][0]["text"]  # 更新最大值对应的字符串
                        font_flags = block["lines"][0]["spans"][0]["flags"]  # 获取第一行第一段文字的字体特征
                        font_size = block["lines"][0]["spans"][0]["size"]  # 获取第一行第一段文字的字体大小
                        # print(font_size)
                        if abs(font_size - max_font_sizes[-1]) < 0.3 or abs(font_size - max_font_sizes[-2]) < 0.3:
                            # print("The string is bold.", max_string, "font_size:", font_size, "font_flags:", font_flags)
                            if len(cur_string) > 4 and "arXiv" not in cur_string:
                                # print("The string is bold.", max_string, "font_size:", font_size, "font_flags:", font_flags)
                                if cur_title == '':
                                    cur_title += cur_string
                                else:
                                    cur_title += ' ' + cur_string
                            self.title_page = page_index
                            # break
        title = cur_title.replace('\n', ' ')
        return title

这里的注释相对比较详细,这里只简单的说一下这段代码得逻辑,是通过什么样的一个方法来准确提取到标题信息的。

简单的说就是通过现成的库读取每页的内容,然后对其进行分块,block的内容如下:
在这里插入图片描述
每一个子模块的内容如下:

在这里插入图片描述
从中读出的内容可以看出,每个子模块,把number数,文档块的坐标位置(bbox),字体的font,块内的字体信息等等诸多信息全部以一个字典的形式表示出来了。
因此就有了后面,根据字体大小的逻辑来判断是否是标题。
当然还做了一些细微的操作,比如比较字体大小,然后如果某个文本块的字体大小非常接近最大字体大小(在这个代码中,差异小于0.3),则假定这个文本块是标题的一部分。最后将整个文本块的内容拼接起来。即找到了所需要文章标题。这就是大概得思路了。

2. 读取文章内部的相关内容

    def parse_pdf(self):
        self.pdf = fitz.open(self.path)  # pdf文档
        self.text_list = [page.get_text() for page in self.pdf]
        self.all_text = ' '.join(self.text_list)
        self.section_page_dict = self._get_all_page_index()  # 段落与页码的对应字典
        print("section_page_dict", self.section_page_dict)
        self.section_text_dict = self._get_all_page()  # 段落与内容的对应字典
        self.section_text_dict.update({"title": self.title})
        self.section_text_dict.update({"paper_info": self.get_paper_info()})
        self.pdf.close()
    def _get_all_page_index(self):
        # 定义需要寻找的章节名称列表
        section_list = ["Abstract",
                        'Introduction', 'Related Work', 'Background',
                        "Preliminary", "Problem Formulation",
                        'Methods', 'Methodology', "Method", 'Approach', 'Approaches',
                        # exp
                        "Materials and Methods", "Experiment Settings",
                        'Experiment', "Experimental Results", "Evaluation", "Experiments",
                        "Results", 'Findings', 'Data Analysis',
                        "Discussion", "Results and Discussion", "Conclusion",
                        'References']
        # 初始化一个字典来存储找到的章节和它们在文档中出现的页码
        section_page_dict = {}
        # 遍历每一页文档
        for page_index, page in enumerate(self.pdf):
            # 获取当前页面的文本内容
            cur_text = page.get_text()
            # 遍历需要寻找的章节名称列表
            for section_name in section_list:
                # 将章节名称转换成大写形式
                section_name_upper = section_name.upper()
                # 如果当前页面包含"Abstract"这个关键词
                if "Abstract" == section_name and section_name in cur_text:
                    # 将"Abstract"和它所在的页码加入字典中
                    section_page_dict[section_name] = page_index
                # 如果当前页面包含章节名称,则将章节名称和它所在的页码加入字典中
                else:
                    if section_name + '\n' in cur_text:
                        section_page_dict[section_name] = page_index
                    elif section_name_upper + '\n' in cur_text:
                        section_page_dict[section_name] = page_index
        # 返回所有找到的章节名称及它们在文档中出现的页码
        return section_page_dict

首先是这段代码,作用是将对应的出现abstract等关键字的页码记录下来,记录成一个字典。具体还有很多很细的操作,这个函数的核心逻辑是根据每个章节的起始页码和结束页码,提取和拼接文本。其具体操作是:遍历 self.section_page_dict(假定为章节名与起始页码的映射字典),使用 sec_index 和 sec_name 分别表示当前章节的索引和名称,如果 sec_index 小于等于0且存在摘要 (self.abs),则跳过当前迭代。
从 self.section_page_dict 中获取当前章节的起始页码,确定章节的结束页码。如果当前章节不是最后一个章节,则结束页码为下一章节的起始页;否则为文本列表的长度。如果章节只包含一页面,则特殊处理以提取章节标题之间的文本。否则,遍历从起始页到结束页的范围,拼接这些页面的文本。
如果是章节的第一页,仅提取章节标题之后的文本。如果是章节的最后一页,仅提取到下一章节标题之前的文本。(特殊处理方式:替换文本中的 ‘-\n’(可能是由于PDF格式造成的断行)和 ‘\n’(换行符)

总结:简单的说就是找到关键字,看下一个关键字是不是和这个关键字在同一页,如果不是则是上一个关键字的页码-1。
这么操作有一定的误差,比如论文的排版的下一个关键字在第3页,但是第三页上也存在着上一个关键字的部分内容,则这一部分内容不会被提取到。

3. 最终结果展示

根据上述方法最后读取出来的内容应如下所示,可以看出,得到的结果还是相对准确的。但是对于各种各样的PDF论文格式并不能一概而论。
在这里插入图片描述

三、深度学习模型

由于现在GPT的API除了需要账户也需要进行充值,每条消息大概0.013刀,因此这里采用了一个代替GPT的summary模型。由于时间关系和工作量关系,我们直接使用huggingface上的mT5-multilingual-XLSum的开源模型,可以总结各种语言的概要。
在这里插入图片描述
使用方法也非常简单,只需要管道调用即可。

        summarizer = pipeline("summarization", model=eng_cache_dir, tokenizer=eng_cache_dir, config=eng_cache_dir)
        #将paper里的Abstract和Discussion提取出来,拼接在一起。如果没有Discussion,就只选Abstract。如果没有Abstract,就只选Discussion。
        for paper in paper_list:
            if paper.section_text_dict.get('Discussion') and paper.section_text_dict.get('Abstract'):
                paper.abstract_and_discussion = paper.section_text_dict.get('Abstract') + paper.section_text_dict.get('Discussion')
            elif paper.section_text_dict.get('Discussion') and not paper.section_text_dict.get('Abstract'):
                paper.abstract_and_discussion = paper.section_text_dict.get('Discussion')
            elif paper.section_text_dict.get('Abstract') and not paper.section_text_dict.get('Discussion'):
                paper.abstract_and_discussion = paper.section_text_dict.get('Abstract')
            else:
                paper.abstract_and_discussion = 'Abstract not found.'
            # 生成摘要
            summary = summarizer(paper.abstract_and_discussion, max_length=230, min_length=100, do_sample=False)
            # 打印标题
            print(paper.title)
            # 打印摘要
            print(summary[0]['summary_text'])

这里还可以指定总结文本的长度,再次不在赘述。


总结

至此,一个简单的pdf总结的模型就做好了。后续还会添加诸多功能。比如说,自动从网站上下载一周的最新论文,比如说根据喜好进行推送,对输入的关键字进行推送,直接推送论文结果等等。如果有后续开发我会同步更新

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值