Precisa analisar alguma coisa? Nunca ouviu falar de um "Combinador de Parsers"? Procurando aprender um pouco de Haskell? Incrível! Abaixo está tudo o que você precisará se levantar e analisar com os combinadores de analisador Haskell. A partir daqui, você pode tentar combater os formatos de serialização de dados esotéricos, extremidades frontais do compilador, idiomas específicos de domínio - você nomeia!
Incluídos neste guia estão dois programas de demonstração.
version-number-parser analisa um arquivo para um número de versão. srt-file-parser analisa um arquivo para legendas SRT. Sinta-se à vontade para experimentá-los com os arquivos encontrados na test-input/ .
Faça o download da pilha de ferramentas Haskell e execute o seguinte.
git clone https://github.com/lettier/parsing-with-haskell-parser-combinators
cd parsing-with-haskell-parser-combinators
stack buildSe estiver usando CABAL, você pode executar o seguinte.
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 installDepois de construir os dois programas de demonstração, você pode executá -los assim.
Para experimentar o analisador número da versão, execute o seguinte.
cd parsing-with-haskell-parser-combinators
stack exec -- version-number-parser
What is the version output file path ?
test-input/gifcurry-version-output.txtPara experimentar o analisador de arquivo SRT, execute o seguinte.
cd parsing-with-haskell-parser-combinators
stack exec -- srt-file-parser
What is the SRT file path ?
test-input/subtitles.srtPara experimentar o analisador número da versão, execute o seguinte.
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 experimentar o analisador de arquivo SRT, execute o seguinte.
cd parsing-with-haskell-parser-combinators
.cabal-sandbox/bin/srt-file-parser
What is the SRT file path ?
test-input/subtitles.srtUma das melhores maneiras de aprender sobre a estratégia de análise, o Parser Combinator, é analisar uma implementação de uma.
Os analisadores construídos usando combinadores são diretos para construir, legíveis, modulares, bem estruturados e facilmente sustentáveis.
—Parser Combinator - Wikipedia
Vamos dar uma olhada sob o capô do Readp, uma biblioteca de combinadores de analisador encontrada na base. Como está na base, você já deve ter.
Observe que você pode experimentar o Parsec depois de se familiarizar com o Readp. Também é uma biblioteca de combinador de pastores que outros preferem ler. Como um bônus adicional, ele está incluído nas bibliotecas de inicialização do GHC na versão 8.4.1 do 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 Começaremos com o tipo de dados P O a em P a depende de você (o usuário da biblioteca) e pode ser o que você quiser. O compilador cria uma instância de functor automaticamente e há instâncias escritas à mão para aplicativos, Monad, MonadFail e alternativa.
Observe, para saber mais sobre funções, aplicativos e mônadas, consulte seu guia fácil para Mônadas, Aplicativos e Functors.
P é um tipo de soma com cinco casos.
Get consome um único caractere da sequência de entrada e retorna um novo P .Look aceita uma duplicata da sequência de entrada e retorna um novo P .Fail indica que o analisador terminou sem resultado.Result mantém uma possível análise e outro caso PFinal é uma lista de dois tuplos. O primeiro elemento de tupla é uma possível análise da entrada e o segundo elemento de tupla é o restante da sequência de entrada que não foi consumida pelo 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 é o coração do analisador Readp. Ele faz todo o levantamento pesado, pois corre recursivamente por todos os declarações do analisador que vimos acima. Você pode ver que é preciso um P e retorna uma ReadS .
-- (c) The University of Glasgow 2002
type ReadS a = String -> [( a , String )] ReadS a é um alias de tipo para String -> [(a,String)] . Então, sempre que você vê ReadS a , pense em 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 _ _ = [] O padrão run corresponde aos diferentes casos de P .
Get , ele se chama com um novo P (retornado passando a função f , no Get f , o próximo caractere c na sequência de entrada) e o restante da sequência de entrada s .Look , ele se chama com um novo P (retornado passando a função f , na Look f , a string de entrada s ) e a sequência de entrada. Observe como Look não consome nenhum caractere da string de entrada como Get faz.Result , ele monta uma dupla tupla-contendo o resultado analisado e o que resta da sequência de entrada-e o antecende ao resultado de uma chamada recursiva que é executada com outro caso P e a sequência de entrada.Final , run retorna uma lista de dois tuplos contendo resultados analisados e as sobras da string de entrada.run retorna uma lista vazia. Por exemplo, se o caso Fail , run retornará uma lista vazia. > run ( Get ( a -> Get ( b -> Result [a,b] Fail ))) " 12345 "
[( " 12 " , " 345 " )] O READP não expõe run , mas se o fizesse, você pode chamá -lo assim. Os dois Get s consumir o '1' e '2' , deixando o "345" para trás.
> 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 " )]Executando cada chamada recursiva, você pode ver como chegamos ao resultado final.
> run ( Get ( a -> Get ( b -> Result [a,b] ( Final [([ ' a ' , ' b ' ], " c " )])))) " 12345 "
[( " 12 " , " 345 " ),( " ab " , " c " )] Usando Final , você pode incluir um resultado analisado na lista final de dois tuplos.
-- (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 ) Enquanto o Readp não expõe run diretamente, ele o expõe via readP_to_S . readP_to_S apresenta um newtype chamado ReadP . readP_to_S aceita um ReadP a , uma string e retorna uma lista de dois tuplos.
-- (c) The University of Glasgow 2002
newtype ReadP a = R ( forall b . ( a -> P b ) -> P b ) Aqui está a definição de ReadP a . Existem instâncias para functor, aplicativo, Monad, MonadFail , Alternative e MonadPlus . O construtor R pega uma função que pega outra função e retorna a P . A função aceita leva o que você escolheu para a e retorna um P .
-- (c) The University of Glasgow 2002
readP_to_S ( R f) = run (f return ) Lembre -se de que P é uma mônada e o tipo de return é a -> ma . Portanto, f é a função (a -> P b) -> Pb e return é a função (a -> P b) . Por fim, run recebe o P b que espera.
-- (c) The University of Glasgow 2002
readP_to_S ( R f) inputString = run (f return ) inputString
-- ^ ^^^^^^^^^^ ^^^^^^^^^^^ Ele é deixado no código -fonte, mas lembre -se de que readP_to_S e run espera uma sequência de entrada.
-- (c) The University of Glasgow 2002
instance Functor ReadP where
fmap h ( R f) = R ( k -> f (k . h)) Aqui está a definição de instância do functor para ReadP .
> readP_to_S ( fmap toLower get) " ABC "
[( ' a ' , " BC " )]
> readP_to_S (toLower <$> get) " ABC "
[( ' a ' , " BC " )] Isso nos permite fazer algo assim. fmap Functor Maps toLower sobre o Functor get , que é igual R Get . Lembre -se de que o tipo de Get é (Char -> P a) -> P a que o construtor ReadP ( R ) aceita.
-- (c) The University of Glasgow 2002
fmap h ( R f ) = R ( k -> f (k . h ))
fmap toLower ( R Get ) = R ( k -> Get (k . toLower)) Aqui você vê a definição de functor reescrita para o exemplo fmap toLower get .
Olhando acima, como readP_to_S retornou [('a',"BC")] quando usamos apenas Get , que não encerrará run ? A resposta está na definição aplicativa para P .
-- (c) The University of Glasgow 2002
instance Applicative P where
pure x = Result x Fail
(<*>) = ap return é igual pure para que possamos reescrever readP_to_S (R f) = run (f return) para ser readP_to_S (R f) = run (f pure) . Ao usar return , ou melhor, pure , readP_to_S define Result x Fail , pois a run do caso final será encontrada. Se alcançado, run será encerrada e obteremos nossa lista de analistas.
> 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 " )] Aqui você vê o fluxo de readP_to_S para o resultado analisado.
-- (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)
-- ... A instância Alternative para P nos permite dividir o fluxo do analisador em um caminho esquerdo e direito. Isso é útil quando a entrada não pode ser nenhuma, uma ou (mais raramente) duas das duas maneiras.
> readP_to_S ((get >>= a -> return a) <|> (get >> get >>= b -> return b)) " ABC "
[( ' A ' , " BC " ),( ' B ' , " C " )] O operador ou função <|> apresenta um garfo no fluxo do analisador. O analisador viajará pelos caminhos esquerdo e direito. O resultado final conterá todas as possíveis águas que foram para a esquerda e todas as possíveis águas que foram corretas. Se os dois caminhos falharem, o analisador inteiro falhará.
Observe, em outras implementações do Combinator Parser, ao usar o operador <|> , o analisador vai para a esquerda ou para a direita, mas não ambos. Se a esquerda for bem -sucedida, a direita será ignorada. A direita só será processada se o lado esquerdo falhar.
> readP_to_S ((get >>= a -> return [a]) <|> look <|> (get >> get >>= a -> return [a])) " ABC "
[( " ABC " , " ABC " ),( " A " , " BC " ),( " B " , " C " )] Você pode encadear o operador <|> por muitas opções ou alternativas que existem. O analisador retornará uma possível análise envolvendo cada um.
-- (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)) Aqui está a instância ReadP MONAD. Observe a definição 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 "
[] Você pode causar um caminho de analisador inteiro para abortar chamando fail . Como o Readp não fornece uma maneira direta de gerar um Result ou Final , o valor de retorno será uma lista vazia. Se o caminho com falha for o único caminho, todo o resultado será uma lista vazia. Lembre -se de que, quando as correspondências run Fail , ele retorna uma lista vazia.
-- (c) The University of Glasgow 2002
instance Alternative P where
-- ...
-- fail disappears
Fail <|> p = p
p <|> Fail = p
-- ... Voltando à instância alternativa P , você pode ver como uma falha de ambos os lados (mas não os dois) não falhará o analisador inteiro.
> readP_to_S (get >>= a -> get >>= b -> pfail >>= c -> return [a,b,c]) " ABC "
[] Em vez de usar fail , o Readp fornece pfail , o que permite gerar um caso Fail diretamente.
Gifcurry, o editor de vídeo construído por Haskell para fabricantes de GIF, abre para vários programas diferentes. Para garantir a compatibilidade, ele precisa do número da versão para cada um dos programas para o qual ela passa. Um desses programas é o 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 Aqui você vê a saída de convert --version . Como você pode analisar isso para capturar os 6, 9, 10 e 14?
Olhando para a saída, sabemos que o número da versão é uma coleção de números separados por um período ou um traço. Essa definição também abrange as datas, portanto, garantiremos que os dois primeiros números sejam separados por um período. Dessa forma, se eles colocarem uma data antes do número da versão, não obteremos o resultado errado.
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 mergulharmos no código, aqui está o algoritmo que seguiremos.
parseVersionNumber
:: [ String ]
-> ReadP [ String ]
parseVersionNumber
nums
= do
_ <- parseNotNumber
num <- parseNumber
let nums' = nums ++ [num]
parseSeparator nums' parseVersionNumber parseVersionNumber é o principal combinador de analisador que analisa uma sequência de entrada para um número de versão. Ele aceita uma lista de strings e retorna uma lista de strings no contexto do tipo de dados ReadP . A lista aceita de strings não é a entrada que é analisada, mas a lista de números encontrados até agora. Para a primeira chamada de função, a lista está vazia, pois ainda não analisou nada.
parseVersionNumber
nums A partir do topo, parseVersionNumber leva uma lista de strings, que são a lista atual de números encontrados até agora.
_ <- parseNotNumber parseNotNumber consome tudo o que não é um número da sequência de entrada. Como não estamos interessados no resultado, descartamos-o ( _ <- ).
num <- parseNumber
let nums' = nums ++ [num]Em seguida, consumimos tudo o que é um número e, em seguida, adicionamos isso à lista de números encontrados até agora.
parseSeparator nums' parseVersionNumber Depois que parseVersionNumber processou o próximo número, ele passa a lista de números encontrados e para 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 Aqui você vê parseSeparator .
next <- look
case next of
" " -> return nums
(c : _) -> look nos permite obter o que resta da sequência de entrada sem consumi -la. Se não resta mais nada, ele retorna os números encontrados. No entanto, se restar algo, ele analisa o primeiro personagem.
case c of
' . ' -> f nums
' - ' -> if length nums == 1 then f [] else f nums
_ -> if length nums == 1 then f [] else return nums Se o próximo caractere for um período, ligue para parseVersionNumber novamente com a lista atual de números encontrados. Se for um traço e temos exatamente um número, ligue para parseVersionNumber com uma lista vazia de números, pois é uma data. Se for um traço e não temos exatamente um número, ligue para parseVersionNumber com a lista de números encontrados até agora. Caso contrário, ligue para parseVersionNumber com uma lista vazia se tivermos exatamente um número ou retornar os números encontrados se não tivermos exatamente um número.
parseNotNumber
:: ReadP String
parseNotNumber
=
munch ( not . isNumber) parseNotNumber usa munch que ReadP fornece. munch recebe o predicado (not . isNumber)
munch :: ( Char -> Bool ) -> ReadP String munch chamadas continuamente get se o próximo caractere na sequência de entrada atende ao predicado. Caso contrário, munch retorna os personagens que o fizeram, se houver. Como ele só usa get , o Munch sempre é bem -sucedido.
Observe que parseNumber é semelhante ao parseNotNumber . Em vez de not . isNumber , o predicado é apenas isNumber .
parseNotNumber'
:: ReadP String
parseNotNumber'
=
many (satisfy ( not . isNumber)) Em vez de usar munch , você pode escrever parseNotNumber assim, usando many e satisfy - ambos os quais o Readp fornece. Olhando para a assinatura do tipo para many , ele aceita um único combinador de analisador ( ReadP a ). Nesse caso, está recebendo o satisfy do Combinador de Parsers.
> readP_to_S (satisfy ( not . isNumber)) " a "
[( ' a ' , " " )]
> readP_to_S (satisfy ( not . isNumber)) " 1 "
[] satisfy exige um predicado e os usos get o próximo caractere. Se o predicado aceito retornar verdadeiro, satisfy retornar o personagem. Caso contrário, satisfy as chamadas pfail e falha.
> 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 pode fornecer resultados indesejados. Por fim, many introduzem um ou mais casos Result . Por causa disso, many sempre conseguem.
> readP_to_S (many look) " abc123 "
-- Runs forever. many executarão seu analisador até que ele falhe ou fique sem entrada. Se o seu analisador nunca falhar ou nunca ficar sem entrada, many nunca retornarão.
> 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 no resultado, o resultado analisado será o resultado de ter executado os tempos de índice de analisador em toda a 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 ], " " )] Aqui está uma definição alternativa para many . No lado esquerdo de <|> , ele retorna os resultados atuais do analisador. No lado direito de <|> , ele executa o analisador, acrescenta que resultam aos resultados atuais do analisador e se chama com os resultados atualizados. Isso tem um efeito de tipo de soma cumulativo em que o índice i é o resultado do analisador anexado ao resultado do analisador em i - 1 , i - 2 , ... e 1 .
Agora que construímos o analisador, vamos executá -lo.
> 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. " )]Você pode ver que ele extraiu o número da versão corretamente, mesmo com a data que vem antes.
Agora vamos analisar algo mais complicado - arquivos SRT.
Para o lançamento do Gifcurry Six, eu precisava analisar os arquivos SRT (texto de sub -rip). Os arquivos SRT contêm legendas que os programas de processamento de vídeo usam para exibir o texto em cima de um vídeo. Normalmente, este texto é a caixa de diálogo de um filme traduzido em vários idiomas diferentes. Ao manter o texto separado do vídeo, só precisa haver um vídeo que economize tempo, espaço de armazenamento e largura de banda. O software de vídeo pode trocar o texto sem precisar trocar o vídeo. Contraste isso com a queima ou codifica as legendas em que o texto se torna parte dos dados da imagem que compõem o vídeo. Nesse caso, você precisaria de um vídeo para cada coleção de legendas.
Vídeo interno © Blender Foundation | www.sintel.org
O Gifcurry pode pegar um arquivo SRT e queimar as legendas para a fatia de vídeo, sua seleção.
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?Aqui você vê as legendas em inglês para Sintel (© Blender Foundation | www.sintel.org).
O SRT é talvez o mais básico de todos os formatos de legenda.
—Srt Legenda | Matrosk
O formato do arquivo SRT consiste em blocos, um para cada legenda, separados por uma linha vazia.
2Na parte superior do bloco está o índice. Isso determina a ordem das legendas. Espero que as legendas já estejam em ordem e todas elas tenham índices exclusivos, mas esse pode não ser o caso.
01:04:13,000 --> 02:01:01,640 X1:167 X2:267 Y1:33 Y2:63Depois que o índice é o horário de início, o horário de término e um conjunto opcional de pontos que especifica o retângulo, o texto da legenda deve entrar.
01:04:13,000 O formato de registro de data e hora é hours:minutes:seconds,milliseconds .
Observe a vírgula em vez do período que separa os segundos dos milissegundos.
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>.A terceira e última parte de um bloco é o texto da legenda. Ele pode abranger várias linhas e termina quando houver uma linha vazia. O texto pode incluir tags de formatação remanescente do HTML.
parseSrt
:: ReadP [ SrtSubtitle ]
parseSrt
=
manyTill parseBlock (skipSpaces >> eof) parseSrt é o principal combinador de analisador que lida com tudo. Ele analisa cada bloco até atingir o final do arquivo ( eof ) ou entrada. Para estar do lado seguro, pode haver espaço em branco entre o último bloco e o final do arquivo. Para lidar com isso, ele analisa zero ou mais caracteres de espaço em branco ( skipSpaces ) antes de analisar o final do arquivo ( skipSpaces >> eof ). Se ainda houver entrada no momento em que eof for alcançado, eof falhará e isso não retornará nada. Portanto, é importante que parseBlock não deixe nada além de espaço em branco para trás.
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
} Como fomos mais cedo, um bloco consiste em um índice, registro de data e hora, possivelmente algumas coordenadas e algumas linhas de texto. Nesta versão do parseBlock , você vê o estilo de anotação mais imperativo com a sintaxe do registro.
parseBlock'
:: ReadP SrtSubtitle
parseBlock'
=
SrtSubtitle
<$> parseIndex
<*> parseStartTimestamp
<*> parseEndTimestamp
<*> parseCoordinates
<*> parseTextLines Aqui está outra maneira de escrever parseBlock . Este é o estilo aplicativo. Apenas certifique -se de acertar o pedido. Por exemplo, eu poderia ter misturado acidentalmente os registros de data e hora de partida e final.
parseIndex
:: ReadP Int
parseIndex
=
skipSpaces
>> readInt <$> parseNumber Na parte superior do bloco está o índice. Aqui você vê skipSpaces novamente. Depois de pular o espaço em branco, ele analisa a entrada de números e o converte em um número inteiro real.
readInt
:: String
-> Int
readInt
=
read readInt se parece com isso.
> read " 123 " :: Int
123
> read " 1abc " :: Int
*** Exception : Prelude. read : no parse Normalmente, usar read diretamente pode ser perigoso. read pode não ser capaz de converter a entrada para o tipo especificado. No entanto, parseNumber retornará apenas os 10 caracteres numéricos de dígitos ( ['0'..'9'] ), portanto, o uso read diretamente se torna seguro.
Analisar os registros de data e hora estão um pouco mais envolvidos do que analisar o índice.
parseTimestamps
:: ReadP ( Timestamp , Timestamp )
parseTimestamps
= do
_ <- char ' n '
s <- parseTimestamp
_ <- skipSpaces
_ <- string " --> "
_ <- skipSpaces
e <- parseTimestamp
return (s, e)Este é o principal combinador para analisar os registros de data e hora.
char analisa o personagem que você dá ou falha. Se falhar, parseTimestamps falhará, fazendo com que parseSrt falhe, para que haja um caractere de nova linha após o índice.
string é como char , exceto em vez de apenas um caractere, ele analisa a sequência de caracteres que você dá ou falha.
parseStartTimestamp
:: ReadP Timestamp
parseStartTimestamp
=
char ' n '
>> parseTimestamp parseTimestamps analisa os dois timestamps, mas para o estilo aplicativo ( parseSrt' ), precisamos de um analisador apenas para o registro de data e hora inicial.
parseEndTimestamp
:: ReadP Timestamp
parseEndTimestamp
=
skipSpaces
>> string " --> "
>> skipSpaces
>> parseTimestampIsso analisa tudo entre os registros de data e hora e retorna o timestamp 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'
}Isso analisa os quatro números que compõem o registro de data e hora. Os três primeiros números são separados por um cólon e o último é separado por uma vírgula. Para ser mais perdoador, no entanto, permitimos a possibilidade de haver um período em vez de uma vírgula.
> readP_to_S (char ' . ' <|> char ' , ' ) " ... "
[( ' . ' , " .. " )]
> readP_to_S (char ' . ' <|> char ' , ' ) " ,.. "
[( ' , ' , " .. " )] Observe que, ao usar char com <|> , apenas um lado pode ter sucesso (dois char enter, um char de licença), já que char consome um único caractere e dois caracteres não podem ocupar o mesmo espaço.
As coordenadas são uma parte opcional do bloco, mas, se incluídas, estarão na mesma linha que os registros de data e hora.
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 leva dois argumentos. O primeiro argumento é retornado se o segundo argumento, um analisador, falhar. Portanto, se as coordenadas o analisador falharem, parseCoordinates não retornarão Nothing . Dito de outra forma, a falha do analisador das coordenadas não faz com que o analisador inteiro falhe. Este bloco não terá Nothing para suas 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 analisador permite que os rótulos de coordenadas estejam em maiúsculas ou minúsculas. Por exemplo, x1:1 X2:2 Y1:3 y2:4 seria bem -sucedido.
Analisar o texto é a parte mais envolvida devido à formatação de tags do tipo HTML.
A análise de tags pode ser desafiadora - pergunte a quem os analisa com uma expressão regular. Para facilitar isso para nós - e para o usuário - usaremos um tipo de abordagem de sopa de tags. O analisador permitirá tags não tocadas e/ou aninhadas incorretamente. Também permitirá qualquer tag e não apenas b , u , i e font .
parseTextLines
:: ReadP [ TaggedText ]
parseTextLines
=
char ' n '
>> (getTaggedText <$> manyTill parseAny parseEndOfTextLines) Começamos combinando com um personagem de nova linha. Depois disso, funcione o mapa ou o FMAP ( <$> ) getTaggedText sobre os caracteres de texto da legenda até chegarmos ao final das linhas de texto.
parseEndOfTextLines
:: ReadP ()
parseEndOfTextLines
=
void (string " nn " ) <|> eof Paramos de parar de coletar caracteres ( parseAny ) quando chegamos a dois caracteres da Newline ou no final do arquivo. Isso sinaliza o final do bloco.
getTaggedText
:: String
-> [ TaggedText ]
getTaggedText
s
=
fst
$ foldl
folder
( [] , [] )
parsed
where getTaggedText dobra o texto analisado da esquerda para a direita, retornando o texto marcado acumulado.
parsed
:: [ String ]
parsed
=
case readP_to_S (parseTaggedText [] ) s of
[] -> [s]
r @ (_ : _) -> ( fst . last ) r parsed retorna uma lista de uma ou mais cordas. Ele tenta analisar o texto de entrada para tags. Se isso falhar, parsed retorna a sequência de entrada dentro de uma lista. Caso contrário, se parseTaggedText for bem -sucedido, parse retornará a última análise possível ( (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) À medida que folder se move da esquerda para a direita, sobre as cordas analisadas, verifica se a string atual é uma tag. Se for uma tag, ele atualiza o conjunto atual de tags ativas ( t ). Caso contrário, ele anexa outra peça de texto marcada associada ao conjunto de tags ativas.
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 atualiza as tags fornecidas removendo ou adicionando a tag fornecida ( x ), dependendo se for uma tag de fechamento ou abertura. Se não for, ele apenas retorna o conjunto de tags aprovado. add substituirá uma tag existente se tags já tiverem uma tag com o mesmo nome. Você pode ver isso na função compare' dada.
Para manter o analisador simples, se uma etiqueta de abertura T for encontrada, T é adicionado à lista de tags ou substitua um T sai se já estiver presente. Se um fechamento /T correspondente for encontrado, T é removido da lista de tags, se presente. Não importa se há dois ou mais T s em uma linha, um ou mais T s sem um fechamento /T e /ou há um fechamento /T sem uma abertura T .
makeTag
:: String
-> Tag
makeTag
s
=
Tag
{ name = getTagName s
, attributes = getTagAttributes s
} makeTag monta uma tag da (s) string ( s ) dada (s). Cada Tag tem um nome e zero ou mais atributos.
parseTaggedText
:: [ String ]
-> ReadP [ String ]
parseTaggedText
strings
= do
s <- look
case s of
" " -> return strings
_ -> do
r <- munch1 ( /= ' < ' ) <++ parseClosingTag <++ parseOpeningTag
parseTaggedText $ strings ++ [r] parseTaggedText Retorna a sequência de entrada quebrada em pedaços. Cada peça é o texto fechado por tags, uma tag de fechamento ou uma tag de abertura. Depois de dividir uma peça, ela a adiciona às outras peças e se chama novamente. Se a sequência de entrada restante estiver vazia, ele retornará a lista de strings 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 " )] O operador <++ é deixado tendencioso, o que significa que, se o lado esquerdo for bem -sucedido, ele nem se incomoda com a direita. Lembre -se de que, quando executamos o analisador, temos uma lista de todas as águas possíveis. Todas essas áreas possíveis são o resultado de o analisador ter percorrido todos os caminhos possíveis. Usando <++ , recebemos as possíveis analistas do caminho esquerdo e do caminho direito se e somente se o lado esquerdo falhar. Se você quiser todas as águas possíveis através do lado esquerdo e direito, pode usar o operador +++ fornecido pelo ReadP . +++ é apenas <|> que vimos acima.
parseOpeningTag
:: ReadP String
parseOpeningTag
= do
_ <- char ' < '
t <- munch1 ( c -> c /= ' / ' && c /= ' > ' )
_ <- char ' > '
return $ " < " ++ t ++ " > "Uma etiqueta de abertura é um suporte de ângulo de abertura, algum texto que não inclui uma barra para a frente e o próximo suporte de ângulo de fechamento imediato.
parseClosingTag
:: ReadP String
parseClosingTag
= do
_ <- char ' < '
_ <- char ' / '
t <- munch1 ( /= ' > ' )
_ <- char ' > '
return $ " </ " ++ t ++ " > "Uma etiqueta de fechamento é um suporte de ângulo de abertura, uma barra para a frente, algum texto e o próximo suporte de ângulo de fechamento imediato.
getTagAttributes
:: String
-> [ TagAttribute ]
getTagAttributes
s
=
if isOpeningTag s
then
case readP_to_S (parseTagAttributes [] ) s of
[] -> []
(x : _) -> fst x
else
[] As tags de abertura podem ter atributos. Por exemplo, <font color="#101010"> . Cada atributo é um par de valores-chave de duas tuples. No exemplo acima, color seria a chave e #101010 seria o valor.
getTagName
:: String
-> String
getTagName
s
=
case readP_to_S parseTagName s of
[] -> " "
(x : _) -> toLower' $ fst xIsso retorna o nome da tag em minúsculas.
parseTagName
:: ReadP String
parseTagName
= do
_ <- char ' < '
_ <- munch ( == ' / ' )
_ <- skipSpaces
n <- munch1 ( c -> c /= ' ' && c /= ' > ' )
_ <- munch ( /= ' > ' )
_ <- char ' > '
return nO nome da tag é a primeira sequência de caracteres que não são do WhitaSpace após o suporte do ângulo de abertura, uma possível barra para a frente e algum espaço em branco possível e antes de mais um espaço em branco e/ou o suporte de ângulo de fechamento.
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 passa recursivamente pela sequência de entrada, coletando os pares de valor-chave. No início da tag ( < ), primeiro apara o nome da tag antes de enfrentar os atributos. Ele para de analisar os atributos quando atinge o suporte do ângulo de fechamento ( > ). Se uma tag tiver atributos duplicados (com base na chave), add garantirá que apenas o mais recente permaneça na lista.
trimTagname
:: ReadP ()
trimTagname
=
char ' < '
>> skipSpaces
>> munch1 ( c -> c /= ' ' && c /= ' > ' )
>> return ()Isso apara ou descarta o nome da tag.
parseTagAttribute
:: ReadP TagAttribute
parseTagAttribute
= do
_ <- skipSpaces
k <- munch1 ( /= ' = ' )
_ <- string " = " "
v <- munch1 ( /= ' " ' )
_ <- char ' " '
_ <- skipSpaces
return (toLower' k, v)A chave de atributo é qualquer string de caracteres não-whitespace antes do sinal igual. O valor do atributo é qualquer caractere após o sinal igual e a cotação dupla e antes da próxima cotação dupla imediata.
isTag
:: String
-> Bool
isTag
s
=
isOpeningTag s || isClosingTag sUma string é uma tag se for uma tag de abertura ou uma tag de fechamento.
isOpeningTag
:: String
-> Bool
isOpeningTag
s
=
isPresent $ readP_to_S parseOpeningTag sUma string é uma tag de abertura se o analisador de abertura for bem -sucedido.
isClosingTag
:: String
-> Bool
isClosingTag
s
=
isPresent $ readP_to_S parseClosingTag sUma string é uma etiqueta de fechamento se o analisador de tag de fechamento for bem -sucedido.
Agora que montamos o analisador, vamos experimentar.