import json import re from dataclasses import dataclass from html.parser import HTMLParser from pathlib import Path from urllib.parse import unquote_plus from exclude import EXCLUDED_WORDS # Einstellungen SOURCE_DIR = Path('data') SOURCE_FILENAME = 'index.txt' OUTPUT_FILE = 'tags.json' TAGS_PER_ARTICLE = 5 JSON_INDENT = 2 EXCLUDED_HTML_TAGS = {'code'} # Wegen Performance vordefinierte Variablen _UPPER_CHECK = re.compile(r'[A-Z]') _LINK_PATTERN = re.compile(r'https?://\S+') @dataclass class Tag: name: str score: int class FileScanner(HTMLParser): def __init__(self, file: Path): super().__init__() self.file = file self.texte = [] self.links = [] self._current_html_tag = None def scan_file(self): # Datei einlesen content = read_file(self.file) # HTMLParser aufrufen, um HTML-Syntax-Elemente zu entfernen. self.feed(content) words_with_usage = {} words = [] for text in self.texte: # Textteile in einzelne Wörter aufteilen words += re.split(r'[ \n/]', text) # Die Anzahl, der Wörter in der aktuellen Datei, auf der Konsole ausgeben title = self.file.parent.name print(f'\nFile {title} contains {len(words)} words') # Titel in einzelne Wörter aufteilen title_words = set(title.split('-')) for word in words: # Verschiedene Zeichen vom Anfang und Ende der Wörter entfernen. tag_name = word.strip(".,:;!?\"'()-„“«» ") # Leere Wörter ignorieren if not tag_name: continue # Alle Buchstaben verkleinern, aber gleichzeitig originales Wort merken word = tag_name.lower() # Standard Bewertung für jedes Wort ist 10 score = 10 # Wörter, die in der Liste der ausgeschlossenen Wörter stehen, ignorieren if word in EXCLUDED_WORDS: continue # Wörter, die nur aus Zahlen bestehen, ignorieren if word.isdigit(): continue # Die Bewertung von Wörtern, die im Titel vorkommen, deutlich verbessern. if word in title_words: score *= 4 # Die Bewertung von Wörtern, die kürzer oder gleich lang sind als 3 Buchstaben, # entsprechend der Länge des Wortes verringern. word_length = len(word) if word_length <= 3: score = int(score * word_length / 4) # Die Anzahl der Großbuchstaben in dem originalen Wort zählen ... upper_letters_count = len(_UPPER_CHECK.findall(tag_name)) # ... und die Bewertung entsprechen der Anzahl verbessern. score += upper_letters_count * 10 # Die Bewertung leicht erhöhen, wenn ein Bindestrich im Wort enthalten ist. if '-' in word: score += 1 if word not in words_with_usage: # Die Bewertung für das Wort speichern. words_with_usage[word] = Tag(name=tag_name, score=score) else: # Wenn das Wort bereits eine Bewertung besitzt, werden die beiden Bewertungen zusammen gerechnet. words_with_usage[word].score += score link_words = [] for link in self.links: # Eventuelle URL-codierte Zeichen in die eigentlichen Zeichen umwandeln. (z.B. %2F -> /) link = unquote_plus(link) # Link-Teile in einzelne Wörter aufteilen link_words += re.split(r'[/\-_#.?&=]', link) for link_word in link_words: # Alle Buchstaben verkleinern link_word = link_word.lower() # Wenn ein Wort aus dem Text auch in einem Link vorkommt, wird die Bewertung erhöht. # Somit kann verhindert werden, dass Link-Bestandteile als Tags vorgeschlagen werden (z.B. E7xcsFpR). if link_word in words_with_usage: words_with_usage[link_word].score += 10 # Die Wörter nach ihrer Bewertung sortieren return sorted(words_with_usage.values(), key=lambda tag: tag.score, reverse=True) def handle_starttag(self, tag, attrs): self._current_html_tag = tag # Die Links, die in den 'href' Attributen eines HTML-Elements stehen, mit einbeziehen. if tag != "a": return for attr_name, attr_value in attrs: if attr_name == "href": self.links.append(attr_value) break def handle_data(self, data): # Den Inhalt des aktuellen HTML-Tags ignorieren, wenn dieser auf der Liste der ausgeschlossenen HTML-Tags steht. if self._current_html_tag in EXCLUDED_HTML_TAGS: return # Links aus dem HTML-Text extrahieren und entfernen data = _LINK_PATTERN.sub(self._link_replace, data) # Den restlichen Text (ohne Links) innerhalb eines HTML-Elements mit einbeziehen. self.texte.append(data) def _link_replace(self, link_match): self.links.append(link_match.group(0)) return '' def display_tags(tags, max_tags): # Die Ergebnisse auf der Konsole ausgeben. for index, tag in enumerate(tags,): if index >= max_tags: break print(f"Score: {tag.score:>3} Word: {tag.name}") class CustomJsonEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, Tag): return obj.name return super().default(obj) def write_tags(tags): # Die Ergebnisse in JSON umwandeln. content = json.dumps(tags, indent=JSON_INDENT, cls=CustomJsonEncoder, ensure_ascii=False) # Das JSON in eine Datei schreiben. with open(OUTPUT_FILE, 'w') as file: file.write(content) def read_file(file: Path) -> str: # Eine Datei einlesen with open(file, 'r') as file: return file.read() def main(): final_tags = {} # Nach allen Quelldateien suchen for file in SOURCE_DIR.glob(f'**/{SOURCE_FILENAME}'): # Die Dateien, deren Ordner mit 'autosave-' beginnen, ignorieren. title = file.parent.name if title.startswith('autosave-'): continue # Die Datei analysieren scanner = FileScanner(file) tags = scanner.scan_file() # Die Ergebnisse auf der Konsole ausgeben display_tags(tags, max_tags=10) # Die eingestellte Anzahl an Tags für die Ausgabedatei übernehmen, sofern vorhanden. final_tags[title] = tags[:TAGS_PER_ARTICLE] if len(tags) > TAGS_PER_ARTICLE else tags # Die Ausgabedatei schreiben write_tags(final_tags) if __name__ == '__main__': main()