HaskellとUTF8での日本語文字列の取扱い

Haskellの日本語の取扱いで困っているような同じような人(Haskell初心者さん)へ向けて。本記事では、Ubuntuで全てUTF8で統一しているにも関わらず困った事象です。WindowsでDOS窓環境だったりすると、また違った困りかたになると思います(DOS窓SJISのため)。

概要

Haskellを使ってHTMLのスクレーピングしようとしたときに、はまった日本語の取扱いについて書く。

具体的に困った事象

UTF8で保存したファイルのHaskellプログラムの中で

hoge == "次ページ"

のように、ごく普通に日本語の文字列マッチングした場合の話。
hogeは、tagSoupを通して加工した文字列が入っている変数。次ページというのが、ここでマッチさせたい日本語の文字列。
Webブラウザで保存してあったHTMLファイルをreadFileで、読み込んだ場合はTrueが返ってくる。しかし、Network.HTTP.simpleHTTPでネットから、同じホームページを直接読み込むと、マッチングにFalseが返ってくる。同じプログラムのはずなのに。もちろん、utf8で記述されているホームページですよ。

解決方法

cabalでutf8-srtingをインストールする。
そこで、一時的にファイル保存せずにネットから直接、読んできたときに、

hoge == Codec.Binary.UTF8.String.encodeString "次ページ"

とencodeString関数を通せばTrueになった。

原因追求編

環境

  • Ubuntu 12.04
  • GHC7.4.1
  • cabal install tagsoup
  • cabal install utf8-string

文字コードについて

すべてUTF8のはず。つまり、、、、

  • HaskellプログラムのファイルはUTF8で保存する。
  • 対象ホームページのエンコードもUTF8で記述されている。
  • 対象ホームページを「Webブラウザ」で、ファイルとして保存。当然、ファイルはUTF8で保存されている。

調査

GHCの日本語の扱いを把握する必要がある。後ほど、推測した原因を書くが、とりあえず、GHCiで確認できる事実を列挙する。

Main> putStrLn "日本語"
日本語
Main> print "日本語"
"\26085\26412\35486"
Main> Codec.Binary.UTF8.String.encode "日本語"
[230,151,165,230,156,172,232,170,158]
Main> Codec.Binary.UTF8.String.encodeString "日本語"
"\230\151\165\230\156\172\232\170\158"
Main> Codec.Binary.UTF8.String.decodeString "\230\151\165\230\156\172\232\170\158"
"\26085\26412\35486"
Main> putStrLn "\26085\26412\35486"
日本語

推測1(GHCとライブラリ)

GHCiで調べた事実と、http://itpro.nikkeibp.co.jp/article/COLUMN/20100406/346695/?P=5
から、以下のように推測している。

  • GHCは、デフォルトのままでUTF8を扱える。
  • printの場合は、ShowクラスでUTF8を、数値で表示する。
  • putStrの場合は、そのまま日本語として表示される。
  • readFileで、UTF8のファイルは、そのまま読み込まれGHC側に解釈される。
  • Network.HTTP.simpleHTTPは、encodingした形で読み込まれる(というより、ネットワーク越しから生バイナリを、単純に取り込んでいるということなのかな?)
  • tagSoupは素通し。
  • Webブラウザで保存した場合は、WebブラウザがUTF-8と解釈してデコードしてくれている。たぶん。

推測2(utf8とUnicode

日本語周り(というかユニコード)の知識が、曖昧なため、間違っているかも。
単なるバイナリデータをutf8としてデコード(Codec.Binary.UTF8.String.decode)すると、Unicodeに変換されるということなのではないか?
つまり、"\230\151\165\230\156\172\232\170\158"が生のバイナリで、これをutf8としてデコードすると"\26085\26412\35486"というユニコードに変換される。

わかりやすく書くと

バイナリ  = utf-8としてdecode => ユニコード
バイナリ <= utf-8としてencode => ユニコード

という感じ。

まとめ

  • 最近のGHCとしては、内部的にユニコードで扱えるようになった。
  • Sytetem.IO.readFileや、putStrは、UTF8を自動的に解釈できるようになっている。
  • System.IO.printは、Showクラスで、ユニコードの数値で表示するようにされている。つまり、たとえ内部でユニコードとして扱えても、数値で表示するということ。
  • Network.HTTP系の関数とtagSoupにユニコードを認識する能力がなくて素通ししている。

と推測しつつも、にわか仕込みの調査なんで、いまいち理解していません。あしからず。

所感

すごいHaskell本程度を読んだ程度の自分(つまり、Haskell初心者)には、本筋じゃない、こういう細かいところを調査することが面倒だな。普及しまくっているメジャー言語とかだと、日本語の取扱いごときでひっかからないと思う。
Haskellが、もっと普及して、Haskeller人口が増えれば、こういうことを気にしなくてもよいようになると思う。