一、【需求澄清】

很多领域需要对关键信息进行处理,比如带有业务数据的文件,比如带有敏感信息(身份证手机号)的各种文件,再比如有一些敏感词的文件。当文件多了之后,如果不搞一个工具来批量脱敏,会非常痛苦,笔者前段时间抽个晚上用py写了一个简陋的脱敏工具,感兴趣可以看一下。

不是生产代码,没有分享风险

二、【效果展示】

直接上效果图

python脱敏字符串工具包 python数据脱敏处理_开发语言

如图,可以对单个文件或文件夹做脱敏,然后根据需要选中需要脱敏的数据类型,当选中keywords时需要额外加一个字典文件(每行一个关键词的字典)。没有太复杂的地方主要用了三个包:re(正则)、thinter(面板)、docx(处理word文档)

使用时通过前端代码进入程序,然后选择需要脱敏的目标,如果要做关键词脱敏,需要准备一个字典文件,一行一个关键词即可,不要加额外符号。

笔者这边准备了一个待脱敏的文本,可以看一下效果

python脱敏字符串工具包 python数据脱敏处理_敏捷开发_02

三、【痛点暴露】

这种简陋的工具,通过正则和字典规避一些关键词还是可以做到的,但是对于这种语句

“大家好,我要给自己开个盒我的密码是:mypa$$word怎么样是不是密码很简单”

这种语句程序只会把密码变成非敏感词但是后面的密码并没有完全隐去,我们需要一种更强力的混淆方法,笔者之前没有接触过数据脱敏相关知识,想出了一个馊主意:

对一行之内出现关键词之后的内容,每个字符中间随机插入1~3个字母,来做到脱敏的效果,但是如果关键词之后还有其他内容,这个内容可读性变差,关于这一点会在后面的代码中注释说明。

四、【代码分享】

这个代码笔者跑过了,可以直接跑,如果要打包成exe,可以使用pyinstaller

pyinstaller --onefile --noconsole --icon=path/to/your/icon.ico 前端文件.py

然后一共两个文件,一个前端一个后端,前端文件做程序入口。

# -*- encoding: utf-8 -*-
'''
@File    :   Checking_Sensitive.py
@Time    :   
@Author  :   
@Version :   1.0
@Contact :   
@describe:   这个是后端文件,不作为程序入口,
             写个简单的脚本,用来脱敏文件,结束后把这个打包成exe
'''
import re
import os
import random,string
from docx import Document


# 这个函数正则替换,没有太多意义,只是调换了一下入参顺序和改名字
def re_replaces(input_str, pattern, replacement, flag=True):
    if flag:
        return re.sub(pattern, replacement, input_str, flags=re.I)
    return input_str

def obfuscate_password(input_str, keyword):
    # 诸如 password: this is password这种敏感信息,如果只删除password关键词不能完全脱敏,
    # 这个函数用来将关键词之后的内容,每隔一个字符插入1~3个随机字母,这种方法会导致大量内容不可读
    pattern = f"{re.escape(keyword)}(.+)"
    match = re.search(pattern, input_str, flags=re.IGNORECASE)
    
    if match:
        original_key = match.group(1)
        # 在每个字母之间插入1~3个随机字母
        # obfuscated_key = ''.join([char + random.choice(string.ascii_letters) for char in original_key])
        obfuscated_key = ''.join([char + ''.join(random.choices(string.ascii_letters, k=random.randint(1, 3))) for char in original_key])
        
        # 替换原始密钥部分,内容是拼接来的
        replacement = f"{keyword}{obfuscated_key}"
        input_str = re_replaces(input_str, pattern, replacement)
    
    return input_str
    
