spaCy entity_ruler, html#

Demo-Notebook für Studierende der Lehrveranstaltung “Text Mining (dsci-txt)”, SS 2022, HAW Landshut

Autor: Johannes Busse, 2021-07-08, 2022-04-19

Lizenz: public domain / CC 0

import spacy
from spacy import displacy

# conda install -c conda-forge spacy-model-en_core_web_sm
# EN: nlp = spacy.load("en_core_web_sm")
nlp = spacy.load("de_core_news_sm")

Einlesen:

phrase_matcher_attr = "LEMMA" # LOWER
ruler = nlp.add_pipe( "entity_ruler", 
    config={"phrase_matcher_attr": phrase_matcher_attr })
patterns = [
    {"label": "CT",  # irgend etwas, hier: Contolled Term
     "pattern": "fettfreie Milch", 
     "id": "ex:Magermilch"},
    {"label": "CT", 
     "pattern": "skim milk", 
     "id": "ex:Magermilch"},
    {"label": "CT", 
     "pattern": "skimmed milk", 
     "id": "ex:Magermilch"},
    {"label": "CT", 
     "pattern": "Milch", 
     "id": "ex:Milch"},
    {"label": "CT", 
     "pattern": "fettfrei", 
     "id": "ex:fettfrei"},
    {"label": "CT", 
     "pattern": "Kuriosität", 
     "id": "ex:Kuriosität"},
]

ruler.add_patterns(patterns)
import copy
def annotate_etree(nlp,
            xml_element, # typically body, p, ul etc.
            result_element,
            elements_deepcopy = [
                'a',     # notwendig, denn ein a-Element zu analysieren ist sinnlos
                'quote', # auch Zitate lassen wir unverändert
                'pre' ], # und auch code
            elements_analyse = ['p', 'li', 'body'],
            lang = 'de'):  

    
    if xml_element.tag in elements_deepcopy:
        # leave element untouched: simply deepcopy into result tree, no further recursion required
        result_xml_element = copy.deepcopy(xml_element)
        result_xml_element.tail = xml_element.tail
        result_element.append(result_xml_element)
        # print(f"deepcopy {xml_element.tag}")
        
    else:
        # process xml_element with/out NLP
        
        text = xml_element.text  # yields a copy
        
        # allocate new xml_element in result tree
        result_xml_element = etree.SubElement(result_element, xml_element.tag, attrib = xml_element.attrib )
        
        if not(xml_element.tag in elements_analyse) or len(text) == 0:
            # annotation not in positive list, or no text available
            result_xml_element.text = text
    
        else:
            # analyse and annotate xml_element
            doc = nlp(text)
            displacy.render(doc, style="ent", jupyter=True)

            ent_position_tuples = [ (e.start_char, e.end_char) for e in doc.ents]
            # debug print("ent_position_tuples: ", ent_position_tuples)

            # create flat list, c.f. e.g. https://stackoverflow.com/questions/10632839/transform-list-of-tuples-into-a-flat-list-or-a-matrix/35228431
            ent_position_flatlist = list(sum (ent_position_tuples, ()))  # warum geht das?
            #ent_position_flatlist = [item for sublist in ent_position_tuples for item in sublist] # warum geht das?

            # add start and end index of total string
            ent_positions = [0] + ent_position_flatlist + [len(text)]
            # debug print("ent_positions: ", ent_positions)

            # string between start of text and first entity
            chunk = text[ent_positions[0]:ent_positions[1]]
            # debug print(f"\n\ntrailing: {chunk}", end="")
            result_xml_element.text = chunk

            for i in range(len(doc.ents)):
                ent = doc.ents[i]
                # debug print(f"\n--- i: {i}, ent: {ent.text} ---") 

                # entity
                #chunk = text[ent_positions[2*i+1]:ent_positions[2*i+2]]
                chunk =  doc.ents[i].text
                # debug print(f"\nenty: {chunk}", end = "")
                result_element_tag = etree.SubElement(result_xml_element, "a", attrib = {"href": ent.ent_id_, "label": ent.label_})
                result_element_tag.text = chunk

                # text immediately following the entity
                chunk = text[ent_positions[2*i+2]:ent_positions[2*i+3]]
                # debug print(f"\ntail: {chunk}", end = "")
                result_element_tag.tail = chunk
        
                                        
        for n in xml_element.findall("*"):
            annotate_etree(nlp,
                           n,
                           result_xml_element,
                           elements_deepcopy = elements_deepcopy,
                           elements_analyse = elements_analyse,
                           lang = lang)
            
