🐡

[Julia] StringEncodings パッケージ - ShiftJIS のCSVファイルとも仲良しになろう

2023/12/17に公開

これはJulia Advent Calendar 2023の17日目の記事です。

Juliaは、Unicodeを使って文字列を表しますから、Unicode以外で符号化されたテキストを読むには、Unicodeへの符号変換が必要になります。

この記事では、文字の符号を変換しながらテキストを読み書きできる機能を提供する StringEncodingsパッケージ を紹介します。このパッケージを使えば、Excelが吐く Shift-JIS の CSV (comma separated value)ファイルも楽々読み書きできます。

導入

StringEncodings パッケージを読み込みましょう。
最近の Juliaでは、未導入のパッケージを usingすると、その旨が表示され、パッケージマネージャーに入らなくてもインストールされるのが便利ですね。

julia> using StringEncodings

StringEncodings パッケージは、GNUソフトウエアの libiconvライブラリを用いるパッケージです

encodings() 関数は、使える符号の種類を文字列の配列として出力します。
まず登録された符号の総数を出力しましょう。

julia> encodings() |> length
651

length(encodings()) と書く代わりに、関数適用の演算子 |> を使うのが軽快です。x |> f と書くのは、f(x) と同じです。Ruby の method chain に似ていて大好きです。

私のPC環境では、651種類の符号が使えるとのことです。

では、符号の名前に、ji の綴りが含まれるものを列挙しましょう。

julia> encodings() |> filter(contains(r"ji"i))
23-element Vector{String}:
 "CSISO14JISC6220RO"
 "CSISO159JISX02121990"
 "CSISO87JISX0208"
 "CSSHIFTJIS"
 "EUC-JISX0213"
 "JIS_C6220-1969-RO"
 "JIS_C6226-1983"
 "JIS_X0201"
 "JIS_X0208"
 "JIS_X0208-1983"
 "JIS_X0208-1990"
 "JIS_X0212"
 "JIS_X0212-1990"
 "JIS_X0212.1990-0"
 "JIS0208"
 "JISX0201-1976"
 "MS_KANJI"
 "shift_jis"
 "SHIFT_JIS"
 "SHIFT_JISX0213"
 "shift-jis"
 "SHIFT-JIS"
 "SJIS"

引数一つの contains(pat) は、s -> contains(s, pat) と同じ意味です。r"ji"iは、大文字小文字を区別せず ji の綴りにマッチする正規表現です。filter(f, a) は、コレクション a の要素 e のうち f(e) が成り立つ(trueとなる)要素のみを選び出す関数です。ですから、上のコードは filter(s -> contains(s, r"ji"i), encodings()) と書くのと同じです。

列挙された中に JISkanji という綴りが見えますね。

Windows のExcel が吐く CSVファイルは、MS漢字コード CP932 と呼ばれる SHIFT-JIS の独自拡張で符号化されています(あまり深入りしたくないです→ CP932誕生の経緯 )。
今度は CP9 という綴りで始まる符号名を出力しましょう。

julia> encodings() |> filter(startswith("CP9"))
6-element Vector{String}:
 "CP905"
 "CP922"
 "CP932"
 "CP936"
 "CP949"
 "CP950"

引数一つの startswith(pat) は、s -> startswith(s, pat) と同じ表現です。

ちゃんと CP932 が見つかりましたね。

encode関数は、Juliaの文字列 (UTF-8)を別の符号に変換します。第一引数に文字列を、第二引数に符号の種類を指定します。符号の種類の指定には、enc"符号名"という接頭辞付きの文字列 Non-Standard String Literalsを使います。
結果は、負号なし1バイト整数の配列です。

julia> encode("a", enc"CP932")
1-element Vector{UInt8}:
 0x61

julia> 'a' |> UInt8
 0x61

julia> encode("あ", enc"CP932")
2-element Vector{UInt8}:
 0x82
 0xa0

いわゆる半角アルファベットは ASCII符号です。CP932 でも同じ表現(数字)です。

