読者です 読者をやめる 読者になる 読者になる

Haskellのパース処理(文字コードについて)

文字コードについて

  • Unicode文字集合である。Unicodeで規定される文字を、UTF-8UTF-16などの符号化方式で扱うことができる(e.g. Shift-JISやEUC-JPは別の文字集合を扱う)。Unicodeでは文字集合を、コードポイントという各文字に対する通し番号のようなもので管理する。コードポイントは符号化方式を意識しておらず、すなわち符号化されたバイト列ではない(バイト列は符号化方式によって決まる)。

  • UTF-16は、文字を16bit単位で表すための符号化方式で、ASCII非互換である。最小単位が2byteであるため、ビッグエンディアンか、リトルエンディアンか、で符号化されたバイト列の解釈が変わってしまうため、エンディアンを示すBOM(バイトオーダーマーク)をテキストデータの先頭に付加する。ただしUTF-16BE・UTF-16LEという符号化方式では、符号化方式自体でそれを示しており、BOMは付加されない。HaskellではChar型の値を内部的にUTF-16として扱うUnicodeのコードポイントとして保持する*1UTF-16では、1符号(2byte)で文字を表す場合、Unicodeのコードポイントをそのまま使う。2符号(4byte)で文字を表す場合、サロゲートコードポイント(コードポイントとして割り当てられていないU+D800 から U+DFFFの範囲)をペアで使うため、サロゲートペアと呼ばれる。

  • Haskellprintの出力は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.Parsecが使いやすい。使い方についてはUnbui.ltが分かり易い。モナドのdo記法をDesugarした考察が載っていて親切。

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の先頭にマッチするものの、その次のptextの先頭にはマッチしないのでエラーとなり、そこでマッチングが終了してしまう。
  • そこで、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.

10. プラグマ - とりあえず雑記帳

補足: Python2とPython3の文字型の扱いについて

  • Python2系とPython3系では文字型の扱いが異なっている。
    • Python2 : 文字型は、エンコーディングされたバイト列を保持する。ただし、uフラグ*11をつけたリテラルは、ユニコード文字型として、Unicodeのコードポイントを保持する。
    • Python3 : 文字型は、Unicodeのコードポイントを保持する、ただし、bフラグをつけたリテラルは、バイト型*12として0<=x<256の間の整数値のシーケンスを保持する。256以上のコードポイントが割り当てられている文字(ASCIIで扱える文字以外)は、バイト型として扱えないことに注意。
  • つまり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.PosixUnicodeに非対応なだけ?

*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型、の区別がある