par_1 = "Einige Fakten über fettfreie Milch:"
par_2 = "In England heißt diese Kuriosität skimmed milk, in der USA skim milk."
par_3 = """Zur Einstellung des Fettgehalts wird die Milch zunächst 
        in Rahm, Magermilch und Nichtmilchbestandteile getrennt."""

source_html_string = f"""<html>
  <head>
    <title>fettfreie Milch</title>
  </head>
  <body>(ab hier wollen wir annotieren)
    <ul>{par_1}
        <li>{par_2}</li>
    </ul>
    <quote>(In Zitaten bitte keine Annotationen)
        <p>{par_3}</p>
    </quote>
    Das war alles über Milch!
  </body>
</html>"""
# https://lxml.de/
from lxml import etree
parser = etree.XMLParser(remove_blank_text=False) # strip e.g. indentation blanks 
source_html = etree.fromstring(source_html_string, parser)
source_body = source_html.find(".//body")

print(etree.tostring(source_html, pretty_print=True, encoding="unicode"))
print("elements in source_body:", source_body.text, [(elem.tag, elem.text) for elem in source_body.findall("*") ] )
<html>
  <head>
    <title>fettfreie Milch</title>
  </head>
  <body>(ab hier wollen wir annotieren)
    <ul>Einige Fakten über fettfreie Milch:
        <li>In England heißt diese Kuriosität skimmed milk, in der USA skim milk.</li>
    </ul>
    <quote>(In Zitaten bitte keine Annotationen)
        <p>Zur Einstellung des Fettgehalts wird die Milch zunächst 
        in Rahm, Magermilch und Nichtmilchbestandteile getrennt.</p>
    </quote>
    Das war alles über Milch!
  </body>
</html>

elements in source_body: (ab hier wollen wir annotieren)
     [('ul', 'Einige Fakten über fettfreie Milch:\n        '), ('quote', '(In Zitaten bitte keine Annotationen)\n        ')]
result_html = etree.fromstring("""
<html>
  <head>
    <title>neu aufgebautes html</title>
  </head>
</html>
""")

annotate_etree(nlp, source_body, result_html)

print(etree.tostring(result_html, pretty_print=True, encoding="unicode"))
/home/dsci/miniconda3/lib/python3.9/site-packages/spacy/displacy/__init__.py:205: UserWarning: [W006] No entities to visualize found in Doc object. If this is surprising to you, make sure the Doc was processed using a model that supports named entity recognition, and check the `doc.ents` property manually if necessary.
  warnings.warn(Warnings.W006)
(ab hier wollen wir annotieren)
In England LOC heißt diese Kuriosität CT skimmed milk, in der USA LOC skim milk.
<html>
  <head>
    <title>neu aufgebautes html</title>
  </head>
<body>(ab hier wollen wir annotieren)
    <ul>Einige Fakten über fettfreie Milch:
        <li>In <a href="" label="LOC">England</a> heißt diese <a href="ex:Kuriosität" label="CT">Kuriosität</a> skimmed milk, in der <a href="" label="LOC">USA</a> skim milk.</li></ul><quote>(In Zitaten bitte keine Annotationen)
        <p>Zur Einstellung des Fettgehalts wird die Milch zunächst 
        in Rahm, Magermilch und Nichtmilchbestandteile getrennt.</p>
    </quote>
    Das war alles über Milch!
  </body></html>