Python Textfilter mit Json-Dictionary, Codierungsprobleme

Python in C/C++ embedden, C-Module, ctypes, Cython, SWIG, SIP etc sind hier richtig.
Antworten
Schuschu92
User
Beiträge: 4
Registriert: Donnerstag 9. Mai 2019, 14:26

Hallo Forum

Ich möchte einen Text filter bauen der anhand von Keywords, Sätze/Texte vordefinierten Kategorien zuordnet. Als Dictionary dient ein json file wo die ganzen Klassen mit den zugehörigen Keywords gelistet sind.

Der python code:

Code: Alles auswählen

import json
import re


# convenience function: check if any topic term is contained within a sentence
def contains(a_list, a_sentence):
    for item in a_list:
        # handle patterned case (prefaced by asterisk)
        # eg. "*dd:ddam" will catch the pattern "10:30am"
        if item[0] == '*':
            if re.search(item.replace('*', '').replace('d', '\d'), a_sentence):
                return (item)
        else:
            if item.lower() in a_sentence.lower():
                return (item)


# convenience function: split text into one or more sentences
def split(text, rows_split_delim=['.', '!', '?']):
    # sentences after the split
    rows_split = []
    # pointer to position within text
    pointer = 0

    # don't try to split messages with links
    if 'http' in text.lower():
        return [text]

    # loop through each character in the message text
    for char in text:
        # if character is a sentence delimeter
        if char in rows_split_delim:
            # split out the text from the previous pointer to this delimeter
            sentence = text[pointer:text.index(char, pointer) + 1]
            # remove extra spaces
            sentence = sentence.lstrip().strip()
            rows_split.append(sentence)
            # update the pointer
            pointer = text.index(char, pointer) + 1

    # finish by splitting out the remaining text
    # from the previous pointer to this delimeter
    # this handles the case of text with no split sentences
    sentence = text[pointer:].lstrip().strip()
    rows_split.append(sentence)

    return rows_split


# Classify class definition
class Classifier(object):
    """A classification object, for topics defined in a json definiton

    Attributes:
        topics_file: a json structure containing words/patterns for a list of topics
    """

    def __init__(self, topics_file):
     
        # load topics and their words
        try:
            self.topics = json.load(open(topics_file))
        except:
            print ('error opening file', topics_file)

    def classify(self, text):
      

        topics_data = {}
        # split out sentences from the text
        sentences = split(text)

        for sentence in sentences:
            # loop through the topics
            for key in self.topics.keys():
                # if the sentence contains any of the words for this topic, add to results
                if contains(self.topics[key], sentence):
                    if key not in topics_data:
                        topics_data[key] = [sentence]
                    else:
                        topics_data[key].append(sentence)

        return topics_data
Das json file (topics.json) ist in etwa so aufgebaut:

Code: Alles auswählen


"Sport": ["Skifahren", "Fußball", "Hängegleiten", "joggen"], 
"Musik": ["Jazz", "Rock", "rap", "Classic"]
 
etc etc..


bei ausprobieren im jupyter notebook:

Code: Alles auswählen

from msgClassify import *

c = Classifier('topics.json')

topics = c.classify("ich gehe gerne hängegleiten")

if len(topics) == 1:
    print(topics)
elif len(topics) == 0:
    print("Kein passendes Thema gefunden")
else:
    print("mehrere Themen gefunden")
    print(topics)
        


kommt folgende meldung:

Code: Alles auswählen

error opening file topics6.json
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-64-843108aa2b3d> in <module>()
      3 c = Classifier('topics6.json')
      4 
----> 5 topics = c.classify("gerät")
      6 
      7 if len(topics) == 1:

G:\10_others\sonstige_2019\msgClassify.py in classify(self, text)
     89         for sentence in sentences:
     90             # loop through the topics
---> 91             for key in self.topics.keys():
     92                 # if the sentence contains any of the words for this topic, add to results
     93                 if contains(self.topics[key], sentence):

AttributeError: 'Classifier' object has no attribute 'topics'

und bei der ausgabe des json-files in jupyter mit diesem code

Code: Alles auswählen

import json

with open('topics.json', encoding="utf-8") as topics:
    data = json.load(topics)
    
print(data)
kommt das hier:

Code: Alles auswählen

---------------------------------------------------------------------------
JSONDecodeError                           Traceback (most recent call last)
<ipython-input-66-0375af9e0575> in <module>()
      2 
      3 with open('topics.json', encoding="utf-8") as topics:
----> 4     data = json.load(topics)
      5 
      6 print(data)

    297         cls=cls, object_hook=object_hook,
    298         parse_float=parse_float, parse_int=parse_int,
