Kleines Problem mit SQL-Imports

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Antworten
Novar
User
Beiträge: 5
Registriert: Mittwoch 4. Mai 2022, 19:45

N'Abend zusammen,

ich habe letzte Woche privat ein kleines Projekt angefangen, um ein Forum inhaltlich für dessen Community - war vor ~10 Jahren irgend ne Foren-Eigenentwicklung, auf der es keinerlei Suchfunktionen oder ähnliches mehr gibt - auszulesen, relevante Informationen in eine eigene Datenbank zu schreiben (konform in eine phpBB-Installation), die Inhalte dann vom Forum indexieren zu lassen um letztlich eine Suchfunktion anzubieten, die von der restlichen Forensoftware abgetrennt wurde.
Insgesamt sind das mehrere Millionen Beiträge.

Soviel zum Plan.
Weil sich dazu Python, das habe ich schon in der Vergangenheit immer wieder mal irgendwo aufgeschnappt, so hervorragend eignet habe ich also zu Python gegriffen. Mit Python hatte ich aber noch nie zu tun, abgesehen von kleinen und nicht nennenswerten Script-Anpassungen auf dem Raspberry. Das Script ist knapp 270 Zeilen lang und soweit auch eigentlich(!) fertig.
Vor dem Start gebe ich noch den auszulesenden Foren-Bereich als URL an, setze die Variable für die posts und topics auf das jeweils derzeitige autoincrement der Datenbank, gebe die datenbankinterne foren-id ebenfalls einer Variable und lets go... das Script gräbt sich über alle Seiten hinweg durch die Thread-Seiten und nimmt alle jeweils darin enthaltenen Beiträge mit.

Knapp 800.000 Beiträge in etwa 45.000 Themen habe ich durch. Weil ich meinen Laptop aber ebenso wenig mehrere Wochen mit dem Script laufen lassen möchte wie meinen Rechner, habe ich das jetzt alles auf einen vServer geschmissen, auf dem das dann am Ende eh der Community bereit gestellt wird.

Das zur Sache insgesamt; jetzt zum Problem:
Für mich nicht nachvollziehbar wird das Script manchmal mit Errors beendet; im Script intern immer bei einem der SQL-Imports, beim Lauf aber "spontan".
Ist mir schon lokal aufgefallen... passierte immer wieder. Mal nach 30 Minuten, mal nach 15 Stunden, usw. Nicht super, weil ich dann den jeweiligen Lauf oft von vorne beginnen musste: Also Datenbank zurück spielen, Lauf neu starten. Ist also nicht so, dass das immer bei einem bestimmten Beitrag oder so, also an immer den gleichen Stellen des Auslesens passiert wäre.

Auf dem vServer jetzt tritt das Problem aber schon nach wenigen Threads auf. Mal nach 10, mal nach 15, dann nach 13. Länger als 30 Sekunden läuft es nicht. Der Fehler wird damit wirklich nervend...

Mal die Funktion für den Import der Beiträge:

Code: Alles auswählen

# Ausgelesene Beitragsinformationen als ganze Datensätze in die Datenbank importieren
def sql_import_post(topicid, forumid, timestamp, topictitle, text4sql, posturl, author4sql, url):
    db = pymysql.connect(host='localhost', user='ckadmin', password='blablubb', db='cforum', port=3306, charset='utf8')
    cursor = db.cursor()

    sql1 = """
                insert into phpbb_posts(
                topic_id,
                forum_id,
                post_time,
                post_username,
                post_subject,
                post_text,
                post_url )value('%s','%s','%s','%s','%s','%s','%s')
    """ % (
    topicid, forumid, timestamp, author4sql, topictitle, text4sql, posturl)
    # Auf Ausnahmefall prüfen...
    try:
        cursor.execute(sql1)
        db.commit() #SQL-Kommando wird abgesetzt, die Datenbank beschrieben
    except:
        cursor.rollback()
        print('Write failed')

    cursor.close()
    db.close()
*das angegebene Datenbank-Passwort ist natürlich nicht zutreffend...

Und folgender Fehler:

Code: Alles auswählen

Traceback (most recent call last):
  File "/home/chef.py", line 188, in sql_import_post
    cursor.execute(sql1)
  File "/usr/local/lib/python3.9/dist-packages/pymysql/cursors.py", line 148, in execute
    result = self._query(query)
  File "/usr/local/lib/python3.9/dist-packages/pymysql/cursors.py", line 310, in _query
    conn.query(q)
  File "/usr/local/lib/python3.9/dist-packages/pymysql/connections.py", line 548, in query
    self._affected_rows = self._read_query_result(unbuffered=unbuffered)
  File "/usr/local/lib/python3.9/dist-packages/pymysql/connections.py", line 775, in _read_query_result
    result.read()
  File "/usr/local/lib/python3.9/dist-packages/pymysql/connections.py", line 1156, in read
    first_packet = self.connection._read_packet()
  File "/usr/local/lib/python3.9/dist-packages/pymysql/connections.py", line 725, in _read_packet
    packet.raise_for_error()
  File "/usr/local/lib/python3.9/dist-packages/pymysql/protocol.py", line 221, in raise_for_error
    err.raise_mysql_exception(self._data)
  File "/usr/local/lib/python3.9/dist-packages/pymysql/err.py", line 143, in raise_mysql_exception
    raise errorclass(errno, errval)
