no-image

Python 3 + pyodbc + unixODBC + FreeTDS の日本語でハマる

TL;DR

connect = pyodbc.connect('DSN=SQLServer;UID=user;PWD=password;')
connect.setencoding('utf-8')
connect.cursor.execute('SELECT 列 FROM テーブル WHERE 番号 = 5;')

connect.setencoding() でエンコーディングを指定したら解決しました。

経緯

古い基幹システムに SQL Server 2008 が使われており、テーブル名やカラム名が全て日本語で構成されていた関係で、Linux 上の Python から接続するのにひと手間かかっていました。DB 自体は UTF-8 で処理されているので、Python 2 からはクエリを query.encode('utf-8') とすることでうまく処理できていました。さらっと書いていますが、Python にまだ慣れていないときに開発していたので、かなり苦労して辿りついた結論です。

今回は Bottle を使うに当って Python 3 に移行する関係から DB 接続周りを一新する必要があったのが事の始まりです。DB への接続自体は Python 2 と同じで特に問題もなかったのですが、いざ SELECT 分を execute してみると次のようなエラーを吐きました。

pyodbc.ProgrammingError: ('42S22', "[42S22] [FreeTDS][SQL Server]Invalid column name 'F'. (207) (SQLExecDirectW)")

このエラーには見覚えがあり、さらにカラム名は日本語で指定したはずなのに見慣れない文字になっていることから、日本語のエンコーディング周りの問題だろうということはすぐにわかりました。

まず試してみたのが Python 2 と同じ手法でクエリを encode('utf-8') する方法です。これの結果は

TypeError: The first argument to execute must be a string or unicode query.

ユニコードでよこせと怒られてしまいました。なら最初はユニコードで渡してるからそれで良いのではないか。

次に疑ったのはファイルの文字コードを他のエンコーディングで保存していないかです。でもソースコード冒頭には # -*- coding: utf-8 -*- と記述した上で、間違いなく UTF-8N で保存しています。試しに Shift JIS で保存すると、次のエラーで実行すらできません。

SyntaxError: (unicode error) 'utf-8' codec can't decode byte 0x8f in position 0: invalid start byte

調べていても、そもそも Linux の Python から Windows Server の SQL Server を扱う事例自体が少なすぎて、情報があまり得られません。そこで、pyodbc のソースを読んでみようと思ったら、そのものずばりの情報がありました。

記事冒頭に書いた、DB に connect() した後 connection.setencoding('utf-8') するだけです。これで無事に日本語のテーブル名、列名を処理できました。

たった一文のことですが、情報量が少ないとこれに辿りついて気づくまでが本当に大変です。