Zeile um Zeile in MariaDB schreiben (Pandas Dataframe)

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
ForelliX
User
Beiträge: 8
Registriert: Samstag 2. Mai 2020, 08:58

Guten Abend

Ich habe einen Output der so aussieht:

Code: Alles auswählen

AiFi
adds
its
cart
expand
autonomous
retail
footprint
A
rough
draft
...
Ursprünglich ist es ein Pandas Dataframe ohne Index und ohne Header. Ich möchte nun jede Zeile einzeln in eine MariaDB schreiben. Das Schreiben in die DB habe ich hinbekommen, aber ich steh auf dem Schlauch was Zeile für Zeile in eine Variable abfüllen anbelangt.

Der bisherige, unvollständige Code sieht so aus:

Code: Alles auswählen

import os
import feedparser
import pandas as pd
import mysql.connector

pd.set_option('max_colwidth', -1)

RSS_URL = "https://techcrunch.com/startups/feed/"

feed = feedparser.parse(RSS_URL)
entries = pd.DataFrame(feed.entries)

entries = entries[['title']]
entries = entries.to_string(index=False, header=False)
entries = entries.replace(' ', '\n')
entries = os.linesep.join([s for s in entries.splitlines() if s])

### MariaDB Connector
mydb=mysql.connector.connect(
    host='***.***.***.***',
    port=3306,
    user="***************",
    password="*********************",
    database="dbname",
)

print(entries)

rp_factor = "1"

#############Bis hierhin ok#############

#### jede Zeile in Variable rp_words

mycursor = mydb.cursor()
mycursor.execute("INSERT INTO ************ (rp_words,rp_factor) VALUES (%s,%s)",(rp_words,rp_factor))
Kann mir jemand auf die Sprünge helfen?
Benutzeravatar
__blackjack__
User
Beiträge: 14078
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@ForelliX: Ich finde es ja ein bisschen fragwürdig hier einen Pandas-Dataframe zu verwenden und dann auch noch so verschiedene Objekte an den gleichen Namen `entries` zu binden. Sofern das überhaupt funktioniert was da steht.

Zumal das komisch ist erst eine Zeichenkette mit Zeilenumbrüchen zu *erzeugen* um dann etwas mit jeder Zeile aus dieser Zeichenkette machen zu wollen. Warum dann erst diese *eine* Zeichenkette aus Einzelteilen zusammenfügen um die dann wieder zu trennen‽

Im Beispiel kommen keine Worte mehrfach vor, in der Realität dürfte das aber durchaus passieren. Wie soll damit dann in der Datenbank umgegangen werden? Und warum ist ein Faktor mit dem Wert "1" eine Zeichenkette und keine Zahl? Was bedeutet der `rp_*`-Präfix? Kann man den bitte ausschreiben? Sowohl bei den Namen im Programm, als auch bei den Spaltennamen in der Datenbank.

Warum ist der Tabellenname ein Geheimnis?
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
ForelliX
User
Beiträge: 8
Registriert: Samstag 2. Mai 2020, 08:58

Meine Idee von diesem Script ist ein "Wort-Radar" zu erstellen. Ich möchte aus verschiedenen Quellen (im Beispiel ein RSS-Feed) einzelne Begriffe/Wörter in eine Datenbank schreiben. Danach mit einem weiteren Script die identischen Begriffe zusammen zu zählen und dann schlussendlich auf einer Webseite die Top-Begriffe anzeigen lassen.

Ich weiss gar nicht mehr wie ich zu Pandas gekommen bin. Muss also keine Dataframe sein. Die einzige Anforderung ist, dass ich einfach bestimmte Wörter ersetzen, löschen oder umschreiben kann. Weder der Datenbankname, noch die Felder sind ein Geheimnis. Habe ich im Verlauf der Erstellung irgendwann so benannt.
Benutzeravatar
__blackjack__
User
Beiträge: 14078
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@ForelliX: Das ist dann aber komisch ein Wort mehrfach in die Datenbank einzutragen, statt gleich dort zu zählen.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
ForelliX
User
Beiträge: 8
Registriert: Samstag 2. Mai 2020, 08:58