pymysql.err.DataError: (1366, "Incorrect string value: '\\xF0\\x9F\\xA4\\xAD\\xF0\\x9F...' for column `cforum`.`phpbb_posts`.`post_text` at row 1")

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/chef.py", line 276, in <module>
    get_topic_content(url, lauf, topicid)
  File "/home/chef.py", line 62, in get_topic_content
    get_post_content(get_domain(url) + link, topictitle, topicid)
  File "/home/chef.py", line 121, in get_post_content
    sql_import_post(topicid, forumid, timestamp, topictitle, text4sql, posturl, author4sql, url)
  File "/home/chef.py", line 191, in sql_import_post
    cursor.rollback()
AttributeError: 'Cursor' object has no attribute 'rollback'
Ich hoffe wirklich sehr, dass ihr mir helfen könnt. Ich kann nicht mehr... :D
Benutzeravatar
sparrow
User
Beiträge: 4165
Registriert: Freitag 17. April 2009, 10:28

Der zweite Teil der Fehlermeldung sagt schon einmal, dass du versuchst ".rollback()" auf einen Cursor aufzurufen. Das gibt es dort aber nicht.
Die Datenbankconnection eine Methode "rollback", so wie sie auch eine "commit"-Methode hat.

Dann ist es ungewöhnlich und langsam für jeden einzelnen insert einen Datenbankverbindung aufzubauen. Ich würde sogar sagen, es ist ungewöhnlich das auch noch alles in einzelnen Transaktionen zu machen. Dafür gibt es .executemany()

Und das Problem mit dem Insert (nicht "import" übrigens) ist, dass du da versuchst etwas in die Datenbank zu schreiben, von dem die Datenbank der Meinung ist, dass es falsch ist. Ich tippe auf ein Encoding-Problem.
Und das kann daher kommen, dass du SQL-Statements irgendwie per Hand zusammen bastelst. Man nimmt dafür keine String-Formatierung. Wo hast du in der Dokumentation gelesen, dass das so geht?

Edit:
Hier steht wie man es richtig macht. Execute aufrufen, das SQL-Statement mit Platzhaltern für die Values als ersten Parameter, ein Tuple mit Werten als zweiten parameter.

EditEdit:
Dem ganzen geht man prima aus dem Weg, wenn man ein ORM wie SQLAlchemy verwendet.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Du hast einige Fehler in Deiner Funktion: Exceptions sollten immer so konkret sein wie möglich, also nur das Abfangen, was Du auch erwartest. Und dann solltest Du hier in diesem Fall auch ausgeben, was in der Exception steht, sonst ist es ja unmöglich, zu erfahren, warum der INSERT nicht funktioniert hat. Und dann solltest Du Dir überlegen, was im Falle eines Fehlers passieren soll. Soll das Programm abbrechen? Soll der INSERT ignoriert werden, und dann mit dem nächsten weiter gemacht werden? Im Moment ignorierst Du den Eintrag einfach, gibst aber auch dem Nutzer nicht die Möglichkeit, nachzuvollziehen, welcher Eintrag kaputt ist, um den später noch zu reparieren.

Du hast Glück, dass Du im except-Block nochmal eine Exception auslöst, denn sonst hättest Du den eigentlichen Fehler gar nicht gesehen.

Und der eigentliche Fehler ist, dass man niemals Werte in einen SQL-Ausdruck hineinformatiert, sondern immer Platzhalter verwendet.
Literale Strings wie HOST, USER, PASSWORD, etc. definiert man als Konstanten am Anfang der Datei, damit man sie leicht findet und ändern kann.

Ich reiche hier die Exception einfach nach oben weiter, weil es vielleicht dort sinnvoller ist, den Fehler zu behandeln.

Code: Alles auswählen

from contextlib import closing

SQL_POSTS_INSERT = """
            insert into phpbb_posts(
            topic_id,
            forum_id,
            post_time,
            post_username,
            post_subject,
            post_text,
            post_url )value(%s, %s, %s, %s, %s, %s, %s)
"""

