CoolFace
Apppublic

Jean-Baptiste/email_parser

sourceHugging Faceupdated 5mo agoView on Hugging Face
20likes
nlp.py336 linesDownload Raw Back to email_parser
1import logging2import os3import regex4from transformers import AutoModelForTokenClassification, AutoTokenizer, pipeline5import pandas as pd6import numpy as np7 8from . import utils, _models_signatures9from .utils import timing10from langid.langid import LanguageIdentifier11from langid.langid import model as model_langid12 13# Creating language_identifier object for usage in function f_detect_language14language_identifier = LanguageIdentifier.from_modelstring(model_langid, norm_probs=True)15language_identifier.set_languages(['en', 'fr'])16 17 18logging.info(f"Reading config file from folder:{os.path.join(os.path.dirname(__file__))}")19 20config = utils.f_read_config(os.path.join(os.path.dirname(__file__), 'config.ini'))21 22device = int(config["DEFAULT"]["device"])23default_lang = config["DEFAULT"]["default_lang"]24 25tokenizer_dict = {}26models_dict = {}27nlp_dict = {}28 29 30dict_regex_pattern = dict(EMAIL=r'[\p{L}\p{M}\-\d._]{1,}@[\p{L}\p{M}\d\-_]{1,}(\.[\p{L}\p{M}]{1,}){1,}',31                          TEL=r'(?<!\d)(\+?\d{1,2}[ -]?)?\(?\d{3}\)?[ .-]?\d{3}[ .-]?\d{4}(?!\d|\p{P}\d)',32                          POST=r'\b([A-z][0-9][A-z][ -]?[0-9][A-z][0-9]|[A-z][0-9][A-z])\b',33                          PRICE=r"(([\s:,]|^){1}\$*(CA|CAD|USD|EUR|GBP|\$|\€|\£|\¢){1}\$*[\d., ]*[\d]{1,}\b)" +34                                "|([\d]{1,}[\d., ]*(CA|CAD|USD|EUR|GBP|\$|\€|\£|k|m|\¢){1,}\$*(?=\s|\p{P}|$))",35                          WEB=r"((www(\.[\p{L}\p{M}\-0-9]]{1,}){2,})" +36                              "|(https?:[^ ]*)"+37                              # r"|(([\p{L}\p{M}\.]{3,}){2,})|"38                              r"|((?<=[\s:]|^)([\p{L}\p{M}\-0-9]{1,}\.){1,}(com|ca|org|fr){1,}\b))")39                          # WEB=r"(http(s)?:\/\/)?[a-z0-9]{1}[a-z0-9-._~]+[.]{1}(com|ca)(?![\p{L}\p{M}])")40 41def f_load_tokenizer_and_model_for_nlp(model_name, pipeline_type='ner'):42    """43    Loading model and tokenizer takes a long time.44    We do it once and store the model and tokenizer in global dict for next usage45    Args:46        name: Name of the model that should be loaded and stored47        pipeline_type: type of pipeline that should be initialized48 49    Returns: tokenizer, model50 51    """52    global tokenizer_dict, models_dict, nlp_dict53    auto_model = None54    if pipeline_type == "ner":55        auto_model = AutoModelForTokenClassification56 57    if model_name not in tokenizer_dict.keys() or model_name not in models_dict.keys() or model_name not in nlp_dict.keys():58        logging.info(59            f"Loading tokenizer and model: {model_name}")60        try:61            tokenizer_dict[model_name] = AutoTokenizer.from_pretrained(model_name)62            models_dict[model_name] = auto_model.from_pretrained(model_name)63        except OSError as exc:64            raise OSError(65                f"Failed to load Hugging Face model '{model_name}'. "66                "Check outbound network access and make sure 'sentencepiece' is installed "67                "for CamemBERT-based tokenizers."68            ) from exc69        if pipeline_type == 'ner':70            nlp_dict[model_name] = pipeline(pipeline_type, model=models_dict[model_name], tokenizer=tokenizer_dict[model_name],71                                      aggregation_strategy="simple", device=device)72 73 74def f_ner(text, lang=default_lang):75    df_result = f_ner_regex(text)76    df_result = f_ner_model(text, lang=lang, df_result=df_result)77    return df_result78 79 80@timing81def f_ner_model(text,  lang=default_lang, df_result=pd.DataFrame()):82    list_result = []83    # We split the text by sentence and run model on each one84    sentence_tokenizer = f_split_text_by_lines(text)85    for start, end, value in sentence_tokenizer:86        if value != "":87            results = f_ner_model_by_sentence(value, lang=lang, pos_offset=start)88            if len(results) != 0:89                list_result += results90    return f_concat_results(df_result, list_result)91 92 93@timing94def f_ner_model_by_sentence(sentence, lang=default_lang, df_result=pd.DataFrame(), pos_offset=0):95    """ Run ner algorithm96 97    Args:98        sentence : sentence on which to run model99        lang : lang to determine which model to use100        df_result : If results of f_ner should be combined with previous value101        (in this case we will keep the previous values if tags overlapsed)102 103    Returns:104        Dataframe with identified entities105 106    """107 108    if not config.has_option('DEFAULT', 'ner_model_' + lang):109        raise ValueError(f"No model was defined for ner in {lang}")110 111    model_name = config['DEFAULT']['ner_model_' + lang]112    f_load_tokenizer_and_model_for_nlp(model_name)113    logging.debug(f"starting {model_name} on sentence:'{sentence}'")114 115    results = nlp_dict[model_name](sentence)116    list_result = []117    for result in results:118        if result["word"] != "" and result['entity_group'] in ["PER", "LOC", "ORG", "DATE"]:119 120            # Required because sometimes spaces are included in result["word"] value, but not in start/end position121            value = sentence[result["start"]:result["end"]]122 123            # We remove any special character at the beginning124            pattern = r"[^.,'’` \":()\n].*"125            result_regex = regex.search(pattern, value, flags=regex.IGNORECASE)126 127            if result_regex is not None:128                word_raw = result_regex.group()129                word = word_raw130                real_word_start = result["start"] + result_regex.start()131                real_word_end = result["start"] + result_regex.start() + len(word_raw)132                # We check if entity might be inside a longer word, if this is the case we ignore133                letter_before = sentence[max(0, real_word_start - 1): real_word_start]134                letter_after = sentence[real_word_end: min(len(sentence), real_word_end + 1)]135                if regex.match(r"[A-z]", letter_before) or regex.match(r"[A-z]", letter_after):136                    logging.debug(f"Ignoring entity {value} because letter before is"137                                  f" '{letter_before}' or letter after is '{letter_after}'")138                    continue139 140                list_result.append(141                    [result["entity_group"],142                     word,143                     real_word_start + pos_offset,144                     real_word_end + pos_offset,145                     result["score"]])146 147    return list_result148 149 150@timing151def f_concat_results(df_result, list_result_new):152    """ Merge results between existing dataframe and a list of new values153 154    Args:155        df_result: dataframe of entities156        list_result_new: list of new entities to be added in df_result157 158    Returns:159        Dataframe with all entities. Entities in list_result_new that were overlapping position of another entity in160        df_result are ignored.161 162    """163    # If df_result and list_result_new are both empty, we return an empty dataframe164    list_columns_names = ["entity", "value", "start", "end", "score"]165    if (df_result is None or len(df_result) == 0) and (list_result_new is None or len(list_result_new) == 0):166        return pd.DataFrame()167    elif len(list_result_new) > 0:168        if df_result is None or len(df_result) == 0:169            return pd.DataFrame(list_result_new,170                                columns=list_columns_names)171        list_row = []172        for row in list_result_new:173            df_intersect = df_result.query("({1}>=start and {0}<=end)".format(row[2], row[3]))174            if len(df_intersect) == 0:175                list_row.append(row)176        df_final = pd.concat([df_result,177                              pd.DataFrame(list_row,178                                           columns=list_columns_names)],179                             ignore_index=True) \180            .sort_values(by="start")181        return df_final182    else:183        # If list_result_new was empty we just return df_result184        return df_result185 186 187@timing188def f_detect_language(text, default=default_lang):189    """ Detect language190 191    Args:192        text: text on which language should be detected193        default: default value if there is an error or score of predicted value is to low (default nlp.default_lang)194 195    Returns:196        "fr" or "en"197 198    """199    lang = default200    try:201        if text.strip() != "":202            lang, score = language_identifier.classify(text.strip().replace("\n"," ").lower())203            # If scroe is not high enough we will take default value instead204            if score < 0.8:205                lang = default_lang206    except Exception as e:207        logging.error("following error occurs when trying to detect language: {}".format(e))208    finally:209        return lang210 211@timing212def f_find_regex_pattern(text, type_, pattern):213    """ Find all occurences of a pattern in a text and return a list of results214    Args:215        text:  the text to be analyzed216        type_:  the entity type (value is added in result)217        pattern: regex pattern to be found218 219    Returns:220        A list containing type, matched value, position start and end of each result221 222    """223    list_result = []224    results = regex.finditer(pattern, text, flags=regex.IGNORECASE)225    for match in results:226        value = match.string[match.start(): match.end()].replace("\n", " ").strip()227        list_result.append([type_,228                            value,229                            match.start(),230                            match.end(),231                            1])232    return list_result233 234 235@timing236def f_ner_regex(text, dict_pattern=dict_regex_pattern,237                df_result=pd.DataFrame()):238    """Run a series of regex expression to detect email, tel and postal codes in a full text.239 240    Args:241        text: the text to be analyzed242        dict_pattern: dictionary of regex expression to be ran successively (default nlp.dict_regex_pattern)243        df_result: results of this function will be merged with values provided here.244                   If value is already found at an overlapping  position in df_results, the existing value will be kept245 246    Returns:247        Dataframe containing results merged with provided argument df_result (if any)248    """249    logging.debug("Starting regex")250    list_result = []251 252    # we run f_find_regex_pattern for each pattern in dict_regex253    for type_, pattern in dict_pattern.items():254        result = f_find_regex_pattern(text, type_, pattern)255        if len(result) != 0:256            list_result += result257 258    df_result = f_concat_results(df_result, list_result)259    return df_result260 261@timing262def f_split_text_by_lines(text, position_offset=0):263    """264    :param text: text that should be split265    :return: list containing for each line:  [position start, position end, sentence]266    """267    results = []268    # iter_lines = regex.finditer(".*(?=\n|$)", text)269    iter_lines = regex.finditer("[^>\n]((.*?([!?.>] ){1,})|.*(?=\n|$))", text)270    for line_match in iter_lines:271        start_line = line_match.start()272        end_line = line_match.end()273        line = line_match.group()274        if len(line.strip()) > 1:275            results.append([start_line + position_offset, end_line + position_offset, line])276    return results277 278 279def f_detect_email_signature(text, df_ner=None, cut_off_score=0.6, lang=default_lang):280    # with tf.device("/cpu:0"):281    if text.strip() == "":282        return None283    if df_ner is None:284        df_ner = f_ner(text, lang=lang)285 286    try:287        df_features = _models_signatures.f_create_email_lines_features(text, df_ner=df_ner)288 289        if len(df_features)==0:290            return None291 292        # We add a dummy value for signature in order to use the same function as training.293        df_features["is_signature"] = -2294 295        x, y_out, y_mask, _, _ = _models_signatures.generate_x_y(296            df_features,297            _models_signatures.minmax_scaler,298            _models_signatures.standard_scaler,299        )300 301        y_predict = _models_signatures.predict_signature_scores(x)302        y_predict_value = (y_predict[y_mask != -1] > cut_off_score).reshape([-1])303        y_predict_value = np.pad(y_predict_value, (len(df_features) - len(y_predict_value), 0), constant_values=0)[304                          -len(df_features):]305        y_predict_score = y_predict[y_mask != -1].reshape([-1])306        y_predict_score = np.pad(y_predict_score, (len(df_features) - len(y_predict_score), 0), constant_values=1)[307                          -len(df_features):]308    except Exception as exc:309        logging.warning("Email signature detection is unavailable with the current runtime: %s", exc)310        return None311 312    # return(y_predict, y_mask)313    df_features["prediction"] = y_predict_value314    df_features["score"] = y_predict_score315    # return df_features316    series_position_body = df_features.query(f"""prediction==0""")['end']317    if len(series_position_body) > 0:318        body_end_pos = max(series_position_body)319    else:320        # In this case everything was detected as a signature321        body_end_pos = 0322    score = df_features.query(f"""prediction==1""")["score"].mean()323    signature_text = text[body_end_pos:].strip().replace("\n", " ")324    if signature_text != "":325        list_result = [326            # ["body", text[:body_end_pos], 0 + pos_start_email, body_end_pos + pos_start_email, 1, ""],327            ["SIGNATURE", signature_text, body_end_pos, len(text), score]]328 329        df_result = f_concat_results(pd.DataFrame(), list_result)330    else:331        df_result = None332 333    return df_result334 335 336