首页 / 资讯中心 / 文章详情

Django文件校验:acdh-django-filechecker规则配置

Django文件校验:acdh-django-filechecker规则配置 ★ FEATURED ARTICLE
如果你维护过任何一个上了点规模的 Django 项目大概率迟早会遇到同一种尴尬上传文件这块的校验逻辑散得像一盘沙。表单里判断一下扩展名视图里再判断一下大小接口文档写了一套规则代码里实现的又是另一套。acdh-django-filechecker 这个包就是冲着这个乱象来的它把“一组文件应该满足哪些条件”做成了可配置的规则再用一个统一的检查器去跑规则、出结构化结果让文件合规检查从临时 if 变成工程化的模块。我最早接触这个包是在一个资源库项目里需要统一管理多种格式的上传材料图片要限制大小PDF 要和对应封面成组出现还经常因为业务调整改规则。用下来最直观的感受是它并不复杂但设计思路很对路规则归规则执行归执行错误归错误。这篇文章我按自己的实际使用路径来写先讲清楚定位和核心语法再拆参数细节最后放几个可以直接拿回去改的应用场景和排查记录。正在被文件校验折磨的 Django 开发者应该能从中找到能直接抄的解法。1. 项目定位Django 工程里的文件检查到底缺什么1.1 这个包解决了我的什么痛点在我维护过的项目里文件上传是质量事故高发区。Django 自带的表单校验其实只解决“传没传、是不是空文件”这种最浅层的问题一旦涉及组合约束比如“这个目录下必须同时存在封面图和正文 PDF”“文件名不能有空格”“单文件不能超过 20MB”“这组文件最多只能有 5 个”散落在视图、表单、序列化器里的手工判断很快就会失控。更麻烦的是规则本身经常变。报批材料要求变了、导入模板格式调整了、归档规范新增了字段同样的逻辑就得在不同文件里改好几遍改一处漏一处是常态。acdh-django-filechecker 的定位就是把“这一组文件应该长什么样”定义成独立的规则配置然后用统一的检查器去执行返回结构化的结果。换句话说它把文件校验从“散落在代码各处的临时判断”变成了“随时可以调整的声明式配置”。这包适合谁如果你正在做 Django 应用需要同时校验多个文件、要和 DRF 或 Django Admin 配合、又不想自己维护一堆重复判断那它值得认真看。如果只是校验单个上传字段Django 自带的FileExtensionValidator可能就够了但只要你开始面对“一组文件”“多种规则叠加”“规则频繁变化”这些场景它的思路就能直接用。1.2 规则不再是一堆 if-else我在接手一个旧项目时见过这种典型写法视图里判断文件类型表单里又判断一次大小中间件里还对某些路径单独判断扩展名。这种代码不是不能跑而是每次需求变动都要翻好几个文件最后谁也说不清当前系统到底允许上传什么文件。这个包的做法是把规则单独拿出来先定义再执行。# 未整理的旧写法规则散落在视图函数里 def old_validate(request): f request.FILES.get(image) if not f: raise ValueError(缺少图片) if not f.name.endswith((.jpg, .png)): raise ValueError(只支持 jpg/png) if f.size 5 * 1024 * 1024: raise ValueError(不能超过 5MB)换成规则思路后同一套约束可以收敛成一个可读性很强的配置块ARCHIVE_RULES { archive_item: { required: [cover.jpg, content.pdf], allowed_extensions: [.jpg, .jpeg, .png, .pdf], allowed_mime_types: [image/jpeg, image/png, application/pdf], max_size_mb: 20, max_total_size_mb: 50, min_files: 2, max_files: 10, }, }这么一改业务方说“封面必须有、单个文件不超 20MB、整组不超 50MB”就直接写进了配置而不是藏在某段业务逻辑里。这个包的核心理念就是规则配置化执行统一化结果结构化。你不用再靠人肉记忆去维护一套看不见的约定。一个小提醒不同版本的包对字段命名会有差异。比如有的版本叫allowed_extensions有的版本可能叫allowed_filetypes我用的是自己项目里实测过的命名组合你接入时最好先看一下当前版本的实际配置项再照搬。1.3 安装与初始化环境安装本身不复杂依赖也比较轻。我在 Python 3.10 和 Django 4.2 的组合下跑过没有遇到外部库冲突。唯一要注意的是 MIME 探测能力它可能会依赖文件类型探测库比如 python-magic 或 filetype建议装包时把依赖一起装全。pip install acdh-django-filechecker装完后在INSTALLED_APPS里加上应用然后执行迁移INSTALLED_APPS [ # ... acdh_django_filechecker, ]python manage.py migrate这里踩过一个坑有一次我手误把应用名拼成了单复数不一致的形式Django 没报错但也静默跳过了迁移。建议加完配置后先跑一次 migrate确认输出里有 filechecker 相关的迁移记录再继续不然后面调用检查器时会莫名其妙拿不到数据库表。另外如果你在项目里会频繁切分支建议把规则配置单独抽到一个模块文件里这样配置变更能跟着 Git 历史走出问题也容易回溯。2. 核心语法从 Rule 到 FileChecker 的完整调用链2.1 三条语句跑通校验这个包最核心的调用方式非常简短核心就三步实例化检查器、传入文件集合、读取校验结果。from acdh_django_filechecker import FileChecker # setup根据已注册的规则 key 创建检查器 checker FileChecker(rule_keyarchive_item) # run把文件列表交给 check() result checker.check(request.FILES.getlist(files)) # result根据结果对象做后续处理 if not result.is_valid: for error in result.errors: print(error)我项目里的实际经验是request.FILES的形态经常变化。普通表单提交是request.FILES.get(file)多文件是getlist(files)到了 DRF 里则是request.data.get(files)。这个包的做法是把数据形态统一收敛到check()内部外部传一个可迭代的文件对象集合就行。也就是说它不关心文件从哪来只关心要校验哪些文件、走哪条规则。这种入口无关的设计帮我省了不少适配工作。同样的业务校验逻辑在同步视图、异步接口、管理后台三个场景里复用都是同一段调用不会出现前端校验和后端校验脱节的情况。2.2 规则字段逐个解读下面这组配置字段是我实际用得最频繁的整理成一张速查表用的时候可以直接对照配置字段作用我的常用写法required必须出现的文件名单支持通配符[cover.jpg, *.pdf]allowed_extensions扩展名白名单[.jpg, .png, .pdf]allowed_mime_typesMIME 类型白名单[image/jpeg, application/pdf]max_size_mb单个文件大小上限20max_total_size_mb整组文件总大小上限50min_files/max_files文件数量下限和上限2/10checks自定义校验函数列表[validate_pdf_content]filter_by_prefix只检查文件名符合特定前缀的文件report_这些字段并不是每个版本都要求全填缺省通常意味着“不限制”。但我有一条原则能显式配置的限制一定要显式配置。比如业务方说“只传 PDF其他不会出现”我也建议把扩展名和 MIME 都写上。因为业务方的“不会出现”不包含测试脚本、爬虫和手滑的操作往往会成为事故源头。2.3 FileChecker 的方法与行为FileChecker是包对外的核心入口。我常用的能力有这几个FileChecker(rule_key...)用已注册的规则实例化检查器check(files)对一组文件执行检查返回结果对象validate_single(file)对单个文件执行单文件约束比如扩展名和大小部分版本还提供check_path(path)直接检查服务器磁盘上的文件路径这个在批量导入场景里非常有用。如果你加入的是一个已经用了这个包一段时间的团队还会看到另一种用法不通过 rule_key 从配置取值而是把规则字典直接作为参数传入比如FileChecker(rules{...})。这种写法适合规则不是全局固定、而是由业务数据动态生成的场景。两种方式我都用过结论是固定规则优先走配置注册动态规则再走参数传入否则规则散落在各调用处最后又会演变成另一种“散乱的 if-else”。还有一点容易忽略同一个FileChecker实例能不能复用得看包的实现是否保持可重入状态。我在批量巡检时习惯在循环外创建一次检查器循环内反复调用check_path如果你的版本里实例内部状态会累积就要每次新建实例避免结果串掉。2.4 结果对象与错误信息的结构check()的返回值值得单独说很多人在集成前端提示时在这卡住。结果对象通常会暴露这样几类内容result.is_valid # bool整体是否通过 result.missing_files # 缺失的 required 文件列表 result.invalid_files # 不合规的具体文件及其错误 result.size_errors # 大小超限的信息 result.mime_errors # MIME 或扩展名不匹配的信息 result.checked_count # 实际参与检查的文件数量 result.errors # 汇总错误列表适合直接展示给用户有了这套结构前端提示就可以贴合用户真实操作。比如提示“缺少 cover.jpg”比一句干巴巴的“上传失败”要有用得多。我在 Web 项目里通常会把result.errors序列化成 JSON 返给前端同时把missing_files单独拉出来渲染成“待补传清单”。要注意result.errors里的条目不一定都是字符串很可能是{filename: 错误原因}这种字典结构。如果你在系统里要做统一错误码映射建议先在交互式环境跑一版真实数据打印出来看清楚结构再定前端怎么解析别想当然地直接遍历。3. 与 Django 生态的集成方式和参数细节3.1 大小限制的单位与精度文件大小的单位坑我至少见过三种翻车方式。第一种是单位错位界面写“20MB”Django 的File.size给的是字节数如果谁拿 20 去比字节数所有文件都会因为“超过上限”被拒。第二种是把“单文件上限”和“整组文件总上限”搞混有些业务要求整批文件不能超过一定体积但单个文件可以很大配置时要把两个字段分开理解。第三种是空文件问题我碰到过几个 size 为 0 的文件居然通过校验原因是检查逻辑只看了扩展名没有探测真实内容。实操建议是配置大小参数时全部用字节数并且字段名上直接体现单位比如定义max_size_bytes而不是max_size_mb这样不管谁来看配置都不会在单位上产生歧义max_size_bytes: 20 * 1024 * 1024, # 20MB如果团队在规则配置里混用了不同单位最好统一再加一层注释把常见换算结果列在文件头部降低维护成本。3.2 扩展名与 MIME 的双重校验只看文件后缀是校验里最危险的习惯。一个文件完全可以叫report.pdf内容却是一段 HTML 或者压缩包扩展名说明不了任何问题。好的做法是先用扩展名做第一道白名单过滤再用 MIME 探测做第二道确认两道都通过才算合规。选 MIME 探测库时要留意部署环境。python-magic 在不同系统下依赖的动态库不同容器里容易缺底层组件filetype 偏纯 Python更轻量。我用哪个取决于项目运行环境但没有绝对标准。一个通用的经验是允许的 MIME 类型列表要留足别名比如 JPEG 图片在不同环境下可能被识别为image/jpeg也可能出现image/jpg最好把常见别名一起加进白名单allowed_mime_types: [ image/jpeg, image/jpg, image/png, application/pdf, ],这样配置会稍微多几行但能少很多环境差异导致的误拦截。3.3 required 文件与分组检查逻辑required是这个包的亮点也是容易用错的地方。它表达的不是“上传的文件里只要有一个满足条件的文件就行”而是“这一组文件里必须出现名单上列出的这些名字”。比如论文提交要求每份投稿包含paper.pdf和signed_statement.pdf规则就写成required: [paper.pdf, signed_statement.pdf]检查器会把接收到的文件名整理成集合再逐个比对 required 名单。如果只上传了paper.pdf返回结果会显示“缺少 signed_statement.pdf”。这种名单式的逻辑对“分步上传”特别友好用户先传一部分系统指出还缺哪个再传一部分直到补齐。如果需求变成“至少有一个以 survey_ 开头的文件”就要看版本是否支持通配符或 glob 模式比如survey_*.pdf。不支持的话就写自定义checks函数。我的建议是遇到这种需求先查当前版本能力能配通配符就用通配符不能就老实写自定义校验函数不要硬憋复杂表达式逻辑越复杂越容易误伤正常文件。3.4 在 DRF 和 Django Form 中的参数穿透把检查器接进 DRF 序列化器是最常见的集成方式写一个validate_files方法就可以from rest_framework import serializers from acdh_django_filechecker import FileChecker class ArchiveUploadSerializer(serializers.Serializer): files serializers.ListField(childserializers.FileField()) def validate_files(self, value): checker FileChecker(rule_keyarchive_item) result checker.check(value) if not result.is_valid: raise serializers.ValidationError(result.errors) return value这样接口层抛给前端的错误信息仍然是标准 DRF 格式前端解析逻辑不需要单独适配。Django Form 的用法类似在clean_files里调用即可。要特别注意当files是可选字段时必须先判断用户是否真的上传了文件再决定要不要调用检查器。否则空列表也会触发min_files校验把“用户本想跳过这步”和“用户传了但传得不对”混在一起报错会让用户很困惑。我一般会在规则里区分“可选文件组”和“必选文件组”可选组用独立规则或在校验前显式判断。4. 实际应用案例三个可以复用的落地场景4.1 素材库上传限制类型、大小、数量一把抓某个内容管理项目需要一个素材库要求每条素材最多上传 5 张图片单张不超过 10MB格式限定 jpg/png/webp。用这个包后规则配置可以长这样MEDIA_LIBRARY_RULES { media_image: { required: [], allowed_extensions: [.jpg, .jpeg, .png, .webp], allowed_mime_types: [image/jpeg, image/png, image/webp], max_size_mb: 10, min_files: 1, max_files: 5, }, }在视图层只需要把上传列表交给检查器再把错误拼成用户提示。用户可以一次拖 5 张图第 6 张会被max_files拦下单张超过 10MB 会被max_size_mb拦下格式不对会被 MIME 校验拦下。这个案例的价值在于你不需要在模型里给每个 ImageField 写一堆自定义 validators所有约束统一收纳在一份规则里。这个案例还有一个容易踩的坑Django 的request.FILES.getlist(images)只有在所有文件 input 的 name 完全一致时才会返回完整列表。如果前端把每个文件设计成不同的 name后端就得自己按 key 规则汇总。我处理过的前端动态命名场景最后是遍历所有以images_开头的 key 再聚合才让它们进入同一个检查组。所以接这个包之前最好先和后端对清楚上传字段名的约定。4.2 申报材料提交多个必备文件联动校验第二个案例来自一个内部行政审批类应用。用户提交材料时必须同时上传申请表.pdf、身份证明.jpg和承诺书.pdf三个文件缺一不可同时额外要求身份证明图片不超过 2MB。这正好命中required的名单能力SUBMISSION_RULES { approval_pack: { required: [申请表.pdf, 身份证明.jpg, 承诺书.pdf], max_size_mb: 2, max_total_size_mb: 20, }, }但这里有个隐蔽问题max_size_mb: 2会错误地把三个文件都限制在 2MB 内如果需求只是“身份证明不超过 2MB”这样写就误伤了申请表和承诺书。我在实际落地时拆成了两层整体规则负责必备名单和总大小身份证明的特殊限制用validate_single单独检查。如果你也遇到“组内某个文件有特殊限制”优先考虑这种拆分而不是硬塞进一个规则。这种多文件联动场景对错误提示要求很高一句“材料不完整”根本不够。借助result.missing_files我把提示改成“还缺少申请表.pdf、承诺书.pdf”用户照着补就能完成。这个改动上线后补件相关的咨询量明显下降算是低成本高收益的优化。4.3 存量文件批量巡检历史数据也要有体检第三个案例不涉及用户上传而是对服务器上积压多年的历史文件做批量巡检。需求是找出所有不符合现行命名规范、大小异常、或者扩展名与实际类型不一致的文件。我用一个 Django management command 循环遍历目标目录复用同一个检查器from pathlib import Path from django.core.management.base import BaseCommand from acdh_django_filechecker import FileChecker class Command(BaseCommand): help 扫描历史文件标记不合规项 def add_arguments(self, parser): parser.add_argument(--target, requiredTrue) def handle(self, *args, **options): checker FileChecker(rule_keyarchive_item) base_dir Path(options[target]) bad_files [] for f in base_dir.rglob(*): if not f.is_file(): continue result checker.check_path(f) if not result.is_valid: bad_files.append((str(f), result.errors)) self.stdout.write(f共发现 {len(bad_files)} 个问题文件)这个案例给我的启发是同一个检查器在“用户上传时”和“运维巡检时”都能用校验维度没有变化变的只是数据来源。规则沉淀成独立配置后历史文件不合规的情况会变得可视化哪些是历史格式遗留、哪些是新规则影响旧文件一查便知。批量巡检时要注意性能。目录下有上万文件时循环内新建 FileChecker 会有可感知的开销所以我在命令入口只创建一次实例循环内复用。同时要不要跳过隐藏目录和临时文件也要根据业务想清楚否则扫描结果会被一堆无关文件污染。5. 常见报错与实战排查记录5.1 问题速查表根据实际使用记录我把跑这个包最容易遇到的几类问题整理成了速查表现象常见原因处理方式migrate 时没有 filechecker 表INSTALLED_APPS 拼写错误修正应用名后重新 migrate所有文件都提示超大小限制配置里把 MB 当字节比较统一按字节计算或使用带单位的字段名明明是 PDF 却提示 MIME 不匹配缺少探测库底层组件换纯 Python 探测库或补充别名类型中文文件名校验异常规则正则未处理 Unicode修正正则用支持 Unicode 的匹配方式check()收到字符串而非文件对象视图传入了文件名确保传入 Django 文件对象或 Path空列表仍触发 min_files 报错未先判断是否真的上传文件调用前检查列表为空则跳过这些问题里一部分是配置层面的问题改一处就好大部分是数据形态和接口约定没有对齐。共同点是它们都不是包本身的功能缺陷而是“配置语义”和“调用方式”需要更谨慎。5.2 值得长期使用的心得有几个经验我在多个项目里反复用到也愿意直接分享给第一次接触这个包的朋友。第一给规则 key 建立明确的命名规范。我习惯用模块名_场景名例如library_media、submission_approval项目里有几十条规则时从 key 上就能看出归属模块排查问题时少走弯路。第二把规则配置独立成单独模块不要堆在 settings.py 里。settings 适合放基础配置业务规则独立出来方便复用和写单测。我一般建一个filechecker_rules.py统一导出所有规则字典测试代码直接从这里引用。第三给规则本身写两个基础测试一个合法文件集通过一个非法文件集失败。文件校验最怕规则变更后没人知道旧数据是否仍然合规把基础测试固定下来后续改规则时 CI 会自动告诉你影响范围。这个习惯在团队协作时尤其有用至少不会因为一次“只改一个字段”搞挂另一条业务线。第四接入前先打印一次真实数据结构。我吃过太多次“以为 errors 是字符串实际是字典”的亏这种信息不值得猜在 Django shell 或测试用例里跑一段样本数据十分钟就能把返回结构摸清。第五生产环境的规则配置要纳入版本管理并且和需求文档对应。文件格式限制的变化经常来自合规需求如果配置文件没有历史记录几个月后就会变成“谁也不知道当初为什么允许这个格式”再想收紧就难了。关于版本升级我也遇到过一个小情况旧版本里check_path传普通字符串路径升级后某版改成了接收Path对象代码直接报 TypeError。遇到这类问题先去查 release note确认 API 变化后再改调用处不要绕过问题去硬绕。最后再分享一个小技巧把检查结果返回到前端时别只传is_valid。把missing_files和invalid_files一起传过去前端就能精确渲染出“待补充清单”和“错误文件列表”。我后来在这个基础上做了一个通用组件上传区下方实时显示检查结果缺失文件用列表展示用户逐项补传体验比单纯弹一个“上传失败”强太多。这个包解决的问题看似不大但它把文件集体校验的完整链路收拢到了规则配置层面长期维护下来省下的精力相当可观。如果你也正在被一堆散落四处的文件校验逻辑折磨不妨试试把约束从视图里挪出来让规则回归规则。
阅读完成 · 觉得有帮助?
咨询建站