def sql_import_post(db, topicid, forumid, timestamp, topictitle, text4sql, posturl, author4sql, url):
    try:
        with closing(db.cursor()) as cursor:
            cursor.execute(SQL_POSTS_INSERT, [topicid, forumid, timestamp, author4sql, topictitle, text4sql, posturl])
    except pymysql.err.DataError:
        print(f"Error writing {topicid}")
        db.rollback()
        raise
    else:
        db.commit()
Novar
User
Beiträge: 5
Registriert: Mittwoch 4. Mai 2022, 19:45

Sorry... bin gestern weggepennt... :D
Danke euch beiden.

Da hab ich wohl einiges ziemlich vermurkst. Gerade noch in der Arbeit, werde ich das im Feierabend später aber noch entsprechend eurer Empfelungen umsetzen.
Nur die Verwendung von SQLAlchemy würde ich vorerst noch sein lassen - oder wäre das doch dringend zu empfehlen? ^^
Novar
User
Beiträge: 5
Registriert: Mittwoch 4. Mai 2022, 19:45

Okay... offensichtlich machen die Emoji Probleme.
Jetzt führt beim Import immer der gleiche Post zum Fehler; nämlich ein Beitrag mit "🤭".

Code: Alles auswählen

Error writing 49054
Traceback (most recent call last):
  File "/home/chef.py", line 260, in <module>
    get_topic_content(url, lauf, topicid, db)
  File "/home/chef.py", line 64, in get_topic_content
    get_post_content(get_domain(url) + link, topictitle, topicid, db)
  File "/home/chef.py", line 115, in get_post_content
    sql_import_post(db, topicid, forumid, timestamp, topictitle, text4sql, posturl, author4sql, url)
  File "/home/chef.py", line 180, in sql_import_post
    cursor.execute(SQL_POSTS_INSERT, [topicid, forumid, timestamp, author4sql, topictitle, text4sql, posturl])
  File "/usr/local/lib/python3.9/dist-packages/pymysql/cursors.py", line 148, in execute
    result = self._query(query)
  File "/usr/local/lib/python3.9/dist-packages/pymysql/cursors.py", line 310, in _query
    conn.query(q)
  File "/usr/local/lib/python3.9/dist-packages/pymysql/connections.py", line 548, in query
    self._affected_rows = self._read_query_result(unbuffered=unbuffered)
  File "/usr/local/lib/python3.9/dist-packages/pymysql/connections.py", line 775, in _read_query_result
    result.read()
  File "/usr/local/lib/python3.9/dist-packages/pymysql/connections.py", line 1156, in read
    first_packet = self.connection._read_packet()
  File "/usr/local/lib/python3.9/dist-packages/pymysql/connections.py", line 725, in _read_packet
    packet.raise_for_error()
  File "/usr/local/lib/python3.9/dist-packages/pymysql/protocol.py", line 221, in raise_for_error
    err.raise_mysql_exception(self._data)
  File "/usr/local/lib/python3.9/dist-packages/pymysql/err.py", line 143, in raise_mysql_exception
    raise errorclass(errno, errval)
pymysql.err.DataError: (1366, "Incorrect string value: '\\xF0\\x9F\\xA4\\xAD\\xF0\\x9F...' for column `cforum`.`phpbb_posts`.`post_text` at row 1")
Also wohl wirklich ein Encoding-Problem, wie von sparrow vermutet. Ich habe zwar noch vor, die SQL-Inserts von der Häufigkeit her zu reduzieren, indem ich auf Listen umsteige - aber das wird dieses Problem nicht lösen.

Habt ihr Vorschläge? ^^
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Novar
User
Beiträge: 5
Registriert: Mittwoch 4. Mai 2022, 19:45

Ah... vielen Dank.
Läuft jetzt seit 10 Minuten durch. Super. :D
Novar
User
Beiträge: 5
Registriert: Mittwoch 4. Mai 2022, 19:45

Eine kleine Frage hätte ich da noch; könnte das einfach mal zwei stunden laufen lassen und sehen was passiert, aber ich denke ihr habt da schon genug Erfahrung... :D
Alle Beiträge pro Thread werden jetzt übrigens schon in ner Liste gesammelt und dann in einem Schwung in die DB geschrieben.

Ich mache vor jedem Request (beim Seitenwechsel einer Themenübersicht, beim Thema selbst, ...) eine Sekunde pause - einfach, weil ich das zu Beginn irgendwo im Netz gelesen habe:

Code: Alles auswählen

def get_url_content(url):
    # wartet 1 Sekunde zwischen jeder anfrage
    time.sleep(1)
    return requests.get(url).text
Ist das überhaupt sinnvoll, oder ziehe ich den gesamten Vorgang damit nur unnötig in die Länge?
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Novar: Das kommt darauf an wie viel Last das bei der abgefragten Website erzeugt und ob die Betreiber das mögen. Falls sie das nicht mögen, könnten sie irgendwann anfangen Gegenmassnahmen zu ergreifen.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten