[Julia] StringEncodings パッケージ - ShiftJIS のCSVファイルとも仲良しになろう
これは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())
と書くのと同じです。
列挙された中に JIS
やkanji
という綴りが見えますね。
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}
)が得られて、その値は 0x82
、0xa0
となりました。これは、まさに、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
する際に符号を指定しなくとも、read
や readline
などで enc"符号名"
の符号指定が使えるます。StringEncodingsパッケージのドキュメントでも、その例を先に説明しています。とはいえ、符号の種類が途中で変わるような状況は少ないでしょうから、open
するときに指定する方が簡潔でしょう。
ところで、StringEncodings
が依拠する libiconv
には、テキストの符号の種類を「推定」する機能はありません。日本語テキストの漢字符号を推定したいなら NKFtoolパッケージを検討してください(2020年の Julia advent calendar の当方記事で紹介しました)。
Shift_JIS CSVファイルの読み書き
文字符号を指定して CSV を読み書きしたい場合も、テキストと同じ方法が使えます。
例として、日本語テキストを含む DataFrame を作っておきましょう。
julia> using CSV, DataFrames, StringEncodings
julia> df = DataFrame( 数字=[1,2,3], よみ=["いち", "に", "さん"] )
3×2 DataFrame
Row │ 数字 よみ
│ Int64 String
─────┼───────────────
1 │ 1 いち
2 │ 2 に
3 │ 3 さん
作った 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
─────┼────────────────
1 │ 1 いち
2 │ 2 に
3 │ 3 さん
正しく読み込めましたね。
StringEncoder
と StringDecoder
文字符号の変換機能を持つファイルストリームIOStream
は StringEncoder
型または StringDecoder
型として作成されます。
メモリ上のストリーム IOBuffer
に対しても、StringEncoder
や StringDecoder
を作成できます。
下のコードは、メモリ上のストリーム 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
時の符号指定を紹介したく、本記事を書いてみました。
このパッケージを最初に発見したのは、前出の NKFtool の紹介記事を書いていた3年前の今頃ですが、当時は libiconv
ライブラリを別途導入する必要がありました。その後、外部ファイル(artifacts) を同梱(実体はLibiconv_jll.jl)したパッケージを手軽に配布できるようになりました (BinaryBuilderのおかげですね)。
以前は Rubyで扱っていたテキストやCSVの処理も、Juliaで扱っています。Juliaは、テキスト処理も得意な汎用言語です。
Discussion