--> 299         parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw)
    300 
    301 

\\nzpro001\SOFTWARE32P\Python\A3_6\Anaconda3\lib\json\__init__.py in loads(s, encoding, cls, object_hook, parse_float, parse_int, parse_constant, object_pairs_hook, **kw)
    342         if s.startswith('\ufeff'):
    343             raise JSONDecodeError("Unexpected UTF-8 BOM (decode using utf-8-sig)",
--> 344                                   s, 0)
    345     else:
    346         if not isinstance(s, (bytes, bytearray)):

JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)

Hab übrigens alles in UTF-8 codiert und hat Nüsse gebracht. Was soll ich noch ausprobieren? Ich brauche einen Textfilter der auch ä, ü. ß, ö filtern kann. Vielleicht gibt es noch eine bessere Methode?

Lieben Dank
Schuschu92
User
Beiträge: 4
Registriert: Donnerstag 9. Mai 2019, 14:26

edit: Bitte nicht wundern warum einmal topics6.json und einmal topics.json steht, das war ein versehen(hab vergessen nachträglich den 6er rauszulöschen)
__deets__
User
Beiträge: 14480
Registriert: Mittwoch 14. Oktober 2015, 14:29

Na in der Fehlermeldung steht doch die Lösung: als codec utf-8-sig verwenden.
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Schuschu92: Das erste Problem hast Du Dir selbst gebastelt: Es macht keinen Sinn mit einem nackten ``except`` ohne konkrete Ausnahme einfach *alle* Ausnahmen mit einer `print()`-Ausgabe zu behandeln, und dann einfach so weiter zu machen als sei alles in Ordnung. Ein `Classifier` ohne `topics`-Attribut macht anscheinend keinen Sinn, also lass diese blödsinnige (Nicht)Behandlung von Ausnahmen an der Stelle einfach weg.

Ein nacktes ``except:`` ohne konkrete Ausnahme(n) sollte man nur machen wenn in dem Block die gleiche Ausnahme auch wieder ausgelöst wird (``raise`` ohne alles) oder zumindest die Ausnahme samt Traceback irgendwo protokolliert wird. Sonst kann die Fehlersuche wirklich *sehr* schwer werden.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

Zum Code: was soll denn das `a_` bei a_list und a_sentence? list ich auch ein zu allgemeiner Name für eine Keywords. Die Klammern bei den return sind überflüssig.

`split` wäre mit einem regulären Ausdruck ein Einzeiler.
Dateien die man öffnet sollte man auch wieder schließen, am besten mit dem with-Statement.
In `classify` könnte defaultdict etwas vereinfachen.
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Schuschu92: Anmerkungen zum Quelltext: Die Kommentare die Beschreiben was eine Funktion macht, sollten als Doctring in der Funktion gesetzt werden.

`a_list` und `item` sind ziemlich generische Namen. Der Kommentar suggeriert das die besser `terms` und `term` heissen sollten. Und der `a_*`-Präfix bei den beiden Argumenten macht auch keinen Sinn.

Das ``item[0]`` löst einen `IndexError` aus wenn `item` leer ist. Bei ``item.startswith('*')`` passiert das nicht.

`replace()` entfernt *alle* vorkommen, Du willst an der Stelle im Code doch aber eigentlich nur den '*' am Anfang, auf den gerade geprüft wurde, entfernen.

Das ist aber auch eine etwas komische API mit dem '*', denn durch das ersetzen von 'd' durch r'\d' kann man nach nichts suchen was auch kleine d's enthält und man muss alles was in regulären Ausdrücken eine besondere Bedeutung hat escapen. Robust und flexibel ist das nicht gerade.

Vom Namen der Funktion und der Beschreibung her würde man erwarten, dass die Funktion `True` und `False` zurück gibt. Selbst wenn man den gefundenen Wert und `None` zurück gibt, sollte man das `None` *explizit* zurückgeben.

Ich würde das auf zwei Funktionen aufteilen – die die den Test für einen `term` macht, und darauf aufbauend dann eine die das für mehrere macht.

Bei der `split()`-Funktion könnte man Kommentare sparen wenn man in den Namen nicht `rows` stehen hätte, deren Sinn ich da so gar nicht sehe. Wenn `rows_split` beispielsweise `sentences` hiesse, bräuchte man nicht kommentieren das da die Sätze rein kommen.

Der Test auf Links ist ziemlich weit gefasst denn es gibt sicher viele Texte in denen 'http' oder 'HTTP' vorkommt, die trotzdem keine Links enthalten.

`pointer` hiesse besser `start_index`, dann weiss man a) das es ein Index ist, und b) worauf der zeigt.

