import spacy from collections import Counter import re as regex import os from saxonche import PySaxonProcessor #### Loads all of the necessary variables and functions. nlp = spacy.load("en_core_web_lg") ######################################################################################### # ebb: After reading the NLP output, we know spaCy is making some mistakes. # So, here let's try adding an EntityRuler to customize spaCy's classification. We need # to configure this BEFORE we send the tokens off to nlp() for processing. ########################################################################################## # Create the EntityRuler and set it so the ner comes after, so OUR rules take precedence # Sources: # W. J. B. Mattingly: https://ner.pythonhumanities.com/02_01_spaCy_Entity_Ruler.html # spaCy documentation on NER Entity Ruler: https://spacy.io/usage/rule-based-matching#entityruler config = {"spans_key": None, "annotate_ents": True, "overwrite": True, "validate": True} ruler = nlp.add_pipe("span_ruler", before="ner", config=config) # 2023-04-07: ebb: NOTE: before="ner" setting seems to allow the spaCy NER rules to prevail over these patterns where # there is a conflict. # after="ner" means that the spaCy NER is TOTALLY OVERWRITTEN and invalidated by our patterns. # Notes: Mattingly has this: ruler = nlp.add_pipe("entity_ruler", after="ner", config={"validate": True}) # But this only works when spaCy doesn't recognize a word / phrase as a named entity of any kind. # If it recognizes a named entity but tags it wrong, we correct it with the span_ruler, not the entity_ruler patterns = [ {"label": "NULL", "pattern": [{"TEXT": {"REGEX": "^-\w+?"}}]}, {"label": "NULL", "pattern": [{"TEXT": {"REGEX": "^.$"}}]}, {"label": "NULL", "pattern": [{"TEXT": {"REGEX": "^\w\w$"}}]}, {"label": "NULL", "pattern": [{"TEXT": {"REGEX": "^[a-z]+\s+[a-z]+$"}}]}, {"label": "NULL", "pattern": [{"TEXT": {"REGEX": "^.*?__{2,}.*?$"}}]}, {"label": "NORP", "pattern": [{"TEXT": {"REGEX": "CHRISTIAN(ITY|DOM)"}}]}, {"label": "NORP", "pattern": [{"TEXT": {"REGEX": "CHRISTIAN\s+NETWORK"}}]}, # ebb: Don't match on any single characters! {"label": "NULL", "pattern": [{"TEXT": {"REGEX": "[A-Z]{2,}[A-Z][a-z]+"}}]}, {"label": "NULL", "pattern": [{"TEXT": {"REGEX": "[a-z]{2,}[A-Z][a-z]+"}}]}, {"label": "NULL", "pattern": [{"TEXT": {"REGEX": "^.*?[a-z][A-Z].*?$"}}]}, # ebb: Above line attempts to stop matching things like Oak IslandThe Method {"label": "NULL", "pattern": [{"TEXT" : {"REGEX": "^[Mm\-]+$"}}]}, # SOCIALISMBY RICHARD # ebb: Above line attempts to stop matching things Mmm-mm or mm , etc. {"label": "GPE", "pattern": [{"TEXT": {"REGEX": "Babylon(ia)?"}}]}, {"label": "NORP", "pattern": [{"TEXT": {"REGEX": "Christiani\s*ty"}}]}, {"label": "NULL", "pattern": "Christiani"}, {"label": "NULL", "pattern": "di"}, {"label": "NULL", "pattern": "ORG"}, {"label": "PERSON", "pattern": [{"TEXT": {"REGEX": "(Ludwig [Vv]an )?Beethoven"}}]}, {"label": "ORG", "pattern": "Falangist"}, {"label": "NORP", "pattern": "Dropa"}, {"label": "GPE", "pattern": "Nazareth"}, {"label": "NULL", "pattern": "Bab"}, ] ruler.add_patterns(patterns) workingDir = os.getcwd() CollPath = os.path.join(workingDir, '../regexConsp') outputPath = os.path.join(workingDir, 'personTaggedOutput/') # Everything in original conspiracy directory. insideDir = os.listdir(CollPath) print(insideDir) # Copies files in case they do not exist def copyTextFiles(file): content = [] # Reads the contents of file, and saves each line of file into the content array. with open(CollPath + "/" + file, 'r', encoding='utf8') as inFile: for line in inFile: content.append(line) print(" ~~~~~~~~~~~~~~~~~~~~~~~~~~~ copying " + file + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ") inFile.close() # With the contents copied, a loop will go through the array and write it all in a new file in output folder. with open(outputPath + "/" + file, 'w', encoding='utf8') as f: for line in content: f.write(str(line)) # Function runs through the tokens of given file. Entities are stored in array, then returned. Called by regexFile(). def entitycollector(tokens): # creates a new file that includes all of the found entities. with open('output.txt', 'w') as f: entities = {} # goes through each entity in the token list. for ent in sorted(tokens.ents): entityInfo = [ent.text, ent.label_, spacy.explain(ent.label_)] stringify = str(entityInfo) f.write(stringify) f.write('\n') entities[ent.text] = ent.label_ # return all entities with its label and text. return entities # Function runs regex through given file. def regexFile(file): fileDir = os.path.join(outputPath, file) with PySaxonProcessor(license=False) as proc: # grabs the original xml file and stores it in a variable for later. this some xquery bs xml = open(fileDir, encoding='utf-8').read() xp = proc.new_xpath_processor() node = proc.parse_xml(xml_text=xml) xp.set_context(xdm_item=node) # xquery goes through original text, and stores it all in a single string. xpath = xp.evaluate('//p ! normalize-space() => string-join()') string = str(xpath) # regex goes through the text and deletes anything that is not a letter or space. cleanedText = regex.sub(r'[^A-z ]+', ' ', string) # gets the tokens of the clean text. tokens = nlp(cleanedText) wrappedText = xml # grabs all the entities in file and stores it in a list/array. dictEntities = entitycollector(tokens) # if anything exists in the list, the following code will run. if dictEntities: # it will check through each entity in the list and see its entity type. it is looking for "PERSON" tokens # in this instance, which includes of nouns and names. for entity in dictEntities.keys(): if dictEntities[entity] == "PERSON": # key_template variable is the elements we wrap around found instances. key_template = "" + entity + "" # loops through wrappedText until all entities are wrapped. wrappedText = wrappedText.replace(entity, key_template) # Saves newly wrapped elements and then writes it into new file. with open(fileDir, 'w', encoding='utf8') as f: f.write(wrappedText) print("WRAPPING " + entity) checkTags(file) # ebb: Added above line to send the tagged file to the checkTags() function for cleaning. # This part of the code is a WIP. # ebb: I just activated it, and it works! (Nice job.) I altered it just a bit. May need more regexes to match. ## It tries to find weird or invalid elements/tags and fix them. def checkTags(file): content = [] fileDir = os.path.join(outputPath, file) with open(fileDir, 'r', encoding='utf8') as inFile: for line in inFile: content.append(line) # With the contents copied, a loop will go through the array and write it all in a new file in output folder. with open(fileDir, 'w', encoding='utf8') as f: for line in content: # match = regex.search(r"([^<>]*?)]+?>([^<>]+?)([^<>]*?)", line) # if match: # print("broken line found, fixing...") # ebb: NOTE: IF this function only processes a line when there's a regex match, we'd have a serious problem: # we'd not output the rest of the file--only the cleaned matches. So the output files would be mostly empty! # Better to just string-clean every line using regex.sub(). Where there's no regex match, no substitution will happen. origLine = line # newLine = regex.sub(r"([^<>]*?)]+?>([^<>]+?)([^<>]*?)", r"\1\2\3",line) # cial> newLine = regex.sub(r"((cia)(l>)", r"\1\2\3", origLine) # newLine = regex.sub(r"(<)(di)(v>)", r"\1\2\3", newLine) newLine = regex.sub(r"([^<]*?)([^<]+?)([^<]*?)", r"\1\2\3", newLine) newLine = regex.sub(r"([^<]*?)([^<]+?)([^<]*?)", r"\1\2\3", newLine) newLine = regex.sub(r"([^<]*?)([^<]+?)([^<]*?)", r"\1\2\3", newLine) # ebb: I'm repeating the above just in case of the weird event of triple or quadruple nested tags in tags. # We saw it happen on the LOTR project and running it through multiple passes of the above line ultimately got rid of them all # preserving only the outermost tags. newLine = regex.sub(r"((ORG)('>)", r"\1\2\3", newLine) # # cial> # <div> if origLine != newLine: print("broken line found, fixing...") print(origLine + "\n INTO.") print(newLine) f.write(str(newLine)) print("File checking finished.") for file in insideDir: copyTextFiles(file) regexFile(file) #checkTags(file) # ebb: You don't really want to activate checkTags here, # because it would run over the untagged input files.