
简介一套基于 Python 的 PDF 识别与分析项目重点围绕信息抽取、知识图谱构建与信息检索展开适合用作毕业设计、期末大作业或课程设计的高分参考。系统功能完整界面简洁代码包含注释新手也能较快上手经调试可运行下载后简单部署即可使用。压缩包共 119 个文件约 4.8MB核心为 47 个 Python 脚本及编译文件配合 15 个前端 JS、2 个 Vue 组件与 JSON 配置覆盖后端处理、界面展示和项目配置另含 SQLite 数据库、Excel 数据及说明文档便于本地运行和结果查看。目录结构清晰从数据导入到知识图谱展示、检索入口均有对应实现。目前已有 106 人学习下载适合需要完成 PDF 内容分析并落地知识图谱应用的开发者。项目作为导师认可的高分方案既能用于答辩演示也为二次开发提供了可读性较强的代码基础。1. 基于Python做PDF知识图谱毕设难点在哪毕业设计里做“PDF识别与分析、信息抽取、构建知识图谱、信息检索”最容易踩空的不是单个模型而是链路衔接。PDF解析出来的文本带着页眉和乱序信息抽取好不容易抽出几个实体图谱里却因为缺少唯一约束反复膨胀到了检索环节一个关键词查出来一堆重复节点。这里面的每一个坑都能单独写一篇博客。这篇文章从一份论文PDF开始用Python把它拆成干净的结构化文本再用同一条管线上沉淀出来的三元组写入Neo4j最后用Cypher在知识图谱上做信息检索。适合正在做相关毕业设计、或者想快速把PDF变成可检索知识库的开发者读完可以直接照抄再按自己的schema改。你不需要先掌握图算法把每一步输入输出对齐这条链路就能真正跑通。2. PDF识别与分析文本提取与OCR兜底PDF不是txt页面上的文字是一块块绘图指令直接读文件读不出内容。常见的做法是先判断这份PDF到底有没有文本层有就用PyMuPDF提取没有就按扫描图片处理交给OCR。顺序反了会浪费半天时间。为了少踩坑我在解析前会先跑一遍页数和文本长度统计再决定用哪条分支。2.1 为什么PDF解析这一步决定了抽取质量很多毕设把力气全花在信息抽取模型上忽略了PDF层的噪声。页面页眉、页脚、DOI前缀和分栏顺序都会变成模型输入里的干扰。比如双栏论文PyMuPDF默认按文本块的坐标顺序输出读完左栏再读右栏但阅读顺序应该是左右交替。这种情况下实体在原文中相隔很远模型没法判断“Transformer”和“Attention机制”在同一句话里的关系。PDF识别与分析的核心不是把字符抠出来而是尽可能还原阅读顺序保留下能被下游使用的文本语义。如果PDF有目录和超链接还可以保留页码来对齐引用关系但毕设阶段先做到“按页输出、过滤噪声”就够用了。使用pip install pymupdf pdfplumber pdf2image pytesseract把后面几个小节用到的库装齐其中pytesseract还需要额外安装Tesseract OCR的程序本体和中文语言包Python库只是客户端。2.2 用PyMuPDF提取可选中文本PyMuPDF模块名fitz读取速度和页面体验都比较好是常规选型。下面这段代码把PDF逐页读出来过滤掉太短的噪声行。import fitz # PyMuPDF def extract_pure_text(pdf_path): doc fitz.open(pdf_path) pages [] for page_no in range(len(doc)): page doc[page_no] raw_text page.get_text(text, sortTrue) lines [line.strip() for line in raw_text.splitlines() if len(line.strip()) 5] pages.append(\n.join(lines)) doc.close() return pages for no, text in enumerate(extract_pure_text(paper.pdf)[:2]): print(f--- page {no 1} ---) print(text[:300])get_text(text, sortTrue)第一个参数是输出格式text表示纯文本sortTrue表示按阅读顺序排序。这里的排序依赖PDF内容流里的位置信息对单栏论文效果明显但对复杂表格不一定好。len(line.strip()) 5过滤掉页码、短装饰符这样的噪声同时要求每一页至少保留一个小段落避免后面整页空白进模型。如果这个方法抽出来是空串可以先取一页转成图片人工确认是不是图片版如果抽出来有很多孤立的数字和竖线说明这份PDF用了特殊字体编码不要继续硬调直接进入OCR分支。2.3 扫描版PDF走OCR兜底扫描版的PDF没有文本层只能把页面先转为图片再识别。分辨率是OCR识别率最大的变量。常用参数是dpi300低于200时中文笔画会连在一起高于400时只是增加计算量。下面这段代码用pdf2image转图再用pytesseract走中英文混合识别。import pytesseract from pdf2image import convert_from_path def ocr_pdf(pdf_path, dpi300): images convert_from_path(pdf_path, dpidpi) pages [] for img in images: text pytesseract.image_to_string(img, langchi_simeng, config--psm 6) pages.append(text) return pageslangchi_simeng表示使用简体中文和英文两个语言包如果没有chi_sim就只识别英文中文会变成乱码。--psm 6把页面当成一个统一文本块适合论文这种版面规整的整页内容如果页面是多栏且OCR结果顺序混乱可以去掉--psm 6改为默认模式让Tesseract自己找边框。OCR输出会有大量空格、换行和识别错误的字符建议在下游抽取前统一用正则把多余空白压缩掉。注意OCR占用内存较高一次塞几百页容易把内存打满最好按批次处理每20页写一次临时结果。2.4 表格数据用pdfplumber补充论文里的参数表格如果用纯文本抽数字和表头会失去行列关系之后就算抽取到实体也没法对应属性。pdfplumber能根据页面上的线条和文字坐标还原表格结构。import pdfplumber with pdfplumber.open(table.pdf) as pdf: page pdf.pages[0] table page.extract_table() for row in table: print( | .join(cell or for cell in row))extract_table()默认使用页面上的直线类对象做网格线返回值是二维数组每个单元格是文本或None。遇到跨页表格时pages[0]的内容会在下一页重复表头需要在合并时手动丢弃重复行。如果原表没有直线边框设置里需要把vertical_strategy从lines改成text表示按文本列对齐来切分表格否则返回空数组。下面是PDF解析阶段几个方案的定位我在动手前会先按这个判断再用对应代码。方案适用场景主要限制PyMuPDF有文本层的普通PDF表格结构丢失复杂版面会乱序pdfplumber带线条表格的参数页扫描版无法使用速度较慢pdf2image Tesseract扫描版PDF需要中文语言包识别率依赖分辨率PaddleOCR扫描版中文和公式混合页面模型体积大首次初始化慢到这里PDF层的输出已经统一成“每页一个长字符串”或“每份PDF一个小段落列表”。接下来的信息抽取就基于这个输出进行不要再回到原始PDF文件避免重复解析。3. 信息抽取把PDF文本变成实体和关系三元组PDF解析只解决了“机器能读到文本”的问题距离“构建知识图谱”还差中间的“信息抽取”。信息抽取要完成两件事一是把文本里的命名实体标出来二是把实体和实体之间的关系挖出来。毕设中最常见的方法是使用预训练模型做零样本抽取不需要自己标数据训练只要给一个schema就能跑。这样做的好处是当图里的节点类型变化时改schema就行不用重新训模型。3.1 先定schema再抽取很多同学习惯先把所有文本喂给模型让模型“自由发挥”结果抽出来的实体类型随模型心情走图谱里出现几十种标签根本没法维护。正确的顺序是先画一张简单的数据字典规定这个图书库需要哪些实体和关系。实体类型示例描述PaperAttention Is All You Need论文标题AuthorVaswani作者姓名InstitutionGoogle作者所属机构KeywordTransformer, Self-Attention关键词ResearchField自然语言处理研究领域在这张表里关系类型也被限定住Author和Paper之间是AUTHOR_OFPaper和Keyword之间是BELONGS_TOPaper和ResearchField之间是RESEARCH_ON。过多的关系类型会让图查询和可视化都变得非常复杂毕设里建议不要超过8种关系。schema定的越窄抽取准确率越高因为模型不用在多个候选类型之间摇摆。3.2 用PaddleNLP UIE做零样本抽取PaddleNLP的UIE是文本信息抽取中相对好上手的方案它支持中文不需要标注数据直接给schema就能跑。下面这段代码定义了一个包含论文标题、作者、机构、关键词和研究领域的抽取任务。from paddlenlp import Taskflow schema [论文标题, 作者, 机构, 关键词, 研究领域] ie Taskflow(information_extraction, schemaschema) text (Attention Is All You Need 由 Google 团队提出 是 Transformer 模型的核心论文。) result ie(text) print(result)Taskflow(information_extraction, schemaschema)会下载并加载一个预训练模型第一次运行需要联网。schema是抽取目标列表每个元素对应一类实体。返回结果是嵌套结构为了和后面知识图谱的入库格式对齐最好把它拆成“文本、实体类型、开始位置、结束位置”的记录。如果只想要实体名可以直接从result里取text字段。UIE也能直接抽取关系只要把schema从列表变成字典字典的键是头实体类型值是尾实体类型列表。schema_relation { 论文标题: [作者, 研究领域] } ie_relation Taskflow(information_extraction, schemaschema_relation) res ie_relation(《Attention Is All You Need》由Vaswani等人撰写 属于深度学习领域。) print(res)这种写法的意思是对每个“论文标题”实体去找它的“作者”和“研究领域”作为关系宾语。输出会比实体抽取多一些关系字段直接作为三元组的原料比较方便。需要注意UIE对长文本长度敏感超过512个token时会截断因此第2章里按页输出的长文本必须切段后再调用。3.3 正则回退与文本清洗预训练模型不是百分百准确尤其对年份、邮箱、URL这类模式固定的信息正则表达式反而更稳定。我一般采用“UIE为主正则补漏”的组合。比如论文发表年份直接用正则抓四个数字比让模型去识别日期更可靠。import re def extract_year(text): match re.search(r(19|20)\d{2}, text) return match.group() if match else None这段正则只匹配以19或20开头的四位数字能覆盖大多数论文发表年份。对于抽取出来的实体还要做一次统一清洗去掉首尾空格、把全角数字替换成半角、过滤掉只有符号的“实体”。清洗逻辑放在信息抽取之后、写入知识图谱之前避免脏数据进入图数据库。3.4 与PDF解析输出的衔接将UIE应用到整篇PDF时直接传入全文会导致上下文过宽和实体遗漏。常见做法是按段落切分段落边界用空行和章节标题识别。先切出一个段落列表然后逐个调用UIE收集结果。def split_paragraphs(pages): paragraphs [] for page in pages: for para in page.split(\n\n): para para.strip() if len(para) 20: paragraphs.append(para) return paragraphs paragraphs split_paragraphs(pages) all_entities [] for para in paragraphs: result ie(para) # flatten_result 需要根据实际返回结构封装这里示意 all_entities.extend(flatten_result(result))split_paragraphs用连续两个换行符切分文本len(para) 20过滤掉标题页的短行。这样做能避免跨栏段落被模型错误拼接也减少了重复实体的出现。但要注意切分后一个实体可能被拆成两段比如“Attention Is All You Need”被截断所以后续还需要按规则做一次片段合并如果两个实体类型相同、位置连续且中间没有其他实体就拼接成一个完整实体。4. 构建知识图谱三元组入Neo4j的落地细节信息抽取拿到的是“实体-关系-实体”三元组构建知识图谱的核心是把这些三元组持久化到图数据库而不是存成JSON。Neo4j是常用选择它有一套成熟的Cypher查询语言和可视化面板对毕设演示来说很合适。这一步要解决三个问题图模型怎么设计、怎么避免重复节点、批量插入怎么不卡死。4.1 图模型设计标签和关系类型不要超过8个将三元组写入图库之前先制定一个简单的图模型。下面是一个适合论文知识图谱的图模型关系起点终点含义AUTHOR_OFAuthorPaper作者撰写了论文BELONGS_TOPaperKeyword论文包含关键词RESEARCH_ONPaperResearchField论文属于研究领域FROM_INSTAuthorInstitution作者所属机构在这个模型里Paper是核心Author和Keyword都直接与Paper相连。如果后续想扩展引用关系再加一个CITES关系不必一开始就加入。实体标签过多会导致Neo4j内部索引膨胀查询时也容易出现笛卡尔积。关系类型少Cypher写起来直观也便于可视化。4.2 py2neo连接和创建唯一约束py2neo是Neo4j的Python客户端。连接时用bolt协议并显式传入用户名密码。为了防止同一篇论文或同一个作者被重复创建要先在标签属性上创建唯一约束。from py2neo import Graph, Node graph Graph(bolt://localhost:7687, auth(neo4j, password)) graph.run(CREATE CONSTRAINT paper_id IF NOT EXISTS FOR (p:Paper) REQUIRE p.id IS UNIQUE) graph.run(CREATE CONSTRAINT author_name IF NOT EXISTS FOR (a:Author) REQUIRE a.name IS UNIQUE)CREATE CONSTRAINT ... IF NOT EXISTS是Neo4j 4.0之后的写法约束一旦创建后续所有写入都自动检查唯一性。注意如果你用的是Neo4j 3.x需要把REQUIRE p.id IS UNIQUE改为ASSERT p.id IS UNIQUE否则会报语法错误。auth参数接收元组形式不要把密码写在代码里毕设里可以放到环境变量。4.3 批量写入三元组的写法一个比较常见的错误是每来一个三元组就创建一个节点造成大量重复。正确做法是通过merge幂等写入。下面的代码把上一章得到的数据列表写入图库。def write_triple_to_graph(graph, triples): for head_type, rel_type, tail_type, head, tail in triples: head_node Node(head_type, namehead) tail_node Node(tail_type, nametail) graph.merge(head_node, head_type, name) graph.merge(tail_node, tail_type, name) graph.run( MATCH (a {name: $h}), (b {name: $t}) MERGE (a)-[r: rel_type ]-(b), hhead, ttail )这里的merge以name作为唯一键如果节点已存在就只更新属性不重复创建。后一句Cypher的MERGE用来创建关系关系不设唯一性但如果两端节点已经唯一执行两次也不会产生重复关系。rel_type最好从白名单字典里取不要直接拼接用户输入防止Cypher注入。数据量较大时逐条graph.run会很慢因为每个run都会开启一个小事务。更快的做法是把所有节点写入放到一个事务里再集中建立关系。py2neo的graph.begin()可以开启事务但Neo4j单事务写入数建议不超过数万条毕设的数据量通常用不上。4.4 图查询验证与去重插入完所有数据后先跑几个统计查询看规模再继续做信息检索。print(graph.run(MATCH (p:Paper) RETURN count(p) AS papers).data()) print(graph.run(MATCH (a:Author) RETURN count(a) AS authors).data()) print(graph.run(MATCH ()-[r]-() RETURN type(r), count(r) ORDER BY count(r) DESC LIMIT 5).data())前两个查询分别统计论文和作者节点数量第三个查询按关系类型分组统计。如果某个关系类型缺失说明对应schema没有被模型抽出来要做清洗或增加规则。这个环节不需要打开Neo4j Browser直接在Python里就能确认图谱基本连通性。5. 基于知识图谱的信息检索从Cypher到简易问答知识图谱的信息检索和关键词搜索的区别在于用户输入一个实体后可以跟着关系找到多跳内容。比如“某个作者写了哪些论文”是首选的一跳查询而“这些论文都用了哪些关键词”就是二跳查询。毕设展示时一个输入框加上一个查询模板就足够。5.1 实体链接和查询模板用户输入不一定和节点完全同名所以先要做一次模糊匹配找到最接近的实体再把这个实体名放进查询。这里的关键是Cypher参数化。query MATCH (p:Paper)-[:BELONGS_TO]-(k:Keyword) WHERE k.name CONTAINS $kw OR k.name $kw WITH p, count(k) AS score RETURN p.name AS paper_title, score ORDER BY score DESC LIMIT 5 data graph.run(query, kw图神经网络).data()CONTAINS做子串匹配count(k)统计一篇论文命中的关键词数量命中越多排序越靠前。如果传入的是纸这样的短词命中范围会很大所以可以在检索前加一个实体链接函数先从节点里找到最匹配的Keyword或Author再走图查询。5.2 验证检索结果用Neo4j Browser调试上面的Python查询如果返回空问题多半不在Python而在图数据本身。遇到这种情况我会先到Neo4j Browser里跑一句最基础的查询MATCH (n) RETURN labels(n), n.name, count(*) LIMIT 5看这个标签下有没有正确的节点名称。再跑CALL db.schema.visualization()查看全图关系确认每个标签之间的连线存在。很多时候检索查不到不是因为Cypher写错而是关系方向写反了或者实体根本没有和论文建立联系。我一般习惯在Neo4j Browser里先敲通查询再回到Python换成参数化模板省去反复调试连接层的时间。本文还有配套的精品资源点击获取