Es sollen verschiedenste Quellen sein, die Anzahl der Wörter wird unterschiedlich und ich möchte mit einem variablen Wert für Einträge (z.B. die letzten 10'000 Einträge zählen) und auch Zeit (z.B. alle 15 Minuten) testen können.

Ich habe nun mit
for line in entries:
gestestet.

Komischerweise macht es für jeden einzelnen Buchstaben einen Eintrag in der DB.. Da ist wohl ein von dir befürchtetes DataFrame-Problem..
ForelliX
User
Beiträge: 8
Registriert: Samstag 2. Mai 2020, 08:58

Habe es herausgefunden. Vielleich nicht schön, vielleicht nicht schnell und von einem absoluten Python-Amateur erstellt, aber funktioniert:

Code: Alles auswählen

import os
import feedparser
from matplotlib import lines
import pandas as pd
import mysql.connector   

pd.set_option('max_colwidth', -1)

RSS_URL = "https://techcrunch.com/startups/feed/"
rp_factor = "1"
rp_demo = 0 # 1=Demo on 0=Demo Off

feed = feedparser.parse(RSS_URL)
entries = pd.DataFrame(feed.entries)

entries = entries[['title']]
entries = entries.to_string(index=False, header=False)

#Cleanup
entries = entries.replace('  ', ' ')
entries = entries.replace(' ', '\n')
entries = os.linesep.join([s for s in entries.splitlines() if s])
#############################################################################################################


if rp_demo > 0:
    
    print(entries)

else:

    entries = entries.split()
    
    mydb=mysql.connector.connect(
        host='***.***.***.***',
        port=3306,
        user="*******",
        password="*******",
        database="dbname",
    )

    for row in entries:
        rp_words = (row)
        mycursor = mydb.cursor()
        mycursor.execute("INSERT INTO tablename (rp_words,rp_factor) VALUES (%s,%s)",(rp_words,rp_factor))
        mydb.commit()
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

ForelliX hat geschrieben: Freitag 11. März 2022, 23:55 for row in entries:
rp_words = (row)
mycursor = mydb.cursor()
mycursor.execute("INSERT INTO tablename (rp_words,rp_factor) VALUES (%s,%s)",(rp_words,rp_factor))
mydb.commit()
[/code]
Bitte entschuldige, aber ich fürchte, Du hast das mit den Datenbank(en) nicht verstanden. Und das mit dem Parsen auch nicht... und das mit dem Pandas. :-(
ForelliX
User
Beiträge: 8
Registriert: Samstag 2. Mai 2020, 08:58

Das kann gut sein, aber ich lerne gerne. Mich würde natürlich interessieren, wie du das gelöst hättest und wie dein Skript aussehen würde. Natürlich nur wenn es deine Zeit erlaubt, bei mir läuft es ja :-)
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

ForelliX hat geschrieben: Samstag 12. März 2022, 08:27 Das kann gut sein, aber ich lerne gerne. Mich würde natürlich interessieren, wie du das gelöst hättest und wie dein Skript aussehen würde. Natürlich nur wenn es deine Zeit erlaubt, bei mir läuft es ja :-)
Okay, fangen wir mir dem Parsen an... Es gibt (natürlich) mehrere Möglichkeiten, Du verwendest str.split(). Das funktioniert aber aus drei Gründen nicht gut: erstens wegen der Zeichensetzung -- Du entfernst ja nur einen kleinen Teil davon --, zweitens wegen der Stoppwörter [1], und dann ist da ja noch der verdammte Plural... Es nutzt Dir vermutlich wenig, wenn Deine Datenbank hunderte Einträge für Worte wie "is", "a", "are" oder ähnliche enthalten, und wenn die Satzzeichen etc. nicht herausgefiltert werden -- dann hättest Du nämlich womöglich verschiedene Einträge für "company", "company," und "company." -- und wenn dann noch die Pluralformen davon vorkommen, hast Du sechs Einträge, die im Kern trotzdem alle dasselbe sind.

