何かを解析する必要がありますか? 「パーサーコンビネーター」について聞いたことがありませんか? Haskellを学びたいですか?素晴らしい!以下は、Haskell Parserコンビネーターと一緒に立ち上がって解析するために必要なすべてです。ここから、難解なデータシリアル化形式、コンパイラフロントエンド、ドメイン特定の言語に取り組むことができます。
このガイドには、2つのデモプログラムが含まれています。
version-number-parserバージョン番号のファイルを解析します。 srt-file-parser SRT字幕のファイルを解析します。 test-input/で見つかったファイルを使用して、お気軽に試してみてください。
Haskellツールスタックをダウンロードしてから、次のことを実行します。
git clone https://github.com/lettier/parsing-with-haskell-parser-combinators
cd parsing-with-haskell-parser-combinators
stack buildCabalを使用する場合は、次のことを実行できます。
git clone https://github.com/lettier/parsing-with-haskell-parser-combinators
cd parsing-with-haskell-parser-combinators
cabal sandbox init
cabal --require-sandbox build
cabal --require-sandbox install2つのデモプログラムを構築した後、そのように実行できます。
バージョン番号パーサーを試すには、以下を実行します。
cd parsing-with-haskell-parser-combinators
stack exec -- version-number-parser
What is the version output file path ?
test-input/gifcurry-version-output.txtSRTファイルパーサーを試すには、以下を実行します。
cd parsing-with-haskell-parser-combinators
stack exec -- srt-file-parser
What is the SRT file path ?
test-input/subtitles.srtバージョン番号パーサーを試すには、以下を実行します。
cd parsing-with-haskell-parser-combinators
.cabal-sandbox/bin/version-number-parser
What is the version output file path ?
test-input/gifcurry-version-output.txtSRTファイルパーサーを試すには、以下を実行します。
cd parsing-with-haskell-parser-combinators
.cabal-sandbox/bin/srt-file-parser
What is the SRT file path ?
test-input/subtitles.srt解析戦略について学ぶためのより良い方法の1つであるパーサーコンビネーターは、1つの実装を調べることです。
コンビネーターを使用して構築されたパーサーは、構築可能、読み取り可能、モジュール式、十分に構造化され、簡単に保守可能です。
- Parser Combinator -Wikipedia
ベースにあるパーサーコンビネーターライブラリであるReadpのフードの下を見てみましょう。それは基地にあるので、あなたはすでにそれを持っているはずです。
注意してください、READPに精通した後、Parsecを試してみることができます。それも他の人が読み取ることを好むパーサーコンビネーターライブラリです。追加のボーナスとして、GHCバージョン8.4.1の時点でGHCのブートライブラリに含まれています。
-- (c) The University of Glasgow 2002
data P a
= Get ( Char -> P a )
| Look ( String -> P a )
| Fail
| Result a ( P a )
| Final [( a , String )]
deriving Functor Pデータ型から始めます。 P aのaはあなた(ライブラリユーザー)に任されており、あなたが望むものは何でもできます。コンパイラは、Functorインスタンスを自動的に作成し、適用、Monad、 MonadFail 、およびAlternativeのための手書きインスタンスがあります。
注意を払って、ファンクター、アプリケーション、モナドの詳細については、モナド、アプリケーション、および機能者の簡単なガイドをチェックアウトしてください。
Pは5つのケースの合計タイプです。
Get 、新しいPを返します。Look 、入力文字列の複製を受け入れ、新しいPを返します。Fail 、結果なしでパーサーが終了したことを示します。Result 、可能な解析と別のPケースを保持します。Final 、2人のタプルのリストです。最初のタプル要素は、入力の解析の可能性があり、2番目のタプル要素は、 Getによって消費されなかった入力文字列の残りの部分です。 -- (c) The University of Glasgow 2002
run :: P a -> ReadS a
run ( Get f) (c : s) = run (f c) s
run ( Look f) s = run (f s) s
run ( Result x p) s = (x,s) : run p s
run ( Final r) _ = r
run _ _ = [] run READPパーサーの中心です。上で見たすべてのパーサー状態を再帰的に実行するため、すべての重い持ち上げが行われます。 Pが必要で、 ReadSを返します。
-- (c) The University of Glasgow 2002
type ReadS a = String -> [( a , String )] ReadS a String -> [(a,String)]のタイプエイリアスです。したがって、 ReadS aときはいつでも、 String -> [(a,String)]を考えてください。
-- (c) The University of Glasgow 2002
run :: P a -> String -> [( a , String )]
run ( Get f) (c : s) = run (f c) s
run ( Look f) s = run (f s) s
run ( Result x p) s = (x,s) : run p s
run ( Final r) _ = r
run _ _ = [] runパターンは、 Pのさまざまなケースと一致します。
Get場合、新しいP (入力文字列の次の文字cであるget f 、 Get fを渡すことで返されます)と入力文字列sの残りの部分を呼び出します。Lookの場合、新しいP (function f 、 Look f 、input string s )、および入力文字列の渡されて返されます)でそれ自体を呼び出します。 Getのように、入力文字列から文字を消費しないようにLookことに注意してください。Resultの場合、解析された結果と入力文字列の残りを含むツータップルを組み立て、別のPケースと入力文字列で実行される再帰コールの結果にこれを事前に加えます。Finalの場合、 run解析された結果と入力文字列の残りを含む2タプルのリストを返します。run空のリストを返します。たとえば、ケースがFail場合、 run空のリストを返します。 > run ( Get ( a -> Get ( b -> Result [a,b] Fail ))) " 12345 "
[( " 12 " , " 345 " )] readpはrunされませんが、もしそうなら、このように呼び出すことができます。 2人は'1'と'2'をGetし、 "345"を残します。
> run ( Get ( a -> Get ( b -> Result [a,b] Fail ))) " 12345 "
> run ( Get ( b -> Result [ ' 1 ' ,b] Fail )) " 2345 "
> run ( Result [ ' 1 ' , ' 2 ' ] Fail ) " 345 "
> ([ ' 1 ' , ' 2 ' ], " 345 " ) : run ( Fail ) " 345 "
> ([ ' 1 ' , ' 2 ' ], " 345 " ) : []
[( " 12 " , " 345 " )]各再帰コールを実行すると、最終結果にどのように到達したかを確認できます。
> run ( Get ( a -> Get ( b -> Result [a,b] ( Final [([ ' a ' , ' b ' ], " c " )])))) " 12345 "
[( " 12 " , " 345 " ),( " ab " , " c " )] Finalを使用して、2タンツの最終リストに解析結果を含めることができます。
-- (c) The University of Glasgow 2002
readP_to_S :: ReadP a -> ReadS a
-- readP_to_S :: ReadP a -> String -> [(a,String)]
readP_to_S ( R f) = run (f return ) readpは直接runしませんが、 readP_to_Sを介して露出します。 readP_to_S 、 ReadPと呼ばれるnewtypeを導入します。 readP_to_S 、 ReadP aと文字列を受け入れ、2タプルのリストを返します。
-- (c) The University of Glasgow 2002
newtype ReadP a = R ( forall b . ( a -> P b ) -> P b )これがReadP aの定義です。 Functor、Applicative、Monad、 MonadFail 、Alternative、 MonadPlusのインスタンスがあります。 Rコンストラクターは、別の関数を取得し、 Pを返す関数を取得します。受け入れられている関数は、あなたがaだものをすべて受け取り、 Pを返します。
-- (c) The University of Glasgow 2002
readP_to_S ( R f) = run (f return ) Pはモナドであり、 returnのタイプはa -> maであることを思い出してください。したがって、 f (a -> P b) -> Pb関数であり、 return (a -> P b)関数です。最終的に、 run予想されるP bを取得します。
-- (c) The University of Glasgow 2002
readP_to_S ( R f) inputString = run (f return ) inputString
-- ^ ^^^^^^^^^^ ^^^^^^^^^^^ソースコードでは中断されていますが、 readP_to_Sとrun入力文字列を期待していることを覚えておいてください。
-- (c) The University of Glasgow 2002
instance Functor ReadP where
fmap h ( R f) = R ( k -> f (k . h))これは、 ReadPのFunctorインスタンス定義です。
> readP_to_S ( fmap toLower get) " ABC "
[( ' a ' , " BC " )]
> readP_to_S (toLower <$> get) " ABC "
[( ' a ' , " BC " )]これにより、このようなことができます。 fmap機能装置は、 R Get等しいfunctor get上にtoLowerマッピングします。 Get ReadPタイプR (Char -> P a) -> P aであることを思い出してください。
-- (c) The University of Glasgow 2002
fmap h ( R f ) = R ( k -> f (k . h ))
fmap toLower ( R Get ) = R ( k -> Get (k . toLower))ここではfmap toLower get模範のために書き換えられたファンチャーの定義が表示されます。
上を見上げて、 readP_to_Sどのようにして[('a',"BC")]使用したときに、 runしないGetのみを使用しましたか?答えは、 Pの適用定義にあります。
-- (c) The University of Glasgow 2002
instance Applicative P where
pure x = Result x Fail
(<*>) = ap return pureに等しいのでreadP_to_S (R f) = run (f return) readP_to_S (R f) = run (f pure) return)を書き換えることができます。 returnまたはむしろpure使用により、 readP_to_SはResult x Fail 。最終的なケースのrunが遭遇します。到達した場合、 run終了し、解析のリストを取得します。
> readP_to_S ( fmap toLower get) " ABC "
-- Use the functor instance to transform fmap toLower get.
> readP_to_S ( R ( k -> Get (k . toLower))) " ABC "
-- Call run which removes R.
> run (( k -> Get (k . toLower)) pure ) " ABC "
-- Call function with pure to get rid of k.
> run ( Get ( pure . toLower)) " ABC "
-- Call run for Get case to get rid of Get.
> run (( pure . toLower) ' A ' ) " BC "
-- Call toLower with 'A' to get rid of toLower.
> run ( pure ' a ' ) " BC "
-- Use the applicative instance to transform pure 'a'.
> run ( Result ' a ' Fail ) " BC "
-- Call run for the Result case to get rid of Result.
> ( ' a ' , " BC " ) : run ( Fail ) " BC "
-- Call run for the Fail case to get rid of Fail.
> ( ' a ' , " BC " ) : []
-- Prepend.
[( ' a ' , " BC " )]ここでは、 readP_to_Sから解析結果への流れが表示されます。
-- (c) The University of Glasgow 2002
instance Alternative P where
-- ...
-- most common case: two gets are combined
Get f1 <|> Get f2 = Get ( c -> f1 c <|> f2 c)
-- results are delivered as soon as possible
Result x p <|> q = Result x (p <|> q)
p <|> Result x q = Result x (p <|> q)
-- ... PのAlternativeインスタンスにより、パーサーの流れを左と右のパスに分割できます。これは、入力が2つ、1つ、または(よりまれに)2つの方法のうちの2つの方法で移動できる場合に役立ちます。
> readP_to_S ((get >>= a -> return a) <|> (get >> get >>= b -> return b)) " ABC "
[( ' A ' , " BC " ),( ' B ' , " C " )] <|>演算子または関数は、パーサーの流れにフォークを導入します。パーサーは、左右のパスの両方を通過します。最終結果には、左になった可能性のあるすべての解析と、右に進んだ可能性のあるすべての解析が含まれます。両方のパスが失敗した場合、パーサー全体が失敗します。
他のパーサーコンビネーターの実装では、 <|>オペレーターを使用する場合、パーサーは左または右に進みますが、両方ではありません。左が成功した場合、右は無視されます。右側は左側が故障した場合にのみ処理されます。
> readP_to_S ((get >>= a -> return [a]) <|> look <|> (get >> get >>= a -> return [a])) " ABC "
[( " ABC " , " ABC " ),( " A " , " BC " ),( " B " , " C " )]多くのオプションや代替案については、 <|>オペレーターをチェーンできます。パーサーは、それぞれが関与する可能性のある解析を返します。
-- (c) The University of Glasgow 2002
instance Monad ReadP where
fail _ = R ( _ -> Fail )
R m >>= f = R ( k -> m ( a -> let R m' = f a in m' k))これがReadP Monadインスタンスです。 failの定義に注意してください。
> readP_to_S (( a b c -> [a,b,c]) <$> get <*> get <*> get) " ABC "
[( " ABC " , " " )]
> readP_to_S (( a b c -> [a,b,c]) <$> get <*> fail " " <*> get) " ABC "
[]
> readP_to_S (get >>= a -> get >>= b -> get >>= c -> return [a,b,c]) " ABC "
[( " ABC " , " " )]
> readP_to_S (get >>= a -> get >>= b -> fail " " >>= c -> return [a,b,c]) " ABC "
[] fail呼び出すことにより、パーサーパス全体を中止することができます。 READPはResultまたはFinalケースを生成する直接的な方法を提供しないため、返品値は空のリストになります。失敗したパスが唯一のパスである場合、結果全体が空のリストになります。 run Failと、空のリストが返されることを思い出してください。
-- (c) The University of Glasgow 2002
instance Alternative P where
-- ...
-- fail disappears
Fail <|> p = p
p <|> Fail = p
-- ...代替Pインスタンスに戻ると、両側の障害がどのようにパーサー全体に失敗しないかがわかります。
> readP_to_S (get >>= a -> get >>= b -> pfail >>= c -> return [a,b,c]) " ABC "
[] READPはfailを使用する代わりに、 Failを直接生成できるpfailを提供します。
GIFメーカー向けのHaskell製のビデオエディターであるGifcurryは、さまざまなプログラムに砲撃しています。互換性を確保するには、それが販売する各プログラムのバージョン番号が必要です。これらのプログラムの1つはImageMagickです。
Version: ImageMagick 6.9.10-14 Q16 x86_64 2018-10-24 https://imagemagick.org
Copyright: © 1999-2018 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenCL OpenMPここでは、 convert --versionの出力が表示されます。 6、9、10、および14をキャプチャするためにこれを解析するにはどうすればよいですか?
出力を見ると、バージョン番号は、期間またはダッシュのいずれかで区切られた数字のコレクションであることがわかります。この定義は日付もカバーするため、最初の2つの数値が期間ごとに区切られることを確認します。そうすれば、バージョン番号の前に日付を入れた場合、誤った結果は得られません。
1. Consume zero or more characters that are not 0 through 9 and go to 2.
2. Consume zero or more characters that are 0 through 9, save this number, and go to 3.
3. Look at the rest of the input and go to 4.
4. If the input
- is empty, go to 6.
- starts with a period, go to 1.
- starts with a dash
- and you have exactly one number, go to 5.
- and you have more than one number, go to 1.
- doesn't start with a period or dash
- and you have exactly one number, go to 5.
- you have more than one number, go to 6.
5. Delete any saved numbers and go to 1.
6. Return the numbers found.コードに飛び込む前に、フォローするアルゴリズムを次に示します。
parseVersionNumber
:: [ String ]
-> ReadP [ String ]
parseVersionNumber
nums
= do
_ <- parseNotNumber
num <- parseNumber
let nums' = nums ++ [num]
parseSeparator nums' parseVersionNumber parseVersionNumberは、バージョン番号の入力文字列を解析するメインパーサーコンビネーターです。文字列のリストを受け入れ、 ReadPデータ型のコンテキストで文字列のリストを返します。受け入れられた文字列のリストは、解析される入力ではなく、これまでに見つかった数字のリストです。最初の関数呼び出しでは、まだ何も解析していないため、リストは空です。
parseVersionNumber
nums上から始めて、 parseVersionNumber 、これまでに見つかった数字の現在のリストである文字列のリストを取ります。
_ <- parseNotNumber parseNotNumber 、入力文字列からの数ではないすべてを消費します。結果には興味がないので、廃棄します( _ <- )。
num <- parseNumber
let nums' = nums ++ [num]次に、数字のすべてを消費し、それをこれまでに見つかった数字のリストに追加します。
parseSeparator nums' parseVersionNumber parseVersionNumberが次の番号を処理した後、見つかった数字のリストを渡し、それ自体をparseSeparatorに渡します。
parseSeparator
:: [ String ]
-> ([ String ] -> ReadP [ String ])
-> ReadP [ String ]
parseSeparator
nums
f
= do
next <- look
case next of
" " -> return nums
(c : _) ->
case c of
' . ' -> f nums
' - ' -> if length nums == 1 then f [] else f nums
_ -> if length nums == 1 then f [] else return numsここにparseSeparatorが表示されます。
next <- look
case next of
" " -> return nums
(c : _) -> look使用すると、入力文字列を消費せずに残したものを取得できます。何も残っていない場合、見つかった数字を返します。ただし、何かが残っている場合、最初のキャラクターを分析します。
case c of
' . ' -> f nums
' - ' -> if length nums == 1 then f [] else f nums
_ -> if length nums == 1 then f [] else return nums次の文字が期間の場合は、現在の数字のリストが見つかった現在のリストで再びparseVersionNumberに電話してください。ダッシュであり、正確に1つの番号がある場合は、日付であるため、数字の空のリストでparseVersionNumberに電話してください。ダッシュであり、1つの数字が1つない場合は、これまでに見つかった数字のリストでparseVersionNumberに電話してください。それ以外の場合は、1つの数字が1つある場合は、空のリストでparseVersionNumberを呼び出すか、1つの数字がない場合に見つかった数値を返します。
parseNotNumber
:: ReadP String
parseNotNumber
=
munch ( not . isNumber) parseNotNumber 、 ReadPが提供するmunchを使用します。 munchは、0から9ではないキャラクターにtrueを返す述語(not . isNumber)が与えられます。
munch :: ( Char -> Bool ) -> ReadP String入力文字列の次の文字が述語を満たしている場合、 munch継続的に呼び出しget 。そうでない場合、 munch 、もしあれば、したキャラクターを返します。 getのみを使用するため、Munchは常に成功します。
注意してください、 parseNumberはparseNotNumberに似ています。そうではnot . isNumber 、述語はただのisNumberです。
parseNotNumber'
:: ReadP String
parseNotNumber'
=
many (satisfy ( not . isNumber)) munchを使用する代わりsatisfy 、このようにparseNotNumberを書くことができますmany manyのタイプの署名を見ると、単一のパーサーコンビネーター( ReadP a )を受け入れます。この例では、パーサーコンビネーターがsatisfyが与えられています。
> readP_to_S (satisfy ( not . isNumber)) " a "
[( ' a ' , " " )]
> readP_to_S (satisfy ( not . isNumber)) " 1 "
[] satisfyと述語を取り、次のキャラクターを消費getために使用します。受け入れられている述語がtrueを返す場合、 satisfyキャラクターを返します。それ以外の場合は、 pfailと失敗をsatisfy 。
> readP_to_S (munch ( not . isNumber)) " abc123 "
[( " abc " , " 123 " )]
> readP_to_S (many (satisfy ( not . isNumber))) " abc123 "
[( " " , " abc123 " ),( " a " , " bc123 " ),( " ab " , " c123 " ),( " abc " , " 123 " )] many使用すると、不要な結果が得られます。最終的に、 many 1つ以上のResultケースを導入します。このため、 many常に成功します。
> readP_to_S (many look) " abc123 "
-- Runs forever. manyそれが故障するか、入力がなくなるまでパーサーを実行します。パーサーが失敗しないか、入力が不足していない場合、 many決して戻りません。
> readP_to_S (many (get >>= a -> return ( read (a : " " ) :: Int ))) " 12345 "
[( [] , " 12345 " ),([ 1 ], " 2345 " ),([ 1 , 2 ], " 345 " ),([ 1 , 2 , 3 ], " 45 " ),([ 1 , 2 , 3 , 4 ], " 5 " ),([ 1 , 2 , 3 , 4 , 5 ], " " )]結果のすべてのインデックスについて、解析された結果は、入力全体でパーサーインデックス時間を実行した結果になります。
> let parser = get >>= a -> return ( read (a : " " ) :: Int )
> let many' results = return results <|> (parser >>= result -> many' (results ++ [result]))
> readP_to_S (many' [] ) " 12345 "
[( [] , " 12345 " ),([ 1 ], " 2345 " ),([ 1 , 2 ], " 345 " ),([ 1 , 2 , 3 ], " 45 " ),([ 1 , 2 , 3 , 4 ], " 5 " ),([ 1 , 2 , 3 , 4 , 5 ], " " )] manyの代替定義は次のとおりです。 <|>の左側では、現在のパーサー結果を返します。 <|>の右側では、パーサーを実行し、その結果を現在のパーサーの結果に追加し、更新された結果を呼び出します。これには累積合計型効果があり、インデックスiはi - 1 、 i - 2 、...、および1でパーサー結果に追加されたパーサー結果です。
パーサーを構築したので、実行しましょう。
> let inputString =
> " Some Program (C) 1234-56-78 All rights reserved. n
> Version: 12.345.6-7 n
> License: Some open source license. "
> readP_to_S (parseVersionNumber [] ) inputString
[([ " 12 " , " 345 " , " 6 " , " 7 " ], " n License: Some open source license. " )]その前に日付が来ていても、バージョン番号を正しく抽出したことがわかります。
それでは、より複雑なもの、つまりSRTファイルを解析しましょう。
Gifcurry Sixのリリースには、SRT(Subripテキスト)ファイルを解析する必要がありました。 SRTファイルには、ビデオ処理プログラムがビデオの上にテキストを表示するために使用する字幕が含まれています。通常、このテキストは、さまざまな言語に翻訳された映画の対話です。テキストをビデオから分離することにより、時間、ストレージスペース、帯域幅を節約するビデオが1つだけある必要があります。ビデオソフトウェアは、ビデオを交換することなくテキストを交換できます。これを、ビデオを構成する画像データの一部になる字幕を燃やしたり、ハードコーディングしたりすることとは対照的です。この場合、字幕のコレクションごとにビデオが必要です。
インナービデオ©Blender Foundation | www.sintel.org
Gifcurryは、SRTファイルを使用して、ビデオの字幕を燃やすことができます。
7
00:02:09,400 --> 00:02:13,800
What brings you to
the land of the gatekeepers?
8
00:02:15,000 --> 00:02:17,500
I'm searching for someone.
9
00:02:18,000 --> 00:02:22,200
Someone very dear?
A kindred spirit?ここには、Sintel(©Blender Foundation | www.sintel.org)の英語字幕が表示されます。
SRTは、おそらくすべてのサブタイトル形式の中で最も基本的なものです。
—SRTサブタイトル|マトロスク
SRTファイル形式は、空の線で区切られた各サブタイトル用のブロックで構成されています。
2ブロックの上部にはインデックスがあります。これにより、字幕の順序が決まります。うまくいけば、字幕がすでに整っており、それらのすべてが一意のインデックスを持っていることを願っていますが、これは当てはまらないかもしれません。
01:04:13,000 --> 02:01:01,640 X1:167 X2:267 Y1:33 Y2:63インデックスの後、開始時間、終了時間、および字幕テキストが入力する長方形を指定するオプションのポイントセットです。
01:04:13,000タイムスタンプ形式はhours:minutes:seconds,milliseconds 。
ミリ秒から秒を分離する期間ではなく、コンマに注意してください。
This is the actual subtitle
text. It can span multiple lines.
It may include formating
like <b>bold</b>, <i>italic</i>,
<u>underline</u>,
and <font color="#010101">font color</font>.ブロックの3番目と最後の部分は、サブタイトルテキストです。空の線があるときに複数の線にまたがって終了できます。テキストには、HTMLを連想させるタグのフォーマットを含めることができます。
parseSrt
:: ReadP [ SrtSubtitle ]
parseSrt
=
manyTill parseBlock (skipSpaces >> eof) parseSrtは、すべてを処理するメインパーサーコンビネーターです。ファイル( eof )または入力に到達するまで、各ブロックを解析します。安全な側にあるために、最後のブロックとファイルの終わりの間に走行距離がある可能性があります。これを処理するために、ファイルの端( skipSpaces >> eof )を解析する前に、ゼロ( skipSpaces )のゼロ以上の文字を解析します。 eofに到達するまでにまだ入力が残っている場合、 eof失敗し、これは何も返しません。したがって、 parseBlock Whitespace以外に何も残さないことが重要です。
parseBlock
:: ReadP SrtSubtitle
parseBlock
= do
i <- parseIndex
(s, e) <- parseTimestamps
c <- parseCoordinates
t <- parseTextLines
return
SrtSubtitle
{ index = i
, start = s
, end = e
, coordinates = c
, taggedText = t
}以前に行ったとき、ブロックはインデックス、タイムスタンプ、おそらくいくつかの座標、およびいくつかのテキストで構成されています。 parseBlockのこのバージョンでは、レコード構文を備えたより必須の表記スタイルが表示されます。
parseBlock'
:: ReadP SrtSubtitle
parseBlock'
=
SrtSubtitle
<$> parseIndex
<*> parseStartTimestamp
<*> parseEndTimestamp
<*> parseCoordinates
<*> parseTextLines parseBlock書くことができるもう1つの方法は次のとおりです。これが適用スタイルです。必ず注文を正しく取得してください。たとえば、スタートと終了のタイムスタンプを誤って混同した可能性があります。
parseIndex
:: ReadP Int
parseIndex
=
skipSpaces
>> readInt <$> parseNumberブロックの上部にはインデックスがあります。ここでは、 skipSpaces再び表示されます。 Whitespaceをスキップした後、数値の入力を解析し、実際の整数に変換します。
readInt
:: String
-> Int
readInt
=
read readIntこのように見えます。
> read " 123 " :: Int
123
> read " 1abc " :: Int
*** Exception : Prelude. read : no parse通常、直接readを使用することは危険です。 read 、入力を指定されたタイプに変換できない場合があります。ただし、 parseNumber 10個の数値文字( ['0'..'9'] )のみを返します。したがって、 readを直接使用することが安全になります。
タイムスタンプを解析することは、インデックスを解析するよりも少し複雑です。
parseTimestamps
:: ReadP ( Timestamp , Timestamp )
parseTimestamps
= do
_ <- char ' n '
s <- parseTimestamp
_ <- skipSpaces
_ <- string " --> "
_ <- skipSpaces
e <- parseTimestamp
return (s, e)これは、タイムスタンプを解析するための主要な組み合わせです。
char 、あなたがそれを与えるキャラクターを解析するか、失敗します。それが失敗した場合、 parseTimestampsは失敗し、最終的にparseSrt失敗させるため、インデックスの後に新しいライン文字が必要です。
char stringは、1つの文字だけではなく、それを与えた文字列を解析するか、失敗する文字列を解析します。
parseStartTimestamp
:: ReadP Timestamp
parseStartTimestamp
=
char ' n '
>> parseTimestamp parseTimestamps両方のタイムスタンプを解析しますが、アプリケーションスタイル( parseSrt' )には、開始タイムスタンプのためだけにパーサーが必要です。
parseEndTimestamp
:: ReadP Timestamp
parseEndTimestamp
=
skipSpaces
>> string " --> "
>> skipSpaces
>> parseTimestampこれは、タイムスタンプの間のすべてを解析し、終了タイムスタンプを返します。
parseTimestamp
:: ReadP Timestamp
parseTimestamp
= do
h <- parseNumber
_ <- char ' : '
m <- parseNumber
_ <- char ' : '
s <- parseNumber
_ <- char ' , ' <|> char ' . '
m' <- parseNumber
return
Timestamp
{ hours = readInt h
, minutes = readInt m
, seconds = readInt s
, milliseconds = readInt m'
}これは、タイムスタンプを構成する4つの数字を解析します。最初の3つの数字はコロンで区切られ、最後の数字はコンマで分離されます。しかし、より寛容になるために、私たちはコンマの代わりに期間がある可能性を許します。
> readP_to_S (char ' . ' <|> char ' , ' ) " ... "
[( ' . ' , " .. " )]
> readP_to_S (char ' . ' <|> char ' , ' ) " ,.. "
[( ' , ' , " .. " )]注意して、 <|>でcharを使用する場合、 char単一のキャラクターを消費し、2人のキャラクターが同じスペースを占有できないため、片側のみが成功することができます(2つのchar exter、1つのchar休暇)。
座標はブロックのオプションの部分ですが、含まれる場合は、タイムスタンプと同じ行になります。
parseCoordinates
:: ReadP ( Maybe SrtSubtitleCoordinates )
parseCoordinates
=
option Nothing $ do
_ <- skipSpaces1
x1 <- parseCoordinate ' x ' 1
_ <- skipSpaces1
x2 <- parseCoordinate ' x ' 2
_ <- skipSpaces1
y1 <- parseCoordinate ' y ' 1
_ <- skipSpaces1
y2 <- parseCoordinate ' y ' 2
return
$ Just
SrtSubtitleCoordinates
{ x1 = readInt x1
, x2 = readInt x2
, y1 = readInt y1
, y2 = readInt y2
} option 2つの引数を取ります。最初の引数は、2番目の引数であるパーサーが失敗した場合に返されます。したがって、座標パーサーが失敗した場合、 parseCoordinates Nothing返しません。別の言い方をすれば、座標パーサーの失敗は、パーサー全体が失敗することはありません。このブロックは、そのcoordinates 「フィールド」にはNothingありません。
parseCoordinate
:: Char
-> Int
-> ReadP String
parseCoordinate
c
n
= do
_ <- char ( Data.Char. toUpper c) <|> char ( Data.Char. toLower c)
_ <- string $ show n ++ " : "
parseNumberこのパーサーにより、座標ラベルは大文字または小文字のいずれかになります。たとえば、 x1:1 X2:2 Y1:3 y2:4成功します。
テキストを解析することは、HTMLのようなタグのフォーマットにより、最も関与する部分です。
タグの解析は挑戦的です。正規表現で解析する人に尋ねるだけです。これを私たちにとって、そしてユーザーにとってこれを簡単にするために、タグスープのようなアプローチを使用します。パーサーは、閉鎖および/または誤ってネストされたタグを許可します。また、 b 、 u 、 i 、およびfontだけでなく、任意のタグも許可します。
parseTextLines
:: ReadP [ TaggedText ]
parseTextLines
=
char ' n '
>> (getTaggedText <$> manyTill parseAny parseEndOfTextLines)新しいライン文字を一致させることから始めます。その後、テキスト行の最後に到達するまで、サブタイトルのテキスト文字を介してマップまたはfmap( <$> )をgetTaggedText (<$>)にファクターします。
parseEndOfTextLines
:: ReadP ()
parseEndOfTextLines
=
void (string " nn " ) <|> eof 2つの新しい文字またはファイルの最後に到達すると、文字( parseAny )の収集を停止します。これは、ブロックの端を信号します。
getTaggedText
:: String
-> [ TaggedText ]
getTaggedText
s
=
fst
$ foldl
folder
( [] , [] )
parsed
where getTaggedText 、蓄積されたタグ付きテキストを返す、解析されたテキストを左から右に折りたたみます。
parsed
:: [ String ]
parsed
=
case readP_to_S (parseTaggedText [] ) s of
[] -> [s]
r @ (_ : _) -> ( fst . last ) r parsed 、1つ以上の文字列のリストを返します。タグの入力テキストを解析しようとします。それが失敗した場合、 parsedリスト内の入力文字列を返します。それ以外の場合、 parseTaggedTextが成功した場合、 parse最後の可能な解析( (fst . last) r )を返します。
folder
:: ([ TaggedText ], [ Tag ])
-> String
-> ([ TaggedText ], [ Tag ])
folder
(tt, t)
x
| isTag x = (tt, updateTags t x)
| otherwise = (tt ++ [ TaggedText { text = x, tags = t}], t) folder解析された文字列を左から右に移動すると、現在の文字列がタグであるかどうかをチェックします。タグの場合、現在のアクティブタグ( t )のセットを更新します。それ以外の場合は、アクティブタグのセットに関連付けられた別のタグ付きテキストを追加します。
updateTags
:: [ Tag ]
-> String
-> [ Tag ]
updateTags
tags
x
| isClosingTag x = remove compare' tags (makeTag x)
| isOpeningTag x = add compare' tags (makeTag x)
| otherwise = tags
where
compare'
:: Tag
-> Tag
-> Bool
compare'
a
b
=
name a /= name b updateTags 、閉じたタグまたは開閉タグのかどうかに応じて、指定されたタグ( x )を削除または追加することにより、与えられたtagsを更新します。どちらでもない場合は、パスされたタグのセットを返すだけです。 add tags既に同じ名前でタグがある場合は、既存のタグを上書きします。これは、指定されたcompare'で見ることができます。
パーサーをシンプルに保つために、オープニングタグTが見つかった場合、 Tタグのリストに追加されるか、既に存在する場合は終了Tを上書きします。対応するクロージング/Tが見つかった場合、存在する場合はタグのリストからTが削除されます。 2つ以上のTが連続している場合、閉じた/Tなしで1つ以上のT /T存在するかどうかは関係ありませんT
makeTag
:: String
-> Tag
makeTag
s
=
Tag
{ name = getTagName s
, attributes = getTagAttributes s
} makeTag 、指定されたs列からタグを組み立てます。各Tagには、名前とゼロ以上の属性があります。
parseTaggedText
:: [ String ]
-> ReadP [ String ]
parseTaggedText
strings
= do
s <- look
case s of
" " -> return strings
_ -> do
r <- munch1 ( /= ' < ' ) <++ parseClosingTag <++ parseOpeningTag
parseTaggedText $ strings ++ [r] parseTaggedText 、分割された入力文字列をバラバラに返します。各ピースは、タグ、クロージングタグ、またはオープニングタグで囲まれたテキストです。ピースを分割した後、それを他のピースに追加し、再び自らを呼び出します。残りの入力文字列が空の場合、見つかった文字列のリストを返します。
> readP_to_S (string " ab " <++ string " abc " ) " abcd "
[( " ab " , " cd " )]
> readP_to_S (string " ab " +++ string " abc " ) " abcd "
[( " ab " , " cd " ),( " abc " , " d " )]
> readP_to_S (string " ab " <|> string " abc " ) " abcd "
[( " ab " , " cd " ),( " abc " , " d " )] <++演算子は、左側が成功した場合、右側にさえ悩まされないという意味で偏ったままです。パーサーを実行すると、考えられるすべての解析のリストが表示されることを思い出してください。これらの可能な解析はすべて、可能なパスのすべてを通過したパーサーの結果です。 <++使用することにより、左側が故障した場合にのみ、左の経路と右の経路から可能な解析を受け取ります。左側と右側から可能なすべての解析が必要な場合は、 ReadPが提供する+++オペレーターを使用できます。 +++ 、上で見た<|>です。
parseOpeningTag
:: ReadP String
parseOpeningTag
= do
_ <- char ' < '
t <- munch1 ( c -> c /= ' / ' && c /= ' > ' )
_ <- char ' > '
return $ " < " ++ t ++ " > "オープニングタグは、オープニング角度ブラケット、フォワードスラッシュを含むテキスト、次の即時閉鎖角度ブラケットです。
parseClosingTag
:: ReadP String
parseClosingTag
= do
_ <- char ' < '
_ <- char ' / '
t <- munch1 ( /= ' > ' )
_ <- char ' > '
return $ " </ " ++ t ++ " > "クロージングタグは、オープニング角度ブラケット、フォワードスラッシュ、いくつかのテキスト、次の即時閉鎖角度ブラケットです。
getTagAttributes
:: String
-> [ TagAttribute ]
getTagAttributes
s
=
if isOpeningTag s
then
case readP_to_S (parseTagAttributes [] ) s of
[] -> []
(x : _) -> fst x
else
[]オープニングタグには属性があります。たとえば、 <font color="#101010"> 。各属性は、2項、キー価値ペアです。上記の例では、 colorキーであり、 #101010値になります。
getTagName
:: String
-> String
getTagName
s
=
case readP_to_S parseTagName s of
[] -> " "
(x : _) -> toLower' $ fst xこれにより、小文字のタグ名が返されます。
parseTagName
:: ReadP String
parseTagName
= do
_ <- char ' < '
_ <- munch ( == ' / ' )
_ <- skipSpaces
n <- munch1 ( c -> c /= ' ' && c /= ' > ' )
_ <- munch ( /= ' > ' )
_ <- char ' > '
return nタグ名は、オープニング角度ブラケット、可能性のあるフォワードスラッシュ、およびいくつかの可能性のある空白の後、そしていくつかの白面および/または閉じた角度ブラケットの前の非白文字の文字の最初の文字列です。
parseTagAttributes
:: [ TagAttribute ]
-> ReadP [ TagAttribute ]
parseTagAttributes
tagAttributes
= do
s <- look
case s of
" " -> return tagAttributes
_ -> do
let h = head s
case h of
' > ' -> return tagAttributes
' < ' -> trimTagname >> parseTagAttributes'
_ -> parseTagAttributes'
where
parseTagAttributes'
:: ReadP [ TagAttribute ]
parseTagAttributes'
= do
tagAttribute <- parseTagAttribute
parseTagAttributes
( add
( a b -> fst a /= fst b)
tagAttributes
tagAttribute
) parseTagAttributesは、入力文字列を通過して、キー価値のペアを収集します。タグ( < )の開始時に、属性に取り組む前に最初にタグ名をトリミングします。クロージング角度ブラケット( > )に到達すると、属性の解析が停止します。タグがたまたま重複した属性(キーに基づいて)がある場合、 add最新のもののみがリストに残っていることを確認します。
trimTagname
:: ReadP ()
trimTagname
=
char ' < '
>> skipSpaces
>> munch1 ( c -> c /= ' ' && c /= ' > ' )
>> return ()これにより、タグ名がトリミングまたは破棄されます。
parseTagAttribute
:: ReadP TagAttribute
parseTagAttribute
= do
_ <- skipSpaces
k <- munch1 ( /= ' = ' )
_ <- string " = " "
v <- munch1 ( /= ' " ' )
_ <- char ' " '
_ <- skipSpaces
return (toLower' k, v)属性キーは、等記号の前の任意の非白文字文字の文字列です。属性値は、等記号と二重引用の後、次の即時の二重引用の前に任意の文字です。
isTag
:: String
-> Bool
isTag
s
=
isOpeningTag s || isClosingTag s文字列は、オープニングタグまたはクロージングタグのいずれかである場合、タグです。
isOpeningTag
:: String
-> Bool
isOpeningTag
s
=
isPresent $ readP_to_S parseOpeningTag s文字列は、オープニングタグパーサーが成功した場合のオープニングタグです。
isClosingTag
:: String
-> Bool
isClosingTag
s
=
isPresent $ readP_to_S parseClosingTag s文字列は、クロージングタグパーサーが成功した場合のクロージングタグです。
パーサーを組み立てたので、試してみましょう。