Besoin d'analyser quelque chose? Vous n'avez jamais entendu parler d'un "combinateur d'analyseur"? Vous cherchez à apprendre du Haskell? Génial! Vous trouverez ci-dessous tout ce dont vous avez besoin pour vous lever et analyser les combinateurs de Parser Haskell. De là, vous pouvez essayer de s'attaquer aux formats de sérialisation des données ésotériques, aux extrémités frontales du compilateur, aux langues spécifiques au domaine - vous le nommez!
Ce guide comprend deux programmes de démonstration.
version-number-parser analyser un fichier pour un numéro de version. srt-file-parser analyse un fichier pour les sous-titres SRT. N'hésitez pas à les essayer avec les fichiers trouvés dans test-input/ .
Téléchargez la pile d'outils Haskell, puis exécutez ce qui suit.
git clone https://github.com/lettier/parsing-with-haskell-parser-combinators
cd parsing-with-haskell-parser-combinators
stack buildSi vous utilisez Cabal, vous pouvez exécuter ce qui suit.
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 installAprès avoir construit les deux programmes de démonstration, vous pouvez les exécuter comme ainsi.
Pour essayer l'analyseur de numéro de version, exécutez ce qui suit.
cd parsing-with-haskell-parser-combinators
stack exec -- version-number-parser
What is the version output file path ?
test-input/gifcurry-version-output.txtPour essayer l'analyseur de fichier SRT, exécutez ce qui suit.
cd parsing-with-haskell-parser-combinators
stack exec -- srt-file-parser
What is the SRT file path ?
test-input/subtitles.srtPour essayer l'analyseur de numéro de version, exécutez ce qui suit.
cd parsing-with-haskell-parser-combinators
.cabal-sandbox/bin/version-number-parser
What is the version output file path ?
test-input/gifcurry-version-output.txtPour essayer l'analyseur de fichier SRT, exécutez ce qui suit.
cd parsing-with-haskell-parser-combinators
.cabal-sandbox/bin/srt-file-parser
What is the SRT file path ?
test-input/subtitles.srtL'une des meilleures façons de se renseigner sur la stratégie d'analyse, Anser Combinator, est de consulter une mise en œuvre d'une.
Les analyseurs construits à l'aide de combinateurs sont simples à construire, lisibles, modulaires, bien structurés et facilement maintenables.
—PARSER Combinator - Wikipedia
Jetons un coup d'œil sous le capot de Readp, une bibliothèque de combinateurs d'analyseurs trouvés dans la base. Comme il est en base, vous devriez déjà l'avoir.
Remarque, vous voudrez peut-être essayer PaSec après vous être familiarisé avec ReadP. C'est aussi une bibliothèque de combinateurs d'analyseurs que d'autres préfèrent lire. En prime, il est inclus dans les bibliothèques de démarrage de GHC à partir de la version 8.4.1 de 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 Nous allons commencer par le type de données P L' a in P a dépend de vous (l'utilisateur de la bibliothèque) et peut être ce que vous souhaitez. Le compilateur crée automatiquement une instance de fonctor et il existe des instances rédigées pour applicatifs, monad, MonadFail et alternative.
Remarque, pour en savoir plus sur les fonds, les applications et les monades, consultez votre guide facile des monades, des applications et des fonds.
P est un type de somme avec cinq cas.
Get Consume un seul caractère de la chaîne d'entrée et renvoie un nouveau P .Look accepte un double de la chaîne d'entrée et renvoie un nouveau P .Fail indique que l'analyseur est terminé sans résultat.Result contient un analyse possible et un autre cas PFinal est une liste de deux tuples. Le premier élément Tuple est un analyse possible de l'entrée et le deuxième élément Tuple est le reste de la chaîne d'entrée qui n'a pas été consommée par 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 est le cœur de l'analyseur readp. Il fait tout le levage de lourds car il traverse de manière récursive tous les États d'analyse que nous avons vus ci-dessus. Vous pouvez voir qu'il faut un P et renvoie une ReadS .
-- (c) The University of Glasgow 2002
type ReadS a = String -> [( a , String )] ReadS a est un alias de type pour String -> [(a,String)] . Ainsi, chaque fois que vous voyez ReadS a , pensez 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 _ _ = [] Le motif run correspond aux différents cas de P .
Get , il s'appelle avec un nouveau P (renvoyé en passant la fonction f , dans Get f , le caractère suivant c dans la chaîne d'entrée) et le reste de la chaîne d'entrée sLook , il s'appelle avec un nouveau P (renvoyé en passant la fonction f , en Look f , la chaîne d'entrée s ) et la chaîne d'entrée. Remarquez comment Look ne consomme pas de caractères de la chaîne d'entrée comme Get fait.Result , il assemble un deux-couple - contenant le résultat analysé et ce qui reste de la chaîne d'entrée - et l'appareilde au résultat d'un appel récursif qui s'exécute avec un autre boîtier P et la chaîne d'entrée.Final , run renvoie une liste de deux tuples contenant des résultats analysés et des restes de chaîne d'entrée.run renvoie une liste vide. Par exemple, si le cas est Fail , run renvoie une liste vide. > run ( Get ( a -> Get ( b -> Result [a,b] Fail ))) " 12345 "
[( " 12 " , " 345 " )] ReadP n'expose pas run mais si c'était le cas, vous pouvez l'appeler comme ça. Les deux Get consommés les '1' et '2' , laissant le "345" derrière.
> 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 " )]En parcourant chaque appel récursif, vous pouvez voir comment nous sommes arrivés au résultat final.
> run ( Get ( a -> Get ( b -> Result [a,b] ( Final [([ ' a ' , ' b ' ], " c " )])))) " 12345 "
[( " 12 " , " 345 " ),( " ab " , " c " )] En utilisant Final , vous pouvez inclure un résultat analysé dans la liste finale de deux tuples.
-- (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 ) Bien que ReadP n'expose pas directement run , il l'expose via readP_to_S . readP_to_S présente un newtype appelé ReadP . readP_to_S accepte une ReadP a , une chaîne et renvoie une liste de deux tuples.
-- (c) The University of Glasgow 2002
newtype ReadP a = R ( forall b . ( a -> P b ) -> P b ) Voici la définition de ReadP a . Il existe des cas pour Functor, Applicative, Monad, MonadFail , Alternative et MonadPlus . Le constructeur R prend une fonction qui prend une autre fonction et renvoie un P . La fonction acceptée prend tout ce que vous avez choisi pour a et renvoie un P .
-- (c) The University of Glasgow 2002
readP_to_S ( R f) = run (f return ) Rappelons que P est une monade et que le type return est a -> ma . Donc f est la fonction (a -> P b) -> Pb et return est la fonction (a -> P b) . En fin de compte, run obtient le P b qu'il attend.
-- (c) The University of Glasgow 2002
readP_to_S ( R f) inputString = run (f return ) inputString
-- ^ ^^^^^^^^^^ ^^^^^^^^^^^ Il est laissé de côté dans le code source, mais n'oubliez pas que readP_to_S et run attend une chaîne d'entrée.
-- (c) The University of Glasgow 2002
instance Functor ReadP where
fmap h ( R f) = R ( k -> f (k . h)) Voici la définition d'instance Functor pour ReadP .
> readP_to_S ( fmap toLower get) " ABC "
[( ' a ' , " BC " )]
> readP_to_S (toLower <$> get) " ABC "
[( ' a ' , " BC " )] Cela nous permet de faire quelque chose comme ça. fmap Functor Maps toLower sur le Fonctor get les égaux R Get . Rappelons que le type de Get est (Char -> P a) -> P a que le constructeur ReadP ( R ) accepte.
-- (c) The University of Glasgow 2002
fmap h ( R f ) = R ( k -> f (k . h ))
fmap toLower ( R Get ) = R ( k -> Get (k . toLower)) Ici, vous voyez la définition de Functor réécrite pour l'exemple fmap toLower get .
En regardant ci-dessus, comment readP_to_S est-il renvoyé [('a',"BC")] lorsque nous avons utilisé seulement Get ce qui ne termine pas run ? La réponse réside dans la définition applicative de P .
-- (c) The University of Glasgow 2002
instance Applicative P where
pure x = Result x Fail
(<*>) = ap return est égal pure afin que nous puissions réécrire readP_to_S (R f) = run (f return) pour être readP_to_S (R f) = run (f pure) . En utilisant return ou plutôt pure , readP_to_S définit Result x Fail car l' run du cas final rencontrera. S'il est atteint, run se terminera et nous obtiendrons notre liste d'analyses.
> 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 " )] Ici, vous voyez le flux de readP_to_S au résultat analysé.
-- (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)
-- ... L'instance Alternative pour P nous permet de diviser l'écoulement de l'analyseur en un chemin gauche et droit. Cela est utile lorsque l'entrée ne peut pas aller, une, ou (plus rarement) deux de deux manières.
> readP_to_S ((get >>= a -> return a) <|> (get >> get >>= b -> return b)) " ABC "
[( ' A ' , " BC " ),( ' B ' , " C " )] L'opérateur ou la fonction <|> introduit une fourche dans le flux de l'analyseur. L'analyseur voyagera à travers les chemins gauche et droit. Le résultat final contiendra tous les analyses possibles qui sont allées à gauche et tous les analyses possibles qui ont fait à droite. Si les deux chemins échouent, alors l'ensemble de l'analyseur échoue.
Remarque, dans d'autres implémentations de combinateur d'analyseur, lorsque vous utilisez l'opérateur <|> , l'analyseur ira à gauche ou à droite mais pas les deux. Si la gauche réussit, la droite est ignorée. La droite n'est traitée que si le côté gauche échoue.
> readP_to_S ((get >>= a -> return [a]) <|> look <|> (get >> get >>= a -> return [a])) " ABC "
[( " ABC " , " ABC " ),( " A " , " BC " ),( " B " , " C " )] Vous pouvez enchaîner l'opérateur <|> pour de nombreuses options ou alternatives. L'analyseur retournera un analyse possible impliquant chacun.
-- (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)) Voici l'instance ReadP Monad. Remarquez la définition de 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 "
[] Vous pouvez provoquer un chemin d'analyseur entier pour abandonner en appelant fail . Étant donné que ReadP ne fournit pas de moyen direct de générer un Result ou un cas Final , la valeur de retour sera une liste vide. Si le chemin d'échec est le seul chemin, le résultat entier sera une liste vide. Rappelons que lorsque les correspondances run Fail , il renvoie une liste vide.
-- (c) The University of Glasgow 2002
instance Alternative P where
-- ...
-- fail disappears
Fail <|> p = p
p <|> Fail = p
-- ... Pour en revenir à l'instance P alternative, vous pouvez voir comment un échec de chaque côté (mais pas les deux) n'échouera pas à l'ensemble de l'analyseur.
> readP_to_S (get >>= a -> get >>= b -> pfail >>= c -> return [a,b,c]) " ABC "
[] Au lieu d'utiliser fail , ReadP fournit pfail qui vous permet de générer directement un cas Fail .
Gifcurry, l'éditeur vidéo construit par Haskell pour GIF Makers, est à différents programmes. Pour garantir la compatibilité, il a besoin du numéro de version pour chacun des programmes où il sache. L'un de ces programmes est 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 Ici, vous voyez la sortie de convert --version . Comment pourriez-vous analyser cela pour capturer les 6, 9, 10 et 14?
En regardant la sortie, nous savons que le numéro de version est une collection de nombres séparés par une période ou un tableau de bord. Cette définition couvre également les dates, nous nous assurerons donc que les deux premiers nombres sont séparés par une période. De cette façon, s'ils mettent une date avant le numéro de version, nous n'obtiendrons pas le mauvais résultat.
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.Avant de plonger dans le code, voici l'algorithme que nous suivrons.
parseVersionNumber
:: [ String ]
-> ReadP [ String ]
parseVersionNumber
nums
= do
_ <- parseNotNumber
num <- parseNumber
let nums' = nums ++ [num]
parseSeparator nums' parseVersionNumber parseVersionNumber est le combinateur d'analyse principal qui analyse une chaîne d'entrée pour un numéro de version. Il accepte une liste de chaînes et renvoie une liste de chaînes dans le contexte du type de données ReadP . La liste des chaînes acceptée n'est pas l'entrée qui est analysée mais plutôt la liste des nombres trouvés jusqu'à présent. Pour le premier appel de fonction, la liste est vide car elle n'a encore rien analysé.
parseVersionNumber
nums À partir du haut, parseVersionNumber prend une liste de chaînes qui sont la liste actuelle des nombres trouvés jusqu'à présent.
_ <- parseNotNumber parseNotNumber consomme tout ce qui n'est pas un nombre de la chaîne d'entrée. Puisque nous ne sommes pas intéressés par le résultat, nous le jetons ( _ <- ).
num <- parseNumber
let nums' = nums ++ [num]Ensuite, nous consommons tout ce qui est un nombre, puis ajoutons cela à la liste des nombres trouvés jusqu'à présent.
parseSeparator nums' parseVersionNumber Une fois que parseVersionNumber a traité le numéro suivant, il transmet la liste des nombres trouvés et lui-même à 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 Ici, vous voyez parseSeparator .
next <- look
case next of
" " -> return nums
(c : _) -> look nous permet d'obtenir ce qui reste de la chaîne d'entrée sans le consommer. S'il ne reste plus rien, il renvoie les chiffres trouvés. Cependant, s'il reste quelque chose, il analyse le premier caractère.
case c of
' . ' -> f nums
' - ' -> if length nums == 1 then f [] else f nums
_ -> if length nums == 1 then f [] else return nums Si le personnage suivant est une période, appelez à nouveau parseVersionNumber avec la liste actuelle des numéros trouvés. S'il s'agit d'un tableau de bord et que nous avons exactement un numéro, appelez parseVersionNumber avec une liste vide de numéros car c'est une date. S'il s'agit d'un tableau de bord et que nous n'avons pas exactement un numéro, appelez parseVersionNumber avec la liste des numéros trouvés jusqu'à présent. Sinon, appelez parseVersionNumber avec une liste vide si nous avons exactement un numéro ou renvoyez les numéros trouvés si nous n'avons pas exactement un numéro.
parseNotNumber
:: ReadP String
parseNotNumber
=
munch ( not . isNumber) parseNotNumber utilise munch que ReadP fournit. munch reçoit le prédicat (not . isNumber) qui renvoie vrai pour tout caractère qui n'est pas 0 à 9.
munch :: ( Char -> Bool ) -> ReadP String munch appelle en continu get Si le caractère suivant dans la chaîne d'entrée satisfait le prédicat. Si ce n'est pas le cas, munch renvoie les personnages qui l'ont fait, le cas échéant. Comme il n'utilise que get , Munch réussit toujours.
Remarque, parseNumber est similaire à parseNotNumber . Au lieu de not . isNumber , le prédicat est juste isNumber .
parseNotNumber'
:: ReadP String
parseNotNumber'
=
many (satisfy ( not . isNumber)) Au lieu d'utiliser munch , vous pouvez écrire parseNotNumber comme celui-ci, en utilisant many et satisfy - dont ReadP fournit. En regardant la signature de type pour many , il accepte un seul combinateur d'analyse ( ReadP a ). Dans ce cas, il est donné que le combinateur d'analyseur satisfy .
> readP_to_S (satisfy ( not . isNumber)) " a "
[( ' a ' , " " )]
> readP_to_S (satisfy ( not . isNumber)) " 1 "
[] satisfy prend un prédicat et utilise get consommer le personnage suivant. Si le prédicat accepté renvoie True, satisfy renvoie le personnage. Sinon, satisfy les appels pfail et échoue.
> readP_to_S (munch ( not . isNumber)) " abc123 "
[( " abc " , " 123 " )]
> readP_to_S (many (satisfy ( not . isNumber))) " abc123 "
[( " " , " abc123 " ),( " a " , " bc123 " ),( " ab " , " c123 " ),( " abc " , " 123 " )] L'utilisation many peut vous donner des résultats indésirables. En fin de compte, many introduisent un ou plusieurs cas Result . Pour cette raison, many réussissent toujours.
> readP_to_S (many look) " abc123 "
-- Runs forever. many exécuteront votre analyseur jusqu'à ce qu'il échoue ou manquera d'entrée. Si votre analyseur n'échoue jamais ou ne manque jamais de contribution, many ne reviendront jamais.
> 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 ], " " )]Pour chaque index dans le résultat, le résultat analysé sera le résultat d'avoir exécuté les temps d'index de l'analyseur sur l'ensemble de l'entrée.
> 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 ], " " )] Voici une définition alternative pour many . Sur le côté gauche de <|> , il renvoie les résultats de l'analyseur actuel. Sur le côté droit de <|> , il exécute l'analyseur, ajoute ce résultat aux résultats de l'analyseur actuels et s'appelle avec les résultats mis à jour. Cela a un effet de type de somme cumulatif où l'index i est le résultat de l'analyseur annexé au résultat de l'analyseur en i - 1 , i - 2 , ... et 1 .
Maintenant que nous avons construit l'analyseur, faisons-le.
> 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. " )]Vous pouvez le voir extrait correctement le numéro de version, même avec la date qui l'avait saisie.
Panons maintenant quelque chose de plus compliqué: les fichiers SRT.
Pour la publication de Gifcurry Six, j'avais besoin d'analyser les fichiers SRT (Subrip Text). Les fichiers SRT contiennent des sous-titres que les programmes de traitement vidéo utilisent pour afficher du texte au-dessus d'une vidéo. En règle générale, ce texte est la boîte de dialogue d'un film traduit dans différentes langues. En gardant le texte séparé de la vidéo, il suffit d'une seule vidéo qui gagne du temps, de l'espace de stockage et de la bande passante. Le logiciel vidéo peut échanger le texte sans avoir à échanger la vidéo. Comparez cela avec l'incendie ou le codage dur des sous-titres où le texte devient une partie des données d'image qui composent la vidéo. Dans ce cas, vous auriez besoin d'une vidéo pour chaque collection de sous-titres.
Vidéo intérieure © Blender Foundation | www.sintel.org
Gifcurry peut prendre un fichier SRT et brûler les sous-titres de la tranche vidéo votre sélection.
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?Ici, vous voyez les sous-titres anglais pour Sintel (© Blender Foundation | www.Sintel.org).
Le SRT est peut-être le plus fondamental de tous les formats de sous-titres.
—Srt Sous-titre | Matrosk
Le format de fichier SRT se compose de blocs, un pour chaque sous-titre, séparés par une ligne vide.
2En haut du bloc se trouve l'index. Cela détermine l'ordre des sous-titres. Espérons que les sous-titres sont déjà en ordre et que tous ont des indices uniques, mais ce n'est peut-être pas le cas.
01:04:13,000 --> 02:01:01,640 X1:167 X2:267 Y1:33 Y2:63Une fois que l'index est l'heure de début, l'heure de fin et un ensemble de points facultatifs spécifiant le rectangle dans lequel le texte du sous-titre doit entrer.
01:04:13,000 Le format d'horodatage est hours:minutes:seconds,milliseconds .
Notez la virgule au lieu de la période séparant les secondes des millisecondes.
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>.La troisième et dernière partie d'un bloc est le texte du sous-titre. Il peut s'étendre sur plusieurs lignes et se termine lorsqu'il y a une ligne vide. Le texte peut inclure des balises de formatage rappelant HTML.
parseSrt
:: ReadP [ SrtSubtitle ]
parseSrt
=
manyTill parseBlock (skipSpaces >> eof) parseSrt est le principal combinateur d'analyseur qui gère tout. Il analyse chaque bloc jusqu'à ce qu'il atteigne l'extrémité du fichier ( eof ) ou de l'entrée. Pour être en toute sécurité, il pourrait y avoir un espace de traînée entre le dernier bloc et la fin du fichier. Pour gérer cela, il analyse zéro ou plus de caractères de l'espace blanc ( skipSpaces ) avant d'analyser la fin du fichier ( skipSpaces >> eof ). S'il reste encore d'entrée au moment où eof est atteint, eof échouera et cela ne rendra rien. Par conséquent, il est important que parseBlock ne laisse rien mais le blanc.
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
} Comme nous le faisions plus tôt, un bloc se compose d'un index, de horodatages, peut-être de certaines coordonnées et de certaines lignes de texte. Dans cette version de parseBlock , vous voyez le style de notation plus impératif avec la syntaxe enregistrée.
parseBlock'
:: ReadP SrtSubtitle
parseBlock'
=
SrtSubtitle
<$> parseIndex
<*> parseStartTimestamp
<*> parseEndTimestamp
<*> parseCoordinates
<*> parseTextLines Voici une autre façon d'écrire parseBlock . Ceci est le style applicatif. Assurez-vous simplement de passer la commande correcte. Par exemple, j'aurais pu accidentellement mélangé les horodatages de début et de fin.
parseIndex
:: ReadP Int
parseIndex
=
skipSpaces
>> readInt <$> parseNumber En haut du bloc se trouve l'index. Ici, vous revenez à nouveau skipSpaces . Après avoir sauté sur l'espace, il analyse l'entrée pour les nombres et le convertit en un entier réel.
readInt
:: String
-> Int
readInt
=
read readInt ressemble à ceci.
> read " 123 " :: Int
123
> read " 1abc " :: Int
*** Exception : Prelude. read : no parse Normalement, l'utilisation read directement peut être dangereuse. read peut ne pas être en mesure de convertir l'entrée en type spécifié. Cependant, parseNumber ne renverra que les 10 caractères numériques numériques ( ['0'..'9'] ), l'utilisation read est donc directement en sécurité.
L'analyse des horodatages est un peu plus impliquée que l'analyse de l'indice.
parseTimestamps
:: ReadP ( Timestamp , Timestamp )
parseTimestamps
= do
_ <- char ' n '
s <- parseTimestamp
_ <- skipSpaces
_ <- string " --> "
_ <- skipSpaces
e <- parseTimestamp
return (s, e)Il s'agit du combinateur principal pour analyser les horodatages.
char analyse le personnage que vous lui donnez ou il échoue. S'il échoue, parseTimestamps échoue, ce qui a finalement échoué parseSrt , il doit donc y avoir un caractère Newline après l'index.
string est comme char , sauf qu'au lieu d'un seul caractère, il analyse la chaîne de caractères que vous lui donnez ou il échoue.
parseStartTimestamp
:: ReadP Timestamp
parseStartTimestamp
=
char ' n '
>> parseTimestamp parseTimestamps analyse les deux horodatages, mais pour le style applicatif ( parseSrt' ), nous avons besoin d'un analyseur juste pour le horodat de début.
parseEndTimestamp
:: ReadP Timestamp
parseEndTimestamp
=
skipSpaces
>> string " --> "
>> skipSpaces
>> parseTimestampCela analyse tout entre les horodatages et renvoie l'horodatage final.
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'
}Cela analyse les quatre nombres qui composent l'horodatage. Les trois premiers nombres sont séparés par un côlon et le dernier est séparé par une virgule. Pour être plus indulgent, cependant, nous permettons à la possibilité qu'il y ait une période au lieu d'une virgule.
> readP_to_S (char ' . ' <|> char ' , ' ) " ... "
[( ' . ' , " .. " )]
> readP_to_S (char ' . ' <|> char ' , ' ) " ,.. "
[( ' , ' , " .. " )] Remarque, lors de l'utilisation char avec <|> , un seul côté peut réussir (deux char à char, un congé char ) car char consomme un seul caractère et deux caractères ne peuvent pas occuper le même espace.
Les coordonnées sont une partie facultative du bloc mais si elles sont incluses, sera sur la même ligne que les horodatages.
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 prend deux arguments. Le premier argument est retourné si le deuxième argument, un analyseur, échoue. Donc, si l'analyseur des coordonnées échoue, parseCoordinates ne rendront Nothing . Autrement dit, l'échec des coordonnées de l'analyseur ne fait pas échouer l'ensemble de l'analyseur. Ce bloc n'aura Nothing pour ses coordinates "champ".
parseCoordinate
:: Char
-> Int
-> ReadP String
parseCoordinate
c
n
= do
_ <- char ( Data.Char. toUpper c) <|> char ( Data.Char. toLower c)
_ <- string $ show n ++ " : "
parseNumber Cet analyseur permet aux étiquettes de coordonnées d'être en majuscules ou en minuscules. Par exemple, x1:1 X2:2 Y1:3 y2:4 réussirait.
L'analyse du texte est la partie la plus impliquée en raison du formatage de balises de type HTML.
L'analyse de tag peut être difficile - il suffit de demander à quiconque les analyse avec une expression régulière. Pour nous faciliter la manière dont nous, et pour l'utilisateur - nous utiliserons une approche de soupe de tag. L'analyseur autorisera des balises non infiltrées et / ou à tort. Il permettra également n'importe quelle balise et pas seulement b , u , i et font .
parseTextLines
:: ReadP [ TaggedText ]
parseTextLines
=
char ' n '
>> (getTaggedText <$> manyTill parseAny parseEndOfTextLines) Nous commençons par correspondre sur un personnage de Newline. Après cela, nous Functor Map ou FMAP ( <$> ) getTaggedText sur les caractères de texte du sous-titre jusqu'à ce que nous atteignions la fin des lignes de texte.
parseEndOfTextLines
:: ReadP ()
parseEndOfTextLines
=
void (string " nn " ) <|> eof Nous cessons de collecter des caractères ( parseAny ) lorsque nous atteignons deux caractères Newline ou la fin du fichier. Cela signale la fin du bloc.
getTaggedText
:: String
-> [ TaggedText ]
getTaggedText
s
=
fst
$ foldl
folder
( [] , [] )
parsed
where getTaggedText se replie à travers le texte analysé de gauche à droite, renvoyant le texte tagué accumulé.
parsed
:: [ String ]
parsed
=
case readP_to_S (parseTaggedText [] ) s of
[] -> [s]
r @ (_ : _) -> ( fst . last ) r parsed renvoie une liste d'une ou plusieurs chaînes. Il tente d'analyser le texte d'entrée pour les balises. Si cela échoue, parsed renvoie la chaîne d'entrée dans une liste. Sinon, si parseTaggedText réussit, parse renvoie le dernier analyse possible ( (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) Au fur et à mesure que folder se déplace de gauche à droite, sur les chaînes analysées, il vérifie si la chaîne actuelle est une balise. S'il s'agit d'une balise, il met à jour l'ensemble actuel des balises actives ( t ). Sinon, il ajoute un autre texte tagué associé à l'ensemble des balises actives.
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 met à jour les tags données par la suppression ou l'ajout de la balise donnée ( x ) selon qu'il s'agit d'une balise de clôture ou d'ouverture. S'il n'est ni l'un ni l'autre, il ne renvoie que l'ensemble des balises passées. add écrasera une balise existante si tags ont déjà une balise du même nom. Vous pouvez le voir dans la fonction compare' donnée.
Pour garder l' T T ajouté à la liste des balises ou écrase un sort T si déjà présent. Si une fermeture /T correspondante est trouvée, alors T est supprimé de la liste des balises, si elle est présente. Peu importe s'il y a deux T ou plus dans une rangée, un ou plusieurs T s sans fermeture /T , et / ou il y a une fermeture /T sans ouverture T .
makeTag
:: String
-> Tag
makeTag
s
=
Tag
{ name = getTagName s
, attributes = getTagAttributes s
} makeTag assemble une balise à partir de la (s) chaîne ( s ) donnée. Chaque Tag a un nom et zéro ou plus d'attributs.
parseTaggedText
:: [ String ]
-> ReadP [ String ]
parseTaggedText
strings
= do
s <- look
case s of
" " -> return strings
_ -> do
r <- munch1 ( /= ' < ' ) <++ parseClosingTag <++ parseOpeningTag
parseTaggedText $ strings ++ [r] parseTaggedText renvoie la chaîne d'entrée divisée en morceaux. Chaque pièce est soit le texte entouré de balises, une balise de clôture ou une balise d'ouverture. Après avoir divisé un morceau, il l'ajoute aux autres pièces et s'appelle à nouveau. Si la chaîne d'entrée restante est vide, elle renvoie la liste des chaînes trouvées.
> 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 " )] L'opérateur <++ est partisan de gauche, ce qui signifie que si le côté gauche réussit, il ne se souciera même pas de la droite. Rappelons que lorsque nous exécutons l'analyseur, nous obtenons une liste de tous les analyses possibles. Tous ces analyses possibles sont le résultat de l'analyseur ayant parcouru tous les chemins possibles. En utilisant <++ , nous recevons les analyses possibles du chemin de gauche et du chemin droit si et seulement si le côté gauche a échoué. Si vous souhaitez tous les analyses possibles à travers le côté gauche et droit, vous pouvez utiliser l'opérateur +++ fourni par ReadP . +++ est juste <|> que nous avons vu ci-dessus.
parseOpeningTag
:: ReadP String
parseOpeningTag
= do
_ <- char ' < '
t <- munch1 ( c -> c /= ' / ' && c /= ' > ' )
_ <- char ' > '
return $ " < " ++ t ++ " > "Une étiquette d'ouverture est un support d'angle d'ouverture, du texte qui n'inclut pas une barre oblique avant et le support d'angle de fermeture immédiat suivant.
parseClosingTag
:: ReadP String
parseClosingTag
= do
_ <- char ' < '
_ <- char ' / '
t <- munch1 ( /= ' > ' )
_ <- char ' > '
return $ " </ " ++ t ++ " > "Une étiquette de clôture est un support d'angle d'ouverture, une barre oblique avant, du texte et le support d'angle de clôture immédiat suivant.
getTagAttributes
:: String
-> [ TagAttribute ]
getTagAttributes
s
=
if isOpeningTag s
then
case readP_to_S (parseTagAttributes [] ) s of
[] -> []
(x : _) -> fst x
else
[] Les balises d'ouverture peuvent avoir des attributs. Par exemple, <font color="#101010"> . Chaque attribut est une paire de valeurs clés à deux tunières. Dans l'exemple ci-dessus, color serait la clé et #101010 serait la valeur.
getTagName
:: String
-> String
getTagName
s
=
case readP_to_S parseTagName s of
[] -> " "
(x : _) -> toLower' $ fst xCela renvoie le nom de la balise en minuscules.
parseTagName
:: ReadP String
parseTagName
= do
_ <- char ' < '
_ <- munch ( == ' / ' )
_ <- skipSpaces
n <- munch1 ( c -> c /= ' ' && c /= ' > ' )
_ <- munch ( /= ' > ' )
_ <- char ' > '
return nLe nom de la balise est la première chaîne de caractères sans espace après le support d'angle d'ouverture, une éventuelle barre oblique avant et un espace blanc possible et avant un espace plus blanc et / ou le support d'angle de fermeture.
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 passe récursivement par la chaîne d'entrée, collectant les paires de valeurs clés. Au début de la balise ( < ), il coupe d'abord le nom de la balise avant de s'attaquer aux attributs. Il cesse d'analyser les attributs lorsqu'il atteint le support d'angle de fermeture ( > ). Si une balise a des attributs en double (en fonction de la clé), add garantira que seul le dernier reste dans la liste.
trimTagname
:: ReadP ()
trimTagname
=
char ' < '
>> skipSpaces
>> munch1 ( c -> c /= ' ' && c /= ' > ' )
>> return ()Cela coupe ou rejette le nom de la balise.
parseTagAttribute
:: ReadP TagAttribute
parseTagAttribute
= do
_ <- skipSpaces
k <- munch1 ( /= ' = ' )
_ <- string " = " "
v <- munch1 ( /= ' " ' )
_ <- char ' " '
_ <- skipSpaces
return (toLower' k, v)La touche d'attribut est toute chaîne de caractères non espaces avant le signe égal. La valeur d'attribut est tous les caractères après le signe égal et la double citation et avant la prochaine double citation immédiate.
isTag
:: String
-> Bool
isTag
s
=
isOpeningTag s || isClosingTag sUne chaîne est une balise s'il s'agit d'une balise d'ouverture ou d'une balise de clôture.
isOpeningTag
:: String
-> Bool
isOpeningTag
s
=
isPresent $ readP_to_S parseOpeningTag sUne chaîne est une balise d'ouverture si l'analyseur de balise d'ouverture réussit.
isClosingTag
:: String
-> Bool
isClosingTag
s
=
isPresent $ readP_to_S parseClosingTag sUne chaîne est une balise de clôture si l'analyseur de balise de clôture réussit.
Maintenant que nous avons assemblé l'analyseur, essayons-le.