Deswegen ist es sinnvoll, einen besseren Tokenizer zu benutzen. Mein Code weiter unten verwendet spaCy, aber in scikit-learn und im NLTK gibt es ähnliche Möglichkeiten und obendrein noch ein paar "freie" Projekte um das Thema. Solche Tokenizer benutzen meistens vorgefertigte oder eigene MachineLearning-Modelle, um verschiedene Eigenschaften eines Wortes im Satzkontext zu berechnen, etwa: ist das Wort ein Stoppwort, ist es ein Verb, ein Substantiv, und so weiter. Der spaCy-Tokenizer erzeugt zudem für jedes Wort das zugehörige Lemma, also die Grundform eines Wortes, für "companies" also "company".

Nur um das kurz zu verdeutlichen: wenn ich die Stopwords und die Zeichensetzung aus dem von Dir verlinkten RSS-Feed nicht entferne, sind die häufigsten "Worte" diese (hier aus Platzgründen nur die ersten fünf, aber es geht noch lange so weiter):

Code: Alles auswählen

,                               915
the                             650
.                               582
to                              475
be                              444
Nicht so wertvoll für Deine WordCloud, oder? Und es geht noch viele, viele Zeilen lang so weiter... Wenn ich Zeichensetzung, Stopwords etc ausfiltere, bekomme ich dagegen dieses Ergebnis:

Code: Alles auswählen

company                         105
startup                          95
code                             64
work                             54
Das sieht doch schon viel besser aus, finde ich. Hier ist mein Code:

Code: Alles auswählen

#!/usr/bin/env python

RSS_URL = 'https://techcrunch.com/startups/feed/'

from collections import Counter

import feedparser
from bs4 import BeautifulSoup
import spacy


def filter_tokens(text):
    '''filter alphanumeric and stopwords and return list of tokens'''
    return [token.lemma_ for token in text
            if token.is_alpha and not token.is_stop]


def main():
    nlp = spacy.load('en_core_web_md')
    tokens = list()
    for entry in feedparser.parse(RSS_URL).entries:
        tokens += filter_tokens(nlp(entry.title))
        for item in entry.content:
            tokens += filter_tokens(nlp(BeautifulSoup(item.value, 'lxml').text))
    for word, count in Counter(tokens).most_common(10):
        print('{:<30s} {:>4d}'.format(word, count))
    

if __name__ == '__main__':
    main()
Was relationale Datenbanken angeht, so nutzt man dort üblicherweise eine Technik namens Normalisierung [2]. Dies würde für Dich bedeuten, daß Du Deine Daten in zwei Tabellen abbilden müßtest: einer Tabelle, die jeweils eine feste ID für jedes einzelne Wort beinhaltet (im Beispiel unten: Tabelle "words") und einer zweiten Tabelle, in der diese IDs referenziert und auf Deine Zeitstempel gemappt werden (Tabelle "counter").

