基于Django的专家信息管理与抽取系统:规则引擎与人工复核实战

发布时间:2026/10/30 15:51:15
基于Django的专家信息管理与抽取系统:规则引擎与人工复核实战

简介:基于Python与Django开发的专家信息管理与抽取系统,是一份完整的毕业设计源码资源,主要面向计算机专业学生、需要完成Web课程设计或毕业设计的开发者。系统围绕专家信息管理场景,实现信息录入、查询、修改、删除以及信息抽取功能,覆盖Django框架的MTV分层架构、ORM对象关系映射、模板渲染、用户登录认证等核心知识点。压缩包共68个文件,包含23个Python源码文件、16个编译缓存文件、14个HTML模板页面、7个JavaScript脚本、3个CSS样式文件,以及SQLite数据库、Markdown说明文档和Word文档,整体大小仅为180KB,结构紧凑,便于按目录快速定位代码。目前已有246人学习下载。通过阅读源码,可以了解Django项目从模型设计、视图逻辑到前端展示的完整实现流程,掌握信息抽取模块的正则表达式与文本处理思路,同时参考其中的数据库关联设计和安全防护配置,对独立完成同类系统开发或毕业设计答辩都具有直接帮助。

1. 专家信息管理系统的真正难点:不是增删改查,是“抽取”

做职称评审、科研项目申报、学会委员遴选这类业务时,最烦人的不是维护专家库里的几百条记录,而是把一堆半结构化文本变成结构化字段。专家推荐表可能是 Word、PDF、网页表格,里面写着“张三,男,1972年生,XX大学教授,博士生导师,长期从事……方向”,你要把姓名、职称、单位、研究方向拆出来塞进数据库。人工复制粘贴一天能做几十条,录入人员手指酸,数据质量还看心情。这个基于 Python 和 Django 做的《专家信息管理与抽取系统》,核心价值就是把“人工读文本、手填字段”这个环节改成“规则抽取 + 人工复核”。

整个系统由两部分组成:Django 负责专家信息的管理——增删改查、分类、检索、权限;抽取模块负责从非结构化文本里自动识别专家的姓名、单位、职称、研究方向等字段。对新手来说,这是一个能完整练到 Django 建模、视图、表单、模板、ORM 查询的实战项目;对有经验的开发者来说,这套“规则引擎先行、模型后补”的做法,是处理垂直领域信息抽取最务实的路线。不建议一上来就上 BERT 或者大模型,后面会讲到为什么。

2. 技术选型先把地基打牢:Django 版本、数据库与抽取策略

2.1 为什么选 Django 而不是 Flask:管理后台和 ORM 是刚需

专家信息管理系统的本质是“内部工具”,不是面向 C 端的互联网产品。它要求开发速度快、维护简单、有现成的后台管理界面。Django 自带 Admin 后台,模型一注册就能直接增删改查,这对内部系统来说省掉了大量前端工作量。Flask 灵活,但需要自己拼装 ORM、表单校验、Admin 插件,做完一套“够用的”后台,时间成本差不多多出三分之一。

版本选择上,我建议直接用 Django 3.2 LTS 或 4.x 的稳定版本。3.2 是最后一个支持 Python 3.6 的 LTS 版本,如果你所在的服务器环境还锁在 Python 3.6,就选它;如果 Python 版本在 3.8 以上,直接上 4.2 或更新的 LTS。不要追最新大版本,Django 每次大版本升级都有一些模型字段和 settings 配置的调整,内部项目没必要承担这个风险。

数据库层面,SQLite 在小规模内部系统里完全够用,但有一个必须提前知道的限制:SQLite 的 LIKE 查询对中文没有大小写问题,但对英文字段默认不区分大小写,而且模糊查询性能会随着数据量上升明显下降。如果专家库预估会超过五万条,或者需要并发写入,建议换 PostgreSQL。Django 切换数据库只改 settings 里的 ENGINE 和 NAME,代价很低,但要在项目一开始就配好,等数据灌进去再迁库就麻烦了。

2.2 抽取策略确定:先规则,后模型,永远保留人工复核位

