• 10 źródeł
  • Najlepszy* Newsletter
  • Kontakt
✕

Kodowanie znaków przy połączeniu do bazy danych zdradliwe bywa

Python | 2021-04-28

Kodowanie znaków przy połączeniu do bazy danych zdradliwe bywa

Kodowanie znaków to temat, który czasem potrafi dać w kość. Niezależnie od technologii, przyjdzie taki dzień, że ktoś nam CP-1250 a my byśmy chcieli UTF-8 (nie mylić z WTF, he he).

Kiedyś wszystko było w ASCII i nie było problemów. Kiedyś to były czasy. Teraz nie ma czasów.

Stary Informatyk

Ktoś w pewnym momencie wpadł na pomysł, że ASCII, to za mało. Że potrzebujemy zestaw znaków, który nam będzie obejmować wszystkie pisma używane na świecie. I tak powstał Unicode.

Sam w sobie jest super wynalazkiem. Mamy dzięki niemu emoji! 🙌

Musimy jednak zwracać uwagę na to, w jaki sposób są zakodowane nasze pliki, nasze bazy danych czy jakiego kodowania znaków używamy w naszych programach.

A dzisiaj chciałem podzielić się moją historią.

Tło – łączymy się MySQL przy pomocy SQLAlchemy

Chciałem w Pythonie podłączyć do MySQL i załadować trochę danych.

Jak na budowie, pustaki na taczkę i z taczki na górę.

Jak się podłączyć do MySQL z Pythona? Ano zainstalowałem SQLAlchemy, bo wyskoczyło w google na górze. Przygotowałem kawałek kodziku, Miał on za zadanie podłączyć się do bazy danych MySQL, załadować dane z csv do DataFrame, a następnie wrzucić DataFrame do bazy danych (pandas ma do tego gotową metodę to_sql())

import pandas as pd
import sqlalchemy

def get_db_connection():
    # connect to the database
    engine = sqlalchemy.create_engine(
        "mysql://user:passhardasfuck@server/database", encoding="utf-8"
    )
    connection = engine.connect()
    return connection

def ingest_data(db_conn):
    data_df = pd.read_csv("/data/file.csv", sep=",", encoding="utf-8")
    data_df.to_sql(
        "table", db_conn, if_exists="append", index=False
    )

if __name__ == "__main__":
    with get_db_connection() as db_connection:
        ingest_data(db_connection)

Fajnie. Testuję lokalnie – działa. Opakowuję skrypt w kontener, bo ma być częścią większej całości. Definiuję prosty Dockerfile

FROM python:3.9

WORKDIR /app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY ingest_data.py ./
ENTRYPOINT ./ingest_data.py

I można powoli kończyć na dzisiaj. Jeszcze ostatnie testy. W konsoli wklepuję

docker run my-fancy-ingestion

I zonk. Błąd, dane się nie ładują.

UnicodeEncodeError: 'charmap' codec can't encode characters in position 0-3: character maps to <undefined>

// Pełny stacktrace na końcu

WAT - kaczka na wodzie

Coś nie tak z kodowaniem znaków. Zaczynamy szukać w Internetach.

Kodowanie znaków w Pythonie – gdzie szukać.

Najpierw sprawdziłem, czy na pewno mam w kontenerze odpowiednie poustawiane LOCALE. Wszystkie zgodnie na UTF-8, czyli tak jak ma być.

Na wszelki wypadek ustawiam zmienną PYTHONIOENCODING na UTF-8 – nie pomaga.

Sprawdzam z PYTHONUTF8 ustawionym na 1, ale też nie pomaga.

Przecież wszystko poustawiane. Wszystkie metody czytające pliki mają kodowanie ustawione na UTF-8 (encoding="utf-8")

Powoli się poddaję. Niby wszystko dobrze poustawiane. A mimo to – nie działa. I to tylko w Dockerze, bo lokalnie śmigało…

Rozwiązanie – kodowanie znaków przy łączeniu do MySQL

Dopiero gdzieś w odmętach internetu znalazłem, że trzeba dodać ?charset=utf8mb4 do connection string.

I to pomimo tego, że czytałem wszystkie pliki w utf-8, że w metodzie create_engine miałem ustawione encoding="utf-8". Dopiero dodanie odpowiedniego ustawienie charset w connection string pomogło. Tak wyglądał poprawny fragment odpowiedzialny za połączenie do bazy:

    engine = sqlalchemy.create_engine(
        "mysql://user:passhardasfuck@server/database?charset=utf8mb4", encoding="utf-8"
    )

Po tej zmianie wszystko zaczęło magicznie działać. A ja mogłem przestać wyrywać włosy z głowy i odtańczyć taniec radości #tenuczućgdykoddziała.

Pełny stacktrace, dla zainteresowanych

Traceback (most recent call last):
  File "/app/./ingest_data.py", line 72, in <module>
    ingest_people_csv(db_connection)
  File "/app/./ingest_data.py", line 15, in ingest_people_csv
    load_df_to_database(db_conn, people_df, "person")
  File "/app/./ingest_data.py", line 46, in load_df_to_database
    df.to_sql(
  File "/usr/local/lib/python3.9/site-packages/pandas/core/generic.py", line 2779, in to_sql
    sql.to_sql(
  File "/usr/local/lib/python3.9/site-packages/pandas/io/sql.py", line 601, in to_sql
    pandas_sql.to_sql(
  File "/usr/local/lib/python3.9/site-packages/pandas/io/sql.py", line 1411, in to_sql
    table.insert(chunksize, method=method)
  File "/usr/local/lib/python3.9/site-packages/pandas/io/sql.py", line 845, in insert
    exec_insert(conn, keys, chunk_iter)
  File "/usr/local/lib/python3.9/site-packages/pandas/io/sql.py", line 762, in _execute_insert
    conn.execute(self.table.insert(), data)
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1201, in execute
    return meth(self, multiparams, params, _EMPTY_EXECUTION_OPTS)
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/elements.py", line 313, in _execute_on_connection
    return connection._execute_clauseelement(
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1390, in _execute_clauseelement
    ret = self._execute_context(
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1749, in _execute_context
    self._handle_dbapi_exception(
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1934, in _handle_dbapi_exception
    util.raise_(exc_info[1], with_traceback=exc_info[2])
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/util/compat.py", line 211, in raise_
    raise exception
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1706, in _execute_context
    self.dialect.do_execute(
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 716, in do_execute
    cursor.execute(statement, parameters)
  File "/usr/local/lib/python3.9/site-packages/MySQLdb/cursors.py", line 199, in execute
    args = tuple(map(db.literal, args))
  File "/usr/local/lib/python3.9/site-packages/MySQLdb/connections.py", line 280, in literal
    s = self.string_literal(o.encode(self.encoding))
  File "/usr/local/lib/python3.9/encodings/cp1252.py", line 12, in encode
    return codecs.charmap_encode(input,errors,encoding_table)
UnicodeEncodeError: 'charmap' codec can't encode characters in position 0-3: character maps to <undefined>

Najlepszy* Newsletter o Machine Learning w Polsce

Co tydzień (albo dwa) wyciągam smaczki z różnych miejsc w Internecie na temat Machine Learning, Data Science, AI i Python i przesyłam na Twoją skrzynkę mailową

Przesyłam też krótkie wyjaśnienia niektórych machine learningowych pojęć.

Autor: Konrad Łyda

AI Engineer działający głównie w Lingaro. Zajmuje się głównie maszynerią potrzebną do wytworzenia produktów opartych o uczenia maszynowe - od danych przez modele do wdrożenia. W wolnych chwilach zgłębia szeroki ocean zastosowań Machine/Deep Learningu w różnych dziedzinach życia oraz współorganizuje spotkania społeczności Data Science Lublin

Poprzedni Wpis

Jak mergować Jupyter notebooki w GIT i nie osiwieć? Proste narzędzie!

Następny Wpis

[#w3linijkach] Jak pobrać dane z MySQL i zapisać do CSV w Pythonie?

Ostatnie Wpisy

Od danych do produktu – automatyzacja w obszarze Machine Learning dzięki MLOps

Od danych do produktu – automatyzacja w obszarze Machine Learning dzięki MLOps

[#w3linijkach] Jak pobrać dane z MySQL i zapisać do CSV w Pythonie?

[#w3linijkach] Jak pobrać dane z MySQL i zapisać do CSV w Pythonie?

Kodowanie znaków przy połączeniu do bazy danych zdradliwe bywa

Kodowanie znaków przy połączeniu do bazy danych zdradliwe bywa

10 źródeł wiedzy o uczeniu maszynowym - pobierz!

Newsletter o ML za free?

Gdzie wejść?


Ostatnie wpisy

  • Od danych do produktu – automatyzacja w obszarze Machine Learning dzięki MLOps
  • [#w3linijkach] Jak pobrać dane z MySQL i zapisać do CSV w Pythonie?
  • Kodowanie znaków przy połączeniu do bazy danych zdradliwe bywa
  • Jak mergować Jupyter notebooki w GIT i nie osiwieć? Proste narzędzie!
  • [WDI2021] Szukamy Szopów, czyli Computer Vision w 30 minut
Arba WordPress Theme by XstreamThemes.
  • UczymyMaszyny.pl
  • Polityka prywatności
  • Kontakt