1. 从需求到方案:为什么企业需要一套文档内容扫描系统
1.1 这个项目到底在解决什么问题
日常工作中,文档流转是再普通不过的事情。一份合同、一张报价单、一份会议纪要,从一个人手里传到另一个人手里,中间可能经过邮件、即时通讯、共享盘、移动存储设备等多个渠道。问题在于,这些文档里可能夹带着一些不该外传的内容——比如客户名单、内部定价策略、未公开的财务数据、个人身份信息等。一旦这些内容流出去,轻则商业被动,重则面临合规风险。
这个项目的核心目标很明确:给定一批Office文档(doc、docx、xls、xlsx、ppt、pptx)、PDF和纯文本文件,自动解析出其中的文本内容,然后按照预设的敏感词规则进行扫描,判断文件是否“涉敏”,并输出扫描结果。说白了,就是给文档做一次“体检”,看看里面有没有不该有的东西。
适合谁来参考这套方案?企业内部的IT运维人员、信息安全岗的工程师、需要做文档合规审查的行政或法务人员,以及任何有批量文档内容筛查需求的开发者。哪怕你之前没接触过文档解析,只要会一点Python,跟着思路走也能搭出一套可用的系统。
1.2 为什么选择“解析文本+关键词匹配”这条路线
市面上做内容安全的产品不少,有基于机器学习的分类模型,有基于规则引擎的复杂系统。但对于大多数中小规模场景来说,最务实、最可控的方案就是“把文本抽出来,然后做关键词匹配”。原因有三:
第一,可解释性强。机器学习模型告诉你“这个文件涉敏概率87%”,你没法跟业务方解释为什么。但关键词匹配可以精确告诉你:第3页第2段出现了“内部报价”这个词,所以判定涉敏。这种可追溯性在合规审查场景里极其重要。
第二,维护成本低。敏感词库就是一个文本文件,加一行减一行随时生效,不需要重新训练模型,不需要标注数据。业务方说“把‘项目代号X’加进去”,你改一行配置就完事了。
第三,覆盖面可控。不同部门、不同时期的敏感词定义可能完全不同。规则引擎可以按部门、按文件类型、按时间范围灵活组合规则,这是通用模型很难做到的。
当然,这条路线也有明显的短板:它只能发现“字面出现”的敏感内容,对于改写、隐喻、图片中的文字无能为力。但在绝大多数实际场景中,需要防范的恰恰是那些“原样复制粘贴”导致的信息泄露,所以这套方案的实际命中率相当可观。
1.3 整体架构长什么样
整个系统的逻辑链条可以拆成四段:文件识别与分类 → 文本内容提取 → 敏感词扫描 → 结果输出与处置。
文件识别阶段要解决的是“这个文件到底是什么格式”的问题。不能光看扩展名,因为有人会把.xlsx改成.doc,也有人会把PDF直接命名为.txt。稳妥的做法是结合扩展名和文件头魔数(Magic Number)双重判断。
文本提取阶段是整个系统最复杂的部分,因为八种文件格式的解析方式完全不同。doc和docx虽然都是Word文档,但底层结构一个是二进制OLE复合文档,一个是ZIP压缩的XML包,处理方式天差地别。xls和xlsx、ppt和pptx同理。PDF更是出了名的难啃,纯文本PDF、扫描件PDF、加密PDF的处理路径完全不一样。
扫描阶段相对简单,核心就是字符串匹配。但要做好也不容易——大小写要不要忽略?全角半角要不要统一?敏感词要不要支持正则表达式?这些细节直接决定了误报率和漏报率。
结果输出阶段需要考虑的是:扫描结果给谁看、怎么用。是生成一份Excel报告,还是直接对接工单系统触发人工复核,还是自动隔离文件?不同场景需求不同。
2. 八种文件格式的文本提取实战
2.1 Word文档:doc与docx的分野
docx本质上是一个ZIP压缩包,里面存放着XML格式的文档内容。用Python处理的话,python-docx库是最成熟的选择。它的基本用法很直观:
from docx import Document doc = Document("example.docx") full_text = [] for para in doc.paragraphs: full_text.append(para.text) # 别忘了表格里的内容 for table in doc.tables: for row in table.rows: for cell in row.cells: full_text.append(cell.text)这里有个容易踩的坑:很多人只遍历paragraphs,结果表格里的敏感信息全部漏掉。实际上,Word文档中的表格、页眉、页脚、文本框、批注,都可能藏着关键内容。完整的提取逻辑应该把这些角落都覆盖到。
页眉页脚的提取需要访问section.header和section.footer:
for section in doc.sections: for para in section.header.paragraphs: full_text.append(para.text) for para in section.footer.paragraphs: full_text.append(para.text)doc格式(Word 97-2003)就麻烦多了。它是二进制格式,python-docx不支持。常见的做法有两种:一是用antiword或catdoc这类命令行工具转换,二是用LibreOffice的无头模式批量转成docx再处理。我实测下来,LibreOffice的方案更稳定,虽然速度慢一些,但对复杂文档的兼容性明显更好。
libreoffice --headless --convert-to docx --outdir /output /input/old.doc注意:LibreOffice转换时会启动一个后台进程,批量处理时要注意控制并发数,否则内存容易爆掉。建议用进程池限制在CPU核心数以内。
2.2 Excel表格:xls与xlsx的提取要点
xlsx用openpyxl处理是最顺手的。但Excel的复杂性在于:单元格里可能有公式、有合并单元格、有隐藏的行列、有多个工作表。敏感信息可能藏在任何一个角落。
import openpyxl wb = openpyxl.load_workbook("example.xlsx", data_only=True) for sheet in wb.worksheets: for row in sheet.iter_rows(): for cell in row: if cell.value is not None: full_text.append(str(cell.value))data_only=True这个参数很关键。默认情况下,openpyxl读取的是公式本身(比如=A1+B1),而不是计算结果。但敏感信息往往在计算结果里,所以必须加上这个参数。代价是:如果文件从未被Excel打开过,缓存的计算结果可能不存在,读出来是None。这种情况就需要用formulas模式读公式,或者用LibreOffice先转换一遍。
xls格式(Excel 97-2003)用xlrd库处理。注意xlrd从2.0版本开始不再支持xlsx,所以如果你的环境里同时有xls和xlsx,需要根据扩展名分别调用不同的库。
隐藏行列的问题值得单独说。有些文件故意把敏感内容放在隐藏的行或列里,普通遍历是读不到的。openpyxl可以通过sheet.row_dimensions和sheet.column_dimensions判断隐藏状态,但更简单的做法是:不管隐藏不隐藏,全部读出来。反正扫描阶段多读一些内容不会有害,漏读才是大问题。
2.3 PPT演示文稿:pptx与ppt的处理
pptx用python-pptx处理。PPT的内容分布在幻灯片、备注页、母版等多个位置。最容易被忽略的是演讲者备注——很多人会把敏感信息写在备注里提醒自己,结果忘了删除。
from pptx import Presentation prs = Presentation("example.pptx") for slide in prs.slides: for shape in slide.shapes: if shape.has_text_frame: for para in shape.text_frame.paragraphs: full_text.append(para.text) # 备注页 if slide.has_notes_slide: notes = slide.notes_slide.notes_text_frame.text full_text.append(notes)表格和图表中的文字也需要单独处理。shape.has_table判断是否为表格,然后遍历单元格。图表中的文字比较麻烦,python-pptx对图表的支持有限,可能需要解析底层XML。
ppt格式(PowerPoint 97-2003)同样建议用LibreOffice转换后再处理。直接解析二进制格式的库(如python-pptx的旧版本)维护状况堪忧,不值得冒险。
2.4 PDF:最棘手的格式
PDF的文本提取是八种格式里最复杂的。原因在于PDF本质上是一种“页面描述语言”,它描述的是“在某个坐标画某个字符”,而不是“这里有一段文字”。所以提取出来的文本顺序可能是乱的,甚至可能提取不出来。
纯文本PDF用pdfplumber或PyMuPDF(fitz)效果最好。pdfplumber的优势是能保留版面信息,适合需要按页、按段落分析的场景:
import pdfplumber with pdfplumber.open("example.pdf") as pdf: for page in pdf.pages: text = page.extract_text() if text: full_text.append(text)PyMuPDF速度更快,适合大批量处理:
import fitz doc = fitz.open("example.pdf") for page in doc: full_text.append(page.get_text())扫描件PDF(本质上是图片)用上述方法提取出来是空的。这种情况需要OCR。pytesseract配合pdf2image是常见方案,但OCR的准确率和速度都是问题。实际项目中,我建议先判断PDF是否包含可提取文本,如果整份文件提取出的字符数低于某个阈值(比如每页少于50个字符),就标记为“疑似扫描件”,转人工处理,而不是硬上OCR。
加密PDF需要先解密。PyMuPDF支持传入密码:
doc = fitz.open("encrypted.pdf") if doc.needs_pass: doc.authenticate("password")如果不知道密码,那就只能标记为“无法解析”,跳过处理。
2.5 纯文本文件:看似简单,暗藏玄机
txt文件看起来最简单,但编码问题能让人抓狂。GBK、UTF-8、UTF-16、带BOM的UTF-8……不同来源的txt文件编码五花八门。用chardet或charset-normalizer自动检测编码是常规做法:
import chardet with open("example.txt", "rb") as f: raw = f.read() encoding = chardet.detect(raw)["encoding"] text = raw.decode(encoding or "utf-8", errors="ignore")errors="ignore"这个参数很重要。遇到无法解码的字节时直接跳过,而不是抛异常中断整个流程。代价是可能丢失少量字符,但对于敏感词扫描来说,丢失几个乱码字符通常不影响判断。
实操心得:有些系统导出的txt文件是UTF-16编码,但文件头没有BOM。这种情况下chardet可能误判为其他编码。一个兜底策略是:如果检测出的编码解码后出现大量乱码字符(比如替换字符U+FFFD),就换一种编码重试。
3. 敏感词扫描引擎的设计与实现
3.1 敏感词库的组织方式
敏感词库不是简单的一行一个词。实际场景中,敏感词往往需要分类、分级、分场景。比如“内部”这个词单独出现可能没问题,但“内部定价”就是敏感词;“客户”单独出现没问题,“客户名单”就是敏感词。所以词库需要支持词组匹配。
我通常把词库组织成JSON格式,每个词条包含词本身、类别、风险等级、备注:
{ "words": [ {"term": "内部定价", "category": "商业机密", "level": "高"}, {"term": "客户名单", "category": "个人信息", "level": "高"}, {"term": "项目代号", "category": "商业机密", "level": "中"}, {"term": "身份证号", "category": "个人信息", "level": "高", "regex": "\\d{17}[\\dXx]"} ] }支持正则表达式的词条可以匹配更复杂的模式,比如身份证号、手机号、银行卡号等。但正则匹配的性能开销比普通字符串匹配大得多,所以只对确实需要正则的词条启用。
3.2 匹配算法的选择:从朴素匹配到AC自动机
如果词库只有几十个词,用Python的in操作符逐个判断就够了。但当词库达到几千个词、待扫描文件有上万个时,朴素匹配的性能瓶颈就非常明显了。
这时候就需要Aho-Corasick自动机。它的核心思想是:把所有敏感词构建成一棵Trie树,然后一次性扫描文本,在O(n)时间内找出所有匹配的词,其中n是文本长度,与词库大小无关。
Python中可以用pyahocorasick库:
import ahocorasick A = ahocorasick.Automaton() for idx, word in enumerate(sensitive_words): A.add_word(word, (idx, word)) A.make_automaton() for end_index, (idx, word) in A.iter(text): start_index = end_index - len(word) + 1 matches.append((word, start_index, end_index))实测下来,对于5000个敏感词、10MB文本的场景,AC自动机比朴素匹配快两个数量级。这个性能差距在批量处理时是决定性的。
3.3 误报与漏报的平衡
敏感词扫描最头疼的问题就是误报和漏报的平衡。误报太多,业务方会被大量无意义的告警淹没,最终导致整个系统被弃用;漏报太多,系统就失去了存在的意义。
降低误报的常见手段:
- 白名单机制:某些词在特定上下文中不敏感。比如“内部”在“内部员工培训”中没问题,在“内部定价”中就有问题。可以通过上下文窗口判断,或者维护一个白名单短语列表。
- 词边界判断:英文敏感词需要判断词边界,避免“secret”匹配到“secretary”。中文虽然没有天然的词边界,但可以通过标点符号和空格做粗略切分。
- 最小长度限制:过短的敏感词(如单个汉字)误报率极高,建议至少两个字符。
降低漏报的手段:
- 同义词扩展:敏感词库中手动添加常见同义词和变体。比如“报价”可以扩展为“价格”“价位”“报价单”。
- 全角半角统一:扫描前把文本中的全角字符转为半角,避免“VIP”匹配不到“VIP”。
- 简繁转换:如果文档可能包含繁体中文,需要做简繁转换后再匹配。
- 去除干扰字符:有些人会在敏感词中间插入空格或特殊符号来规避扫描,比如“内 部 定 价”。扫描前需要把这类干扰字符去掉。
import re import unicodedata def normalize_text(text): # 全角转半角 text = unicodedata.normalize("NFKC", text) # 去除零宽字符 text = re.sub(r"[\u200b-\u200f\u2028-\u202f]", "", text) # 去除敏感词中间的空格和常见分隔符 text = re.sub(r"(?<=\w)[\s\-_\.]+(?=\w)", "", text) return text注意:去除空格这个操作要谨慎。它会把“项目 代号”变成“项目代号”,但也会把正常的英文短语“project code”变成“projectcode”。所以这个规则只对中文敏感词启用,或者只对特定模式启用。
4. 批量扫描系统的工程化落地
4.1 文件遍历与任务调度
批量扫描的核心是遍历目录、识别文件类型、分发处理任务。用os.walk或pathlib遍历目录树,根据扩展名和文件头判断类型,然后分发给对应的解析器。
from pathlib import Path from concurrent.futures import ProcessPoolExecutor def scan_directory(root_dir): files = [] for path in Path(root_dir).rglob("*"): if path.is_file() and path.suffix.lower() in SUPPORTED_EXTS: files.append(path) return files with ProcessPoolExecutor(max_workers=4) as executor: results = list(executor.map(scan_single_file, files))用ProcessPoolExecutor而不是ThreadPoolExecutor,是因为文档解析是CPU密集型任务,Python的GIL会严重限制多线程的性能。进程池虽然启动开销大一些,但整体吞吐量高得多。
并发数怎么定?经验值是CPU核心数。如果文件普遍很大(比如超过50MB),可以适当降低并发数,避免内存不足。如果文件都很小(几百KB),可以适当提高,但不要超过核心数的两倍。
4.2 大文件与超时处理
有些文件可能异常大,比如几百MB的PDF或者包含大量图片的PPT。这类文件解析起来可能耗时几分钟甚至更久。如果不加限制,一个文件就能把整个扫描任务卡死。
解决方案是给每个文件的解析设置超时:
import signal class TimeoutError(Exception): pass def timeout_handler(signum, frame): raise TimeoutError("解析超时") signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(60) # 60秒超时 try: text = extract_text(file_path) finally: signal.alarm(0)signal.alarm只在Unix系统上有效,Windows上需要用其他方式(比如在子进程中运行解析逻辑,主进程等待固定时间后终止子进程)。
对于确实无法在合理时间内解析的文件,直接标记为“解析超时,跳过”,记录到日志中,而不是无限等待。
4.3 结果输出与报告生成
扫描结果需要以业务方容易理解的方式呈现。我通常生成一份Excel报告,包含以下字段:
| 文件路径 | 文件类型 | 文件大小 | 是否涉敏 | 命中词 | 命中次数 | 风险等级 | 处理建议 |
|---|---|---|---|---|---|---|---|
| /data/合同.docx | docx | 245KB | 是 | 内部定价 | 3 | 高 | 人工复核 |
| /data/报告.pdf | 1.2MB | 否 | - | 0 | - | 无需处理 | |
| /data/名单.xlsx | xlsx | 89KB | 是 | 客户名单,身份证号 | 15 | 高 | 立即隔离 |
用openpyxl或pandas生成这份报告都很方便。如果结果需要对接工单系统,可以输出JSON格式,方便程序化处理。
import pandas as pd df = pd.DataFrame(results) df.to_excel("scan_report.xlsx", index=False)对于命中高风险词的文件,建议在报告中直接给出上下文片段,方便复核人员快速判断。上下文取命中位置前后各50个字符即可。
5. 常见问题与排查技巧实录
5.1 解析失败类问题
问题一:docx文件打开报错“不是有效的ZIP文件”
这种情况通常是文件损坏,或者文件实际上不是docx格式(比如被重命名了)。排查方法是读取文件头,看前两个字节是否为PK(ZIP文件的魔数)。如果不是,说明文件格式与扩展名不符。
问题二:PDF提取出来全是乱码
大概率是PDF使用了非标准编码,或者字体没有嵌入。可以尝试换一个解析库(比如从pdfplumber换到PyMuPDF),或者用pdfminer.six的底层API手动指定编码。
问题三:Excel读取时提示“文件格式不支持”
xls和xlsx用错了库。xls用xlrd,xlsx用openpyxl。如果扩展名和实际格式不符,需要先检测文件头再选择库。
5.2 性能类问题
问题一:扫描速度太慢
排查顺序:先看是不是用了朴素匹配而不是AC自动机;再看是不是单进程处理而不是多进程;最后看是不是每次扫描都重新加载了敏感词库。敏感词库应该在进程启动时加载一次,而不是每个文件都重新加载。
问题二:内存占用过高
大文件解析时内存飙升是正常的,但如果同时处理多个大文件,内存就会爆。解决方案是限制并发数,或者把大文件单独排队处理。
问题三:某些文件解析特别慢
通常是PDF或PPT中的图片导致的。可以在解析前先判断文件大小,超过阈值的文件走特殊处理流程(比如只提取前N页)。
5.3 准确率类问题
问题一:敏感词明明存在却扫不出来
排查步骤:先确认文本是否成功提取(打印提取出的文本看看);再确认敏感词库是否正确加载;然后检查是否有全角半角、简繁体、空格干扰等问题;最后确认匹配算法是否正确处理了大小写。
问题二:误报太多
检查敏感词库中是否有过短的词条(如单个汉字);检查是否缺少白名单机制;检查是否对英文词做了词边界判断。
问题三:同一个文件多次扫描结果不一致
这种情况通常是因为文件在扫描过程中被修改了,或者解析库的行为不稳定(比如PDF的文本提取顺序可能变化)。解决方案是扫描前先计算文件的哈希值,确保文件未被修改;对于解析结果不稳定的格式,可以多次解析取并集。
5.4 独家避坑技巧
技巧一:先做小规模验证再全量扫描
不要一上来就扫描整个文件服务器。先选100个文件做测试,人工核对扫描结果,确认准确率可接受后再扩大范围。我见过太多人直接全量扫描,结果误报率高达30%,业务方直接拒绝使用。
技巧二:敏感词库要版本化管理
敏感词库会不断调整,每次调整都应该记录版本号和变更内容。否则出了问题很难追溯是哪个版本的词库导致的。
技巧三:保留原始文件不动,只读不写
扫描系统应该只读文件,绝对不要修改或移动原始文件。所有处置动作(隔离、删除)都应该由人工确认后执行,或者由独立的处置模块负责。扫描系统把文件搞丢了,这个责任谁都担不起。
技巧四:日志要详细,但不要记录敏感内容
日志中记录文件路径、扫描时间、命中词、处理结果即可,不要把命中的敏感内容原文写进日志。否则日志本身就成了泄露渠道。
技巧五:定期用已知涉敏文件做回归测试
准备一批“已知答案”的测试文件,每次更新词库或修改代码后,跑一遍回归测试,确保没有引入新的漏报或误报。这批测试文件应该覆盖所有八种格式,以及各种边界情况(空文件、加密文件、超大文件、纯图片PDF等)。
6. 从单机脚本到可扩展系统的演进思路
6.1 什么时候需要从脚本升级到系统
单机脚本适合文件量在几千个以内、扫描频率不高的场景。当出现以下信号时,就该考虑升级了:
- 文件量超过十万,单次全量扫描超过一小时
- 需要支持增量扫描(只扫描新增或修改的文件)
- 需要多用户同时提交扫描任务
- 需要对接其他系统(如OA、工单、邮件告警)
- 需要保留历史扫描记录并支持查询
6.2 增量扫描的实现思路
增量扫描的核心是记录每个文件的“指纹”(通常是文件路径+修改时间+文件大小,或者文件内容的哈希值)。每次扫描时,只处理指纹发生变化的文件。
import hashlib import json import os def get_file_fingerprint(file_path): stat = os.stat(file_path) content = f"{file_path}|{stat.st_mtime}|{stat.st_size}" return hashlib.md5(content.encode()).hexdigest() # 加载上次扫描记录 with open("scan_cache.json", "r") as f: cache = json.load(f) # 只扫描指纹变化的文件 for file_path in all_files: fp = get_file_fingerprint(file_path) if cache.get(file_path) != fp: scan_single_file(file_path) cache[file_path] = fp这个方案简单有效,但要注意:如果文件内容变了但修改时间和大小没变(比如原地替换了相同大小的内容),指纹就不会变化,导致漏扫。更稳妥的做法是对文件内容做哈希,但那样每次都要读整个文件,性能开销大。折中方案是:日常用修改时间+大小做快速判断,定期(比如每周)做一次全量内容哈希校验。
6.3 分布式扫描的考量
当文件量达到百万级,单机无论如何优化都扛不住时,就需要分布式方案。核心思路是把文件列表分片,每台机器处理一部分,最后汇总结果。
分片策略可以按目录哈希、按文件大小、按文件类型等多种方式。最简单的是按文件路径的哈希值取模:
def get_shard(file_path, total_shards): return hash(file_path) % total_shards每台机器只处理属于自己分片的文件。汇总时把所有机器的结果合并即可。
分布式方案引入的复杂性在于:任务调度、故障重试、结果一致性。如果团队没有分布式系统的经验,建议先用单机多进程方案撑一撑,实在撑不住了再考虑分布式。很多情况下,优化解析速度(比如换更快的库、跳过不必要的文件)比上分布式更划算。
6.4 与现有系统的集成方式
扫描系统很少独立存在,通常需要与现有系统集成。常见的集成方式有:
- 命令行调用:把扫描脚本封装成命令行工具,其他系统通过subprocess调用。最简单,但灵活性差。
- HTTP API:把扫描功能封装成RESTful接口,其他系统通过HTTP调用。适合需要跨语言、跨平台集成的场景。
- 消息队列:扫描任务通过消息队列分发,扫描结果也通过消息队列回传。适合高并发、异步处理的场景。
- 文件系统监控:监控特定目录,发现新文件自动扫描。适合与共享盘、网盘等存储系统集成。
选择哪种方式取决于现有系统的架构和团队的技术栈。没有最好的方案,只有最合适的方案。
实操心得:不管用哪种集成方式,都要做好幂等性设计。同一个文件被重复提交扫描时,应该返回缓存结果而不是重新扫描。这既能提高响应速度,也能避免重复计算浪费资源。
7. 敏感词库的运营与迭代
7.1 词库从哪来
敏感词库的来源通常有以下几个渠道:
- 业务方提供:法务、合规、业务部门根据实际需求提供的词表。这是最核心的来源。
- 历史泄露事件复盘:从过往的信息泄露事件中提取关键词,补充到词库中。
- 行业通用词表:一些行业协会有公开的敏感信息分类标准,可以参考。
- 自动挖掘:通过分析大量文档,用TF-IDF等算法找出高频且具有区分度的词,人工审核后加入词库。
7.2 词库的定期评审机制
词库不是建好就一劳永逸的。业务在变化,敏感信息的定义也在变化。建议每季度做一次词库评审,内容包括:
- 删除已经过时的词条(比如已经公开的项目代号)
- 添加新出现的敏感词(比如新项目的代号)
- 调整词条的风险等级
- 根据误报反馈优化白名单
评审应该由IT、法务、业务方共同参与,不能由IT单方面决定。
7.3 误报反馈的收集与处理
误报反馈是优化词库的重要输入。建议在扫描报告中提供一个“反馈”入口,让复核人员可以标记“这是误报”。收集到的误报数据定期分析,找出误报率高的词条,针对性优化。
误报率高的词条通常有几类:过于通用的词(如“内部”“方案”)、多义词(如“苹果”既指水果也指品牌)、常见人名(如果人名本身是敏感词)。对这几类词,要么加白名单,要么加更严格的上下文条件。
8. 一些实际部署中的经验之谈
部署这套系统时,有几个容易被忽视但影响很大的点。
文件权限问题。扫描系统运行的服务账号需要对目标目录有读取权限。如果目录权限配置不当,会出现“文件存在但读不了”的情况。建议在部署前先用服务账号手动测试几个文件的读取。
网络盘性能问题。如果文件存储在网络盘上,读取速度可能比本地盘慢一个数量级。这种情况下,并发数要适当降低,否则大量进程同时等待网络IO,反而降低整体吞吐量。
文件被占用问题。Windows上,如果文件正在被其他程序打开,扫描程序可能无法读取。需要处理PermissionError异常,把这类文件标记为“暂时无法扫描”,稍后重试。
编码问题。文件路径中可能包含中文、空格、特殊字符。在拼接命令行参数时要注意转义,否则会出现“文件找不到”的错误。用pathlib和subprocess的列表参数形式可以避免大部分转义问题。
日志轮转。扫描系统运行久了,日志文件会越来越大。需要配置日志轮转,避免磁盘被日志占满。Python的logging.handlers.RotatingFileHandler可以按大小轮转,TimedRotatingFileHandler可以按时间轮转。
监控告警。扫描系统本身也需要被监控。关键指标包括:扫描任务是否按时执行、扫描成功率、平均扫描耗时、命中敏感词的文件数量趋势。如果命中数量突然暴增,可能意味着词库配置有问题,或者确实发生了大规模泄露事件,需要及时排查。
这套方案从最初的一个简单脚本,到后来支撑日均十万文件的扫描量,中间踩过的坑不计其数。最深的体会是:技术方案本身不难,难的是让业务方信任扫描结果,愿意根据扫描结果采取行动。这需要持续优化准确率、提供清晰的报告、建立反馈闭环。技术只是手段,解决实际问题才是目的。