class AntiSensitive():
    # 这里定义了一个类,这里面写了替换word和txt,然后写了一些需要的函数
    def __init__(self, DOC_file_path, keywords_file, flags) -> None:
        self.DOC_file_path = DOC_file_path
        self.keywords_file = keywords_file
        self.flags = flags  # flags 是一个字典,包含各种脱敏选项的开关

        file_extension = os.path.splitext(DOC_file_path)[1].lower() # 这里初始化了一些需要的
        if file_extension == '.docx':
            self.DOC_new_path = f"{DOC_file_path.split('.')[0]}-new.docx"
        elif file_extension == '.txt':
            self.DOC_new_path = f"{DOC_file_path.split('.')[0]}-new.txt"
        else:
            self.DOC_new_path = f"{DOC_file_path.split('.')[0]}-new{file_extension}"
        
    def replace_words_document(self):  # 该函数用于替换docx文件
        # doc = Document(self.DOC_file_path)
    
        # for paragraph in doc.paragraphs:
        #     paragraph.text = self.process_text(paragraph.text)
        # doc.save(self.DOC_new_path)
        
        try:
            doc = Document(self.DOC_file_path)

            for paragraph in doc.paragraphs:
                paragraph.text = self.process_text(paragraph.text)

            doc.save(self.DOC_new_path)
            print("文档处理完成")

        except Exception as e:
            print(f"发生错误:{e}")

    def replce_words_text(self):  # 该函数用于替换文本文件
        try:
            with open(self.DOC_file_path, 'r', encoding='utf-8') as input_file:
                with open(self.DOC_new_path, 'w', encoding='utf-8') as output_file:
                    for line in input_file:
                        line = self.process_text(line)
                        output_file.write(line)
                        
        # 错误处理,不用太多关注这一块
        except FileNotFoundError:
            print(f"文件 '{self.DOC_file_path}' 未找到。")
        except IOError as e:
            print(f"读取文件 '{self.DOC_file_path}' 时发生 I/O 错误: {e}")
        except Exception as e:
            print(f"发生未知错误: {e}")
            # print(f"Error processing line: {line}\nError: {e}")
            
    def replace_words(self):    # 该函数用来替换docx文档
        if self.DOC_file_path.split('.')[-1] == 'docx':
            self.replace_words_document()
            
        # elif self.DOC_file_path.split('.')[-1] == 'txt':
        else:
            self.replce_words_text()
    
    def safe_words_replace(self,str,file_path):  # 维护一个安全词文件,每行一个
            try:
                with open(file_path, 'r',encoding='utf-8') as lines:
                    safe_words = []
                    line = lines.readline() # 每次读一行,防止文件过大,一次内存过大,虽然VPC应该不会有太多数据,但是好习惯益终身
                    while line:
                        safe_words.append(line.strip())
                        line = lines.readline() 
            
            # 错误处理,不用关注这一块
            except FileNotFoundError:
                print(f"文件 '{file_path}' 未找到。")
            except IOError as e:
                print(f"读取文件 '{file_path}' 时发生 I/O 错误: {e}")
            except Exception as e:
                print(f"发生未知错误: {e}")
            
            # 把安全词丢到了元组里,接下来我们需要写一个替换,替换进来的字符串,替换完成后吐出去
            for i,ele in enumerate(safe_words):
                if ele.lower() in str.lower():
                    # 下面这行是强混淆,打开之后脱敏效果增强但是会影响可读性
                    # str = obfuscate_password(str,ele)       # 建议别用这个,这个是强混淆,会在识别到关键词后,之后的每个关键词随即插入1~3个字母
                    str = re_replaces(str,ele+'.{3}',f"{ele[:1]}1{ele[1:]}****{''.join(random.sample(string.ascii_letters, 2))}")
                    
            return str
    
    def process_text(self, text):
        # 根据标志执行脱敏,用的正则
        text = re_replaces(text, r'((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}', "IPADDRESS_WARNING", self.flags.get('ip', False))
        text = re_replaces(text, r'\b(([\w-]+://?|www[.])[^\s()<>]+(?:[\w\d]+[\w\d]+|([^[:punct:]\s]|/)))', "AN_URL", self.flags.get('url', False))
        text = re_replaces(text, r'0?(13|14|15|18|17)[0-9]{9}', "AN_PHONE_NUMBER", self.flags.get('phone', False))
        text = re_replaces(text, r'[0-9-()()]{7,18}', "FIXED_LINE", self.flags.get('fixed_line', False))
        text = re_replaces(text, r'\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}', "AN_EMAIL", self.flags.get('email', False))
        text = re_replaces(text, r'\d{17}[\d|x]|\d{15}', "PERSONAL_ID", self.flags.get('personal_id', False))

        if self.flags.get('keywords', False):
            text = self.safe_words_replace(text, self.keywords_file)

        return text
    
    def process_folder(self, input_folder, output_folder):  # 二期工程,加一个按照文件夹批量处理
        for root, dirs, files in os.walk(input_folder):
            for file in files:
                file_path = os.path.join(root, file)
                file_extension = os.path.splitext(file)[1].lower()

                # 生成在输出文件夹中的相对路径
                relative_path = os.path.relpath(root, input_folder)
                output_path = os.path.join(output_folder, relative_path)

                # 确保输出路径存在,最好做一下,考虑稍微充分一点
                os.makedirs(output_path, exist_ok=True)
                
                # 生成一个新的文件夹,用于脱敏后的所有文件
                self.DOC_file_path = file_path
                if file_extension == '.docx':
                    self.DOC_new_path = os.path.join(output_path, f"{os.path.splitext(file)[0]}-new.docx")
                    self.replace_words_document()
                else:
                    self.DOC_new_path = os.path.join(output_path, f"{os.path.splitext(file)[0]}-new{file_extension}")
                    self.replce_words_text()

if __name__ == '__main__':
    
    flags = {
    'ip': False,
    'url': False,
    'phone': False,
    'fixed_line': False,
    'email': False,
    'personal_id': False,
    'keywords': False
}
    # region 处理文件夹
    input_folder = r''  # 输入文件夹路径
    output_folder = r''  # 输出文件夹路径
    keywords_file = r''  # 关键词文件路径

    anti_sensitive = AntiSensitive('', keywords_file, flags)
    anti_sensitive.process_folder(input_folder, output_folder)      
    # endregion
    
    # region 处理单个文件
    DOC_FILE        =   r""
    KEYWORDS_FILE   =   r""
    TXT_job = anti_sensitive(DOC_FILE, KEYWORDS_FILE)
    TXT_job.replace_words()
    # endregion

前端代码,从这里做程序入口

from AntiSensiGUI_backend import AntiSensitive  # 引入后端文件
from tkinter import filedialog, messagebox, ttk
import tkinter as tk
import os

class SensitiveReplaceApp:
    def __init__(self, master):
        # 初始化一些东西
        self.master = master
        self.master.title("简陋脱敏程序")
        # 设置窗口图标,如果需要可以自己找个ico图加一下
        self.master.iconbitmap(r"")

        # 单选框选择文件或文件夹,顾名思义
        self.file_or_folder = tk.StringVar(value="file")
        ttk.Radiobutton(master, text="文件", variable=self.file_or_folder, value="file").pack(anchor='w')
        ttk.Radiobutton(master, text="文件夹", variable=self.file_or_folder, value="folder").pack(anchor='w')

        self.doc_file_label = tk.Label(master, text="待脱敏文档或文件夹 (文本 或 docx)" )
        self.doc_file_label.pack(anchor='w')

        self.doc_file_entry = tk.Entry(master, width=70)
        self.doc_file_entry.pack()

        self.browse_button = tk.Button(master, text="浏览", command=self.browse_doc_file)
        self.browse_button.pack()

        # 关键词文件路径框
        self.keywords_file_label = tk.Label(master, text="字典文件 (仅当需要脱敏关键词时):")
        self.keywords_file_label.pack(anchor='w')

        self.keywords_file_entry = tk.Entry(master, width=70)
        self.keywords_file_entry.pack()

        self.browse_keywords_button = tk.Button(master, text="浏览", command=self.browse_keywords_file)
        self.browse_keywords_button.pack()

        # 脱敏选项,用一堆flag表示,多选框
        self.flags_frame = ttk.LabelFrame(master, text='脱敏选项')
        self.flags_frame.pack(fill='x', expand=True)

        self.flag_vars = {
            'ip': tk.BooleanVar(),
            'url': tk.BooleanVar(),
            'phone': tk.BooleanVar(),
            'fixed_line': tk.BooleanVar(),
            'email': tk.BooleanVar(),
            'personal_id': tk.BooleanVar(),
            'keywords': tk.BooleanVar()
        }

        for i, (key, var) in enumerate(self.flag_vars.items()):
            ttk.Checkbutton(self.flags_frame, text=key, variable=var).grid(row=0, column=i, sticky='w')

        # 定义一个开始脱敏按钮
        self.replace_button = tk.Button(master, text="开  始  脱  敏", command=self.replace_sensitive_data, width=40)
        self.replace_button.pack(padx=10)

    def browse_doc_file(self):    # 浏览文件用的
        if self.file_or_folder.get() == "file":
            doc_file_path = filedialog.askopenfilename(title="选择文件", filetypes=[("All Files", "*.*")])
        else:
            doc_file_path = filedialog.askdirectory(title="选择文件夹")
        self.doc_file_entry.delete(0, tk.END)
        self.doc_file_entry.insert(0, doc_file_path)

    def browse_keywords_file(self):
        keywords_file_path = filedialog.askopenfilename(title="选择字典文件", filetypes=[("Text Documents", "*.txt")])
        self.keywords_file_entry.delete(0, tk.END)
        self.keywords_file_entry.insert(0, keywords_file_path)

    def replace_sensitive_data(self):
        doc_file_path = self.doc_file_entry.get()
        keywords_file_path = self.keywords_file_entry.get()
        flags = {k: v.get() for k, v in self.flag_vars.items()}

        # 检查文件路径是否有效,并且当且仅当需要处理关键词时检查字典文件路径
        if os.path.exists(doc_file_path) and (not flags['keywords'] or (flags['keywords'] and os.path.exists(keywords_file_path))):
            doc_job = AntiSensitive(doc_file_path, keywords_file_path, flags)
            if self.file_or_folder.get() == "folder":
                doc_job.process_folder(doc_file_path, doc_file_path+'-new')  # 指定输出文件夹路径
            else:
                doc_job.replace_words()
            messagebox.showinfo("Success", "Sensitive data replaced successfully.")
            print("Sensitive data replaced successfully.")
        else:
            error_message = "Please provide a valid document/file path."
            if flags['keywords'] and not os.path.exists(keywords_file_path):
                error_message += "\nPlease provide a valid keywords file path."
            print(error_message)
            messagebox.showerror("Error", error_message)

if __name__ == '__main__':
    root = tk.Tk()
    app = SensitiveReplaceApp(root)
    root.mainloop()

五、【后期展望】(画饼)

这个自己用还是比较方便的,可以过关键词审查

但是

1. 不支持灵活添加新的正则表达式,比如想匹配一个MD5,但是程序不支持,需要改源码

2. 自己用够了,但是自己做的快没什么用,最好让团队快起来,要推广开的话最好使用flask或者django丢到网页上运行。