文字列 「あ」を CP932 符号に変換すると、2バイトのデータ(2-element Vector{UInt8} )が得られて、その値は 0x820xa0 となりました。これは、まさに、Shift_JISの「あ」 0x82a0 ですね。(たとえば http://charset.7jp.net/jis0208.html 「04区02点」)

Shift_JIS テキストファイルの読み書き

StringEncodings パッケージを導入すると、open関数の二つ目の引数で、テキストファイルの符号を指定できるようになります。

CP932符号でテキストファイルに書き出すコードの例を示します。

julia> open("test-sjis.txt", enc"CP932", "w") do f
       print(f, "こんにちわ")
       println(f, "世界")
       end

1行目の open関数 で、CP932 符号に変換して書き出す IOStream が作成されるので、これを print 関数に渡すだけです。
カレントディレクトリに "test-sjis.txt" というファイルができます。
中身は MS漢字コードで「こんにちわ世界」というテキスト1行になっているはずです。

CP932で符号化されたテキストファイルを読み込むコード例です。

julia> open("test-sjis.txt", enc"CP932") do f
       line = readline(f);
       println(line)
       end
こんにちわ世界

1行目の open関数 で、CP932 符号から変換して書き出す IOStream が作成されます。readline(f) 関数で CP932のテキストを読み込むと、UTF-8 に変換された文字列が得られます。

実は ファイルを open する際に符号を指定しなくとも、readreadline などで enc"符号名" の符号指定が使えるます。StringEncodingsパッケージのドキュメントでも、その例を先に説明しています。とはいえ、符号の種類が途中で変わるような状況は少ないでしょうから、openするときに指定する方が簡潔でしょう。

ところで、StringEncodings が依拠する libiconv には、テキストの符号の種類を「推定」する機能はありません。日本語テキストの漢字符号を推定したいなら NKFtoolパッケージを検討してください(2020年の Julia advent calendar の当方記事で紹介しました)。

https://qiita.com/tenfu2tea/items/e042ab00bc6d81dca8b5

Shift_JIS CSVファイルの読み書き

文字符号を指定して CSV を読み書きしたい場合も、テキストと同じ方法が使えます。

例として、日本語テキストを含む DataFrame を作っておきましょう。

julia> using CSV, DataFrames, StringEncodings

julia> df = DataFrame( 数字=[1,2,3], よみ=["いち", "に", "さん"] )
3×2 DataFrame
 Row │ 数字   よみ   
     │ Int64  String 
─────┼───────────────
   11  いち
   2233  さん

作った Dataframe を、MS漢字コードで CSVファイルとして保存してみます。
テキストファイルへの書き込みと同じように open して得られたストリームを CSV.write 関数に渡せばよいのです。

julia> open("test-csv.csv", enc"CP932", "w") do f
       CSV.write(f,df)
       end

Windows の Excel を使って、作成されたCSVファイルを文字化けせずに読み込めたはずです。

今度は、上で作成した MS漢字コードで書かれたCSVファイルを読み込んで、DataFrame に変換してみましょう。

julia> open("test-csv.csv", enc"CP932") do f
       CSV.read(f,DataFrame)
       end
3×2 DataFrame
 Row │ 数字   よみ    
     │ Int64  String
─────┼────────────────
   11  いち
   2233  さん

正しく読み込めましたね。

StringEncoderStringDecoder

文字符号の変換機能を持つファイルストリームIOStreamStringEncoder型または StringDecoder 型として作成されます。
メモリ上のストリーム IOBuffer に対しても、StringEncoderStringDecoder を作成できます。

下のコードは、メモリ上のストリーム b にMS漢字コードで「あ」の一文字を書き込み、また読み込む例です。

julia> b = IOBuffer();

julia> s = StringEncoder(b, enc"CP932");

julia> write(s, "あ");

julia> close(s);

julia> seek(b,0);

julia> read(b)
2-element Vector{UInt8}:
 0x82
 0xa0

julia> seek(b,0);

julia> s = StringDecoder(b, enc"CP932");

julia> read(s, String)
"あ"

バッファに write してから close()関数を呼び出すと符号変換が実行されます。
バッファを巻き戻してから read(b) の結果を見ると、先ほどの encoding("あ", enc"CP932") と同じ結果が得られました。ファイルに書き出さなくとも、長文のテキストを符号変換できますね。

終わりに

文字符号変換を行う StringEncodings パッケージを紹介しました。
この記事を書くために同様の記事がないか調べてみたのですが、以下の記事ひとつしか見つけられませんでした(調べ方が悪いのかしら)。当該記事には言及されていない open時の符号指定を紹介したく、本記事を書いてみました。
https://qiita.com/bonten999/items/beb6e62c7c658bb11eb6

このパッケージを最初に発見したのは、前出の NKFtool の紹介記事を書いていた3年前の今頃ですが、当時は libiconv ライブラリを別途導入する必要がありました。その後、外部ファイル(artifacts) を同梱(実体はLibiconv_jll.jl)したパッケージを手軽に配布できるようになりました (BinaryBuilderのおかげですね)。

以前は Rubyで扱っていたテキストやCSVの処理も、Juliaで扱っています。Juliaは、テキスト処理も得意な汎用言語です。

Discussion