“抽取系统”这四个字,是最容易让人走偏的地方。很多人一听抽取,就想到命名实体识别(NER)、想到 BERT、想到调模型。但专家信息抽取有一个特点:字段高度固定,格式高度多样。姓名、职称、单位、研究方向,这些字段的写法再多,也有明确的语言模式。比如职称,无非是教授、副教授、研究员、主任医师、正高级工程师、教授级高工这些固定词汇;研究方向通常跟着“从事”“研究方向”“长期致力于”这类引导词走。

这意味着什么?意味着正则表达式和规则模板在这个场景下的准确率就能做到 85% 以上,而且可解释、可调试、不需要标注数据。一个内部管理系统的抽取模块,最怕的是“模型抽错了但没人知道”。规则抽错了,你能看一眼原始文本马上知道哪条规则出了问题;模型抽错了,你要分析样本、重新训练、再验证,一轮下来少说一两天。

所以我的建议是:第一版抽取模块用正则 + 词典匹配,预留一个抽象接口,后续如果需要再接入深度学习模型。这个决策能让项目落地时间压缩一半以上。系统结构上也很清晰:正则规则负责“抽得动”,词典负责“抽得准”,人工复核兜底。

2.3 Django 项目结构和 App 拆分:管理、抽取、复核三个模块各管各的

项目内部我习惯拆三个 App:experts管专家基础信息和分类;extraction管抽取规则、抽取任务和抽取结果;review管人工复核流程。新手常犯的错是把所有模型塞进一个 App,最后 models.py 一千多行,改一个字段牵连一片。

experts的模型分两层:专家主表存权威数据(人工确认过的);抽取记录表存每一次抽取的原始文本、命中的规则和抽取结果。这两层数据永远不混在一个表里。设计原则是:抽取结果默认是不可信的,只有经过人工复核后确认的数据才能写进专家主表。这样即使抽取模块出了问题,专家库里已有的数据不会被脏数据覆盖,这是整个系统最重要的数据安全设计,后面会反复提到。

3. 模型设计与抽取框架:把“专家信息”和“抽取结果”分开存

3.1 专家主表:不追求字段齐全,追求核心字段可检索

专家主表不要一口气设计二十个字段。内部系统的使用场景是:录入、查询、导出。业务部门真正高频检索的字段不超过八个——姓名、性别、出生年份、工作单位、职称、学历、研究方向、所在地区。其他像通讯地址、邮箱、电话,能填就填,但不要作为检索条件。

# experts/models.py from django.db import models class Expert(models.Model): """专家主表:只存经过确认的权威数据""" name = models.CharField("姓名", max_length=50, db_index=True) gender = models.CharField("性别", max_length=10, choices=[("男", "男"), ("女", "女")], blank=True) birth_year = models.IntegerField("出生年份", null=True, blank=True) organization = models.CharField("工作单位", max_length=200, db_index=True) title = models.CharField("职称", max_length=50, blank=True) degree = models.CharField("最高学历", max_length=20, blank=True) research_area = models.TextField("研究方向", blank=True) province = models.CharField("所在地区", max_length=50, blank=True, db_index=True) source_text = models.TextField("原始文本", blank=True, help_text="该条记录来自哪段原始材料") created_at = models.DateTimeField("创建时间", auto_now_add=True) updated_at = models.DateTimeField("更新时间", auto_now=True) class Meta: ordering = ["-updated_at"] indexes = [models.Index(fields=["organization", "title"])] def __str__(self): return f"{self.name} - {self.organization}"

字段上的db_index=True是给高频查询字段加的,姓名、单位、地区这三个字段会在列表页做搜索和筛选,不加索引的话数据量过一万就会出现可感知的卡顿。source_text字段很重要,它记录这条专家数据是从哪段材料里来的,复核时能直接对照原文,避免“数据错了却不知道错在哪”。

organization和title联合索引是给“XX大学 + 教授”这种组合筛选用的。注意gender字段用了choices,但数据库层面不会强制校验,表单层面 Django 会处理,这点够用就行。

3.2 抽取记录表:每次抽取都留痕,规则升级后可以重跑

抽取记录表是系统的“黑匣子”。每一次对文本的抽取,无论成功还是失败,都生成一条记录。这样做至少有三个好处:一是能统计抽取覆盖率,知道哪些文本别人抽不出;二是规则升级后可以批量重跑,对比新旧规则的差异;三是出了问题能回溯,是谁在什么时间用什么规则抽出了错误数据。

# extraction/models.py from django.db import models from django.contrib.postgres.fields import JSONField # 若用 SQLite,改为 TextField 存 JSON 字符串 class ExtractionRecord(models.Model): """抽取记录:原始文本 + 规则命中信息 + 抽取结果""" source_text = models.TextField("原始文本") extracted_data = models.JSONField("抽取结果", default=dict) matched_rules = models.JSONField("命中的规则", default=list) confidence = models.FloatField("置信度", default=0.0) status = models.CharField( "状态", max_length=20, choices=[ ("pending", "待复核"), ("confirmed", "已确认"), ("rejected", "已驳回"), ], default="pending", ) created_at = models.DateTimeField("抽取时间", auto_now_add=True) class Meta: ordering = ["-created_at"]

extracted_data存的是抽取出来的结构化字段,比如{"name": "张三", "title": "教授"}。matched_rules存命中了哪些规则,方便定位是哪条正则抽错了。confidence这个字段看起来简单,实际很关键:如果你同时命中“教授”和“副教授”两个职称词,置信度就要降下来,标记为“待复核”,而不是随便选一个。

状态机设计是pending -> confirmed/rejected。人工复核在 Django Admin 里做,或者单独做一个复核页面,一次显示原文、抽取结果、命中的规则,操作员确认或修改后写入专家主表。

3.3 注册 Admin 并配置列表页:内部系统的后台就是操作台

# experts/admin.py from django.contrib import admin from .models import Expert @admin.register(Expert) class ExpertAdmin(admin.ModelAdmin): list_display = ("name", "gender", "organization", "title", "province", "updated_at") list_filter = ("province", "title") search_fields = ("name", "organization", "research_area") list_per_page = 50

search_fields配置的字段会生成一个搜索框,Django 会用icontains做模糊匹配。这里有一个在 SQLite 下容易忽略的坑:icontains在 SQLite 里对中文按字符匹配是准确的,但对英文是大小写不敏感的——也就是你搜“ABC”能搜出“abc”。业务上通常没问题,但如果以后换到 PostgreSQL,icontains的语义是大小写敏感还是 insensitive 取决于你用的 collation,这个要在换库时重新测试。

list_filter按地区和职称筛选,业务部门用起来最顺手。list_per_page = 50是为了防止一次加载太多行导致页面卡顿,别小看这个参数,数据过万之后每页默认 100 条会让 Admin 列表页明显变慢。保存配置后运行python manage.py createsuperuser创建管理员账号,打开/admin/就能看到后台了。

4. 抽取引擎实现:正则规则、词典匹配和字段置信度计算

4.1 抽取模块的接口设计:屏蔽“用什么抽”,只暴露“抽没抽到”

# extraction/extractor.py import re from abc import ABC, abstractmethod class BaseExtractor(ABC): """抽取器基类:所有抽取策略都实现这个接口""" @abstractmethod def extract(self, text: str) -> dict: """输入原始文本,返回结构化字段字典""" pass class RegexExtractor(BaseExtractor): """基于正则 + 词典的抽取器""" def __init__(self): self.name_patterns = [ r"姓名[::\s]*([\u4e00-\u9fa5]{2,4})", r"([\u4e00-\u9fa5]{2,4}),[男女]", ] self.title_dict = ["教授", "副教授", "研究员", "副研究员", "主任医师", "副主任医师", "正高级工程师"] self.org_patterns = [ r"工作单位[::\s]*([\u4e00-\u9fa5()()·]{4,50})", r"([\u4e00-\u9fa5]{2,30}(?:大学|学院|研究所|研究院|医院|公司))", ] def extract(self, text: str) -> dict: result = {} for pattern in self.name_patterns: match = re.search(pattern, text) if match: result["name"] = match.group(1) break # 职称:词典匹配 + 上下文约束 for title in self.title_dict: if title in text: result["title"] = title break # 单位:正则 + 后缀词典 for pattern in self.org_patterns: match = re.search(pattern, text) if match: result["organization"] = match.group(1) break return result

这段代码展示了抽取器的三个基本策略。姓名的正则姓名[::\s]*([\u4e00-\u9fa5]{2,4})匹配“姓名:张三”这种写法,[\u4e00-\u9fa5]是中文匹配范围,{2,4}限定了姓名的长度是 2 到 4 个汉字。第二个模式([\u4e00-\u9fa5]{2,4}),[男女]匹配“张三,男”这种格式,把逗号后面的性别作为姓名的边界锚点。