Kommentare sollten dem Leser einen Mehrwert über den Code bieten. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da ja bereits als Code, sondern *warum* er das (so) macht, sofern das nicht offensichtlich ist.

Statt den Index vom Satzzeichen mit `index()` zu ermitteln, was ja noch einmal linear sucht, lässt man sich den Index des aktuellen Zeichens einfach mit `enumerate()` in der Schleife mit aufzählen.

Es macht keinen Sinn direkt nach einem `lstrip()` noch ein `strip()` aufzurufen. Einfach `strip()` reicht, denn da ist das `lstrip()` ja mit enthalten.

Die Klasse `Classifier` ist ziemlich grenzwertig. Eine Klasse die nur aus `__init__()` und einer weiteren Methode besteht ist in der Regel eine oder zwei Funktionen unnötigerweise in eine Klasse gesteckt.

Dateien die man öffnet sollte man auch wieder schliessen. Die ``with``-Anweisung ist in dem Zusammenhang sehr praktisch.

Für das Ergebnis von `classify()` bietet sich ein `collections.defaultdict` an.

Wenn man in einer Schleife sowohl den Schlüssel als auch den Wert aus einem Wörterbuch braucht, dann sollte man da auch gleich über beides iterieren.

Aus der `split()`-Funktion könnte man auch eine Generatorfunktion machen.

Einiges davon umgesetzt (ungetestet):

Code: Alles auswählen

import json
import re
from collections import defaultdict


def contains(term, sentence):
    """Check if term is contained within sentence."""
    # 
    # Handle patterned case (prefaced by asterisk),
    # eg. "*dd:ddam" will catch the pattern "10:30am".
    # 
    if term.startswith('*'):
        return bool(re.search(term[1:].replace('d', r'\d'), sentence))
    else:
        return term.lower() in sentence.lower()
    

def contains_any(terms, sentence):
    """Check if any topic term is contained within sentence."""
    return any(contains(term, sentence) for term in terms)


def split_into_sentences(text, delimiters=frozenset('.!?')):
    """Split text into one or more sentences."""
    # 
    # Don't try to split messages with links.
    # 
    if 'http' in text.lower():
        return [text]

    sentences = []
    start_index = 0
    for i, character in enumerate(text):
        if character in delimiters:
            end_index = i + 1
            sentences.append(text[start_index:end_index].strip())
            start_index = end_index

    remaining_text = text[start_index:].strip()
    if remaining_text:
        sentences.append(remaining_text)

    return sentences


def load_topics(filename, encoding='utf-8'):
    with open(filename, encoding=encoding) as file:
        return json.load(file)


def classify(topic2terms, text):
    topic2sentences = defaultdict(list)
    for sentence in split_into_sentences(text):
        for topic, terms in topic2terms.item():
            if contains_any(terms, sentence):
                topic2sentences[topic].append(sentence)

    return topic2sentences
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Schuschu92
User
Beiträge: 4
Registriert: Donnerstag 9. Mai 2019, 14:26

@_blackjack_

Vielen Dank für das Feedback! Hab deinen Code benutzt und es läuft, einzig die Wörter mit Umlauten(ä, ü, ö und ß) werden ignoriert und er kann diese Wörter nicht zuordnen. Komme an diesem Problem einfach nicht weiter :(
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

Dann hast Du wahrscheinlich ein Encoding-Problem. Welches Encoding haben denn Deine Inputdaten und wie gibst Du dieses an?
Schuschu92
User
Beiträge: 4
Registriert: Donnerstag 9. Mai 2019, 14:26

Ich hab nun einfach auf Umlaute komplett verzichtet und es klappt einwandfrei. Aktuell beschäftige ich mich damit, wie ich ein ganzes csv file einlesen kann, (beispielsweise die zu kategorisierenden Texte sind in Spalte 1) und anhand dem Dictionary-Abgleich dann die jeweils ermittelte Kategorie in eine "Kategorien-Spalte" gefüllt wird. Bsp:

Input, csv-file:


Text | Kategorie
_____________________________________________
ich gehe gerne joggen | leer
Ich höre gerne Rock | leer



Output, csv-file:

Text | Kategorie
_____________________________________________
ich gehe gerne joggen | Sport
Ich höre gerne Rock | Musik


Hat vielleicht jemand eine Idee?
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Schuschu92: Es gibt da so ein `csv`-Modul in der Standardbibliothek. Und falls das nicht klar sein sollte: man kann in CSV-Dateien Spalten hinzufügen oder entfernen, dass heisst man wird eine neue Datei erstellen müssen. Die kann man am Ende dann natürlich in die ursprüngliche Datei umbenennen.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten