SQLAlchemy: You must not use 8-bit bytestrings

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
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo Leute,

ich werde so langsam kirre. Ich wollte mir auf die Schnelle einen Quelltext-Schnipsel basteln. Zu Testzwecke wollte zunächst eine *.txt-Datei einlesen und diesen Inhalt in eine SQLite-Datenbank speichern. Es sei angemerkt, dass es Städte gibt, die Umlaute verwenden. Dies habe ich auch berücksichtigt. Aber aus dem schnellen Basteln wurde mal wieder eine ewige Grübelei.

Im nachfolgenden Quellcode sehen wir, dass ich in der populate_database()-Funktion die bytes()-Funktion auf die einzelnen Elemente anwende, in der Hoffnung einen Bytestring erzeugen zu können. Des Weiteren habe ich in der build_engine()-Funktion die berüchtigte text_factory = str auf das engine-Objekt angewendet. Nichts hilft. Weiter unten seht ihr den kompletten TraceBack.

Ich habe die *.txt-Datei hochgeladen. Dafür habe ich extra eine Repository bei BitBucket eingerichtet. Dort könnt ihr die *.txt-Datei herunterladen und es selbst texten.

Code: Alles auswählen

#!/usr/bin/python
# -*- coding: utf-8 -*-

from os import path

from sqlalchemy import create_engine
from sqlalchemy import Column, Integer, Text, String, Date, DECIMAL, ForeignKey
from sqlalchemy import Index
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker

#   setting up root class for declarative declaration
Base = declarative_base()
metadata = Base.metadata

session_factory = sessionmaker()
ScopedSession = scoped_session(session_factory)

class GeneralRegion(Base):

    __tablename__ = "general_region"

    id = Column(Integer, primary_key = True, unique = True, autoincrement = True)
    region = Column(String(100), nullable = False, unique = True)


def populate_database(file_path = None, mode = 'r'):

   with open(file_path, mode) as f: 

      for element in f:
          data = GeneralRegion(region = bytes(element))
          ScopedSession.add(data)

      ScopedSession.commit()
      ScopedSession.close()
      
      return

def build_engine(dbms = None, db_driver = None, sqlite_path = None):
   
    engine = create_engine('{dbms}+{db_driver}:///{file_path}'.format(
                                                                       dbms = 'sqlite',
                                                                       db_driver = u'pysqlite',
                                                                       file_path = sqlite_path,
                                                                       encoding = 'utf8',
                                                                       echo = True))

    engine.connect().connection.connection.text_factory = str

    # We should bind engine to your model.
    ScopedSession.configure(bind=engine)

    return
    
def main():
    dbms = raw_input('Enter dbms (e.g. sqlite): ')
    if dbms == 'sqlite':
        sqlite_path = raw_input('Enter path to sqlite file: ')
        db_driver = 'pysqlite'

    file_path = raw_input('Enter path to to txt-file: ')

    build_engine(dbms = dbms, db_driver = db_driver, sqlite_path = sqlite_path)

    populate_database(file_path = file_path)

    return
    
if __name__ == "__main__":
    main()
Traceback:
Traceback (most recent call last):
File "C:\Users\Sophus\Desktop\line_by_line.py", line 98, in <module>
main()
File "C:\Users\Sophus\Desktop\line_by_line.py", line 93, in main
populate_database(file_path = file_path)
File "C:\Users\Sophus\Desktop\line_by_line.py", line 61, in populate_database
ScopedSession.commit()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\scoping.py", line 157, in do
return getattr(self.registry(), name)(*args, **kwargs)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 906, in commit
self.transaction.commit()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 461, in commit
self._prepare_impl()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 441, in _prepare_impl
self.session.flush()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 2171, in flush
self._flush(objects)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 2291, in _flush
transaction.rollback(_capture_exception=True)
File "C:\Python27\lib\site-packages\sqlalchemy\util\langhelpers.py", line 66, in __exit__
compat.reraise(exc_type, exc_value, exc_tb)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 2255, in _flush
flush_context.execute()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 389, in execute
rec.execute(self)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 548, in execute
uow
File "C:\Python27\lib\site-packages\sqlalchemy\orm\persistence.py", line 181, in save_obj
mapper, table, insert)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\persistence.py", line 835, in _emit_insert_statements
execute(statement, params)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 945, in execute
return meth(self, multiparams, params)
File "C:\Python27\lib\site-packages\sqlalchemy\sql\elements.py", line 263, in _execute_on_connection
return connection._execute_clauseelement(self, multiparams, params)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1053, in _execute_clauseelement
compiled_sql, distilled_params
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1189, in _execute_context
context)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1402, in _handle_dbapi_exception
exc_info
File "C:\Python27\lib\site-packages\sqlalchemy\util\compat.py", line 203, in raise_from_cause
reraise(type(exception), exception, tb=exc_tb, cause=cause)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1182, in _execute_context
context)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\default.py", line 470, in do_execute
cursor.execute(statement, parameters)
ProgrammingError: (sqlite3.ProgrammingError) You must not use 8-bit bytestrings unless you use a text_factory that can interpret 8-bit bytestrings (like text_factory = str). It is highly recommended that you instead just switch your application to Unicode strings. [SQL: u'INSERT INTO general_region (region) VALUES (?)'] [parameters: ('Aasb\xfcttel\n',)]
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Speichere keine binary Strings in einer Datenbank. Wenn du es doch tust nutzt einen entsprechenden Typ für die Spalte wie Binary oder Blob.