职称没有用正则模式,直接词典匹配。原因很简单:职称是封闭集合,常见职称不超过二十个,词典匹配的速度和准确率都优于正则。单位匹配用了两层:先匹配“工作单位:”这种显式引导,再用“大学/学院/研究所”等后缀词做隐式匹配。{4,50}限制单位名长度,避免贪婪匹配把后面所有文字都吃进去。

4.2 置信度计算:多个规则命中冲突时,宁可低分也不要硬选

上面这个抽取器还比较粗,实际使用时要把置信度计算加进去。规则抽取最常见的翻车现场是:文本里同时出现“XX大学副教授”和“曾在XX大学任教授”,词典匹配一口气命中两个职称。此时如果按“找到第一个就停”的策略,抽出来的是错的。

# extraction/extractor.py 增加置信度逻辑 def _calculate_confidence(self, text: str, result: dict) -> float: """根据字段命中情况和冲突情况计算置信度""" score = 0.0 if result.get("name"): score += 0.4 if result.get("organization"): score += 0.3 if result.get("title"): # 职称命中多个时降权 title_matches = [t for t in self.title_dict if t in text] if len(title_matches) == 1: score += 0.3 else: score += 0.1 result["title_conflict"] = title_matches # 记录冲突职称列表 return min(score, 1.0)

置信度计算的逻辑不复杂:每个字段分配权重,命中一个加一部分。关键是“职称命中多个”的场景被单独摘出来,此时职称字段只加 0.1 分,并把冲突列表写进结果里。总分低于 0.7 的记录自动标记为“待复核”,这样操作员可以优先处理低置信度记录,高置信度的批量确认。

实际项目中置信度阈值不是一个固定值,要根据业务容忍度调。如果抽出 100 条里允许错 1 条,阈值可以调高到 0.8;如果只是粗筛,0.5 就够了。我一般把阈值放在 Django settings 里而不是硬编码在代码中,方便随时调整。

4.3 对整批文本批量抽取:用 Django management command 跑批

单条文本抽取没问题后,就该考虑批量导入的场景。业务部门手里往往是一批 Word 转出来的文本文件,几十上百篇。写一个 Django management command,让用户从后台管理页面上传一个 zip 包,解压后逐篇抽取。

# extraction/management/commands/batch_extract.py import zipfile from pathlib import Path from django.core.management.base import BaseCommand from extraction.models import ExtractionRecord from extraction.extractor import RegexExtractor class Command(BaseCommand): help = "批量抽取 zip 包内的文本文件" def add_arguments(self, parser): parser.add_argument("zip_path", type=str, help="包含文本文件的 zip 包路径") def handle(self, *args, **options): extractor = RegexExtractor() zip_path = options["zip_path"] with zipfile.ZipFile(zip_path) as zf: for member in zf.namelist(): if not member.endswith(".txt"): continue raw_text = zf.read(member).decode("utf-8", errors="ignore") extracted = extractor.extract(raw_text) confidence = extractor._calculate_confidence(raw_text, extracted) ExtractionRecord.objects.create( source_text=raw_text, extracted_data=extracted, confidence=confidence, ) self.stdout.write(f"已处理: {member}")

errors="ignore"是处理中文文本时的关键参数。Word 转出的 txt 文件经常带 BOM 头或者混入非法编码,这段decode("utf-8", errors="ignore")会跳过无法解析的字节而不是抛异常,保证一个文件出错不会中断整批任务。

跑批命令是python manage.py batch_extract ./uploads/experts.zip。处理完成后打开 Django Admin 的 ExtractionRecord 列表,按confidence升序排列,优先复核低分记录。这里要强调:批量抽取的产出是待复核记录,不是专家主表数据。很多新手做完抽取直接写入专家库,这是整个项目里最危险的坏习惯。

5. 避坑指南:专家信息抽取系统最常见的五个事故现场

5.1 SQLite 下模糊查询和并发写入的隐性限制

我在测试环境用 SQLite 跑通一切后,部署到服务器上发现两个问题:一是搜索“王”时返回的列表里混入了“王五”和“王小二”,这在业务上没问题,但搜索“主任”时会漏掉“副主任医师”,因为icontains匹配的是“包含”而不是“以…开头”;二是多个用户同时在后台确认专家记录时,偶尔会报 “database is locked” 错误。SQLite 在写并发场景下锁粒度太大,内部系统虽然并发不高,但一旦多个人同时操作就会出现。

解决方法是上线前把数据库切到 PostgreSQL,Django 的配置迁移代价很小:

# settings.py DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "expert_db", "USER": "expert_user", "PASSWORD": "your_password", "HOST": "127.0.0.1", "PORT": "5432", } }

换成 PostgreSQL 后,“database is locked” 不会再出现,icontains的模糊查询也走 PostgreSQL 的索引机制,性能会比 SQLite 好一个档次。数据库层面的切换动作很小,但必须在灌数据之前完成,迁移工具在项目启动阶段用最省力。

5.2 Django ORM 误用 save(update_fields) 导致并发覆盖

设计模型时我提到抽取结果默认不可信,需要人工确认后写入专家主表。这个“写入”动作用到了 Django ORM 的save(update_fields=[...])来做到只更新指定字段。

我用一个实际踩过的坑说明它为什么必须配合代码规范使用。某次我把复核页面里的保存逻辑写成expert.save(),也就是不带参数,结果一段本来只修改了organization的操作,把另一名同事基于同一条专家记录更新的research_area字段覆盖成了旧值。原因在于expert.save()会把所有字段都写回数据库,而复核页面加载的专家对象是几分钟前查出来的,字段值停留在旧状态。

# review/services.py def confirm_extraction(record_id: int, user_edited_fields: dict) -> None: """ 确认一条抽取记录并写入专家主表。 只更新复核人显式确认/修改过的字段,避免并发覆盖。 """ record = ExtractionRecord.objects.get(id=record_id) expert, created = Expert.objects.get_or_create(name=record.extracted_data["name"]) sensible_fields = {"organization", "title", "degree", "research_area"} update_fields = [] for field, value in user_edited_fields.items(): if field in sensible_fields: setattr(expert, field, value) update_fields.append(field) if created: expert.save() # 新记录全字段写入 else: expert.save(update_fields=update_fields) # 只更新确认过的字段

同样是在save方法上做文章,区别是运行时行为完全不同。带update_fields的保存会让 SQL 变为UPDATE ... SET organization=%s WHERE id=%s,不带则会更新所有字段。实际业务里,把“复核确认”和“修改某个字段”视为两个操作,各自写好独立函数,不要混在一个入口里。

5.3 正则表达式贪婪与回溯导致的长文本性能灾难

抽取器的正则里最容易忽略的是贪婪匹配和回溯陷阱。我最初写单位匹配时用的是(.*?)非贪婪模式,看起来没问题,但遇到文本里有两个“工作单位:”时,非贪婪模式会匹配到第一个就停;改为贪婪模式(.*)后,又出现匹配结果把整段正文都吞进organization字段的情况。

结症在{4,50}这个长度限制上。正则引擎先尝试尽量多地匹配,然后回溯到最长的合法结果。在长文本里,这个回溯过程可能非常耗时,严重时一段 5000 字的文本能卡住好几秒。添加长度上限是必要的,同时要配合re.DOTALL的取反思路——单位名不会包含换行符,所以用[^\n]{4,50}代替[\u4e00-\u9fa5()()·]{4,50}会更安全:

org_patterns = [ r"工作单位[::\s]*([^\n]{4,50})", r"([\u4e00-\u9fa5]{2,30}(?:大学|学院|研究所|研究院|医院|公司))", ]

[^\n]匹配除换行符外的任意字符,既解决了跨行误匹配,也把单行的匹配范围限制在了一行文本内,性能大幅提升。这类问题在调试时不容易发现,因为短文本下表现正常,一换真实的长篇专家介绍文本就开始卡。上线前建议做一次性能测试,把代表性文本逐条跑一遍。

5.4 编码问题:Word 转换出的文本常带 BOM 头和全角符号

批量抽取时最隐蔽的坑是文件编码不一致。有的 txt 文件是 UTF-8 带 BOM,有的是 GBK,还有的是 UTF-16。直接按utf-8解码,GBK 文件就报UnicodeDecodeError。解决方法是不要盲目按utf-8解码,而是做编码探测:

