¿Necesitas analizar algo? ¿Nunca has oído hablar de un "combinador de analizador"? ¿Buscas aprender algo de Haskell? ¡Impresionante! A continuación se muestra todo lo que necesitará para levantarse y analizar con los combinadores de análisis de Haskell. Desde aquí puede intentar abordar los formatos de serialización de datos esotéricos, los frontales del compilador, los idiomas específicos del dominio, ¡lo que lo llame!
Se incluyen con esta guía dos programas de demostración.
version-number-parser analiza un archivo para un número de versión. srt-file-parser analiza un archivo para subtítulos SRT. Siéntase libre de probarlos con los archivos que se encuentran en test-input/ .
Descargue la pila de herramientas Haskell y luego ejecute lo siguiente.
git clone https://github.com/lettier/parsing-with-haskell-parser-combinators
cd parsing-with-haskell-parser-combinators
stack buildSi usa Cabal, puede ejecutar lo siguiente.
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 installDespués de construir los dos programas de demostración, puede ejecutarlos así.
Para probar el analizador de número de versión, ejecute lo siguiente.
cd parsing-with-haskell-parser-combinators
stack exec -- version-number-parser
What is the version output file path ?
test-input/gifcurry-version-output.txtPara probar el analizador de archivo SRT, ejecute lo siguiente.
cd parsing-with-haskell-parser-combinators
stack exec -- srt-file-parser
What is the SRT file path ?
test-input/subtitles.srtPara probar el analizador de número de versión, ejecute lo siguiente.
cd parsing-with-haskell-parser-combinators
.cabal-sandbox/bin/version-number-parser
What is the version output file path ?
test-input/gifcurry-version-output.txtPara probar el analizador de archivo SRT, ejecute lo siguiente.
cd parsing-with-haskell-parser-combinators
.cabal-sandbox/bin/srt-file-parser
What is the SRT file path ?
test-input/subtitles.srtUna de las mejores maneras de aprender sobre la estrategia de análisis, el combinador analizador, es analizar una implementación de uno.
Los analizadores construidos con combinadores son sencillos para construir, legibles, modulares, bien estructurados y fácilmente mantenibles.
—Combinator parser - Wikipedia
Echemos un vistazo debajo del capó de Readp, una biblioteca de combinador analizador que se encuentra en la base. Como está en la base, ya deberías tenerlo.
Nota, es posible que desee probar PARSEC después de familiarizarse con Readp. También es una biblioteca combinadora de analizador que otros prefieren leer. Como bono adicional, se incluye en las bibliotecas de arranque de GHC a partir de la versión 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 Comenzaremos con el tipo de datos P El a en P a depende de usted (el usuario de la biblioteca) y puede ser lo que desee. El compilador crea una instancia de functores automáticamente y hay instancias escritas a mano para aplicativas, mónadas, MonadFail y alternativas.
Tenga en cuenta que para obtener más información sobre functores, solicitantes y mónadas, consulte su guía fácil de mónadas, solicitantes y funciones.
P es un tipo de suma con cinco casos.
Get consume un solo carácter de la cadena de entrada y devuelve una nueva PLook acepta un duplicado de la cadena de entrada y devuelve una nueva PFail indica que el analizador terminado sin resultado.Result posee un posible análisis y otro caso PFinal es una lista de dos tuplas. El primer elemento de tupla es un posible análisis de la entrada y el segundo elemento de tupla es el resto de la cadena de entrada que no Get consumió. -- (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 es el corazón del analizador Readp. Hace todo el trabajo pesado, ya que corre recursivamente a través de todos los estados analizadores que vimos arriba. Puede ver que se necesita una P y devuelve una ReadS .
-- (c) The University of Glasgow 2002
type ReadS a = String -> [( a , String )] ReadS a es un tipo de alias para String -> [(a,String)] . Entonces, cada vez que vea, ReadS a , piense en 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 _ _ = [] El patrón run coincide con los diferentes casos de P .
Get , se llama a sí mismo con una nueva P (devuelta al pasar la función f , en Get f , el siguiente carácter c en la cadena de entrada) y el resto de las cadenas de entrada s .Look , se llama a sí mismo con una nueva P (devuelta al pasar la función f , en Look f , las cadenas de entrada s ) y la cadena de entrada. Observe cómo Look no consume ningún caracteres de la cadena de entrada como Get Do.Result , ensambla una dospla, que contiene el resultado analizado y lo que queda de la cadena de entrada, y prepara esto con el resultado de una llamada recursiva que se ejecuta con otra caja P y la cadena de entrada.Final , run devuelve una lista de dos tuplas que contienen resultados analizados y sobras de cadena de entrada.run devuelve una lista vacía. Por ejemplo, si el caso Fail , run devolverá una lista vacía. > run ( Get ( a -> Get ( b -> Result [a,b] Fail ))) " 12345 "
[( " 12 " , " 345 " )] Readp no expone run pero si lo hizo, podría llamarlo así. Los dos Get el '1' y '2' , dejando atrás el "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 " )]Corriendo a través de cada llamada recursiva, puede ver cómo llegamos al resultado final.
> run ( Get ( a -> Get ( b -> Result [a,b] ( Final [([ ' a ' , ' b ' ], " c " )])))) " 12345 "
[( " 12 " , " 345 " ),( " ab " , " c " )] Usando Final , puede incluir un resultado analizado en la lista final de dos tuplas.
-- (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 ) Si bien Readp no expone run directa, lo expone a través de readP_to_S . readP_to_S presenta un newtype llamado ReadP . readP_to_S acepta un ReadP a , una cadena y devuelve una lista de dos tuplas.
-- (c) The University of Glasgow 2002
newtype ReadP a = R ( forall b . ( a -> P b ) -> P b ) Aquí está la definición de ReadP a . Hay casos para functores, aplicativos, monad, MonadFail , alternativos y MonadPlus . El constructor R toma una función que toma otra función y devuelve una P . La función aceptada toma lo que elija para a y devuelva una P
-- (c) The University of Glasgow 2002
readP_to_S ( R f) = run (f return ) Recuerde que P es una mónada y el tipo de return es a -> ma . Entonces f es la función (a -> P b) -> Pb y return es la función (a -> P b) . En última instancia, run obtiene el P b que espera.
-- (c) The University of Glasgow 2002
readP_to_S ( R f) inputString = run (f return ) inputString
-- ^ ^^^^^^^^^^ ^^^^^^^^^^^ Se deja en el código fuente, pero recuerde que readP_to_S y run espera una cadena de entrada.
-- (c) The University of Glasgow 2002
instance Functor ReadP where
fmap h ( R f) = R ( k -> f (k . h)) Aquí está la definición de instancia de functores para ReadP .
> readP_to_S ( fmap toLower get) " ABC "
[( ' a ' , " BC " )]
> readP_to_S (toLower <$> get) " ABC "
[( ' a ' , " BC " )] Esto nos permite hacer algo como esto. fmap Functor Maps toLower sobre el Funcor get que es igual R Get Recuerde que el tipo de Get es (Char -> P a) -> P a que el constructor ReadP ( R ) acepta.
-- (c) The University of Glasgow 2002
fmap h ( R f ) = R ( k -> f (k . h ))
fmap toLower ( R Get ) = R ( k -> Get (k . toLower)) Aquí ves la definición del functor reescrita para el fmap toLower get ejemplo.
Mirando arriba, ¿cómo devolvió readP_to_S [('a',"BC")] cuando solo usamos Get Wat no termina run ? La respuesta radica en la definición aplicativa para P .
-- (c) The University of Glasgow 2002
instance Applicative P where
pure x = Result x Fail
(<*>) = ap return es igual pure para que pudiéramos reescribir readP_to_S (R f) = run (f return) para readP_to_S (R f) = run (f pure) . Al usar return o más bien pure , readP_to_S establece Result x Fail ya que se encontrará la run del caso final. Si se le alcanza, run terminará y obtendremos nuestra lista de analizados.
> 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 " )] Aquí ve el flujo de readP_to_S al resultado analizado.
-- (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)
-- ... La instancia Alternative para P nos permite dividir el flujo del analizador en una ruta izquierda y derecha. Esto es útil cuando la entrada puede ir ninguno, uno o (más raramente) dos de dos maneras.
> readP_to_S ((get >>= a -> return a) <|> (get >> get >>= b -> return b)) " ABC "
[( ' A ' , " BC " ),( ' B ' , " C " )] El operador o función <|> introduce una bifurcación en el flujo del analizador. El analizador viajará a través de los caminos izquierdo y derecho. El resultado final contendrá todos los análisis posibles que salieron a la izquierda y todos los posibles analizados que salieron a la derecha. Si ambas rutas fallan, entonces todo el analizador falla.
Tenga en cuenta que en otras implementaciones del combinador de analizador, cuando se usa el operador <|> , el analizador irá a la izquierda o a la derecha, pero no a ambos. Si la izquierda tiene éxito, se ignora la derecha. La derecha solo se procesa si el lado izquierdo falla.
> readP_to_S ((get >>= a -> return [a]) <|> look <|> (get >> get >>= a -> return [a])) " ABC "
[( " ABC " , " ABC " ),( " A " , " BC " ),( " B " , " C " )] Puede encadenar el operador <|> para muchas opciones o alternativas que existan. El analizador devolverá un posible análisis que involucre a cada uno.
-- (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)) Aquí está la instancia de Mónada ReadP . Observe la definición 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 "
[] Puede hacer que un camino de analizador completo aborta llamando fail . Dado que ReadP no proporciona una forma directa de generar un Result o caso Final , el valor de retorno será una lista vacía. Si la ruta fallida es la única ruta, entonces todo el resultado será una lista vacía. Recuerde que cuando los partidos run Fail , devuelve una lista vacía.
-- (c) The University of Glasgow 2002
instance Alternative P where
-- ...
-- fail disappears
Fail <|> p = p
p <|> Fail = p
-- ... Volviendo a la instancia de alternativa P , puede ver cómo una falla en cada lado (pero no ambos) no fallará todo el analizador.
> readP_to_S (get >>= a -> get >>= b -> pfail >>= c -> return [a,b,c]) " ABC "
[] En lugar de usar fail , ReadP proporciona pfail que le permite generar un caso Fail directamente.
GIFCurry, el editor de video construido en Haskell para los fabricantes de GIF, se lanza a varios programas diferentes. Para garantizar la compatibilidad, necesita el número de versión para cada uno de los programas a los que bombea. Uno de esos programas es 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 Aquí ves la salida de convert --version . ¿Cómo podrías analizar esto para capturar los 6, 9, 10 y 14?
Mirando la salida, sabemos que el número de versión es una colección de números separados por un período o un tablero. Esta definición también cubre las fechas, por lo que nos aseguraremos de que los dos primeros números estén separados por un período. De esa manera, si ponen una fecha antes del número de versión, no obtendremos el resultado incorrecto.
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.Antes de sumergirnos en el código, aquí está el algoritmo que seguiremos.
parseVersionNumber
:: [ String ]
-> ReadP [ String ]
parseVersionNumber
nums
= do
_ <- parseNotNumber
num <- parseNumber
let nums' = nums ++ [num]
parseSeparator nums' parseVersionNumber parseVersionNumber es el combinador de analizador principal que analiza una cadena de entrada para un número de versión. Acepta una lista de cadenas y devuelve una lista de cadenas en el contexto del tipo de datos ReadP . La lista aceptada de cadenas no es la entrada que se analiza, sino la lista de números que se encuentran hasta ahora. Para la primera llamada de función, la lista está vacía ya que aún no ha analizado nada.
parseVersionNumber
nums A partir de la parte superior, parseVersionNumber toma una lista de cadenas que son la lista actual de números que se encuentran hasta ahora.
_ <- parseNotNumber parseNotNumber consume todo lo que no es un número de la cadena de entrada. Como no estamos interesados en el resultado, lo descartamos ( _ <- ).
num <- parseNumber
let nums' = nums ++ [num]A continuación, consumimos todo lo que es un número y luego lo agregamos a la lista de números encontrados hasta ahora.
parseSeparator nums' parseVersionNumber Después de que parseVersionNumber ha procesado el siguiente número, pasa la lista de números encontrados y en sí mismo a 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 Aquí ves parseSeparator .
next <- look
case next of
" " -> return nums
(c : _) -> look nos permite obtener lo que queda de la cadena de entrada sin consumirla. Si no queda nada, devuelve los números encontrados. Sin embargo, si queda algo, analiza el primer carácter.
case c of
' . ' -> f nums
' - ' -> if length nums == 1 then f [] else f nums
_ -> if length nums == 1 then f [] else return nums Si el siguiente personaje es un período, llame parseVersionNumber nuevamente con la lista actual de números encontrados. Si es un tablero y tenemos exactamente un número, llame parseVersionNumber con una lista vacía de números ya que es una fecha. Si es un tablero y no tenemos exactamente un número, llame a parseVersionNumber con la lista de números encontrados hasta ahora. De lo contrario, llame parseVersionNumber con una lista vacía si tenemos exactamente un número o devuelve los números encontrados si no tenemos exactamente un número.
parseNotNumber
:: ReadP String
parseNotNumber
=
munch ( not . isNumber) parseNotNumber usa munch que ReadP proporciona. munch recibe el predicado (not . isNumber) que devuelve verdadero para cualquier personaje que no sea de 0 a 9.
munch :: ( Char -> Bool ) -> ReadP String munch llama continuamente si el siguiente carácter de la cadena de entrada get el predicado. Si no es así, munch devuelve los personajes que lo hicieron, si los hay. Como solo usa get , Munch siempre tiene éxito.
Nota, parseNumber es similar a parseNotNumber . En lugar de not . isNumber , el predicado es solo isNumber .
parseNotNumber'
:: ReadP String
parseNotNumber'
=
many (satisfy ( not . isNumber)) En lugar de usar munch , puede escribir parseNotNumber como este, usando many y satisfy , tanto de los cuales ReadP proporciona. Mirando la firma de tipo para many , acepta un solo combinador de analizador ( ReadP a ). En este caso, se le da satisfy al combinador de analizador.
> readP_to_S (satisfy ( not . isNumber)) " a "
[( ' a ' , " " )]
> readP_to_S (satisfy ( not . isNumber)) " 1 "
[] satisfy toma un predicado y los usos get consumir el siguiente carácter. Si el predicado aceptado devuelve verdadero, satisfy devuelve el carácter. De lo contrario, satisfy las llamadas pfail y fallar.
> readP_to_S (munch ( not . isNumber)) " abc123 "
[( " abc " , " 123 " )]
> readP_to_S (many (satisfy ( not . isNumber))) " abc123 "
[( " " , " abc123 " ),( " a " , " bc123 " ),( " ab " , " c123 " ),( " abc " , " 123 " )] Usar many puede darle resultados no deseados. En última instancia, many introducen uno o más casos Result . Debido a esto, many siempre tienen éxito.
> readP_to_S (many look) " abc123 "
-- Runs forever. many ejecutarán su analizador hasta que falle o se quede sin entrada. Si su analizador nunca falla o nunca se queda sin aportes, many nunca volverán.
> 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 ], " " )]Para cada índice en el resultado, el resultado analizado será el resultado de haber ejecutado los tiempos de índice de analizador en toda la entrada.
> 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 ], " " )] Aquí hay una definición alternativa para many . En el lado izquierdo de <|> , devuelve los resultados actuales del analizador. En el lado derecho de <|> , ejecuta el analizador, agrega ese resultado a los resultados actuales del analizador y se llama a sí mismo con los resultados actualizados. Esto tiene un efecto de tipo de suma acumulativa donde el índice i es el resultado del analizador adjunto al resultado del analizador en i - 1 , i - 2 , ... y 1 .
Ahora que construimos el analizador, lo ejecutemos.
> 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. " )]Puede ver que extrajo el número de versión correctamente incluso con la fecha que se le presenta.
Ahora analicemos algo más complicado: archivos SRT.
Para el lanzamiento de GIFCurry Six, necesitaba analizar archivos SRT (texto subrip). Los archivos SRT contienen subtítulos que utilizan los programas de procesamiento de video para mostrar texto en la parte superior de un video. Por lo general, este texto es el diálogo de una película traducido a varios idiomas diferentes. Al mantener el texto separado del video, solo debe haber un video que ahorre tiempo, espacio de almacenamiento y ancho de banda. El software de video puede cambiar el texto sin tener que cambiar el video. Compare esto con la quema o la codificación dura de los subtítulos donde el texto se convierte en parte de los datos de la imagen que constituye el video. En este caso, necesitaría un video para cada colección de subtítulos.
Video interno © Blender Foundation | www.sintel.org
GIFCurry puede tomar un archivo SRT y grabar los subtítulos para el video de su selección.
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?Aquí ves los subtítulos en inglés para Sintel (© Blender Foundation | www.sintel.org).
SRT es quizás el más básico de todos los formatos de subtítulos.
—Srt Subtitle | Matrosk
El formato de archivo SRT consiste en bloques, uno para cada subtítulo, separado por una línea vacía.
2En la parte superior del bloque está el índice. Esto determina el orden de los subtítulos. Esperemos que los subtítulos ya estén en orden y todos tengan índices únicos, pero este puede no ser el caso.
01:04:13,000 --> 02:01:01,640 X1:167 X2:267 Y1:33 Y2:63Después de que el índice sea la hora de inicio, la hora de finalización y un conjunto opcional de puntos que especifique el rectángulo, el texto del subtítulo debe entrar.
01:04:13,000 El formato de marca de tiempo es hours:minutes:seconds,milliseconds .
Tenga en cuenta la coma en lugar del período que separa los segundos de los milisegundos.
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 tercera y última parte de un bloque es el texto del subtítulo. Puede abarcar varias líneas y termina cuando hay una línea vacía. El texto puede incluir etiquetas de formato que recuerdan a HTML.
parseSrt
:: ReadP [ SrtSubtitle ]
parseSrt
=
manyTill parseBlock (skipSpaces >> eof) parseSrt es el combinador de analizador principal que maneja todo. Analiza cada bloque hasta que alcanza el final del archivo ( eof ) o la entrada. Para estar en el lado seguro, podría haber un espacio en blanco entre el último bloque y el final del archivo. Para manejar esto, analiza cero o más caracteres de Whitespace ( skipSpaces ) antes de analizar el final del archivo ( skipSpaces >> eof ). Si todavía queda información para el momento en que se alcanza eof , eof fallará y esto no devolverá nada. Por lo tanto, es importante que parseBlock no deje nada más que un espacio en blanco.
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
} A medida que avanzamos anteriormente, un bloque consiste en un índice, marcas de tiempo, posiblemente algunas coordenadas y algunas líneas de texto. En esta versión de parseBlock , ves el estilo de notación más imperativo con la sintaxis de registro.
parseBlock'
:: ReadP SrtSubtitle
parseBlock'
=
SrtSubtitle
<$> parseIndex
<*> parseStartTimestamp
<*> parseEndTimestamp
<*> parseCoordinates
<*> parseTextLines Aquí hay otra forma en que podrías escribir parseBlock . Este es el estilo aplicativo. Solo asegúrese de hacer el pedido correcto. Por ejemplo, podría haber mezclado accidentalmente las marcas de tiempo de inicio y finalización.
parseIndex
:: ReadP Int
parseIndex
=
skipSpaces
>> readInt <$> parseNumber En la parte superior del bloque está el índice. Aquí ves skipSpaces nuevamente. Después de omitir el espacio en blanco, analiza la entrada para los números y la convierte en un entero real.
readInt
:: String
-> Int
readInt
=
read readInt se ve así.
> read " 123 " :: Int
123
> read " 1abc " :: Int
*** Exception : Prelude. read : no parse Normalmente, usar read directamente puede ser peligroso. read puede no poder convertir la entrada al tipo especificado. Sin embargo, parseNumber solo devolverá los 10 caracteres de dígitos numéricos ( ['0'..'9'] ), por lo que usar read directamente se vuelve seguro.
Analizar las marcas de tiempo son un poco más involucradas que analizar el índice.
parseTimestamps
:: ReadP ( Timestamp , Timestamp )
parseTimestamps
= do
_ <- char ' n '
s <- parseTimestamp
_ <- skipSpaces
_ <- string " --> "
_ <- skipSpaces
e <- parseTimestamp
return (s, e)Este es el combinador principal para analizar las marcas de tiempo.
char analiza el personaje que le das o falla. Si falla, entonces parseTimestamps falla, lo que finalmente hace que parseSrt falle, por lo que debe haber un carácter de nueva línea después del índice.
string es como char excepto que en lugar de un solo carácter, analiza la cadena de caracteres que le das o falla.
parseStartTimestamp
:: ReadP Timestamp
parseStartTimestamp
=
char ' n '
>> parseTimestamp parseTimestamps analiza ambas marcas de tiempo, pero para el estilo aplicativo ( parseSrt' ), necesitamos un analizador solo para la marca de tiempo de inicio.
parseEndTimestamp
:: ReadP Timestamp
parseEndTimestamp
=
skipSpaces
>> string " --> "
>> skipSpaces
>> parseTimestampEsto analiza todo entre las marcas de tiempo y devuelve la marca de tiempo 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'
}Esto analiza los cuatro números que conforman la marca de tiempo. Los primeros tres números están separados por un colon y el último está separado por una coma. Sin embargo, para ser más indulgente, permitimos la posibilidad de que haya un período en lugar de una coma.
> readP_to_S (char ' . ' <|> char ' , ' ) " ... "
[( ' . ' , " .. " )]
> readP_to_S (char ' . ' <|> char ' , ' ) " ,.. "
[( ' , ' , " .. " )] Tenga en cuenta que cuando se usa char con <|> , solo un lado puede tener éxito (dos char Enter, una licencia char ) ya que char consume un solo carácter y dos caracteres no pueden ocupar el mismo espacio.
Las coordenadas son una parte opcional del bloque, pero si se incluyen, estarán en la misma línea que las marcas de tiempo.
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 toma dos argumentos. El primer argumento se devuelve si el segundo argumento, un analizador, falla. Entonces, si la coordenada falla, parseCoordinates no devolverá Nothing . Dicho de otra manera, la falla del analizador de coordenadas no hace que todo el analizador falle. Este bloque no tendrá Nothing para sus coordinates "campo".
parseCoordinate
:: Char
-> Int
-> ReadP String
parseCoordinate
c
n
= do
_ <- char ( Data.Char. toUpper c) <|> char ( Data.Char. toLower c)
_ <- string $ show n ++ " : "
parseNumber Este analizador permite que las etiquetas coordinadas estén en mayúsculas o minúsculas. Por ejemplo, x1:1 X2:2 Y1:3 y2:4 tendría éxito.
Anular el texto es la parte más involucrada debido al formato de etiqueta tipo HTML.
El análisis de la etiqueta puede ser un desafío: solo pregúntele a cualquiera que los analice con una expresión regular. Para facilitar esto, y para el usuario, usaremos un tipo de enfoque de sopa de etiqueta. El analizador permitirá etiquetas no cerradas y/o erróneas. También permitirá cualquier etiqueta y no solo b , u , i y font .
parseTextLines
:: ReadP [ TaggedText ]
parseTextLines
=
char ' n '
>> (getTaggedText <$> manyTill parseAny parseEndOfTextLines) Comenzamos coincidiendo con un personaje de Newline. Después de eso, funcionamos MAP o FMAP ( <$> ) getTaggedText sobre los caracteres de texto del subtítulo hasta que llegamos al final de las líneas de texto.
parseEndOfTextLines
:: ReadP ()
parseEndOfTextLines
=
void (string " nn " ) <|> eof Dejamos de recopilar personajes ( parseAny ) cuando llegamos a dos caracteres de Newline o al final del archivo. Esto señala el final del bloque.
getTaggedText
:: String
-> [ TaggedText ]
getTaggedText
s
=
fst
$ foldl
folder
( [] , [] )
parsed
where getTaggedText se pliega a través del texto analizado de izquierda a derecha, devolviendo el texto etiquetado acumulado.
parsed
:: [ String ]
parsed
=
case readP_to_S (parseTaggedText [] ) s of
[] -> [s]
r @ (_ : _) -> ( fst . last ) r parsed devuelve una lista de una o más cadenas. Intenta analizar el texto de entrada para las etiquetas. Si eso falla, parsed devuelve la cadena de entrada dentro de una lista. De lo contrario, si parseTaggedText tiene éxito, parse devuelve el último analizador posible ( (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) A medida que folder se mueve de izquierda a derecha, sobre las cadenas analizadas, verifica si la cadena actual es una etiqueta. Si es una etiqueta, actualiza el conjunto actual de etiquetas activas ( t ). De lo contrario, agrega otra pieza de texto etiquetada asociada con el conjunto de etiquetas activas.
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 actualiza las tags dadas al eliminar o agregar la etiqueta dada ( x ) dependiendo de si es una etiqueta de cierre o apertura. Si no es ninguno, solo devuelve el conjunto de etiquetas aprobados. add sobrescribirá una etiqueta existente si tags ya tienen una etiqueta por el mismo nombre. Puede ver esto en la función compare' dada.
Para mantener el analizador simple, si se encuentra una T de apertura, T agrega a la lista de etiquetas o sobrescribe una T de salida si ya está presente. Si se encuentra un cierre correspondiente /T , entonces T elimina de la lista de etiquetas, si está presente. No importa si hay dos o más T S seguidos, uno o más T sin un cierre /T , y /o hay un cierre /T sin una T .
makeTag
:: String
-> Tag
makeTag
s
=
Tag
{ name = getTagName s
, attributes = getTagAttributes s
} makeTag ensambla una etiqueta de las s dadas. Cada Tag tiene un nombre y cero o más atributos.
parseTaggedText
:: [ String ]
-> ReadP [ String ]
parseTaggedText
strings
= do
s <- look
case s of
" " -> return strings
_ -> do
r <- munch1 ( /= ' < ' ) <++ parseClosingTag <++ parseOpeningTag
parseTaggedText $ strings ++ [r] parseTaggedText Devuelve la cadena de entrada dividida en piezas. Cada pieza es el texto encerrado por etiquetas, una etiqueta de cierre o una etiqueta de apertura. Después de que se divide una pieza, la agrega a las otras piezas y se llama nuevamente. Si la cadena de entrada restante está vacía, devuelve la lista de cadenas encontradas.
> 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 " )] El operador <++ queda sesgado, lo que significa que si el lado izquierdo tiene éxito, ni siquiera se molestará con la derecha. Recuerde que cuando ejecutamos el analizador, obtenemos una lista de todos los aprobaciones posibles. Todos estos análisis posibles son el resultado de que el analizador haya viajado a través de todos los caminos posibles. Al usar <++ , recibimos los posibles analizados de la ruta izquierda y desde la ruta derecha si y solo si el lado izquierdo falló. Si desea todos los análisis posibles a través del lado izquierdo y derecho, puede usar el operador +++ proporcionado por ReadP . +++ es solo <|> que vimos arriba.
parseOpeningTag
:: ReadP String
parseOpeningTag
= do
_ <- char ' < '
t <- munch1 ( c -> c /= ' / ' && c /= ' > ' )
_ <- char ' > '
return $ " < " ++ t ++ " > "Una etiqueta de apertura es un soporte de ángulo de apertura, algún texto que no incluye una barra de avance y el siguiente soporte de ángulo de cierre inmediato.
parseClosingTag
:: ReadP String
parseClosingTag
= do
_ <- char ' < '
_ <- char ' / '
t <- munch1 ( /= ' > ' )
_ <- char ' > '
return $ " </ " ++ t ++ " > "Una etiqueta de cierre es un soporte de ángulo de apertura, un corte hacia adelante, algún texto y el siguiente soporte de ángulo de cierre inmediato.
getTagAttributes
:: String
-> [ TagAttribute ]
getTagAttributes
s
=
if isOpeningTag s
then
case readP_to_S (parseTagAttributes [] ) s of
[] -> []
(x : _) -> fst x
else
[] Las etiquetas de apertura pueden tener atributos. Por ejemplo, <font color="#101010"> . Cada atributo es un par de valores clave de dos tuples. En el ejemplo anterior, color sería la clave y #101010 sería el valor.
getTagName
:: String
-> String
getTagName
s
=
case readP_to_S parseTagName s of
[] -> " "
(x : _) -> toLower' $ fst xEsto devuelve el nombre de la etiqueta en minúsculas.
parseTagName
:: ReadP String
parseTagName
= do
_ <- char ' < '
_ <- munch ( == ' / ' )
_ <- skipSpaces
n <- munch1 ( c -> c /= ' ' && c /= ' > ' )
_ <- munch ( /= ' > ' )
_ <- char ' > '
return nEl nombre de la etiqueta es la primera cadena de caracteres no blancos después del soporte del ángulo de apertura, una posible barra de avance y algunos posibles espacios en blanco y antes de un espacio más blanco y/o el soporte del ángulo de cierre.
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 recursivamente atraviesa la cadena de entrada, recolectando los pares de valor clave. Al comienzo de la etiqueta ( < ), primero recorta el nombre de la etiqueta antes de abordar los atributos. Deja de analizar los atributos cuando alcanza el soporte del ángulo de cierre ( > ). Si una etiqueta tiene atributos duplicados (según la clave), add asegurará que solo la última permanezca en la lista.
trimTagname
:: ReadP ()
trimTagname
=
char ' < '
>> skipSpaces
>> munch1 ( c -> c /= ' ' && c /= ' > ' )
>> return ()Esto recorta o descarta el nombre de la etiqueta.
parseTagAttribute
:: ReadP TagAttribute
parseTagAttribute
= do
_ <- skipSpaces
k <- munch1 ( /= ' = ' )
_ <- string " = " "
v <- munch1 ( /= ' " ' )
_ <- char ' " '
_ <- skipSpaces
return (toLower' k, v)La clave de atributo es cualquier cadena de caracteres que no son de espacio para blancos antes del signo igual. El valor del atributo es cualquier caracteres después del signo igual y la cita doble y antes de la próxima cotización doble inmediata.
isTag
:: String
-> Bool
isTag
s
=
isOpeningTag s || isClosingTag sUna cadena es una etiqueta si es una etiqueta de apertura o una etiqueta de cierre.
isOpeningTag
:: String
-> Bool
isOpeningTag
s
=
isPresent $ readP_to_S parseOpeningTag sUna cadena es una etiqueta de apertura si el analizador de la etiqueta de apertura tiene éxito.
isClosingTag
:: String
-> Bool
isClosingTag
s
=
isPresent $ readP_to_S parseClosingTag sUna cadena es una etiqueta de cierre si el analizador de la etiqueta de cierre tiene éxito.
Ahora que hemos reunido el analizador, probemos.