Zuletzt bin ich mir nicht ganz sicher, ob es sinnvoll ist, diese Payload in eine relationale Datenbank zu schreiben -- das hängt natürlich auch von der Datenmenge ab, im Moment reden wir ja noch von nur einem nicht sehr großen Rss-Feed, das sind ja überschaubare Datenmengen. Aber wenn Du größere Datenmengen hast, könnte das mit einer relationalen Datenbank zunehmend imperformanter werden und als Nacharbeit eine Analyse der verwendeten Datenbank-Queries und die Anlage geeigneter Indizes erfordern. Außerdem wäre bei einer relationalen Datenbank natürlich ein Housekeeping sinnvoll, das regelmäßig die veralteten und daher uninteressant gewordenen Datensätze löscht. Womöglich könnte es aber daher einfacher und ressourcenschonender sein, die Daten in einen Pandas-Dataframe zu packen, um ihn einfach mit der Methode pandas.DataFrame.to_pickle() in eine Datei zu schreiben und später mit pandas.read_pickle() wieder einzulesen. Spaßeshalber habe ich das Skript unten einfach 20 Male laufenlassen, bis in der Tabelle "counter" etwas über 160k Einträge vorhanden waren -- die PostgreSQL-Datenbank ist dann ca. 22 MB groß, ein ge-pickle-ter Pandas-Dataframe hingegen nur 44 kB, und die ganzen Netzwerk- oder Socket-Roundtrips auf der relationalen Datenbank sind ja auch nicht ganz ohne -- allerdings funktioniert das natürlich nur, wenn die Datenmenge in den Arbeitsspeicher paßt. Ich persönlich würde aber auch noch andere Möglichkeiten in Betracht ziehen, csv-oder JSON-Dateien, Redis oder (wenn ohnehin schon vorhanden) OpenSearch zum Beispiel. Insbesondere Redis und OpenSearch (Amazons Fork von Elasticsearch) böten hier zum Beispiel auch die Möglichkeit, beim Eintragen neuer Daten gleich ihre Lebensdauer einzustellen, nach deren Ablauf sie automatisch gelöscht werden.

Aber, Du hattest ja nach meiner Lösung gefragt, und die sähe etwa so aus wie folgt, ich habe dabei nur ein bisschen Fehlerbehandlung weggelassen. Ich verwende allerdings kein MySQL, sondern PostgreSQL, allerdings sollte sich mein SQL-Code leicht konvertieren lassen -- hier in diesem Fall betrifft das ja IMHO nur die Primärschlüssel.

Code: Alles auswählen

#!/usr/bin/env python

RSS_URL = 'https://techcrunch.com/startups/feed/'
DB_CONNECTION_STRING = 'dbname=wordklaus'

from collections import Counter

import feedparser
from bs4 import BeautifulSoup
import spacy
import psycopg2


class DbWriter:
    ''' # create tables in PostgreSQL
    create table words (id serial not null unique primary key, word text unique);
    create table counter (id serial not null unique primary key, zeit timestamp not null default now(), word_id integer references words(id));
    # and maybe:
    create view wordcounts as select count(w.word), w.word from counter c left join words w on c.word_id = w.id GROUP BY w.word ORDER BY count(w.word) DESC;
    '''
    def __init__(self, connection):
        self.connection = connection
        self.cache = dict()

    def add_token(self, word):
        try:
            curs = self.connection.cursor()
            curs.execute('INSERT INTO words(word) VALUES (%s)', [word])
            self.connection.commit()
        except psycopg2.errors.UniqueViolation as e:
            pass # okay, we just wanted to make sure the word exists
        finally:
            curs.close()
            self.connection.commit()
            
        if word in self.cache.keys():
            word_id = self.cache[word]
        else:
            curs = self.connection.cursor()
            curs.execute('SELECT id FROM words WHERE word=%s', [word])
            result = curs.fetchall()
            curs.close()
            word_id = result[0][0]
            self.cache[word] = word_id
        
        curs = self.connection.cursor()
        curs.execute('INSERT INTO counter (word_id) VALUES (%s)', [word_id])
        curs.close()
        self.connection.commit()
        

def filter_tokens(text):
    '''filter alphanumeric and stopwords, and return list of tokens'''
    #return [token.lemma_ for token in text if token.is_alpha and not token.is_stop]
    return [token.lemma_.lower() for token in text if token.is_alpha and not token.is_stop]

def main():
    nlp = spacy.load('en_core_web_md')
    tokens = list()
    for entry in feedparser.parse(RSS_URL).entries:
        tokens += filter_tokens(nlp(entry.title))
        for item in entry.content:
            tokens += filter_tokens(nlp(BeautifulSoup(item.value, 'lxml').text))
    connection = psycopg2.connect(DB_CONNECTION_STRING)
    db = DbWriter(connection)
    for token in tokens:
        db.add_token(token)
    connection.close()

if __name__ == '__main__':
    main()

HF, YMMV.

