【数据保护进阶之路】微软开源Presidio项目详解:从入门到精通掌握数据保护之道

文章目录

1 简介

2 敏感数据分析器

2.1 简单示例

2.2 敏感信息列表

2.3 正则表达式

2.4 自定义识别器

2.5 上下文增强机制

2.6 白名单

2.7 追踪决策日志

2.8 YAML配置文件

3 敏感数据匿名器

3.1 简单示例

3.2 自定义匿名配置

3.3 自定义匿名器

3.4 加密和解密

4 NLP模型和语言类型


1 简介

Presidio,源自拉丁语,寓意"保护"或"驻军",是由微软推出的一项开源数据保护计划。该项目致力于协助企业与开发者在处理数据时,快速识别并脱敏敏感信息。它能够识别文本和图像中的多种敏感数据,包括但不限于信用卡号码、个人姓名、地理位置和电话号码等,并通过定制化的格式进行脱敏处理,以增强数据的安全性。

        主要特点

  • 内置40多种敏感数据识别器,包括日期、邮箱地址、银行账号、IP地址、地理信息等。
  • 支持用户自定义敏感数据识别器,包括配置敏感数据列表、正则表达式以及自定义敏感数据检测模块等。
  • 支持使用NLP模型、上下文检测来提升扫描结果置信度。
  • 支持多种NLP模型,包括SpaCy、Transformers和Stanza。
  • 支持多种方式集成,可以作为Python库使用,也可以通过Docker容器化部署。
  • 支持多种脱敏方式,例如替换、哈希、掩码等,包括用户自定义方式。
  • 支持文本和图片类型敏感信息扫描,其中图片类型扫描依赖于OCR库。
  •         模块

  • Presidio analyzer:该模块主要负责文本类数据敏感信息扫描。
  • Presidio anonymizer:该模块主要负责对已检测到的敏感实体进行脱敏处理。
  • Presidio image redactor:该模块主要利用OCR技术识别图片中敏感信息并进行脱敏处理。
  • 2 敏感数据分析器

    Presidio分析器(Analyzer)中包含多种敏感数据识别器(Recognizer),每种识别器支持特定类型的敏感数据扫描。识别器支持正则表达式、敏感数据列表、自定义识别逻辑,并且每个识别器支持使用NLP模型以及上下文检测,对每个敏感信息目标进行评分来体现扫描结果的置信度。

    需要注意的是,下面我在对Presidio介绍的时候会强调区分分析器和识别器,分析器对象中会包含若干个识别器,而识别器主要识别特定类型敏感数据。

    2.1 简单示例

    下面是对Presidio分析器使用的最基础的示例,包括创建分析器对象、扫描文本数据以及打印结果:

    # 导入分析器模块
    from presidio_analyzer import AnalyzerEngine  
    
    # 待扫描的文本数据
    text = "His name is Mr. Jones and his phone number is 212-555-5555"
    
    # 创建分析器对象
    analyzer = AnalyzerEngine()
    # 执行分析器的分析函数,分析文本数据
    analyzer_results = analyzer.analyze(text=text, language="en")
    
    # 打印执行结果
    print(analyzer_results)

    上面示例中没有指定分析器执行哪种类型敏感信息扫描,因此默认情况下分析器将加载所有内置识别器对文本数据执行扫描。扫描结果如下,结果以列表的形式呈现。

    [type: PERSON, start: 16, end: 21, score: 0.85, type: PHONE_NUMBER, start: 46, end: 58, score: 0.75]

    扫描结果中主要包含四个关键字段分别是type、start、end和score,其含义如下:

  • type:敏感信息类型;
  • start:敏感信息起始位置;
  • end:敏感信息结束位置;
  • score:该敏感信息扫描结果置信度。
  • 2.2 敏感信息列表

    Presidio具有极强的可扩展性,除了内置部分敏感信息识别器外,支持用户自定义敏感识别器并加入到分析器中。敏感信息列表是创建识别器的一种方式。

    用户可以将已确定的敏感词语汇总起来,以列表的形式添加到新创建的识别器当中,并将新创建的识别器添加到分析器中执行对目标数据的扫描。如果目标数据中包含敏感信息列表中的词语,该词语会在返回结果中体现。

    假设下面是已汇总的敏感单词信息:

    password、secret、China、Government

    第一步:将这些敏感信息以列表的形式存储:

    sensitive_list = ["password", "secret", "China", "Government"]

    第二步:新建敏感信息识别器,并将敏感信息列表添加到识别器当中:

    from presidio_analyzer import PatternRecognizer
    
    // 参数supported_entity标识敏感信息类型名称,同上面示例中PERSON、PHONE_NUMBER等
    // 参数deny_list即为敏感信息列表。
    sensitive_recognizer = PatternRecognizer(supported_entity="SENSITIVE", deny_list=sensitive_list)

    进行到这一步,其实我们可以直接对文本内容进行扫描了:

    from presidio_analyzer import PatternRecognizer
    
    text1 = "I have a secret, but I can't tell you."
    result = sensitive_recognizer.analyze(text1, entities=["SENSITIVE"])
    print(f"Result:\n {result}")
    
    # 下面是执行结果
    '''
    Result:
     [type: SENSITIVE, start: 9, end: 15, score: 1.0]
    '''

    最后我们需要将新创建的识别器添加到分析器的识别器列表中即可:

    from presidio_analyzer import AnalyzerEngine
    
    analyzer = AnalyzerEngine()
    analyzer.registry.add_recognizer(sensitive_recognizer)

    综上,支持自定义敏感信息列表扫描的分析器创建主要分为以下四步:

    (1)汇总敏感信息词语,并以列表的形式呈现;

    (2)创建敏感信息识别器,将敏感信息列表添加到识别器中;

    (3)创建敏感信息分析器,将识别器添加到识别器列表中;

    (4)执行分析器的分析函数扫描指定目标内容;

    上面四个步骤的示例代码如下:

    from presidio_analyzer import AnalyzerEngine, PatternRecognizer
    
    # (1)汇总敏感信息词语,并以列表的形式呈现;
    sensitive_list = ["password", "secret", "China", "Government"]
    
    text1 = "I have a secret, but I can't tell you."
    
    # (2)创建敏感信息识别器,将敏感信息列表添加到识别器中;
    sensitive_recognizer = PatternRecognizer(supported_entity="SENSITIVE", deny_list=sensitive_list)
    
    # (3)创建敏感信息分析器,将识别器添加到识别器列表中;
    analyzer = AnalyzerEngine()
    analyzer.registry.add_recognizer(sensitive_recognizer)
    
    # (4)执行分析器的分析函数扫描指定目标内容;
    result = analyzer.analyze(text=text1, language="en", entities=["SENSITIVE"])
    print(f"Result:\n {result}")

    2.3 正则表达式

    除了添加敏感信息列表外,Presidio还支持用户通过正则表达式来描述敏感数据特征。

    假设我们需要扫描出文本内容中所有数字信息,那么我们可以设计一个正则表达式用于识别器扫描:

    from presidio_analyzer import Pattern, PatternRecognizer
    
    # 正则表达式"\d+"表示匹配连续一个或多个数字
    # 参数score为基础得分,即命中此正则表达式时,置信度得分0.5
    numbers_pattern = Pattern(name="numbers_pattern", regex="\d+", score=0.5)
    
    # 将带有正则表达式的Pattern对象添加到识别器中
    number_recognizer = PatternRecognizer(
        supported_entity="NUMBER", patterns=[numbers_pattern]
    )

    此时新创建的敏感数据分析器就可以执行扫描操作了:

    text2 = "I live in 510 Broad st."
    
    numbers_result = number_recognizer.analyze(text=text2, entities=["NUMBER"])
    print("Result:\n" ,numbers_result)
    
    # 下面是执行结果
    '''
    Result:
     [type: NUMBER, start: 10, end: 13, score: 0.5]
    '''

    最后我们需要将新创建的识别器添加到分析器的识别器列表中即可。

    综上,支持自定义正则表达式扫描的分析器创建主要分为以下四步:

    (1)根据目标敏感信息特征,编写正则表达式,并创建模式对象(Pattern);

    (2)创建敏感信息识别器,将模式对象添加到识别器中;

    (3)创建敏感信息分析器,将识别器添加到识别器列表中;

    (4)执行分析器的分析函数扫描指定目标内容;

    上面四个步骤的示例代码如下:

    from presidio_analyzer import Pattern, PatternRecognizer, AnalyzerEngine
    
    # (1)根据目标敏感信息特征,编写正则表达式,并创建模式对象(Pattern);
    numbers_pattern = Pattern(name="numbers_pattern", regex="\d+", score=0.5)
    
    # (2)创建敏感信息识别器,将模式对象添加到识别器中;
    number_recognizer = PatternRecognizer(
        supported_entity="NUMBER", patterns=[numbers_pattern]
    )
    
    text2 = "I live in 510 Broad st."
    
    # (3)创建敏感信息分析器,将识别器添加到识别器列表中
    analyzer = AnalyzerEngine()
    analyzer.registry.add_recognizer(number_recognizer)
    
    # (4)执行分析器的分析函数扫描指定目标内容;
    result = analyzer.analyze(text=text2, language="en", entities=["NUMBER"])
    print(f"Result:\n {result}")

    2.4 自定义识别器

    自定义敏感数据识别器是Presidio强大的扩展特性之一。在2.3章节我们提到对于数字的匹配我们可以编写正则表达式来进行扫描,但是所编写的正则表达式具有局限性,例如只能扫描出"1、2、3"这种,而对于"One、Two、Three"这种形式却束手无策。

    针对上面这种情况,用户可以创建自定义敏感数据识别器来执行对数据的扫描。除此之外,在自定义扫描器中还可以利用spaCy库对文本内容分词的结果辅助执行数据扫描。

    第一步:创建自定义识别器类,要求此类要继承EntityRecognizer类型,并且需要重写load和analyze方法。除此之外,analyze方法中传入一个NlpArtifacts类型的对象,此对象中包含NLP模型对文本内容分词的结果。

    from typing import List
    from presidio_analyzer import EntityRecognizer, RecognizerResult
    from presidio_analyzer.nlp_engine import NlpArtifacts
    
    class NumbersRecognizer(EntityRecognizer):
        # 置信度
        expected_confidence_level = 0.7  
    
        def load(self) -> None:
            pass
    
        def analyze(
            self, text: str, entities: List[str], nlp_artifacts: NlpArtifacts
        ) -> List[RecognizerResult]:
            """
            下面算法既能找到"1、2、3",也能找到"One、Two、Three"。
            """
            results = []
    
            # iterate over the spaCy tokens, and call `token.like_num`
            for token in nlp_artifacts.tokens:
                if token.like_num:
                    result = RecognizerResult(
                        entity_type="NUMBER",
                        start=token.idx,
                        end=token.idx + len(token),
                        score=self.expected_confidence_level,
                    )
                    results.append(result)
            return results
    
    
    # 创建新的敏感信息识别器对象。
    new_numbers_recognizer = NumbersRecognizer(supported_entities=["NUMBER"])

    第二步:创建新的分析器,并将自定义识别器添加到识别器列表中即可。

    from presidio_analyzer import AnalyzerEngine
    
    text3 = "Roberto lives in Five 10 Broad st."
    analyzer = AnalyzerEngine()
    analyzer.registry.add_recognizer(new_numbers_recognizer)
    
    numbers_results2 = analyzer.analyze(text=text3, language="en")
    print("Results:")
    print("\n".join([str(res) for res in numbers_results2]))

    上面示例中敏感信息识别器既可以识别数字,也可以识别数字类单词。

    下面是上面示例执行结果:

    Results:

    type: PERSON, start: 0, end: 7, score: 0.85

    type: LOCATION, start: 25, end: 34, score: 0.85

    type: NUMBER, start: 17, end: 21, score: 0.7

    type: NUMBER, start: 22, end: 24, score: 0.7

    2.5 上下文增强机制

    Presidio支持上下文检测机制,该机制能够提升对检测结果的置信度。除此之外,Presidio还支持由用户自己实现上下文检测逻辑。默认情况下,Presidio使用LemmaContextAwareEnhancer来实现上下文增强机制,该类会对所扫描句子中每个token的词元和上下文内容进行比较。

    下面示例中我们将邮政编码作为敏感数据,创建该敏感数据的识别器,并通过上下文机制增强对敏感数据扫描结果的置信度。

    由于邮政编码的组成比较简单,使用六位数字组成。正因如此,扫描文本内容时如果将所有六位数字组成的信息当成邮政编码,那显然会有很多识别结果是错误的。因此如果我们使用六位数字这个特征去匹配的话,匹配结果的置信度是比较低的,在这种情况下,可以使用上下文增强机制来提升检测结果的置信度。

    第一步,创建邮政编码的模式对象(Pattern):

    from presidio_analyzer import (
        Pattern,
        PatternRecognizer,
        RecognizerRegistry,
        AnalyzerEngine,
    )
    
    regex = r"\d{6}"
    # score为得分,即文本内容命中此正则表达式时得分,得分大小标识着扫描结果的置信度,
    # 由于当前敏感数据内容特征简单,因此即使正则表达式命中,那么得分也应该设置为较低值。
    postal_code_pattern = Pattern(name="postal code", regex=regex, score=0.02)

    第二步,创建邮政编码的识别器,并且开启上下文增强机制:

    from presidio_analyzer import PatternRecognizer
    
    # 创建邮政编码识别器,并且将上下文信息传入context参数。
    postal_recognizer_w_context = PatternRecognizer(
        supported_entity="ZH_ZIP_CODE",
        patterns=[postal_code_pattern],
        context=["postal", "postalcode"],
    )

    第三步,创建邮政编码分析器,分析器引擎(AnalyzerEngine)默认创建LemmaContextAwareEnhancer类对象作为上下文增强引擎,该引擎通过比对扫描内容中各个Token的词元与上下文列表是否匹配来进行判断。

    from presidio_analyzer import AnalyzerEngine
    
    analyzer = AnalyzerEngine()
    analyzer.registry.add_recognizer(postal_code_recognizer)
    
    results = analyzer.analyze(text="The postal code for Beijing is 100000.", language="en")
    print(f"Result:\n {results}")

    综上,分析器已支持基于上下文的中国邮政编码扫描。

    示例整体代码如下:

    from presidio_analyzer import (
        Pattern,
        PatternRecognizer,
        RecognizerRegistry,
        AnalyzerEngine,
    )
    
    regex = r"\d{6}"
    postal_code_pattern = Pattern(name="postal code", regex=regex, score=0.02)
    
    postal_code_recognizer = PatternRecognizer(
        supported_entity="CH_POSTAL_CODE", patterns=[postal_code_pattern]
    )
    
    analyzer = AnalyzerEngine()
    analyzer.registry.add_recognizer(postal_code_recognizer)
    
    results = analyzer.analyze(text="The postal code for Beijing is 100000.", language="en")
    
    print(f"Result:\n {results}")

    执行结果如下:

    Result:

     [type: LOCATION, start: 20, end: 27, score: 0.85, type: CH_POSTAL_CODE, start: 31, end: 37, score: 0.4, type: US_DRIVER_LICENSE, start: 31, end: 37, score: 0.01]

    通过分析上面结果我们发现,这段文本除了扫描出我们定义的邮政编码外,还有包含了其它类型敏感信息结果,因为上面代码中分析器除了包含邮政编码的识别器外,还有一些默认的是识别器。更有意思的是,"100000"除了被识别为邮政编码外,还被认为是车牌号,说明这串数字特征过于普通导致。

    下面创建分析器的方法可以不加载默认的识别器:

    from presidio_analyzer import AnalyzerEngine, RecognizerRegistry
    
    registry = RecognizerRegistry()
    registry.add_recognizer(postal_code_recognizer_w_context)
    analyzer = AnalyzerEngine(registry=registry)

    除此之外,我们还注意到扫描结果中,对于CH_POSTAL_CODE的置信度(score)为0.4,在创建模式特征(Pattern)时设置的正则表达式命中的得分(score)是0.02,如果不添加上下文的话,扫描结果置信度或是0.02,但是由于开启了上下文增强机制,且上下文能命中扫描的内容,因此得分就上升到0.4。

    LemmaContextAwareEnhancer类默认将上下文匹配命中的内容实体增加0.35分,而上下文增强模块要求如果内容串上下文也命中的情况下,得分最小应该为0.4分。因此在模式特征为0.02,上下文命中增加0.35分的情况下仍小于0.4分,因此最后得分为0.4。

    当然我们可以通过更改context_similarity_factor和min_score_with_context_similarity参数来更改上下文检测引擎的默认值。context_similarity_factor为命中上下文增加的得分。min_score_with_context_similarity为命中上下文后最低分值。示例如下:

    from presidio_analyzer import (
        Pattern,
        PatternRecognizer,
        RecognizerRegistry,
        AnalyzerEngine,
    )
    from presidio_analyzer.context_aware_enhancers import LemmaContextAwareEnhancer
    
    regex = r"\d{6}"
    postal_code_pattern = Pattern(name="postal code", regex=regex, score=0.02)
    
    postal_code_recognizer = PatternRecognizer(
        supported_entity="CH_POSTAL_CODE", patterns=[postal_code_pattern],
        context=["postal", "PostalCode"]
    )
    
    # 创建LemmaContextAwareEnhancer对象,并将想要更改的得分机制传入
    context_aware_enhancer = LemmaContextAwareEnhancer(
        context_similarity_factor=0.45, min_score_with_context_similarity=0.4
    )
    
    # 下面这种创建分析器的方法只加载邮政编码的识别器,不加载默认的其它识别器。
    registry = RecognizerRegistry()
    registry.add_recognizer(postal_code_recognizer)
    analyzer = AnalyzerEngine(registry=registry, context_aware_enhancer=context_aware_enhancer)
    
    results = analyzer.analyze(text="The postal code for Beijing is 100000.", language="en")
    
    print(f"Result:\n {results}")

    执行结果如下:

    Result:

    [type: CH_POSTAL_CODE, start: 31, end: 37, score: 0.47000000000000003]

    2.6 白名单

    Presidio支持白名单列表,我们只需要将一些关键字放入白名单列表中,即使这些关键字属于敏感信息,也会被忽略掉,不会包含在扫描结果中。

    下面是白名单的使用示例:

    from presidio_analyzer import AnalyzerEngine
    
    white_list = [
        "Beijing",
        "John"
    ]
    
    text = "His name is Mr. John and his phone number is 212-555-5555, he live in Beijing."
    
    analyzer_1 = AnalyzerEngine()
    result = analyzer_1.analyze(text=text, language='en')
    print("添加白名单前扫描结果:", result)
    
    analyzer_2 = AnalyzerEngine()
    result = analyzer_2.analyze(text=text, language='en', allow_list=white_list )
    print("添加白名单后扫描结果:", result)

    上面示例执行结果如下:

    添加白名单前扫描结果: [type: PERSON, start: 16, end: 20, score: 0.85, type: LOCATION, start: 70, end: 77, score: 0.85, type: PHONE_NUMBER, start: 45, end: 57, score: 0.75]

    添加白名单后扫描结果: [type: PHONE_NUMBER, start: 45, end: 57, score: 0.75]

    通过执行结果可以看到,添加白名单之后,"john"和"Beijing"不再视为敏感数据实体。

    2.7 追踪决策日志

    Presidi的分析器支持决策日志输出。决策日志描述了为什么内容实体被判定为敏感数据,决策日志内容包含:

  • 哪个识别器(Recognizer)检测出敏感内容实体;
  • 哪个正则表达式命中敏感内容实体;
  • 上下文增强机制提升的得分值(score);
  • 上下文增强机制中命中的上下文单词;
  • 开启决策日志的输出只需要在执行分析方法时,将return_decision_process参数设置成True即可。示例如下:

    from presidio_analyzer import (
        Pattern,
        PatternRecognizer,
        RecognizerRegistry,
        AnalyzerEngine,
    )
    from presidio_analyzer.context_aware_enhancers import LemmaContextAwareEnhancer
    import pprint
    
    regex = r"\d{6}"
    postal_code_pattern = Pattern(name="postal code", regex=regex, score=0.02)
    
    postal_code_recognizer = PatternRecognizer(
        supported_entity="CH_POSTAL_CODE", patterns=[postal_code_pattern],
        context=["postal", "PostalCode"]
    )
    
    context_aware_enhancer = LemmaContextAwareEnhancer(
        context_similarity_factor=0.45, min_score_with_context_similarity=0.4
    )
    
    registry = RecognizerRegistry()
    registry.add_recognizer(postal_code_recognizer)
    analyzer = AnalyzerEngine(registry=registry, context_aware_enhancer=context_aware_enhancer)
    
    results = analyzer.analyze(text="The postal code for Beijing is 100000.", language="en", return_decision_process=True)
    
    decision_process = results[0].analysis_explanation
    
    pp = pprint.PrettyPrinter()
    print("Decision process output:\n")
    pp.pprint(decision_process.__dict__)

    执行结果如下:

    Decision process output:
    
    {'original_score': 0.02,
     'pattern': '\\d{6}',
     'pattern_name': 'postal code',
     'recognizer': 'PatternRecognizer',
     'regex_flags': regex.I|regex.M|regex.S,
     'score': 0.47000000000000003,
     'score_context_improvement': 0.45,
     'supportive_context_word': 'postal',
     'textual_explanation': 'Detected by `PatternRecognizer` using pattern `postal '
                            'code`',
     'validation_result': None}

    2.8 YAML配置文件

    Presidio支持YAML配置文件解析,YAML配置文件使得即使不懂编码也可以向分析器中添加指定的识别器。用户可将正则表达式和敏感信息列表添加到一个YAML格式的文件中,且此文件在Presidio中指定加载。

    下面示例为只加载指定的YAML文件中包含的识别器:

    from presidio_analyzer import AnalyzerEngine, RecognizerRegistry
    
    yaml_file = "recognizers.yaml"
    registry = RecognizerRegistry()
    registry.add_recognizers_from_yaml(yaml_file)
    
    analyzer = AnalyzerEngine(registry=registry)
    analyzer.analyze(text="Mr. and Mrs. Smith", language="en")

    下面示例为除了加载YAML文件指定的识别器外,还加载内置的识别器:

    from presidio_analyzer import AnalyzerEngine, RecognizerRegistry
    
    yaml_file = "recognizers.yaml" 
    registry = RecognizerRegistry()
    registry.load_predefined_recognizers()  
    # 加载Presidio内置的识别器。
    registry.add_recognizers_from_yaml(yaml_file)
    
    analyzer = AnalyzerEngine(registry=registry)
    analyzer.analyze(text="Mr. Plum wrote a book", language="en")

    3 敏感数据匿名器

    一旦我们识别到内容中包含敏感数据实体,应该使用匿名器对敏感数据进行脱敏处理。匿名器既支持默认的脱敏处理,还支持通过加载配置的形式指定特定敏感数据类型的脱敏方法。匿名器支持重编辑、哈希、掩码、替换和加密等脱敏手段。

    3.1 简单示例

    下面是匿名器根据分析器的扫描结果执行脱敏操作的示例:

    from presidio_anonymizer import AnonymizerEngine
    from presidio_anonymizer.entities import RecognizerResult
    
    # 创建分析器的执行结果。
    analyzer_results = [
        RecognizerResult(entity_type="PERSON", start=11, end=15, score=0.8),
        RecognizerResult(entity_type="PERSON", start=17, end=27, score=0.8),
    ]
    
    # 创建匿名器对象。
    engine = AnonymizerEngine()
    
    # 执行脱敏操作。text参数为分析器扫描的文本内容。analyzer_results参数为分析器
    # 扫描结果。当前匿名其执行默认的匿名操作。
    result = engine.anonymize(
        text="My name is Bond, James Bond", analyzer_results=analyzer_results
    )
    
    print("De-identified text")
    print(result.text)

    上面示例执行结果如下:

    De-identified text

    My name is <PERSON>, <PERSON>

    上面示例我们可以看到,"Bond"和"James Bond"都被姓名识别器识别出来,并由匿名器执行脱敏操作。匿名器默认的脱敏操作是将敏感实体替换为对应的识别器名称。

    3.2 自定义匿名配置

    用户可以通过OperatorConfig类创建匿名配置来指定匿名器对于不同的敏感数据类型执行不同的脱敏操作。

    示例如下:

    operators = {
        # 指定匿名器默认情况下将敏感数据实体替换成<ANONYMIZED>。
        "DEFAULT": OperatorConfig("replace", {"new_value": "<ANONYMIZED>"}),
        # 指定敏感数据类型为PHONE_NUMBER,则将内容替换成"*"。
        "PHONE_NUMBER": OperatorConfig(
            "mask",
            {
                "type": "mask",
                "masking_char": "*",
                "chars_to_mask": 12,
                "from_end": True,
            },
        ),
        # 指定敏感信息列表中内容重新编译为空。
        "SENSITIVE": OperatorConfig("redact", {}),
    }

    下面是示例展示了使用分析器扫描,并将结果和配置传入匿名器对文本内容执行特定的脱敏操作。

    from presidio_analyzer import AnalyzerEngine, PatternRecognizer
    from presidio_anonymizer.entities import OperatorConfig
    from presidio_anonymizer import AnonymizerEngine
    
    from pprint import pprint
    import json
    
    # 待扫描的文本数据
    text = "His name is Mr. Jones and his phone number is 212-555-5555, he live in Beijing."
    
    sensitive_list = ["password", "secret", "phone"]
    sensitive_recognizer = PatternRecognizer(supported_entity="SENSITIVE", deny_list=sensitive_list)
    
    # 创建分析器对象
    analyzer = AnalyzerEngine()
    analyzer.registry.add_recognizer(sensitive_recognizer)
    
    # 执行分析器的分析函数,分析文本数据
    analyzer_results = analyzer.analyze(text=text, language="en")
    
    operators = {
        "DEFAULT": OperatorConfig("replace", {"new_value": "<ANONYMIZED>"}),
        "PHONE_NUMBER": OperatorConfig(
            "mask",
            {
                "type": "mask",
                "masking_char": "*",
                "chars_to_mask": 12,
                "from_end": True,
            },
        ),
        "SENSITIVE": OperatorConfig("redact", {}),
    }
    
    anonymizer = AnonymizerEngine()
    anonymized_results = anonymizer.anonymize(
        text=text, analyzer_results=analyzer_results, operators=operators
    )
    
    print(f"sacn_result: {analyzer_results}")
    print(f"text: {anonymized_results.text}")
    print("detailed result:")
    pprint(json.loads(anonymized_results.to_json()))

    上面示例执行结果如下:

    sacn_result: [type: SENSITIVE, start: 30, end: 35, score: 1.0, type: PERSON, start: 16, end: 21, score: 0.85, type: LOCATION, start: 71, end: 78, score: 0.85, type: PHONE_NUMBER, start: 46, end: 58, score: 0.75]
    text: His name is Mr. <ANONYMIZED> and his  number is ************, he live in <ANONYMIZED>.
    detailed result:
    {'items': [{'end': 85,
                'entity_type': 'LOCATION',
                'operator': 'replace',
                'start': 73,
                'text': '<ANONYMIZED>'},
               {'end': 60,
                'entity_type': 'PHONE_NUMBER',
                'operator': 'mask',
                'start': 48,
                'text': '************'},
               {'end': 37,
                'entity_type': 'SENSITIVE',
                'operator': 'redact',
                'start': 37,
                'text': ''},
               {'end': 28,
                'entity_type': 'PERSON',
                'operator': 'replace',
                'start': 16,
                'text': '<ANONYMIZED>'}],
     'text': 'His name is Mr. <ANONYMIZED> and his  number is ************, he '
             'live in <ANONYMIZED>.'}
    

    通过执行结果我们可以看到,"phone"为敏感信息列表中内容,匿名器根据配置直接文本中该内容替换成空。"Jones"和"Beijing"分别被PERSON识别器和LOCATION识别器识别出来,由于配置中没有针对此两种类型敏感数据指定的脱敏操作,则执行默认的脱敏操作,即将敏感数据内容替换成""。"212-555-5555"被识别处电话号码,由于配置中规则对于PHONE_NUMBER识别器识别的内容将替换成字符"*"。因此文本内容脱敏前后对比如下:

    # 脱敏前 His name is Mr. Jones and his phone number is 212-555-5555, he live in Beijing.

    # 脱敏后 His name is Mr. <ANONYMIZED> and his number is ************, he live in <ANONYMIZED>.

    3.3 自定义匿名器

    Presidio支持用户自定义匿名器对敏感数据实体进行脱敏操作。自定义匿名器以lambda函数形式传入。

    下面示例展示如何创建自定义匿名器:

    from faker import Faker
    from presidio_anonymizer.entities import OperatorConfig
    
    fake = Faker()
    
    # 创建脱敏函数,使用fake对象实现脱敏操作。
    # 注意,该函数需要接收一个参数值。
    def fake_name(x):
        return fake.name()
    
    # 通过OperatorConfig类型创建匿名器配置对象。
    operators = {"PERSON": OperatorConfig("custom", {"lambda": fake_name})}

    下面展示使用自定义匿名器执行脱敏操作的完整实现:

    from presidio_anonymizer import AnonymizerEngine
    from presidio_anonymizer.entities import OperatorConfig, EngineResult, RecognizerResult
    from faker import Faker
    
    fake = Faker()
    
    # 创建脱敏函数。
    def fake_name(x):
        return fake.name()
    
    # 创建匿名器自定义配置对象。
    operators = {"PERSON": OperatorConfig("custom", {"lambda": fake_name})}
    
    # 分析器对文本内容分析结果。
    analyzer_results = [RecognizerResult(entity_type="PERSON", start=11, end=18, score=0.8)]
    
    text_to_anonymize = "My name is Raphael and I like to fish."
    
    anonymizer = AnonymizerEngine()
    anonymized_results = anonymizer.anonymize(
        text=text_to_anonymize, analyzer_results=analyzer_results, operators=operators
    )
    
    print(anonymized_results.text)

    执行结果如下:

    My name is Elaine Austin and I like to fish.

    通过执行结果我们可以看到,"Raphael"已经被随机替换成一个假名"Elaine Austin",fake.name()函数每次生成的假名是不一样的。

    3.4 加密和解密

    Presidio匿名器内置对敏感数据加密和解密实现。加密模块使用AES的CBC模式加密算法,在加密和解密时都需要输入一段字符串作为key。

    对敏感数据进行加解密是可逆的操作,即保障了敏感信息的安全,又能由接收着根据秘钥解密成原始的数据内容。

    下面示例展示了如何设置匿名器使其对指定类型敏感内容实体执行加密操作:

    from presidio_anonymizer import AnonymizerEngine, DeanonymizeEngine
    from presidio_anonymizer.entities import (
        RecognizerResult,
        OperatorResult,
        OperatorConfig,
    )
    
    # 设置加密Key。
    crypto_key = "WmZq4t7w!z%C&F)J"
    
    engine = AnonymizerEngine()
    
    # 执行脱敏操作。
    anonymize_result = engine.anonymize(
        text="My name is James Bond",
        analyzer_results=[
            RecognizerResult(entity_type="PERSON", start=11, end=21, score=0.8),
        ],
        operators={"PERSON": OperatorConfig("encrypt", {"key": crypto_key})},
    )
    
    # 打印脱敏后的文本内容
    print(anonymize_result.text)
    
    # 打印匿名实体的详细内容
    print(anonymize_result.items)

    上面示例执行结果如下:

    My name is 1QZPhRiMCG52oUG99z+qsPFQcpFSwE50fLj7KtbbAFA=

    [{'start': 11, 'end': 55, 'entity_type': 'PERSON', 'text': '1QZPhRiMCG52oUG99z+qsPFQcpFSwE50fLj7KtbbAFA=', 'operator': 'encrypt'}]

    通过分析脱敏结果可以看到,匿名器将"James Bond"加密成"1QZPhRiMCG52oUG99z+qsPFQcpFSwE50fLj7KtbbAFA="。

    下面示例展示了如何对含有加密内容中文本数据进行解密操作。

    from presidio_anonymizer import AnonymizerEngine, DeanonymizeEngine
    from presidio_anonymizer.entities import (
        RecognizerResult,
        OperatorResult,
        OperatorConfig,
    )
    from presidio_anonymizer.operators import Decrypt
    
    crypto_key = "WmZq4t7w!z%C&F)J"
    
    anonymizer_engine = AnonymizerEngine()
    anonymize_result = anonymizer_engine.anonymize(
        text="My name is James Bond",
        analyzer_results=[
            RecognizerResult(entity_type="PERSON", start=11, end=21, score=0.8),
        ],
        operators={"PERSON": OperatorConfig("encrypt", {"key": crypto_key})},
    )
    print("加密后文本内容: ", anonymize_result.text)
    
    # 创建解匿名引擎。
    deanonymize_engine = DeanonymizeEngine()
    deanonymized_result = deanonymize_engine.deanonymize(
        text=anonymize_result.text,
        entities=anonymize_result.items,
        operators={"DEFAULT": OperatorConfig("decrypt", {"key": crypto_key})},
    )
    
    # 解匿名引擎从加密文本中定位到加密内容。
    print("提取到加密内容: ", anonymize_result.items[0].text)
    # 执行解密操作。
    print("解密后内容: ", Decrypt().operate(text=anonymize_result.items[0].text, params={"key": crypto_key}))

    上面示例执行结果为:

    加密后文本内容: My name is O+LlBEHWW8ilbUiYFMZU7XXbrOkFVpj7KVJD86sOywA=

    提取到加密内容: O+LlBEHWW8ilbUiYFMZU7XXbrOkFVpj7KVJD86sOywA=

    解密后内容: James Bond

    注意:通过上面示例可以看到,Presidio内容的加密算法每次加密相同内容,加密后的结果是不一样的。可以多执行几次就可以看出来。

    4 NLP模型和语言类型

    Presidio内部支持的NLP引擎包括spaCy和Stanza两种。因此使得Presidio能够正常运行的前提是至少已经下载安装两种NLP引擎中的一个模型。

    下面示例通过配置,将spaCy作为Presidio的NLP引擎,并且使得NLP引擎既支持英语,也支持西班牙语。

    from presidio_analyzer import AnalyzerEngine
    from presidio_analyzer.nlp_engine import NlpEngineProvider
    
    # 创建NLP配置对象,包含指定的NLP引擎以及支持的NLP模型。
    configuration = {
        "nlp_engine_name": "spacy",
        "models": [
            {"lang_code": "es", "model_name": "es_core_news_md"},
            {"lang_code": "en", "model_name": "en_core_web_lg"},
        ],
    }
    
    # 基于NLP配置信息创建NLP对象。
    provider = NlpEngineProvider(nlp_configuration=configuration)
    # 创建NLP引擎对象。
    nlp_engine_with_spanish = provider.create_engine()
    
    # 创建分析器对象,NLP引擎对象作为参数传入,除此之外,还要传入NLP引擎支持的语言。
    analyzer = AnalyzerEngine(
        nlp_engine=nlp_engine_with_spanish, supported_languages=["en", "es"]
    )
    
    # 分析西班牙语文本内容。
    results_spanish = analyzer.analyze(text="Mi nombre es Morris", language="es")
    print("Results from Spanish request:")
    print(results_spanish)
    
    # 分析英语文本内容。
    results_english = analyzer.analyze(text="My name is Morris", language="en")
    print("Results from English request:")
    print(results_english)

    下面是上面示例执行结果:

    Results from Spanish request:

    [type: PERSON, start: 13, end: 19, score: 0.85]

    Results from English request:

    [type: PERSON, start: 11, end: 17, score: 0.85]

    示例中spaCy支持西班牙语文本的解析,在这之前需要先下载西班牙语NLP模型,即es_core_news_md。spaCy语言模型下载方法可参考这篇文章spaCy语言模型下载。

    作者:_只道当时是寻常

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【数据保护进阶之路】微软开源Presidio项目详解:从入门到精通掌握数据保护之道

    发表回复