ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python+Django的专家信息抽取系统:规则正则与批量导入实战

基于Python+Django的专家信息抽取系统:规则正则与批量导入实战 简介这是一套面向计算机专业学生与Web开发初学者的毕业设计级项目源码基于Python与Django框架实现专家信息管理与抽取系统覆盖信息录入、查询、修改、删除及从非结构化文本中自动提取专家资料等核心场景适合作为课程设计、毕设参考或Django入门实战练习。压缩包共68个文件约180KB以23个py源码文件为主体辅以16个pyc编译文件、14个html模板、7个js脚本与3个css样式另含sqlite3数据库、docx说明文档及README完整呈现Django的MVT分层结构与项目配置。已有246人学习下载。项目涵盖模型设计、视图逻辑、模板渲染、ORM操作、用户认证与CSRF防护等知识点并涉及正则表达式与NLP库的信息抽取思路目录按应用模块划分清晰便于读者快速理解Web项目开发流程、数据库关联设计与安全策略是提升Django实战能力的实用素材。1. 专家信息管理与抽取系统从一堆散乱简历到可检索数据库手里拿到几百份专家简历格式五花八门——有 Word、有 PDF、有扫描件甚至还有微信聊天记录里直接复制粘贴的文本。要从中提取姓名、职称、研究方向、论文成果再按领域分类、按机构检索靠 Excel 手工整理三天都搞不完还容易漏字段。基于 Python Django 的专家信息管理与抽取系统解决的就是这个场景把非结构化的专家资料批量导入自动抽取关键字段落库后提供 Web 端的增删改查、组合筛选和导出。它适合科研管理岗、学会秘书处、企业技术情报团队也适合想拿一个完整 Django 项目练手的开发者。整套方案不依赖外部商业 API抽取层用规则 正则 可选的大模型兜底部署一台 2 核 4G 的云主机就能跑起来。2. 抽取层怎么设计规则、正则与字段映射的取舍2.1 为什么先做规则抽取而不是直接上大模型专家简历的字段有很强的格式规律姓名通常在首行或“姓名”之后职称常出现在“教授”“研究员”“高级工程师”这类词附近邮箱和电话有固定模式研究方向往往跟在“研究方向”“主要研究领域”后面。这些模式用正则和关键词匹配就能覆盖 70% 以上的情况成本几乎为零响应在毫秒级。直接上大模型抽取单份简历的 token 消耗和延迟在批量导入时会被放大而且输出格式不稳定还得额外做校验。常见做法是规则抽取先跑一遍把置信度高的字段直接入库置信度低或缺失的字段标记出来再走人工复核或大模型兜底。这样既控制了成本又保证了准确率。2.2 字段抽取的核心正则与关键词表下面这段代码是抽取层的骨架放在extractor/parser.py里。它定义了姓名、职称、邮箱、电话、研究方向五个字段的抽取逻辑。import re # 职称关键词表按优先级从高到低排列 TITLE_KEYWORDS [ 教授, 副教授, 研究员, 副研究员, 高级工程师, 讲师, 助理研究员, 工程师, 院士 ] # 研究方向常见引导词 RESEARCH_HEADS [研究方向, 主要研究领域, 研究领域, 主要从事] def extract_name(text): 从文本前 5 行中抽取姓名优先匹配姓名格式 lines [l.strip() for l in text.split(\n) if l.strip()][:5] for line in lines: m re.match(r姓\s*名[:]\s*([\u4e00-\u9fa5]{2,4}), line) if m: return m.group(1) # 兜底首行如果是 2-4 个汉字直接当姓名 if lines and re.match(r^[\u4e00-\u9fa5]{2,4}$, lines[0]): return lines[0] return None def extract_title(text): 扫描全文返回第一个命中的职称关键词 for kw in TITLE_KEYWORDS: if kw in text: return kw return None def extract_email(text): 标准邮箱正则兼容常见域名 m re.search(r[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}, text) return m.group(0) if m else None def extract_phone(text): 匹配中国大陆手机号和座机号 m re.search(r(1[3-9]\d{9})|(0\d{2,3}-?\d{7,8}), text) return m.group(0) if m else None def extract_research(text): 在引导词后截取 50 字作为研究方向 for head in RESEARCH_HEADS: idx text.find(head) if idx ! -1: segment text[idx len(head): idx len(head) 50] # 去掉冒号和空白 return segment.strip(: \n) return None def parse_resume(text): 统一入口返回字段字典 return { name: extract_name(text), title: extract_title(text), email: extract_email(text), phone: extract_phone(text), research: extract_research(text), }逻辑说明extract_name先在前 5 行找“姓名”格式找不到就判断首行是不是纯汉字这是简历最常见的两种写法。extract_title用关键词表顺序扫描保证“副教授”不会被“教授”抢先匹配——所以关键词表里“副教授”必须排在“教授”前面。extract_research用引导词定位后截取 50 字避免把整段都塞进字段。参数方面RESEARCH_HEADS和TITLE_KEYWORDS是最需要按实际数据调整的两个列表建议先拿 20 份样本跑一遍看漏抽和误抽集中在哪些词上再往表里补。2.3 抽取结果落库前的清洗与去重抽取出来的字段不能直接写库得先过一遍清洗。邮箱统一转小写电话去掉横线和空格姓名去掉前后空白研究方向如果超过 200 字就截断。去重按“姓名 邮箱”组合判断如果邮箱为空则按“姓名 单位”判断。下面这段清洗逻辑放在extractor/cleaner.pydef clean_record(record): 清洗单条抽取结果 if record.get(email): record[email] record[email].lower().strip() if record.get(phone): record[phone] record[phone].replace(-, ).replace( , ) if record.get(name): record[name] record[name].strip() if record.get(research) and len(record[research]) 200: record[research] record[research][:200] return record def is_duplicate(record, existing_set): 判断是否重复existing_set 是已入库的 (name, email) 集合 key (record.get(name), record.get(email)) if key in existing_set: return True # 邮箱为空时退化为姓名判断 if not record.get(email): name_only record.get(name) return any(k[0] name_only for k in existing_set) return False清洗这步看着简单但漏了就会在后续检索时出现“同一个专家两条记录”的尴尬。is_duplicate里的退化逻辑是血泪经验很多老简历没有邮箱只按姓名去重会误杀同名的人所以实际项目里我一般会再加一个单位字段做二次确认。3. Django 建模与批量导入从模型定义到管理后台3.1 Expert 模型与字段设计Django 的 ORM 让专家信息的增删改查变得很直接。模型定义在experts/models.pyfrom django.db import models class Expert(models.Model): name models.CharField(姓名, max_length50, db_indexTrue) title models.CharField(职称, max_length50, blankTrue) email models.EmailField(邮箱, blankTrue, db_indexTrue) phone models.CharField(电话, max_length20, blankTrue) research models.TextField(研究方向, blankTrue) organization models.CharField(单位, max_length200, blankTrue) source_file models.CharField(来源文件, max_length255, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: verbose_name 专家 verbose_name_plural 专家 indexes [ models.Index(fields[name, email]), ] def __str__(self): return f{self.name}-{self.title}name和email加了db_indexTrue因为检索页最常用的筛选就是按姓名和邮箱查。source_file记录来源文件名方便回溯是哪份简历抽出来的。Meta.indexes里的联合索引用于去重查询。字段长度别设太小organization给到 200 是因为有些单位全称很长。3.2 批量导入命令与事务控制Django 管理命令是批量导入最稳的方式。在experts/management/commands/import_experts.py里写import os from django.core.management.base import BaseCommand from django.db import transaction from experts.models import Expert from extractor.parser import parse_resume from extractor.cleaner import clean_record, is_duplicate class Command(BaseCommand): help 批量导入专家简历文本文件 def add_arguments(self, parser): parser.add_argument(dir_path, typestr, help简历文本所在目录) def handle(self, *args, **options): dir_path options[dir_path] existing set(Expert.objects.values_list(name, email)) success, skipped 0, 0 for fname in os.listdir(dir_path): if not fname.endswith(.txt): continue fpath os.path.join(dir_path, fname) with open(fpath, r, encodingutf-8) as f: text f.read() record clean_record(parse_resume(text)) if is_duplicate(record, existing): skipped 1 continue with transaction.atomic(): Expert.objects.create( namerecord[name] or 未知, titlerecord[title] or , emailrecord[email] or , phonerecord[phone] or , researchrecord[research] or , source_filefname, ) existing.add((record[name], record[email])) success 1 self.stdout.write(f导入完成成功 {success} 条跳过重复 {skipped} 条)逻辑说明先一次性把已入库的(name, email)集合拉到内存避免每条都查库。每条记录用transaction.atomic()包住保证单条写入的原子性。existing.add在写入后立即更新集合防止同一批文件里的重复。参数方面dir_path是必填位置参数指向存放.txt简历的目录。如果简历是 PDF 或 Word需要先用pdfplumber或python-docx转成文本再放进目录这一步我一般单独写个转换脚本不混在导入命令里。3.3 管理后台的检索与导出配置Django admin 开箱即用但默认的列表页不够用。在experts/admin.py里配置from django.contrib import admin from .models import Expert admin.register(Expert) class ExpertAdmin(admin.ModelAdmin): list_display (name, title, organization, email, created_at) list_filter (title, organization) search_fields (name, email, research) list_per_page 50 ordering (-created_at,)list_filter让职称和单位变成侧边栏筛选器search_fields支持姓名、邮箱、研究方向的模糊搜索。list_per_page设 50 是平衡加载速度和翻页体验。导出功能可以装django-import-export在 admin 里加一个ExportMixin就能导出 CSV这里不展开。4. 避坑与排查抽取和导入环节的五个真实翻车点4.1 姓名抽取把“个人简介”当成了人名现象导入后发现有些记录的姓名是“个人简介”“基本信息”这类词。原因extract_name的兜底逻辑只看首行是不是 2-4 个汉字没排除常见标题词。解决在兜底判断前加一个黑名单过滤BLACKLIST {个人简介, 基本信息, 联系方式, 教育背景}命中就跳过首行继续看第二行。4.2 职称匹配到“教授”但实际是“副教授”现象明明是副教授入库后职称显示“教授”。原因关键词表顺序错了“教授”排在“副教授”前面in判断先命中短的。解决把长词排在短词前面TITLE_KEYWORDS里“副教授”必须在“教授”之前“副研究员”在“研究员”之前。这个坑我踩过两次后来直接在代码注释里写了大写加粗的提醒。4.3 批量导入时内存溢出现象导入 5000 份简历时进程被系统杀掉。原因existing集合一次性加载全部记录加上每份简历的文本都读进内存峰值内存超过 4G。解决把existing改成每 500 条重新查一次库文本读取用with open逐行处理不要f.read()全量加载。如果简历量确实大分批跑导入命令每批 1000 份。4.4 邮箱正则把“xxxxxx”这种不完整地址也匹配了现象入库的邮箱有“abcdef”这种没有顶级域名的。原因正则里\.[a-zA-Z]{2,}要求有点号加至少两位字母但有些文本里邮箱后面跟了中文标点正则贪婪匹配截断了。解决在正则末尾加边界判断r[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}\b并且抽取后校验域名部分是否包含点号。4.5 Django admin 搜索中文时结果不全现象搜“张”能搜到“张三”但搜“张三”搜不到。原因search_fields默认用icontains但数据库排序规则或字段类型影响了中文匹配。解决确认数据库用的是utf8mb4字符集MySQL 下检查collation是否为utf8mb4_unicode_ci。如果还不行在search_fields里显式写name__icontains并确保name字段没有做额外的strip处理导致索引失效。5. 抽取准确率验证与规则迭代的实操技巧5.1 用混淆矩阵定位抽取短板规则抽取上线后不能凭感觉说“差不多能用”。我一般会标 100 份样本人工填好正确字段然后跑一遍抽取统计每个字段的准确率和召回率。下面这段脚本放在scripts/evaluate.pyimport json from extractor.parser import parse_resume def evaluate(sample_file, label_file): sample_file 是简历文本列表label_file 是人工标注结果 with open(sample_file, r, encodingutf-8) as f: texts json.load(f) with open(label_file, r, encodingutf-8) as f: labels json.load(f) fields [name, title, email, phone, research] stats {f: {tp: 0, fp: 0, fn: 0} for f in fields} for text, label in zip(texts, labels): pred parse_resume(text) for f in fields: p, g pred.get(f), label.get(f) if p and g and p g: stats[f][tp] 1 elif p and (not g or p ! g): stats[f][fp] 1 elif not p and g: stats[f][fn] 1 for f in fields: tp, fp, fn stats[f][tp], stats[f][fp], stats[f][fn] precision tp / (tp fp) if (tp fp) else 0 recall tp / (tp fn) if (tp fn) else 0 print(f{f}: 准确率 {precision:.2f}, 召回率 {recall:.2f}) if __name__ __main__: evaluate(samples.json, labels.json)逻辑说明tp是抽对了fp是抽错了fn是漏抽了。准确率低说明误抽多去查关键词表是不是太宽召回率低说明漏抽多去补正则或引导词。参数方面samples.json是简历文本的 JSON 数组labels.json是对应的人工标注字典数组两个文件的行数必须一致。跑完看哪个字段的召回率低于 0.8就重点补那个字段的规则。5.2 规则迭代的优先级排序拿到评估结果后不要所有字段一起改。按“影响面 × 修复成本”排序姓名和邮箱是检索的主键召回率低于 0.9 就必须优先修职称和研究方向影响筛选低于 0.8 再动手电话字段如果业务上用得少可以放到最后。每次只改一个字段的规则改完重新跑评估确认指标上升再改下一个。这个习惯能避免“改了一处崩了三处”的连锁反应。5.3 大模型兜底的接入时机当规则抽取的召回率卡在 0.85 上不去且样本量超过 2000 份时可以考虑接大模型兜底。具体做法是规则抽取返回None的字段拼成 prompt 发给模型要求按 JSON 格式返回。注意两点一是 prompt 里必须给字段定义和示例否则模型会自由发挥二是返回结果要做类型校验比如邮箱必须匹配正则不匹配就丢弃。大模型只做兜底不做主力这样成本和准确率都可控。5.4 一个我常用的验证习惯每次改完抽取规则我会从库里随机抽 10 条记录把原始简历文本和抽取结果并排打印出来肉眼过一遍。脚本跑出来的指标再好看也架不住某些“玄学”样本——比如简历里写了两个邮箱正则抓了第二个但业务上要的是第一个。这种问题只有肉眼能发现。这个习惯花不了十分钟但能挡住大部分上线后的尴尬。希望帮到你。本文还有配套的精品资源点击获取
返回列表