무언가를 구문 분석해야합니까? "Parser Combinator"에 대해 들어 본 적이 없습니까? Haskell을 배우고 싶습니까? 엄청난! 다음은 Haskell Parser Combinator로 일어나서 구문 분석하는 데 필요한 모든 것입니다. 여기에서 난해한 데이터 직렬화 형식, 컴파일러 프론트 엔드, 도메인 특정 언어를 다룰 수 있습니다.
이 안내서에는 두 가지 데모 프로그램이 포함되어 있습니다.
version-number-parser 버전 번호의 파일을 구문 분석합니다. srt-file-parser SRT 자막 용 파일을 구문 분석합니다. test-input/ 에있는 파일로 자유롭게 시도하십시오.
Haskell 도구 스택을 다운로드 한 다음 다음을 실행하십시오.
git clone https://github.com/lettier/parsing-with-haskell-parser-combinators
cd parsing-with-haskell-parser-combinators
stack buildCabal을 사용하는 경우 다음을 실행할 수 있습니다.
git clone https://github.com/lettier/parsing-with-haskell-parser-combinators
cd parsing-with-haskell-parser-combinators
cabal sandbox init
cabal --require-sandbox build
cabal --require-sandbox install두 데모 프로그램을 구축 한 후에는 그렇게 실행할 수 있습니다.
버전 번호 파서를 시도하려면 다음을 실행하십시오.
cd parsing-with-haskell-parser-combinators
stack exec -- version-number-parser
What is the version output file path ?
test-input/gifcurry-version-output.txtSRT 파일 파서를 시도하려면 다음을 실행하십시오.
cd parsing-with-haskell-parser-combinators
stack exec -- srt-file-parser
What is the SRT file path ?
test-input/subtitles.srt버전 번호 파서를 시도하려면 다음을 실행하십시오.
cd parsing-with-haskell-parser-combinators
.cabal-sandbox/bin/version-number-parser
What is the version output file path ?
test-input/gifcurry-version-output.txtSRT 파일 파서를 시도하려면 다음을 실행하십시오.
cd parsing-with-haskell-parser-combinators
.cabal-sandbox/bin/srt-file-parser
What is the SRT file path ?
test-input/subtitles.srt구문 분석 전략에 대해 배우는 더 좋은 방법 중 하나 인 Parser Combinator는 하나의 구현을 보는 것입니다.
콤비네이터를 사용하여 건축 된 파서는 구성, 읽기 가능하며 모듈 식, 잘 구조적이며 쉽게 유지 관리하기에 간단합니다.
- 경사 콤비네이터 - 위키 백과
베이스에서 발견되는 파서 콤비네이터 라이브러리 인 Readp의 후드를 살펴 보겠습니다. 기본이므로 이미 가지고 있어야합니다.
Readp에 익숙해지면 Parsec을 시험해 볼 수 있습니다. 또한 다른 사람들이 읽기를 선호하는 파서 콤비네이터 라이브러리입니다. 추가 보너스로 GHC 버전 8.4.1 기준으로 GHC의 부트 라이브러리에 포함되어 있습니다.
-- (c) The University of Glasgow 2002
data P a
= Get ( Char -> P a )
| Look ( String -> P a )
| Fail
| Result a ( P a )
| Final [( a , String )]
deriving Functor P 데이터 유형으로 시작하겠습니다. P a 의 a 귀하 (라이브러리 사용자)에게 달려 있으며 원하는대로 일 수 있습니다. 컴파일러는 untctor 인스턴스를 자동으로 생성하고 적용, Monad, MonadFail 및 대안을위한 직접 작성된 인스턴스가 있습니다.
기능, 응용 프로그램 및 모나드에 대한 자세한 내용은 모나드, 응용 프로그램 및 기능에 대한 쉬운 안내서를 확인하십시오.
P 는 5 가지 사례가있는 합계 유형입니다.
Get 하고 새 P 반환합니다.Look 입력 문자열의 복제를 허용하고 새 P 반환합니다.Fail 결과없이 파서를 완성했음을 나타냅니다.Result 가능한 구문 분석 및 또 다른 P 사례를 보유합니다.Final 두 가지 목록입니다. 첫 번째 튜플 요소는 입력을 구문 분석 할 수 있고 두 번째 튜플 요소는 Get 에 의해 소비되지 않은 나머지 입력 문자열입니다. -- (c) The University of Glasgow 2002
run :: P a -> ReadS a
run ( Get f) (c : s) = run (f c) s
run ( Look f) s = run (f s) s
run ( Result x p) s = (x,s) : run p s
run ( Final r) _ = r
run _ _ = [] run Readp Parser의 핵심입니다. 그것은 우리가 위에서 본 모든 파서 상태를 재귀 적으로 통과함에 따라 모든 무거운 리프팅을 수행합니다. P 필요하고 ReadS 반환한다는 것을 알 수 있습니다.
-- (c) The University of Glasgow 2002
type ReadS a = String -> [( a , String )] ReadS a String -> [(a,String)] 에 대한 유형 별칭입니다. 그래서 당신이 볼 때마다 ReadS a 마다 String -> [(a,String)] .
-- (c) The University of Glasgow 2002
run :: P a -> String -> [( a , String )]
run ( Get f) (c : s) = run (f c) s
run ( Look f) s = run (f s) s
run ( Result x p) s = (x,s) : run p s
run ( Final r) _ = r
run _ _ = [] run 패턴은 P 의 다른 경우와 일치합니다.
Get 되면 새로운 P (함수 f 전달하여 반환, Get f , 입력 문자열의 다음 문자 c ) 및 나머지 입력 문자열 s 로 호출됩니다.Look 새 P (함수 f , Look f , input string s ) 및 입력 문자열을 전달하여 반환하여 반환됩니다. Get 과 마찬가지로 Look 입력 문자열의 문자를 어떻게 소비하지 않는지 주목하십시오.Result 인 경우, 구문 분석 결과와 입력 문자열의 남은 내용을 포함하는 2- 튜플을 조립하고 다른 P 케이스 및 입력 문자열로 실행되는 재귀 호출 결과로이를 전제합니다.Final 인 경우 run 구문 분석 결과와 입력 문자열 남은 음식을 포함하는 두 가지 목록을 반환합니다.run 빈 목록을 반환합니다. 예를 들어, 케이스가 Fail 하면 run 빈 목록을 반환합니다. > run ( Get ( a -> Get ( b -> Result [a,b] Fail ))) " 12345 "
[( " 12 " , " 345 " )] READP는 run 노출시키지 않지만 그렇게한다면 이렇게 부를 수 있습니다. 두 사람은 '1' 과 '2' Get 하고 "345" 를 남겨 둡니다.
> run ( Get ( a -> Get ( b -> Result [a,b] Fail ))) " 12345 "
> run ( Get ( b -> Result [ ' 1 ' ,b] Fail )) " 2345 "
> run ( Result [ ' 1 ' , ' 2 ' ] Fail ) " 345 "
> ([ ' 1 ' , ' 2 ' ], " 345 " ) : run ( Fail ) " 345 "
> ([ ' 1 ' , ' 2 ' ], " 345 " ) : []
[( " 12 " , " 345 " )]각각의 재귀 호출을 통해 우리가 최종 결과에 어떻게 도착했는지 알 수 있습니다.
> run ( Get ( a -> Get ( b -> Result [a,b] ( Final [([ ' a ' , ' b ' ], " c " )])))) " 12345 "
[( " 12 " , " 345 " ),( " ab " , " c " )] Final 사용하여 두 가지의 최종 목록에 구문 분석 결과를 포함시킬 수 있습니다.
-- (c) The University of Glasgow 2002
readP_to_S :: ReadP a -> ReadS a
-- readP_to_S :: ReadP a -> String -> [(a,String)]
readP_to_S ( R f) = run (f return ) READP는 직접 run 되지 않지만 readP_to_S 를 통해 노출됩니다. readP_to_S ReadP 라는 newtype 소개합니다. readP_to_S ReadP a , 문자열을 수락하고 두 가지 목록을 반환합니다.
-- (c) The University of Glasgow 2002
newtype ReadP a = R ( forall b . ( a -> P b ) -> P b ) 다음은 ReadP a 의 정의입니다. Functor, Application, Monad, MonadFail , Alternative 및 MonadPlus 의 사례가 있습니다. R 생성자는 다른 함수를 취하고 A P 반환하는 함수를 취합니다. 허용 된 기능은 a 위해 선택한 모든 것을 가져 가서 P .
-- (c) The University of Glasgow 2002
readP_to_S ( R f) = run (f return ) P 는 모나드이며 return 의 유형은 a -> ma 라는 것을 상기하십시오. 따라서 f 는 (a -> P b) -> Pb 함수이고 return (a -> P b) 함수입니다. 궁극적으로 run 예상되는 P b 얻습니다.
-- (c) The University of Glasgow 2002
readP_to_S ( R f) inputString = run (f return ) inputString
-- ^ ^^^^^^^^^^ ^^^^^^^^^^^ 소스 코드에서 중단되었지만 readP_to_S 및 run 입력 문자열을 기대합니다.
-- (c) The University of Glasgow 2002
instance Functor ReadP where
fmap h ( R f) = R ( k -> f (k . h)) ReadP 의 Functor 인스턴스 정의는 다음과 같습니다.
> readP_to_S ( fmap toLower get) " ABC "
[( ' a ' , " BC " )]
> readP_to_S (toLower <$> get) " ABC "
[( ' a ' , " BC " )] 이것은 우리가 이런 일을 할 수있게 해줍니다. fmap FUNCTOR MAPS는 기능 get R Get 과 동일 toLower . Get 유형은 (Char -> P a) -> P a ReadP 생성자 ( R )가 받아 들인다는 것을 상기하십시오.
-- (c) The University of Glasgow 2002
fmap h ( R f ) = R ( k -> f (k . h ))
fmap toLower ( R Get ) = R ( k -> Get (k . toLower)) 여기에서 fmap toLower get 예제에 대한 FUNCTOR 정의가 다시 작성되었습니다.
위를 살펴보면, Get 만 사용했을 때 readP_to_S [('a',"BC")] 는 어떻게 run 끝내지 않았습니까? 답은 P 에 대한 적용 정의에 있습니다.
-- (c) The University of Glasgow 2002
instance Applicative P where
pure x = Result x Fail
(<*>) = ap return pure 같으므로 readP_to_S (R f) = run (f return) 을 readP_to_S (R f) = run (f pure) 으로 재 작성할 수 있습니다. return 또는 오히려 pure 사용으로, readP_to_S 세트 Result x Fail 는 최종 케이스 run 발생하므로 실패합니다. 도달하면 run 종료되고 파싱 목록을 얻을 수 있습니다.
> readP_to_S ( fmap toLower get) " ABC "
-- Use the functor instance to transform fmap toLower get.
> readP_to_S ( R ( k -> Get (k . toLower))) " ABC "
-- Call run which removes R.
> run (( k -> Get (k . toLower)) pure ) " ABC "
-- Call function with pure to get rid of k.
> run ( Get ( pure . toLower)) " ABC "
-- Call run for Get case to get rid of Get.
> run (( pure . toLower) ' A ' ) " BC "
-- Call toLower with 'A' to get rid of toLower.
> run ( pure ' a ' ) " BC "
-- Use the applicative instance to transform pure 'a'.
> run ( Result ' a ' Fail ) " BC "
-- Call run for the Result case to get rid of Result.
> ( ' a ' , " BC " ) : run ( Fail ) " BC "
-- Call run for the Fail case to get rid of Fail.
> ( ' a ' , " BC " ) : []
-- Prepend.
[( ' a ' , " BC " )] 여기에서 readP_to_S 에서 구문 분석 결과로의 흐름이 나타납니다.
-- (c) The University of Glasgow 2002
instance Alternative P where
-- ...
-- most common case: two gets are combined
Get f1 <|> Get f2 = Get ( c -> f1 c <|> f2 c)
-- results are delivered as soon as possible
Result x p <|> q = Result x (p <|> q)
p <|> Result x q = Result x (p <|> q)
-- ... P 의 Alternative 인스턴스를 사용하면 구문 분석기의 흐름을 왼쪽 및 오른쪽 경로로 분할 할 수 있습니다. 이것은 입력이 두 가지 중 하나 중 하나 또는 (거의) 두 가지 중 하나가 될 수있을 때 유용합니다.
> readP_to_S ((get >>= a -> return a) <|> (get >> get >>= b -> return b)) " ABC "
[( ' A ' , " BC " ),( ' B ' , " C " )] <|> 연산자 또는 기능은 파서의 흐름에 포크를 소개합니다. 파서는 왼쪽과 오른쪽 경로를 모두 여행합니다. 최종 결과에는 왼쪽으로 이동 한 가능한 모든 구문 분석과 제대로 된 가능한 구문 분석이 포함됩니다. 두 경로가 모두 실패하면 전체 구문 분석기가 실패합니다.
다른 Parser Combinator 구현에서 <|> 연산자를 사용할 때 파서는 왼쪽 또는 오른쪽으로 이동하지만 둘 다가 아닙니다. 왼쪽이 성공하면 오른쪽은 무시됩니다. 오른쪽은 왼쪽이 실패한 경우에만 처리됩니다.
> readP_to_S ((get >>= a -> return [a]) <|> look <|> (get >> get >>= a -> return [a])) " ABC "
[( " ABC " , " ABC " ),( " A " , " BC " ),( " B " , " C " )] 그러나 많은 옵션이나 대안에 대해 <|> 연산자를 체인 할 수 있습니다. 파서는 각각과 관련된 가능한 구문 분석을 반환합니다.
-- (c) The University of Glasgow 2002
instance Monad ReadP where
fail _ = R ( _ -> Fail )
R m >>= f = R ( k -> m ( a -> let R m' = f a in m' k)) ReadP Monad 인스턴스는 다음과 같습니다. fail 에 대한 정의에 주목하십시오.
> readP_to_S (( a b c -> [a,b,c]) <$> get <*> get <*> get) " ABC "
[( " ABC " , " " )]
> readP_to_S (( a b c -> [a,b,c]) <$> get <*> fail " " <*> get) " ABC "
[]
> readP_to_S (get >>= a -> get >>= b -> get >>= c -> return [a,b,c]) " ABC "
[( " ABC " , " " )]
> readP_to_S (get >>= a -> get >>= b -> fail " " >>= c -> return [a,b,c]) " ABC "
[] fail 로 전화하여 전체 파서 경로가 중단 될 수 있습니다. READP는 Result 또는 Final 사례를 생성하는 직접적인 방법을 제공하지 않으므로 반환 값은 빈 목록이됩니다. 실패한 경로가 유일한 경로 인 경우 전체 결과는 빈 목록이됩니다. run 매치가 Fail 하면 빈 목록을 반환합니다.
-- (c) The University of Glasgow 2002
instance Alternative P where
-- ...
-- fail disappears
Fail <|> p = p
p <|> Fail = p
-- ... 대체 P 인스턴스로 돌아가서 양쪽의 고장 (둘 다가 아님)이 전체 구문 프로그램에 실패하지 않는 방법을 알 수 있습니다.
> readP_to_S (get >>= a -> get >>= b -> pfail >>= c -> return [a,b,c]) " ABC "
[] READP는 fail 사용하는 대신 pfail 제공하여 Fail 케이스를 직접 생성 할 수 있습니다.
GIF 제조업체의 Haskell이 제작 한 비디오 편집기 인 Gifcurry는 다양한 프로그램으로 껍질을 벗기고 있습니다. 호환성을 보장하려면 각 프로그램에 대한 버전 번호가 필요합니다. 그 프로그램 중 하나는 Imagemagick입니다.
Version: ImageMagick 6.9.10-14 Q16 x86_64 2018-10-24 https://imagemagick.org
Copyright: © 1999-2018 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenCL OpenMP 여기에는 convert --version 의 출력이 나타납니다. 6, 9, 10 및 14를 캡처하기 위해 어떻게 구문 분석 할 수 있습니까?
출력을 살펴보면 버전 번호가 기간 또는 대시로 분리 된 숫자 모음이라는 것을 알고 있습니다. 이 정의는 날짜를 다루므로 처음 두 숫자가 기간으로 분리되도록합니다. 그렇게하면 버전 번호 앞에 날짜를 넣으면 잘못된 결과를 얻지 못합니다.
1. Consume zero or more characters that are not 0 through 9 and go to 2.
2. Consume zero or more characters that are 0 through 9, save this number, and go to 3.
3. Look at the rest of the input and go to 4.
4. If the input
- is empty, go to 6.
- starts with a period, go to 1.
- starts with a dash
- and you have exactly one number, go to 5.
- and you have more than one number, go to 1.
- doesn't start with a period or dash
- and you have exactly one number, go to 5.
- you have more than one number, go to 6.
5. Delete any saved numbers and go to 1.
6. Return the numbers found.코드에 뛰어 들기 전에 다음은 따라갈 알고리즘이 있습니다.
parseVersionNumber
:: [ String ]
-> ReadP [ String ]
parseVersionNumber
nums
= do
_ <- parseNotNumber
num <- parseNumber
let nums' = nums ++ [num]
parseSeparator nums' parseVersionNumber parseVersionNumber 는 버전 번호의 입력 문자열을 구문 분석하는 메인 파서 콤비네이터입니다. 문자열 목록을 수락하고 ReadP 데이터 유형의 맥락에서 문자열 목록을 반환합니다. 허용 된 문자열 목록은 구문 분석되는 입력이 아니라 지금까지 발견 된 숫자 목록입니다. 첫 번째 기능 호출의 경우 아직 아무것도 구문 분석하지 않았기 때문에 목록이 비어 있습니다.
parseVersionNumber
nums 상단부터 시작하여 parseVersionNumber 지금까지 발견 된 숫자의 현재 목록 인 문자열 목록을 가져옵니다.
_ <- parseNotNumber parseNotNumber 입력 문자열에서 숫자가 아닌 모든 것을 소비합니다. 우리는 결과에 관심이 없기 때문에 그것을 버립니다 ( _ <- ).
num <- parseNumber
let nums' = nums ++ [num]다음으로 숫자 인 모든 것을 소비 한 다음 지금까지 발견 된 숫자 목록에 추가합니다.
parseSeparator nums' parseVersionNumber parseVersionNumber 다음 숫자를 처리 한 후, 발견 된 숫자 목록을 전달하고 그 자체를 parseSeparator 로 전달합니다.
parseSeparator
:: [ String ]
-> ([ String ] -> ReadP [ String ])
-> ReadP [ String ]
parseSeparator
nums
f
= do
next <- look
case next of
" " -> return nums
(c : _) ->
case c of
' . ' -> f nums
' - ' -> if length nums == 1 then f [] else f nums
_ -> if length nums == 1 then f [] else return nums 여기서 parseSeparator 가 보입니다.
next <- look
case next of
" " -> return nums
(c : _) -> look 사용하면 입력 문자열이 소비하지 않고 남은 것을 얻을 수 있습니다. 남은 것이 없으면 찾은 숫자를 반환합니다. 그러나 남은 것이 있으면 첫 번째 캐릭터를 분석합니다.
case c of
' . ' -> f nums
' - ' -> if length nums == 1 then f [] else f nums
_ -> if length nums == 1 then f [] else return nums 다음 캐릭터가 기간이면 현재 찾은 숫자 목록으로 parseVersionNumber 다시 전화하십시오. 대시이고 정확히 숫자가있는 경우 날짜이므로 빈 번호 목록으로 parseVersionNumber 에 전화하십시오. 대시이고 정확히 숫자가 없다면 지금까지 발견 된 숫자 목록으로 parseVersionNumber 에 전화하십시오. 그렇지 않으면 정확히 하나의 숫자가 있거나 정확히 하나의 숫자가없는 경우 찾은 숫자를 반환하는 경우 빈 목록으로 parseVersionNumber 전화하십시오.
parseNotNumber
:: ReadP String
parseNotNumber
=
munch ( not . isNumber) parseNotNumber ReadP 제공하는 munch 사용합니다. munch 0에서 9가 아닌 모든 캐릭터에 대해 true를 반환하는 술어 (not . isNumber) 가 주어집니다.
munch :: ( Char -> Bool ) -> ReadP String 입력 문자열의 다음 문자가 술어를 만족시키는 경우 munch 계속 호출 get . 그렇지 않으면 munch 그랬던 캐릭터를 반환합니다. get 만 사용하기 때문에 Munch는 항상 성공합니다.
parseNumber 는 parseNotNumber 와 유사합니다. not . isNumber , 술어는 단지 isNumber 입니다.
parseNotNumber'
:: ReadP String
parseNotNumber'
=
many (satisfy ( not . isNumber)) munch 사용하는 대신, 당신은 many 같이 parseNotNumber 쓸 수 있습니다 satisfy many 의 유형 서명을 살펴보면 단일 파서 콤비네이터 ( ReadP a )를 허용합니다. 이 경우, 파서 콤비네이터 satisfy 주어지고 있습니다.
> readP_to_S (satisfy ( not . isNumber)) " a "
[( ' a ' , " " )]
> readP_to_S (satisfy ( not . isNumber)) " 1 "
[] satisfy 술어를 취하고 다음 캐릭터를 소비 get 데 사용됩니다. 허용 된 술어가 진실을 반환 satisfy 캐릭터를 반환합니다. 그렇지 않으면 통화가 pfail satisfy 실패합니다.
> readP_to_S (munch ( not . isNumber)) " abc123 "
[( " abc " , " 123 " )]
> readP_to_S (many (satisfy ( not . isNumber))) " abc123 "
[( " " , " abc123 " ),( " a " , " bc123 " ),( " ab " , " c123 " ),( " abc " , " 123 " )] many 사용하면 원치 않는 결과를 줄 수 있습니다. 궁극적으로 many 하나 이상의 Result 사례를 소개합니다. 이 때문에 many 항상 성공합니다.
> readP_to_S (many look) " abc123 "
-- Runs forever. many 당신의 파서가 실패하거나 입력이 부족할 때까지 당신의 파서를 실행합니다. 파서가 실패하지 않거나 입력이 부족하지 않으면 many 돌아 오지 않을 것입니다.
> readP_to_S (many (get >>= a -> return ( read (a : " " ) :: Int ))) " 12345 "
[( [] , " 12345 " ),([ 1 ], " 2345 " ),([ 1 , 2 ], " 345 " ),([ 1 , 2 , 3 ], " 45 " ),([ 1 , 2 , 3 , 4 ], " 5 " ),([ 1 , 2 , 3 , 4 , 5 ], " " )]결과의 모든 인덱스에 대해, 구문 분석 결과는 전체 입력에서 파서 인덱스 시간을 실행 한 결과입니다.
> let parser = get >>= a -> return ( read (a : " " ) :: Int )
> let many' results = return results <|> (parser >>= result -> many' (results ++ [result]))
> readP_to_S (many' [] ) " 12345 "
[( [] , " 12345 " ),([ 1 ], " 2345 " ),([ 1 , 2 ], " 345 " ),([ 1 , 2 , 3 ], " 45 " ),([ 1 , 2 , 3 , 4 ], " 5 " ),([ 1 , 2 , 3 , 4 , 5 ], " " )] many 에게 대체 정의가 있습니다. <|> 의 왼쪽에서 현재 파서 결과를 반환합니다. <|> 의 오른쪽에서 구문 분석기를 실행하고 그 결과를 현재 파서 결과에 추가하고 업데이트 된 결과를 호출합니다. 이것은 인덱스 i 가 파서 결과에 i - 1 , i - 2 , ... 및 1 에서 파서 결과에 추가 된 누적 합계 유형 효과를 갖습니다.
이제 우리는 파서를 만들었으므로 실행합시다.
> let inputString =
> " Some Program (C) 1234-56-78 All rights reserved. n
> Version: 12.345.6-7 n
> License: Some open source license. "
> readP_to_S (parseVersionNumber [] ) inputString
[([ " 12 " , " 345 " , " 6 " , " 7 " ], " n License: Some open source license. " )]이전 날짜가 나오더라도 버전 번호를 올바르게 추출 할 수 있습니다.
이제 더 복잡한 파일을 구문 분석하겠습니다.
Gifcurry Six가 릴리스 되려면 SRT (Subrip Text) 파일을 구문 분석해야했습니다. SRT 파일에는 비디오 처리 프로그램이 비디오 위에 텍스트를 표시하는 데 사용하는 자막이 포함되어 있습니다. 일반적 으로이 텍스트는 다양한 언어로 번역 된 영화의 대화입니다. 텍스트를 비디오와 별도로 유지하면 시간, 저장 공간 및 대역폭을 절약하는 비디오가 하나만 있으면됩니다. 비디오 소프트웨어는 비디오를 교체하지 않고도 텍스트를 교체 할 수 있습니다. 텍스트가 비디오를 구성하는 이미지 데이터의 일부가되는 자막을 불 태우거나 하드 코딩하는 것과 대조하십시오. 이 경우 각 자막 모음에 대한 비디오가 필요합니다.
내부 비디오 © Blender Foundation | www.sintel.org
Gifcurry는 SRT 파일을 가져 와서 선택한 비디오 슬라이스의 자막을 태울 수 있습니다.
7
00:02:09,400 --> 00:02:13,800
What brings you to
the land of the gatekeepers?
8
00:02:15,000 --> 00:02:17,500
I'm searching for someone.
9
00:02:18,000 --> 00:02:22,200
Someone very dear?
A kindred spirit?여기 Sintel의 영어 자막 (© Blender Foundation | www.sintel.org)이 표시됩니다.
SRT는 아마도 모든 자막 형식 중 가장 기본적인 것일 수 있습니다.
—Srt 자막 | Matrosk
SRT 파일 형식은 빈 줄로 분리 된 각 자막마다 하나씩 블록으로 구성됩니다.
2블록 상단에는 인덱스가 있습니다. 이것은 자막의 순서를 결정합니다. 바라건대 자막이 이미 순서대로 진행되고 있으며 모두 고유 한 색인이 있지만 그렇지 않을 수도 있습니다.
01:04:13,000 --> 02:01:01,640 X1:167 X2:267 Y1:33 Y2:63인덱스가 시작 시간, 종료 시간 및 자막 텍스트가 들어가야하는 사각형을 지정하는 선택적 포인트 세트가 발생한 후.
01:04:13,000 타임 스탬프 형식은 hours:minutes:seconds,milliseconds 입니다.
밀리 초에서 초를 분리하는 기간 대신 쉼표를 참고하십시오.
This is the actual subtitle
text. It can span multiple lines.
It may include formating
like <b>bold</b>, <i>italic</i>,
<u>underline</u>,
and <font color="#010101">font color</font>.블록의 세 번째 및 마지막 부분은 자막 텍스트입니다. 빈 줄이있을 때 여러 줄에 걸쳐 있고 끝이 끝날 수 있습니다. 텍스트에는 HTML을 연상시키는 태그 서식이 포함될 수 있습니다.
parseSrt
:: ReadP [ SrtSubtitle ]
parseSrt
=
manyTill parseBlock (skipSpaces >> eof) parseSrt 모든 것을 처리하는 메인 파서 콤바이터입니다. 파일 ( eof ) 또는 입력의 끝에 도달 할 때까지 각 블록을 구문 분석합니다. 안전한면에 있으려면 마지막 블록과 파일 끝 사이에 우선 흰색이있을 수 있습니다. 이를 처리하기 위해 파일의 끝을 구문 분석하기 전에 ( skipSpaces >> eof ) whitespace ( skipSpaces )의 0 개 이상의 문자를 구문 분석합니다. eof 에 도달 할 때까지 여전히 입력이 남아 있으면 eof 실패하고 아무것도 반환하지 않습니다. 따라서 parseBlock 아무것도 남지 않는 것이 중요합니다.
parseBlock
:: ReadP SrtSubtitle
parseBlock
= do
i <- parseIndex
(s, e) <- parseTimestamps
c <- parseCoordinates
t <- parseTextLines
return
SrtSubtitle
{ index = i
, start = s
, end = e
, coordinates = c
, taggedText = t
} 앞서 나아갈 때 블록은 인덱스, 타임 스탬프, 일부 좌표 및 일부 텍스트 줄로 구성됩니다. 이 버전의 parseBlock 에서는 레코드 구문으로 더 명령적인 표기법 스타일이 표시됩니다.
parseBlock'
:: ReadP SrtSubtitle
parseBlock'
=
SrtSubtitle
<$> parseIndex
<*> parseStartTimestamp
<*> parseEndTimestamp
<*> parseCoordinates
<*> parseTextLines parseBlock 쓸 수있는 또 다른 방법이 있습니다. 이것은 적용 스타일입니다. 주문을 제대로 얻으십시오. 예를 들어, 실수로 시작 및 종료 타임 스탬프를 혼합 할 수있었습니다.
parseIndex
:: ReadP Int
parseIndex
=
skipSpaces
>> readInt <$> parseNumber 블록 상단에는 인덱스가 있습니다. 여기에서 skipSpaces 다시 나타납니다. 공백을 건너 뛰면 숫자에 대한 입력을 구문 분석하고 실제 정수로 변환합니다.
readInt
:: String
-> Int
readInt
=
read readInt 다음과 같습니다.
> read " 123 " :: Int
123
> read " 1abc " :: Int
*** Exception : Prelude. read : no parse 일반적으로 read 직접 사용하는 것은 위험 할 수 있습니다. read 입력을 지정된 유형으로 변환하지 못할 수 있습니다. 그러나 parseNumber 10 개의 숫자 숫자 문자 ( ['0'..'9'] ) 만 반환하여 read 사용하면 직접 안전 해집니다.
타임 스탬프를 구문 분석하는 것은 색인을 구문 분석하는 것보다 조금 더 관여합니다.
parseTimestamps
:: ReadP ( Timestamp , Timestamp )
parseTimestamps
= do
_ <- char ' n '
s <- parseTimestamp
_ <- skipSpaces
_ <- string " --> "
_ <- skipSpaces
e <- parseTimestamp
return (s, e)이것은 타임 스탬프를 구문 분석하기위한 메인 조합입니다.
char 당신이주는 캐릭터를 파싱하거나 실패합니다. 실패하면 parseTimestamps 실패하여 궁극적으로 parseSrt 실패하여 인덱스 후에 Newline 문자가 있어야합니다.
string 단지 하나의 캐릭터 대신에 char 같습니다. 그것은 당신이주는 문자열을 구문 분석하거나 실패합니다.
parseStartTimestamp
:: ReadP Timestamp
parseStartTimestamp
=
char ' n '
>> parseTimestamp parseTimestamps 두 타임 스탬프를 모두 구문 분석하지만 적용 스타일 ( parseSrt' )의 경우 시작 타임 스탬프를 위해서만 파서가 필요합니다.
parseEndTimestamp
:: ReadP Timestamp
parseEndTimestamp
=
skipSpaces
>> string " --> "
>> skipSpaces
>> parseTimestamp이것은 타임 스탬프 사이의 모든 것을 파싱하고 종료 타임 스탬프를 반환합니다.
parseTimestamp
:: ReadP Timestamp
parseTimestamp
= do
h <- parseNumber
_ <- char ' : '
m <- parseNumber
_ <- char ' : '
s <- parseNumber
_ <- char ' , ' <|> char ' . '
m' <- parseNumber
return
Timestamp
{ hours = readInt h
, minutes = readInt m
, seconds = readInt s
, milliseconds = readInt m'
}이것은 타임 스탬프를 구성하는 네 가지 숫자를 구문 분석합니다. 처음 세 숫자는 결장에 의해 분리되고 마지막 숫자는 쉼표로 분리됩니다. 그러나 더 용서하기 위해, 우리는 쉼표 대신 기간이있을 가능성을 허용합니다.
> readP_to_S (char ' . ' <|> char ' , ' ) " ... "
[( ' . ' , " .. " )]
> readP_to_S (char ' . ' <|> char ' , ' ) " ,.. "
[( ' , ' , " .. " )] char 단일 문자를 소비하고 두 문자가 같은 공간을 차지할 수 없기 때문에 <|> 와 함께 char 사용할 때, <|>와 함께 한쪽 만 성공할 수 있습니다 (2 char 입력, 하나의 char 휴가).
좌표는 블록의 선택적 부분이지만 포함 된 경우 타임 스탬프와 동일한 줄에 있습니다.
parseCoordinates
:: ReadP ( Maybe SrtSubtitleCoordinates )
parseCoordinates
=
option Nothing $ do
_ <- skipSpaces1
x1 <- parseCoordinate ' x ' 1
_ <- skipSpaces1
x2 <- parseCoordinate ' x ' 2
_ <- skipSpaces1
y1 <- parseCoordinate ' y ' 1
_ <- skipSpaces1
y2 <- parseCoordinate ' y ' 2
return
$ Just
SrtSubtitleCoordinates
{ x1 = readInt x1
, x2 = readInt x2
, y1 = readInt y1
, y2 = readInt y2
} option 두 가지 인수를 취합니다. 두 번째 인수 인 파서가 실패하면 첫 번째 인수가 반환됩니다. 따라서 좌표가 파서가 실패하면 parseCoordinates Nothing 반환하지 않을 것입니다. 다시 말해서, 좌표 파서 파서가 실패하면 전체 파서가 실패하지 않습니다. 이 블록은 coordinates "필드"에 대한 Nothing .
parseCoordinate
:: Char
-> Int
-> ReadP String
parseCoordinate
c
n
= do
_ <- char ( Data.Char. toUpper c) <|> char ( Data.Char. toLower c)
_ <- string $ show n ++ " : "
parseNumber 이 파서는 좌표 라벨을 대문자 또는 소문자로 만들 수 있습니다. 예를 들어, x1:1 X2:2 Y1:3 y2:4 성공할 것입니다.
텍스트를 구문 분석하는 것은 HTML 유사 태그 형식으로 인해 가장 관련된 부분입니다.
태그 구문 분석은 어려울 수 있습니다. 정규 표현으로 구문 분석하는 사람에게 물어보십시오. 이를보다 쉽게하기 위해서는 사용자에게 태그 수프 종류의 접근 방식을 사용합니다. 파서는 구부러진 및/또는 잘못된 중첩 태그를 허용합니다. 또한 b , u , i 및 font 만이 아니라 모든 태그를 허용합니다.
parseTextLines
:: ReadP [ TaggedText ]
parseTextLines
=
char ' n '
>> (getTaggedText <$> manyTill parseAny parseEndOfTextLines) 우리는 Newline 캐릭터와 일치하여 시작합니다. 그 후, 우리는 텍스트 줄의 끝에 도달 할 때까지 자막 텍스트 문자를 통해 맵 또는 fmap ( <$> )을 getTaggedText 함유합니다.
parseEndOfTextLines
:: ReadP ()
parseEndOfTextLines
=
void (string " nn " ) <|> eof 우리는 두 개의 Newline 문자 또는 파일의 끝에 도달 할 때 문자 수집 ( parseAny )을 중단합니다. 이것은 블록의 끝을 나타냅니다.
getTaggedText
:: String
-> [ TaggedText ]
getTaggedText
s
=
fst
$ foldl
folder
( [] , [] )
parsed
where getTaggedText 왼쪽에서 오른쪽으로 구문 분석 된 텍스트를 통해 접히고 누적 된 태그가 지정된 텍스트를 반환합니다.
parsed
:: [ String ]
parsed
=
case readP_to_S (parseTaggedText [] ) s of
[] -> [s]
r @ (_ : _) -> ( fst . last ) r parsed 하나 이상의 문자열 목록을 반환합니다. 태그의 입력 텍스트를 구문 분석하려고합니다. 실패하면 parsed 입력 문자열을 목록 내부에 반환합니다. 그렇지 않으면, parseTaggedText 성공하면 parse 가능한 마지막 구문 분석 ( (fst . last) r )을 반환합니다.
folder
:: ([ TaggedText ], [ Tag ])
-> String
-> ([ TaggedText ], [ Tag ])
folder
(tt, t)
x
| isTag x = (tt, updateTags t x)
| otherwise = (tt ++ [ TaggedText { text = x, tags = t}], t) folder 왼쪽에서 오른쪽으로 이동하면 구문 분석 된 문자열을 통해 현재 문자열이 태그인지 확인합니다. 태그 인 경우 현재 활성 태그 ( t ) 세트를 업데이트합니다. 그렇지 않으면 활성 태그 세트와 관련된 다른 태그가 지정된 텍스트에 추가됩니다.
updateTags
:: [ Tag ]
-> String
-> [ Tag ]
updateTags
tags
x
| isClosingTag x = remove compare' tags (makeTag x)
| isOpeningTag x = add compare' tags (makeTag x)
| otherwise = tags
where
compare'
:: Tag
-> Tag
-> Bool
compare'
a
b
=
name a /= name b updateTags 닫는 태그인지 또는 오프닝 태그인지에 따라 주어진 태그 ( x )를 제거하거나 추가하여 주어진 tags 업데이트합니다. 그렇지 않으면 전달 된 태그 세트를 반환합니다. tags 이미 동일한 이름의 태그가있는 경우 add 기존 태그를 덮어 씁니다. 주어진 compare' 함수에서 이것을 볼 수 있습니다.
파서를 간단하게 유지하려면 오프닝 태그 T 가 발견되면 T 태그 목록에 추가되거나 이미 존재하는 경우 종료 T 덮어 씁니다. 해당 폐쇄 /T 발견되면, 존재하는 경우 태그 목록에서 T 제거합니다. 연속에 2 개 이상의 T 가 있는지, 닫는 /T 없는 하나 이상의 T s가 있는지, /또는 오프닝 T 없는 닫는 /T 있는지는 중요하지 않습니다.
makeTag
:: String
-> Tag
makeTag
s
=
Tag
{ name = getTagName s
, attributes = getTagAttributes s
} makeTag 주어진 s 에서 태그를 조립합니다. 각 Tag 에는 이름과 0 이상의 속성이 있습니다.
parseTaggedText
:: [ String ]
-> ReadP [ String ]
parseTaggedText
strings
= do
s <- look
case s of
" " -> return strings
_ -> do
r <- munch1 ( /= ' < ' ) <++ parseClosingTag <++ parseOpeningTag
parseTaggedText $ strings ++ [r] parseTaggedText 입력 문자열을 조각으로 반환합니다. 각 조각은 태그로 둘러싸인 텍스트, 닫는 태그 또는 오프닝 태그입니다. 조각이 나온 후에는 다른 조각에 추가하고 다시 전화합니다. 나머지 입력 문자열이 비어 있으면 찾은 문자열 목록을 반환합니다.
> readP_to_S (string " ab " <++ string " abc " ) " abcd "
[( " ab " , " cd " )]
> readP_to_S (string " ab " +++ string " abc " ) " abcd "
[( " ab " , " cd " ),( " abc " , " d " )]
> readP_to_S (string " ab " <|> string " abc " ) " abcd "
[( " ab " , " cd " ),( " abc " , " d " )] <++ 연산자는 왼쪽 바이어스가 있습니다. 왼쪽이 성공하면 오른쪽으로 귀찮게하지 않을 것입니다. 우리가 파서를 운영 할 때, 우리는 가능한 모든 파싱 목록을 얻습니다. 이러한 가능한 모든 파싱은 파서가 가능한 모든 경로를 여행 한 결과입니다. <++ 사용하면 왼쪽이 실패한 경우에만 왼쪽 경로와 오른쪽 경로에서 가능한 구문 분석을받습니다. 왼쪽과 오른쪽을 통해 가능한 모든 구문 분석을 원하면 ReadP 에서 제공하는 +++ 연산자를 사용할 수 있습니다. +++ 우리가 위에서 본 단지 <|> 입니다.
parseOpeningTag
:: ReadP String
parseOpeningTag
= do
_ <- char ' < '
t <- munch1 ( c -> c /= ' / ' && c /= ' > ' )
_ <- char ' > '
return $ " < " ++ t ++ " > "오프닝 태그는 오프닝 각도 브래킷이며, 일부 텍스트는 전방 슬래시와 다음 즉시 닫는 각도 브래킷이 포함되어 있습니다.
parseClosingTag
:: ReadP String
parseClosingTag
= do
_ <- char ' < '
_ <- char ' / '
t <- munch1 ( /= ' > ' )
_ <- char ' > '
return $ " </ " ++ t ++ " > "닫는 태그는 오프닝 각도 브래킷, 포워드 슬래시, 일부 텍스트 및 다음 즉시 닫는 각도 브래킷입니다.
getTagAttributes
:: String
-> [ TagAttribute ]
getTagAttributes
s
=
if isOpeningTag s
then
case readP_to_S (parseTagAttributes [] ) s of
[] -> []
(x : _) -> fst x
else
[] 열기 태그에는 속성이있을 수 있습니다. 예를 들어, <font color="#101010"> . 각 속성은 2 튜플 키 값 쌍입니다. 위의 예에서는 color 키이고 #101010 값이 될 것입니다.
getTagName
:: String
-> String
getTagName
s
=
case readP_to_S parseTagName s of
[] -> " "
(x : _) -> toLower' $ fst x이것은 태그 이름을 소문자로 반환합니다.
parseTagName
:: ReadP String
parseTagName
= do
_ <- char ' < '
_ <- munch ( == ' / ' )
_ <- skipSpaces
n <- munch1 ( c -> c /= ' ' && c /= ' > ' )
_ <- munch ( /= ' > ' )
_ <- char ' > '
return n태그 이름은 오프닝 각도 브래킷, 가능한 전방 슬래시 및 가능한 일부 흰색 스페이스 및 일부 더 많은 공백 및/또는 닫는 각도 브래킷 이전의 첫 번째 비 whitespace 문자열 문자열입니다.
parseTagAttributes
:: [ TagAttribute ]
-> ReadP [ TagAttribute ]
parseTagAttributes
tagAttributes
= do
s <- look
case s of
" " -> return tagAttributes
_ -> do
let h = head s
case h of
' > ' -> return tagAttributes
' < ' -> trimTagname >> parseTagAttributes'
_ -> parseTagAttributes'
where
parseTagAttributes'
:: ReadP [ TagAttribute ]
parseTagAttributes'
= do
tagAttribute <- parseTagAttribute
parseTagAttributes
( add
( a b -> fst a /= fst b)
tagAttributes
tagAttribute
) parseTagAttributes 재귀 적으로 입력 문자열을 통과하여 키 값 쌍을 수집합니다. 태그 ( < )의 시작 부분에서 먼저 속성을 다루기 전에 태그 이름을 다듬습니다. 닫는 각도 브래킷 ( > )에 도달 할 때 속성에 대한 구문 분석을 중지합니다. 태그에 중복 속성이있는 경우 (키를 기준으로) add 최신 목록 만 목록에 남아 있는지 확인합니다.
trimTagname
:: ReadP ()
trimTagname
=
char ' < '
>> skipSpaces
>> munch1 ( c -> c /= ' ' && c /= ' > ' )
>> return ()이것은 태그 이름을 다듬거나 버립니다.
parseTagAttribute
:: ReadP TagAttribute
parseTagAttribute
= do
_ <- skipSpaces
k <- munch1 ( /= ' = ' )
_ <- string " = " "
v <- munch1 ( /= ' " ' )
_ <- char ' " '
_ <- skipSpaces
return (toLower' k, v)속성 키는 동일 부호 앞에있는 비 whitescace 문자의 문자열입니다. 속성 값은 동일 부호 및 이중 견적 후 및 다음 즉각적인 이중 견적 전에 모든 문자입니다.
isTag
:: String
-> Bool
isTag
s
=
isOpeningTag s || isClosingTag s문자열은 오프닝 태그 또는 닫는 태그 인 경우 태그입니다.
isOpeningTag
:: String
-> Bool
isOpeningTag
s
=
isPresent $ readP_to_S parseOpeningTag s오프닝 태그 파서가 성공하면 문자열은 오프닝 태그입니다.
isClosingTag
:: String
-> Bool
isClosingTag
s
=
isPresent $ readP_to_S parseClosingTag s닫는 태그 파서가 성공하면 문자열은 닫는 태그입니다.
이제 우리는 파서를 조립 했으므로 시도해 봅시다.