# batch_extract 中替换直接 decode 的逻辑 def decode_text(raw_bytes: bytes) -> str: """尝试多种编码,保证批量任务不中断""" for encoding in ["utf-8-sig", "utf-8", "gbk", "utf-16"]: try: return raw_bytes.decode(encoding) except UnicodeDecodeError: continue return raw_bytes.decode("utf-8", errors="ignore")

注意utf-8-sig放在第一位,它会自动处理 BOM 头。全角符号是另一个问题,比如“姓名:张三”里的冒号是中文全角让正则匹配失败。有两种处理方式:在正则里写成[::\s]*同时匹配全角半角;或者在抽取前先做一次文本清洗,把全角标点统一转为半角。我通常两个都做,清洗负责数据预处理,正则保持兼容两种写法,双保险。

5.5 手动在 Admin 里改数据后忘了留痕,“后悔药”给不了

Admin 后台虽然方便,但它默认不记录修改历史。业务人员复核时手动在 Admin 里改错一个字段,事后想追溯是谁在什么时候改的,完全无从下手。这个问题在内部系统里很常见:你用着顺手,但操作审计一片空白。

Django 生态里django-simple-history可以给模型加历史记录表,每次保存自动记录快照。如果不想引入依赖,也可以在 Expert 模型里加一个historyJSONField,在每次save()时把旧值和新值附加进去:

# experts/models.py 扩展 history = models.JSONField("修改历史", default=list) def save(self, *args, **kwargs): if self.pk: old = Expert.objects.filter(pk=self.pk).first() if old: changes = {} for field in self._meta.fields: if getattr(old, field.name) != getattr(self, field.name): changes[field.name] = {"old": getattr(old, field.name), "new": getattr(self, field.name)} if changes: self.history.append({"at": datetime.now().isoformat(), "changes": changes}) super().save(*args, **kwargs)

用_meta.fields遍历所有字段做新旧对比,把变化的字段记录下来。这种方式会轻微增加每次保存的开销,但内部系统的写入频率不高,完全可接受。这是个典型的“后悔药”功能,平时不起眼,出事后才知道钱花在哪。

6. 从规则引擎到模型推理:抽取系统的进阶路线与验证方法

规则抽取做到 85% 准确率后,剩下的 15% 是硬骨头——那些文本表述不规范的,比如“目前担任 XX 大学前沿交叉研究院执行院长,二级教授,博导”。这里的“二级教授”不在词典里,规则抽不出来。这时候再来考虑接模型。

在动手之前,先要回答一个问题:剩下的 15% 值得投入多少成本?如果业务上能接受“人工复核兜底”,规则引擎就够用了;如果需求是自动入库、减少人工量,那下一步是引入号称“专家信息抽取”的专用模型。这里有个实用的折中路线:规则引擎 + 词典增量维护。团队里最了解业务的人每月把新遇到的职称词、单位后缀词补充进词典,抽出率会慢慢逼近 92%,而成本只是几分钟的词典维护。

真到了要上模型那一步,建议顺序是:先用现成的中文分词工具(如 jieba)做候选词识别,再用一个二分类器判断候选词是否属于职称/单位;最后才是微调一个轻量级 NER 模型。评估指标就一个,F1 值——精确率和召回率的调和平均。不要只看准确率,因为“抽不出来”和“抽出错的”都会让业务人员抓狂。

我给自己定过一个规矩:任何抽取策略上线时,必须同步做一份验证集——从真实业务文本里抽 200 条,人工标注标准答案,每次改完规则后批量跑一遍。这 200 条验证集是抽样系统的“体检报告”。我见过太多团队改一条正则,修好了一个字段的匹配,却把另一个字段的准确率打掉五个点,全因为在验证集上跑才知道。

微软的培根说过“阅读使人充实,会谈使人敏捷,写作使人精确”,放在系统建设里,验证集就是那个让你精确的东西。做这个项目最大的教训就是:不要在规则引擎上追求 100% 的完美,完美是不存在的;把 85% 交给规则、14% 交给人工复核、1% 留给以后更好的模型,这个比例才是内部系统最舒服的姿态。希望这套从模型设计到批量抽取再到复核闭环的思路,能帮你在做类似信息抽取系统时少走一段冤枉路。

本文还有配套的精品资源,点击获取