Haskellのパース処理(文字コードについて)
文字コードについて
Unicodeは文字集合である。Unicodeで規定される文字を、UTF-8やUTF-16などの符号化方式で扱うことができる(e.g. Shift-JISやEUC-JPは別の文字集合を扱う)。Unicodeでは文字集合を、コードポイントという各文字に対する通し番号のようなもので管理する。コードポイントは符号化方式を意識しておらず、すなわち符号化されたバイト列ではない(バイト列は符号化方式によって決まる)。
UTF-16は、文字を16bit単位で表すための符号化方式で、ASCII非互換である。最小単位が2byteであるため、ビッグエンディアンか、リトルエンディアンか、で符号化されたバイト列の解釈が変わってしまうため、エンディアンを示すBOM(バイトオーダーマーク)をテキストデータの先頭に付加する。ただしUTF-16BE・UTF-16LEという符号化方式では、符号化方式自体でそれを示しており、BOMは付加されない。HaskellではChar型の値を内部的に
UTF-16として扱うUnicodeのコードポイントとして保持する*1。UTF-16では、1符号(2byte)で文字を表す場合、Unicodeのコードポイントをそのまま使う。2符号(4byte)で文字を表す場合、サロゲートコードポイント(コードポイントとして割り当てられていないU+D800 から U+DFFFの範囲)をペアで使うため、サロゲートペアと呼ばれる。Haskellの
print
の出力はUnicodeのコードポイントを10進数で出力する。ただし、putStr|putStrLn
は、端末のロケールに関する環境変数を参照して、文字コードをUnicodeのコードポイントから変換して表示するので、その環境の文字コードがUnicodeを扱う符号化方式であれば、端末上で正しく表示できる。- UTF-8は、文字を8bit単位で表すための可変長の符号化方式で、ASCII互換である*2。先頭ビットが0の場合は8bitx1、110の場合は8bitx2、1110の場合は8bitx3、11110の場合は8bitx4、という長さで1文字を表す。
Prelude> print "あいうえお" "\12354\12356\12358\12360\12362" Prelude> putStrLn "あいうえお" あいうえお Prelude> Leaving GHCi. $ echo $LANG ja_JP.UTF-8
- Thanks to
Unicode のサロゲートペアとは何か - ひだまりソケットは壊れない
Yapafi/charset.mkdn at master · Songmu/Yapafi · GitHub
文字コード表(Unicode UTF-8 UTF-16) [7000/21420] - [技術資料 + 技術資料] ぺんたん info
Unicode 〜UTF-8、UTF-16との違い〜(文字コード関連) | 読み物 | ウナのIT資格一問一答
Text.Regex.Posixについて
- 正規表現を扱うためのモジュール。
=~
が定義されており、自由に型を指定することで、マッチング有無(Bool)やマッチした文字列(String)、マッチした回数(Int)を取り出すことができる。 - ただし、UTF-8の全角スペース(0xE8,0x80,0x80)が含まれる場合、マッチングがうまくいかなくなる*3。
Prelude> import Text.Regex.Posix Prelude Text.Regex.Posix> "a1b2c3" =~ "[a-z]" :: Int Loading package array-0.4.0.1 ... linking ... done. Loading package deepseq-1.3.0.1 ... linking ... done. Loading package containers-0.5.0.0 ... linking ... done. Loading package bytestring-0.10.0.2 ... linking ... done. Loading package transformers-0.4.3.0 ... linking ... done. Loading package mtl-2.2.1 ... linking ... done. Loading package regex-base-0.93.2 ... linking ... done. Loading package regex-posix-0.95.2 ... linking ... done. 3 Prelude Text.Regex.Posix> "<tag>aaa bbb</tag>" =~ "</tag>" :: Bool True Prelude Text.Regex.Posix> "<tag>aaa bbb</tag>" =~ "</tag>" :: Bool False Prelude Text.Regex.Posix> "<tag>aaaあbbb</tag>" =~ "</tag>" :: Bool True
- 他にもいろいろ制限がありそう
$ cat re_test.hs import Text.Regex.Posix type S = String main = do case "aaa bbb ccc" =~ "([a-z]*) *([a-z]*)" :: (S,S,S,[S]) of (a,b,c,d) -> putStrLn $ a++":"++b++":"++c++":"++(show d) case "aaa bbb ccc" =~ "([^ ]*)[ ]*([^ ]*)" :: (S,S,S,[S]) of (a,b,c,d) -> putStrLn $ a++":"++b++":"++c++":"++(show d) case "aaa bbb ccc" =~ "([a-z]*) *([a-z ]*)" :: (S,S,S,[S]) of (a,b,c,d) -> putStrLn $ a++":"++b++":"++c++":"++(show d) case "aaa bbb ccc" =~ "([^ ]*)[ ]*([^ ]* ?[^ ]*)" :: (S,S,S,[S]) of (a,b,c,d) -> putStrLn $ a++":"++b++":"++c++":"++(show d) case "aaa bbb ccc" =~ "([a-z]*)\\s*([a-z]*)" :: (S,S,S,[S]) of (a,b,c,d) -> putStrLn $ a++":"++b++":"++c++":"++(show d) case "aaa bbb ccc" =~ "([\\S]*) *(\\S*)" :: (S,S,S,[S]) of (a,b,c,d) -> putStrLn $ a++":"++b++":"++c++":"++(show d) case "aaa bbb ccc" =~ "([\\S]*)\\s*(\\S*)" :: (S,S,S,[S]) of (a,b,c,d) -> putStrLn $ a++":"++b++":"++c++":"++(show d) case "aaa bbb ccc" =~ "([\\S]*?)\\s*(\\S*)" :: (S,S,S,[S]) of (a,b,c,d) -> putStrLn $ a++":"++b++":"++c++":"++(show d) $ runghc re_test.hs :aaa bbb: ccc:["aaa","bbb"] :aaa bbb: ccc:["aaa","bbb"] :aaa bbb ccc::["aaa","bbb ccc"] :aaa bbb ccc::["aaa","bbb ccc"] :aaa: bbb ccc:["aaa",""] ::aaa bbb ccc:["",""] ::aaa bbb ccc:["",""] re_test.hs: user error (Text.Regex.Posix.String died: (ReturnCode 13,"repetition-operator operand invalid"))
Text.Parsecについて
Text.ParserCombinators.Parsec.Prim.many: combinator 'many' is applied to a parser that accepts an empty string
- Parsecを使い始めた当初、単語を取り出すパーサを書いてみたが、実行時に上記のエラーとなった。これは、セパレータの定義において、
Parsec.many ""
を受理してしまっていたからだと思われる。Parsec.manyは0以上のパターンにマッチするので、パターンとして空文字列を渡してしまうと、パターンマッチが永久に終わらない。*4
Look ahead
- Parsecを使って、例えばXMLのtextタグ
<text property='hoge'>blah blah ... </text>
からpropertyを意識せずにコンテンツを取り出したい場合、以下のex1.hsのbadparserのように書けそう*5だが、これはうまくいかない。これは、Parsecのマッチングでは性能的な理由から先読み(Look ahead)をせず、一つずつマッチング対象の文字を調べていく。これをconsume
と呼び、マッチングに成功した文字を二度と使わない、という点がうまく表現されている。つまり、textタグが現れるよりも前に他のタグ<page>
などが存在した場合、<
は<text
の先頭にマッチするものの、その次のp
はtext
の先頭にはマッチしないのでエラーとなり、そこでマッチングが終了してしまう。 - そこで、Parsec.tryを使うと、consumeスタイルのマッチングにエラーが発生した場合でも、改めてマッチングをやり直してくれるため、ex1.hsのgoodparserのようにうまくいく*6。
--ex1.hs import Text.Parsec as P badparser = do P.manyTill P.anyChar (P.string "<text") P.manyTill P.anyChar (P.char '>') P.manyTill P.anyChar (P.string "</text>") goodparser = do P.manyTill P.anyChar (P.try $ P.string "<text") P.manyTill P.anyChar (P.char '>') P.manyTill P.anyChar (P.try $ P.string "</text>") main = do let text = "<page>Page content.<text property='hoge'>Text <b>conntent.</b></text></page>" case P.parse badparser "" text of Right text -> putStrLn text Left err -> putStrLn $ show err case P.parse goodparser "" text of Right text -> putStrLn text Left err -> putStrLn $ show err $ runghc ex1.hs (line 1, column 1): unexpected "p" expecting "<text" Text <b>conntent.</b>
ByteStringとData.Textについて
- String型は、Char型のリストであるためメモリ効率が悪く、文字列操作の性能は高くない*7。そのため、Unicodeを扱う場合、IOからメモリ効率の良いByteString型として読み込み*8、それを使いやすいData.Text型に変換して使用するのが良いようだ*9。
- ByteStringを使ってバイト単位から文字コードを生成することもできる。ByteStringとして読み込んだUnicodeのバイト列をString型に変換(してメモリ上に配置)するには
utf8-string
パッケージを利用する。
-- ex2.hs import qualified Data.ByteString as B import Codec.Binary.UTF8.String as C main = do putStrLn $ C.decode [0xE3,0x81,0x82] putStrLn $ C.decode $ B.unpack $ B.pack [0xE3,0x81,0x82] $ runghc ex3.hs あ あ
- c.f.
Haskell Tips (文字列編) - りんごがでている
Haskell Character Data · GitHub
Languageプラグマについて
- プラグマはGHCの拡張機能で、Languageプラグマは言語に関する機能を拡張する。デフォルトではコード中の文字リテラルはString型として扱われるが、
{-# LANGUAGE OverloadedStrings #-}
を付けることによって文字リテラルをString型以外の型として扱うことができる。以下のコードは、Languageプラグマを付けないとコンパイルエラーとなる。ByteString型は1文字を1バイトで格納するため、UTF-8で符号化された*101文字3バイトの文字コードを格納しきれず、1バイトに切り詰められていることが分かる。
-- ex3.hs {-# LANGUAGE OverloadedStrings #-} import qualified Data.ByteString as B import qualified Data.Text as T a :: T.Text b :: B.ByteString c :: String a = "あいうえお" b = "あいうえお" c = "あいうえお" main = do print a print b print c print $ T.length a print $ B.length b print $ length c $ runghc ex3.hs "\12354\12356\12358\12360\12362" "BDFHJ" "\12354\12356\12358\12360\12362" 5 5 5
- c.f.
補足: Python2とPython3の文字型の扱いについて
- Python2系とPython3系では文字型の扱いが異なっている。
つまりPython3では文字列は全て内部的にUnicodeのコードポイントとして保持される。そのため、Unicodeの文字集合を扱わない符号化方式で符号化されたファイルを読み込む場合は、encodingを指定してUnicodeのコードポイントに変換する必要がある。(ASCIIのみの場合はバイト列として保持しておくことも可能)
Thanks to
Python2 と Python3 の数値、文字列データの取り扱い:ある nakagami の日記:So-netブログ
*1:Unicodeに対応しているため、Char型では1文字に対して4バイトが割り当てられるらしい お気楽 Haskell プログラミング入門
*2:UTF-8が扱える範囲は8bitx6までだが、実際に使われているのは8bitx4まで。
*3:全角スペースの問題はこちらに報告済みText.Regex.Lazy / Bugs / #4 Strange behaviour with some non-ASCII symbols。問題はそれだけではなさそうだが。
*4:しかし、当時のコードはどこかへいってしまい再現する方法がない。それにしても、うまく「文字数字シングルクオーテーションハイフン以外」をセパレータのパターンとして指定する方法が分からない...
*5:do式では、最後の式の結果がdo式の値となるので、それ以外の式の結果は単に捨てられている。
*6:Parsecの場合はコンテンツ部分に全角スペースが存在していてもマッチングに問題は無かった。単にText.Regex.PosixがUnicodeに非対応なだけ?
*7:大きなサイズのファイルの内容を読み込む際、String型はhGetLineを使って手軽に行ごとに読み込める利点はある。
*8:ByteStringはWord8と呼ばれる1バイトの単位で管理される。
*9:なお、lengthの計算時間は、StringではO(N)、ByteStringはO(1)らしい。 Haskellライブラリ入門 (2011年版) - あどけない話
*10:エディタ(Vim)の設定は : set enc? encoding=utf-8 である
*11:フラグ?
*12:イミュータブルなbyte型か、ミュータブルなbytearray型、の区別がある