PythonとShellで理解する文字コード

## はじめに
文字化けやPythonのUnicodeEncode/DecodeErrorが発生した時に、根本原因を理解できるよう文字コードについてまとめました。
UnicodeとUTF-8の違い、UTF-8とShift-JISの違い、UTF-8とUTF-16の違いを理解することを目標にしています。

## 前提・環境

OSはMacかLinuxを想定しています。
今回利用するツールはPython3とbash（または他のshell）と[nkf](https://ja.wikipedia.org/wiki/Network_Kanji_Filter)になります。
bashはデフォルトでインストールされていると思います。

### Python3
[pyenv](https://github.com/pyenv/pyenv)でインストールするのが良いと思います。
pyenvの解説はQiitaにもたくさん記事があるので、この記事では省略します。
動作確認はPython 3.6.5で行っています。

### nkf
文字コードの変換を行うツールでパッケージマネージャからインストールできます。

```bash:Homebrew
brew install nkf
```

```bash:yum
yum install nkf
```

```bash:apt
apt-get install nkf
```

## 文字集合と符号化方式

普段は単に文字コードと呼ぶことが多いですが、文字コードは文字集合と符号化方式から成り立っています。
文字集合は表現したい文字を羅列したもので、名前の通り文字を要素とした集合です。
例えば、小文字のアルファベットという文字集合はa, b, c, ..., zの全26文字からなる集合です。
一方で、符号化方式は文字集合の各文字に対して数値（バイト表現）を割り振ったものです。
例えば、小文字のアルファベットの文字集合に1から順に数値を割り振り、a=1, b=2, c=3, ..., z=26と定義したものが符号化方式です。
文字集合に対する数値の割当は1通りではないため、1つの文字集合に対して複数の符号化方式を作ることができます。
先頭を1ではなく0にして、a=0, b=1, c=2, ..., z=25と定義したものも小文字のアルファベットという文字集合に対する符号化方式と呼べます。
具体例を挙げるとUnicodeは文字集合であり、UTF-8やUTF-16は符号化方式になります。
注意点として、Unicodeの各文字には符号化方式とは別にコードポイントと呼ばれる数値が振られていています。
Unicodeでひらがなの「あ」を表すコードポイントは「U+3042」ですが、UTF-8で「あ」を表現するバイト列は16進数で「0xe38182」となりコードポイントとUTF-8やUTF-16といった符号化方式のバイト列で符号化したバイト列は別物です。

## 文字集合の違い

文字集合が異なると表現できる文字の範囲が異なるので、すべての文字を相互変換できるとは限りません。
例えばUnicodeには存在するけれど、JIS X 0208（Shift-JISの文字集合）には存在しない文字があります。

確認のためにPythonのインタラクティブシェルを利用します。

```bash
python
```

ひらがなの「あ」とアクセント付きアルファベットの「à」をそれぞれUTF-8とShift-JISに変換してみます。

```python
# ひらがなの「あ」をUTF-8のバイト列に変換
>>> 'あ'.encode('utf8')
b'\xe3\x81\x82'

# ひらがなの「あ」をShift-JISのバイト列に変換
>>> 'あ'.encode('sjis')
b'\x82\xa0'

# アクセント付きのアルファベットの「à」をUTF-8のバイト列に変換
>>> 'à'.encode('utf8')
b'\xc3\xa0'

# アクセント付きのアルファベットの「à」をShift-JISのバイト列に変換
>>> 'à'.encode('sjis')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'shift_jis' codec can't encode character '\xe0' in position 0: illegal multibyte sequence
```

ひらがなの「あ」は変換後のバイト列は違うものの、UnicodeとJIS X 0208の両方に含まれているため、UTF-8でもShift-JISでも表現できます。
一方でアクセント付きのアルファベットの「à」はUnicodeには存在しますが、JIS X 0208には存在しない文字なので、Shift-JISでは表現できずエラーになります。

## 符号化方式の違い

1つの文字集合に対して複数の符号化方式が存在する理由は様々ですが、符号化方式の違いで最も直感的に理解しやすいのはファイルサイズです。[Djangoの概要](https://docs.djangoproject.com/ja/2.1/intro/overview/)から文章を拝借して、検証用に2つのファイルを作成します。

```:en.txt
Because Django was developed in a fast-paced newsroom environment, it was designed to make common Web-development tasks fast and easy. Here’s an informal overview of how to write a database-driven Web app with Django.
The goal of this document is to give you enough technical specifics to understand how Django works, but this isn’t intended to be a tutorial or reference – but we’ve got both! When you’re ready to start a project, you can start with the tutorial or dive right into more detailed documentation.
```

```:ja.txt
Django は変転の激しいニュースルーム環境で開発された経緯から、よくある Web 開発タスクを迅速かつ簡単化するように設計されました。ここでは Django による データベースを使った Web アプリケーション開発をざっと見てみましょう。
このドキュメントの目的は、 Django の技術的な仕様について述べ、どのように動作するかを理解してもらうことにあり、チュートリアルやリファレンスではあり ません。 (とはいえ、チュートリアルもリファレンスも別に用意していますよ！) プロジェクトを作成する準備ができたら、 チュートリアルを始める ことも、 より詳細なドキュメントを読む こともできます。
```

続いて、検証用のファイルをそれぞれUTF-8とUTF-16で表現したファイルを作成します。

```bash
# UTF-8形式のファイルを作成
nkf -w ja.txt > ja_utf8.txt
nkf -w en.txt > en_utf8.txt

# UTF-16形式のファイルを作成
nkf -w16 ja.txt > ja_utf16.txt
nkf -w16 en.txt > en_utf16.txt
```

最後に、それぞれのファイルサイズを確認します。
自分の環境では次のような結果になりました。

```bash
ls -alh en_*
-rw-r--r--  1 user  group   1.0K 12 27 17:19 en_utf16.txt
-rw-r--r--  1 user  group   522B 12 27 17:19 en_utf8.txt

ls -alh ja_*
-rw-r--r--  1 user  group   604B 12 27 17:19 ja_utf16.txt
-rw-r--r--  1 user  group   813B 12 27 17:18 ja_utf8.txt
```

日本語主体の文字列はUTF-8で表現すると812B、UTF-16で表現すると604B、英語主体の文字列はUTF-8で表現すると522B、UTF-16で表現すると1KB（1024B）となり同じ文字列でも符号化方式によってファイルサイズに違いがあります。
また、日本語の場合はUTF-16の方がファイルサイズが小さいのに対し、英語では逆にUTF-8の方がファイルサイズが小さくなっています。
このような違いが発生する理由は同じ文字であってもUTF-8とUTF-16で1文字に割り当てるバイト数が異なるからです。
UTF-8はアルファベットを1バイト、日本語を3バイトか4バイトで表現します。一方でUTF-16はアルファベットも日本語も2バイトで表現できるものは2バイトで、2バイトに収まりきらない文字を4バイトで表現します。
そのため、アルファベット1文字を表現する場合にUTF-8に対するUTF-16のバイト長は2倍になり、日本語1文字を表現する場合にUTF-8に対するUTF-16のバイト長は2/3倍か1/2倍になります。
以上を踏まえてファイルサイズを比較すると、en_utf8.txtとen_utf16.txtのファイルサイズの比が1024/522で約2倍、ja_utf8.txtとja_utf16.txtのファイルサイズの比が604/813で約3/4倍というのは妥当な値と言えます。
この様に同じ文字集合であっても符号化方式が異なると、同じ情報を表現した時のファイルサイズは変わってきます。

## 最後に
日本語のようなマルチバイト文字を扱う環境で文字コードのトラブルは避けられませんが、原因を正しく理解して文字コードと上手く付き合っていきましょう。