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
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>