Dein Lösungsansatz funktioniert nicht weil du die text_factory nicht richtig setzt.

Darüberhinaus gibt es einige andere unsinnige Dinge in deinem Code:
  • primary_key impliziert unique und bei einem Integer auch auto_increment
  • sqlalchemy.engine.url.{make_url,URL} existiert. Man muss da kein String Formatting betreiben.
  • Du übergibst echo nicht richtig an create_engine. Außerdem wird da glaub ich inzwischen logging empfohlen.
  • scoped_session macht hier nicht nur keinen Sinn sondern es auch praktisch unmöglich den Code zu testen. Vermeide solchen globalen State.
  • Du solltest Bulk Operationen verwenden, die sind wesentlich schneller schon bei moderaten Datenmengen im Bereich von 100K Inserts und mehr.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@DasIch: Ich habe deine kritischen Anmerkungen zur Kenntnis genommen. Bedenke aber, dass es sich hierbei um einen Test-Schnipsel handelt. Demnach bin ich da weniger strenger mit dem Quelltext. Jedoch meinst du, dass ich text_factory falsch setze? Wie sollte es sonst gesetzt werden, wenn nicht auf das engine Objekt?
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Du setzt text_factory nicht auf die Engine, sondern auf ein Connection Objekt des drivers. Das ist zwar prinzipiell richtig aber die Engine kann die Verbindung zu jedem Zeitpunkt schließen oder auch neue erstellen, so dass mehrere Verbindungen existieren mit inkonsistenten text_factory Attributen.

Du musst sicherstellen dass jedesmal wenn eine neue Verbindung erstellt wird die text_factory gesetzt wird. Dazu müsstest du einen Callback für das entsprechende Event registrieren der die text_factory setzt.

Wie aber schon gesagt und von der Fehlermeldung erwähnt, es macht mehr Sinn binary Strings als solche in der Datenbank zu speichern, du würdest ja auch Zahlen, Booleans oder dates nicht als Strings abspeichern.
Sirius3
User
Beiträge: 17745
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: der Fehler ist, `bytes` zu verwenden, denn wie Du ja schreibt, willst Du Text mit Umlauten speichern. Decodier die Text-Datei einfach mit dem richtigen Encoding und benutze nur Unicode.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Sirius3: Ich habe die read_in()-Funktion etwas abgeändert. Dazu habe ich das io-Modul herangezogen. Ich öffne die Datei einfach im UTF-8-Unicode. Die einzelnen Elemente, die aus der *.txt-Dateien gelesen werden, sind da dann in Unicode.

Code: Alles auswählen

import io
def read_in(file_path = None, mode = 'r'):

    list_elements = list()

    list_no_duplicates = []

    with io.open(file_path, mode, encoding = 'utf-8') as f:

        for elem in f:
            if elem.rstrip() not in list_elements:
                list_elements.append(elem.rstrip())

        # Remove all duplicates
        list_no_duplicates = list(set(list_elements))

        # clear first list
        list_elements[:] = []

        return list_no_duplicates

    return
Sirius3
User
Beiträge: 17745
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: was hat jetzt `read_in` mit Deinem ursprüngichen Problem zu tun?

Um Gleichheitszeichen bei Schlüsselwortargumenten werden keine Leerzeichen gesetzt. Defaultwerte, die keinen sinnvollen Default darstellen, sind unsinnig. Warum erzeugst Du Listen mal mit `list()` und mal mit `[]`? `list_no_duplicates` in Zeile 6 wird nie verwendet. Durch die if-Abfrage in Zeile 11 ist schon gewiss, dass es keine Duplikate gibt, warum dann noch `set`? Zeile 18 ist unsinnig, da die Funktionin der nächsten Zeile verlassen wird und damit `list_elements` abgeräumt wird. Zeile 22 wird nie erreicht.

Code: Alles auswählen

import io
def read_in(filename, mode='r'):
    with io.open(filename, mode, encoding='utf-8') as lines:
        return list(set(line.rstrip() for line in lines))
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Sirius3: Das mit list_no_duplicates ist tasächlich unsinnig. Es sollte eigentlich eine Variable sein, in der die Liste, auf die ein set() ausgeführt wurde, gespeichert wird. Ich wollte einfach sicher gehen, dass tatsächlich keine Duplikate mehr vorkommen. Ich bin da wohl etwas paranoid. Die set()-Funktion verschafft mir ein sicheres Gefühl, dass da auch wirklich nichts mehr doppelt vorkommt. Aber deine Variante mit der LC sieht sehr kompakt aus. Und zu deiner Frage: Es ging um das Speichern von Umlauten in die Datenbank. Da die Datei jetzt im UTF-8 geöffnet und eingelesen wird, sind alle Elemente nun vom Typ Unicode. Ich brauche im Anschluss nur noch getrost über die zurückgegebene Liste iterieren und muss mir nicht mehr den Kopf darüber zerbrechen.

Jedoch habe ich eine kleine Frage am Rade. Du meintest, dass bei Schlüsselargumenten zwischen dem Gleichheitszeichen keine Leerzeichen hinsollen. Beziehst du dich auf PEP8? Denn es klappt wunderbar bei mir. Rein optisch sieht es für mein Auge besser aus, als wenn alles NICHT nahe beieinander gerückt ist. Daher habe ich es mir angewöhnt, immer Platz zwischen die Gleichheitszeichen zu verschaffen.
Antworten