UnicodeからUTF-8への変換を手でやってみる

文字コードの一つであるUTF-8 そしてUnicode
この2つをごっちゃになっている人もいるかもしれない。

これは相互に関係はあるが、コードは別のものになっている

簡単にいえばUnicodeは文字の集合、大規模な文字セットの規格で、
UTF-8はUnicodeを符号化する方式の一つという位置づけになっている。

Unicodeでは文字一つ一つに対してコードを割り当てているが、そのコードがUTF-8のコードになるわけではない。

なので、UTF-8で書かれたテキストをバイナリエディタで開いてコードをみて、Unicodeのコードを調べても全く別のものがかかれている。

変換の手順

どのような仕組みでunicodeからUTF-8に変換されるのか、説明しよう。

Unicodeのコードがどの範囲にあるかによって変わってくる。

Unicodeのコードの範囲によって4つのパターンに分けられる。
コードをまず2進数に変換して、それぞれの範囲によってビット数が決まっている。
上記表はそれぞれの範囲の最低値と最大値を2進数に変換した表だ。

7Fまでは7bit
77Fまでは11bit
FFFFまでは16bit
1FFFFFまでは21bitとなっている。

それぞれを上記表と同じように分割をする。
7Fだったら一つだけ、1FFFFFだったら、3桁1つと6桁3つの4つに分割をする。

その後はそれぞれを16進数に変換し直す。

4つの範囲のそれぞれの最小値と最大値を16進数で表したのが上記の表になる。

これに更にそれぞれの数値に特定の値を足していく。
それが次の表

たとえば7Fまでの範囲の文字ならば何もたさない。
7FFまでの範囲の文字ならば1バイト目はC0を足して、次は80を足す。
もちろん80は16進数なので、紛らわしくなく書くならば0x80を足す。

それぞれのバイトに決められた値を足すと、各unicodeの範囲に対して、UTF-8の値の範囲は上記表のようになる

例えば神という字で変換すると

神という漢字のunicodeのコードはu+795Eだそうだ。
これは3バイト文字の範囲になるので、二進数にしたら16bitにして、3つに分けて、最初のバイトには0xE0を足して、残りには0x80を足していく。という操作をする

二進数にして、16bitにして、3つに分けるとこのような数値になる。
‘0111’, ‘100101’, ‘011110’
これをそれぞれ16進数にして、指定の数字を足していくと
E7 A5 9Eという3つの数値になる。

すなわち、0xE7A59Eという3バイトが出来上がる。
これが神という字にUTF-8のコードである。

UTF-8のよく考えられているところ

なんでそれぞれのバイトに決められた数値を足しているのかというと、
表を見るとわかる。

最初のバイトの範囲で何バイト文字かわかる。
そして0x80から0xBFの範囲ならば一つの文字の途中のバイトというのがわかる。
UTF-8は1バイトから4バイトまで幅がある文字コードだが、先頭をみれば何バイトまで見ればわかるし、途中からバイトを読み始めることがあったとしても、文字の途中のバイトの範囲もわかる。
法則がわかれば文字の境界が一目瞭然になっている

ダメ文字も問題ない

Shift-JISにはダメ文字という問題のある文字があった。
Shift-JISでは全角文字は2バイトで表現するが、2バイト目に0x5Cが入ることがある。
この5CはつまりASCIIで\を表しているコードになっている。
これはエスケープ処理などにつかう特殊な文字で、文字コードの解釈の方法によっては2バイト目を\として認識してしまい。それがShift-JISを考慮していないプログラミングに入った場合、予期せぬ動きをすることがあった。

UTF-8はそのようなことが発生しないようになっている。

0x5Cを含むASCIIの0x00から0x7FまでのコードはUnicodeでも共通のコードになっているが、それは1バイトでそのまま表現されるようになっている。
そしてこの範囲になるコードは2バイト文字以上のどの場所にも現れないようになっている。
文字の途中のバイトは0x80以上だし、2バイト文字以上の先頭バイトは0xC2以上になっている。

このようにASCIIのコードはASCIIコードそのものとしてしか登場しないようになっている。
よく考えられているぜ。

変換の処理をPythonで書いてみた。

変換処理をいろいろまとめたが、本当に変換されるのか実際にやらないとわからないので
Pythonで手作業で変換をして文字にするプログラムを書いてみた

引数でUnicodeのHEX表現のコードを引数で渡すと、手で計算してUTF-8にして、さらにそれを文字として表示してくれる。
UTF-8のコード調べるのにも使える。

import sys

# python uni.py 7A
# python uni.py BE
# python uni.py 9999
# python uni.py 1F607


def main(code):
    codenum = int(code,16)
    
    if codenum <= 0x7f:
        maxleng = 7
        f = 7
        l = 1
    elif codenum <= 0x7ff:
        maxleng = 11
        f = 5
        l = 2
       
    elif codenum <= 0xffff:
        maxleng = 16
        f = 4
        l = 3
    elif codenum <= 0x1fffff:
        maxleng = 21
        f = 3
        l = 4
    
    bincode = format(codenum,'b')
    bincode = ('0' * (maxleng - len(bincode))) + bincode
    binary = makeAry(bincode,f,6,l)
    print(binary)
    datastr = toUTF8binstr(binary)
    
    dataint = int(datastr,16)
    
    data = dataint.to_bytes(l,byteorder='big')
    print(data)
    print(data.decode(encoding='utf-8'))

#いい感じの配列をUTF-8のバイトをHEX文字列にした感じにする
def toUTF8binstr(binary):
    
    if len(binary) == 1:
        return format(int(binary[0],2),'x')
        
    if len(binary) == 2:
        tas = 0xC0
    elif len(binary) == 3:
        tas = 0xE0
    elif len(binary) == 4:
        tas = 0xF0
        
        
    resulthex = ''
    for i,b in enumerate(binary):
        num = int(b,2)
        if i == 0:
            num = num + tas
        else:
            num = num + 0x80
        
        resulthex = resulthex + format(num,'x')
     
    return resulthex
        
            
        
        
# 配列をいい感じにする
def makeAry(code,f,o,l):
    ary = []
    code2 = code
    for i in range(l):
        if i == 0:
            ary.append(code2[:f])
            code2 = code2[f:]
        else:
            ary.append(code2[:o])
            code2 = code2[o:]
            
    return ary
            
    

if __name__ == '__main__':
    args = sys.argv
    main(args[1])

スポンサードリンク

関連コンテンツ