PS: Es könnte sinnvoller sein, die Worte in Kleinschreibung zu konvertieren, das habe ich hier mal gelassen. Dazu muß nur "token.lemma_" in der Funktion filter_tokens() durch "token.lemma_.lower()" ersetzt werden.


[1] https://de.wikipedia.org/wiki/Stoppwort
[2] https://de.wikipedia.org/wiki/Normalisi ... Datenbank)
LukeNukem
User
Beiträge: 232
Registriert: Mittwoch 19. Mai 2021, 03:40

LukeNukem hat geschrieben: Sonntag 13. März 2022, 12:01 ...Dinge...
Huch, kleine Korrektur: die Angaben zu Pandas oben gelten nur für bereits akkumulierte Daten, also das Ergebnis von "SELECT * FROM wordcounts". Wie dem auch sei: weil ich's wissen wollte, habe ich drei weitere kleine Skripte gebastelt: eines, das die Daten in einem Pandas DataFrame mit dem Spalten "zeit" und "wort" speichert, also im Prinzip so wie die DB, sowie je ein einfaches Auswerteskript für die Daten in Pandas und die in der Datenbank. Danach habe ich kurz mal die Eigenschaften beider Lösungswege betrachtet, nachdem die "download_to_*.py"-Skripte je zwanzigmal gelaufen waren. Die Zeitangaben betreffen jeweils lediglich nur einen Aufruf des jeweiligen Skripts.

Code: Alles auswählen

download_to_console.py | Lädt RSS-Feed herunter, collections.Counter und Ausgabe |  4.5s
download_to_db.py      | Lädt RSS-Feed herunter und speichert in PostgreSQL      | 26.5s
auswert_db.py          | Wertet Daten aus Datenbank aus                          |  0.15s
download_to_pd.py      | Lädt RSS-Feed herunter und speichert in DataFrame       |  4.8s 
auswert_pd.py          | Wertet Daten aus DataFrame aus                          |  0.36s
Die Datenbank-Größe in PostgreSQL mit 20 Datensätzen und nach einem "VACUUM FULL ANALYZE" beträgt 19 MB, die Dateigröße des Pandas-Dataframe ist 3,9 MB. Die Dateigröße auf Pandas' Seite ließe sich mittels Kompression nochmals deutlich verringern (auf ca. 388 kB), aber dann dauern das Lesen und Schreiben wegen der Kompression und Dekompression natürlich etwas länger.

Das Ergebnis ist, daß die Datenbanklösung etwa um den Faktor 5,5 zum Herunterladen und Speichern der Daten braucht und eine etwa um den Faktor 4,8 größere Datenmenge benötigt als die Lösung mit Pandas. Dafür benötigt die Pandas-Auswertung etwa um den Faktor 2,4 länger.

Natürlich hat die ganze Geschichte mit diesen wenigen Testdaten nur eine begrenzte Aussagekraft, aber ein gewisser Trend läßt sich IMHO schon erkennen.

Die Skripte gibt es unter https://drive.google.com/file/d/1RevkXZ ... sp=sharing in einem Zip-Archiv von ca. 4 kB.

(Konfiguration: Dell Latitude 5580 mit Intel i7-7820HQ, 32 GB RAM DDR4 @ 2400 MHz, Samsung EVO 970 Plus 1TB, Kubuntu Linux 20.04.04 LTS, Python 3.8.10)
ForelliX
User
Beiträge: 8
Registriert: Samstag 2. Mai 2020, 08:58

:shock: Einerseits danke ich dir für diese Erklärungen und vor allem für deine Zeit. Andererseits hält das mir vor Augen, dass mir ungefähr 10 Jahre Programmiererfahrung fehlen.. Ich verstehe kaum zwei Drittel des Scripts, wäre niemals von selbst auf diese Ideen gekommen. Ich werde mich in Zukunft eher an Anfängerforen wenden, da bin ich besser aufgehoben. Aber nochmal: Vielen Dank, ist sehr beeindruckend! :roll:
Antworten