Perlu mengurai sesuatu? Belum pernah mendengar tentang "Parser Combinator"? Ingin belajar haskell? Luar biasa! Di bawah ini adalah semua yang Anda perlukan untuk bangun dan diuraikan dengan kombinator Haskell Parser. Dari sini Anda dapat mencoba mengatasi format serialisasi data esoterik, ujung depan kompiler, bahasa spesifik domain - Anda sebut saja!
Termasuk dengan panduan ini adalah dua program demo.
version-number-parser mem-parsing file untuk nomor versi. srt-file-parser mem-parsing file untuk subtitle SRT. Jangan ragu untuk mencobanya dengan file yang ditemukan di test-input/ .
Unduh tumpukan alat Haskell dan kemudian jalankan berikut ini.
git clone https://github.com/lettier/parsing-with-haskell-parser-combinators
cd parsing-with-haskell-parser-combinators
stack buildJika menggunakan Cabal, Anda dapat menjalankan yang berikut.
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 installSetelah membangun dua program demo, Anda dapat menjalankannya seperti itu.
Untuk mencoba parser nomor versi, jalankan yang berikut.
cd parsing-with-haskell-parser-combinators
stack exec -- version-number-parser
What is the version output file path ?
test-input/gifcurry-version-output.txtUntuk mencoba parser file srt, jalankan yang berikut.
cd parsing-with-haskell-parser-combinators
stack exec -- srt-file-parser
What is the SRT file path ?
test-input/subtitles.srtUntuk mencoba parser nomor versi, jalankan yang berikut.
cd parsing-with-haskell-parser-combinators
.cabal-sandbox/bin/version-number-parser
What is the version output file path ?
test-input/gifcurry-version-output.txtUntuk mencoba parser file srt, jalankan yang berikut.
cd parsing-with-haskell-parser-combinators
.cabal-sandbox/bin/srt-file-parser
What is the SRT file path ?
test-input/subtitles.srtSalah satu cara yang lebih baik untuk belajar tentang strategi parsing, parser combinator, adalah dengan melihat implementasi satu.
Parser yang dibangun menggunakan kombinator mudah untuk membangun, dapat dibaca, modular, terstruktur dengan baik, dan mudah dipelihara.
—Parser Combinator - Wikipedia
Mari kita lihat di bawah tenda Readp, perpustakaan kombinator parser yang ditemukan di pangkalan. Karena ada di pangkalan, Anda seharusnya sudah memilikinya.
Catatan, Anda mungkin ingin mencoba parsec setelah terbiasa dengan Readp. Itu juga adalah perpustakaan parser kombinator yang lebih suka dibaca oleh orang lain. Sebagai bonus tambahan, itu termasuk dalam pustaka boot GHC pada GHC versi 8.4.1.
-- (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 Kami akan mulai dengan tipe data P a di P a terserah Anda (pengguna perpustakaan) dan dapat menjadi apa pun yang Anda inginkan. Kompiler membuat instance functor secara otomatis dan ada instance yang ditulis tangan untuk aplikasi, monad, MonadFail , dan alternatif.
Catatan, untuk lebih lanjut tentang fungsi, aplikasi, dan monad, checkout panduan mudah Anda untuk monad, aplikasi, & functors.
P adalah jenis jumlah dengan lima kasus.
Get mengkonsumsi satu karakter dari string input dan mengembalikan P baru.Look menerima duplikat dari string input dan mengembalikan P baru.Fail menunjukkan parser selesai tanpa hasil.Result memiliki kemungkinan parsing dan kasus P lain.Final adalah daftar dua tupel. Elemen tuple pertama adalah kemungkinan penguraian input dan elemen tuple kedua adalah sisa string input yang tidak dikonsumsi oleh 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 adalah jantung dari parser readp. Itu melakukan semua pengangkatan berat karena secara rekursif berjalan melalui semua negara parser yang kami lihat di atas. Anda dapat melihat bahwa dibutuhkan P dan mengembalikan ReadS .
-- (c) The University of Glasgow 2002
type ReadS a = String -> [( a , String )] ReadS a adalah tipe alias untuk String -> [(a,String)] . Jadi, setiap kali Anda melihat ReadS a , pikirkan 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 _ _ = [] Pola Pola run cocok dengan berbagai kasus P
Get , ia menyebut dirinya dengan P baru (dikembalikan dengan melewati fungsi f , di Get f , karakter c berikutnya dalam string input) dan sisa string input s .Look , ia menyebut dirinya dengan P baru (dikembalikan dengan melewati fungsi f , dalam Look f , string input s ) dan string input. Perhatikan bagaimana Look tidak mengkonsumsi karakter apa pun dari string input seperti Get DO.Result , ia merakit dua-tuple-yang mengandung hasil yang diuraikan dan apa yang tersisa dari string input-dan mempersiapkan ini untuk hasil panggilan rekursif yang berjalan dengan case P lain dan string input.Final , run mengembalikan daftar dua tupel yang berisi hasil yang diuraikan dan leftover string input.run kembali daftar kosong. Misalnya, jika kasusnya Fail , run akan mengembalikan daftar kosong. > run ( Get ( a -> Get ( b -> Result [a,b] Fail ))) " 12345 "
[( " 12 " , " 345 " )] READP tidak mengekspos run tetapi jika itu terjadi, Anda bisa menyebutnya seperti ini. Keduanya Get S mengkonsumsi '1' dan '2' , meninggalkan "345" di belakang.
> 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 " )]Menjalankan setiap panggilan rekursif, Anda dapat melihat bagaimana kami sampai pada hasil akhir.
> run ( Get ( a -> Get ( b -> Result [a,b] ( Final [([ ' a ' , ' b ' ], " c " )])))) " 12345 "
[( " 12 " , " 345 " ),( " ab " , " c " )] Menggunakan Final , Anda dapat menyertakan hasil parsed dalam daftar akhir dua-tupel.
-- (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 ) Sementara readp tidak mengekspos run secara langsung, itu memang mengeksposnya melalui readP_to_S . readP_to_S memperkenalkan newtype yang disebut ReadP . readP_to_S menerima ReadP a , string, dan mengembalikan daftar dua tupel.
-- (c) The University of Glasgow 2002
newtype ReadP a = R ( forall b . ( a -> P b ) -> P b ) Inilah definisi ReadP a . Ada beberapa contoh untuk functor, aplicative, monad, MonadFail , alternatif, dan MonadPlus . Konstruktor R mengambil fungsi yang mengambil fungsi lain dan mengembalikan P Fungsi yang diterima mengambil apa pun yang Anda pilih untuk a dan mengembalikan P
-- (c) The University of Glasgow 2002
readP_to_S ( R f) = run (f return ) Ingatlah bahwa P adalah tipe Monad dan return adalah a -> ma . Jadi f adalah fungsi (a -> P b) -> Pb dan return adalah fungsi (a -> P b) . Pada akhirnya, run mendapatkan P b yang diharapkan.
-- (c) The University of Glasgow 2002
readP_to_S ( R f) inputString = run (f return ) inputString
-- ^ ^^^^^^^^^^ ^^^^^^^^^^^ Itu ditinggalkan dalam kode sumber tetapi ingat bahwa readP_to_S dan run mengharapkan string input.
-- (c) The University of Glasgow 2002
instance Functor ReadP where
fmap h ( R f) = R ( k -> f (k . h)) Inilah definisi instance functor untuk ReadP .
> readP_to_S ( fmap toLower get) " ABC "
[( ' a ' , " BC " )]
> readP_to_S (toLower <$> get) " ABC "
[( ' a ' , " BC " )] Ini memungkinkan kita untuk melakukan sesuatu seperti ini. fmap functor memetakan toLower di atas functor get yang R Get . Ingatlah bahwa jenis Get adalah (Char -> P a) -> P a yang diterima konstruktor 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)) Di sini Anda melihat definisi Functor ditulis ulang untuk fmap toLower get contoh.
Melihat ke atas, bagaimana readP_to_S mengembalikan [('a',"BC")] Ketika kita hanya menggunakan Get yang tidak diakhiri run ? Jawabannya terletak pada definisi aplikasi untuk P .
-- (c) The University of Glasgow 2002
instance Applicative P where
pure x = Result x Fail
(<*>) = ap return sama pure sehingga kita dapat menulis ulang readP_to_S (R f) = run (f return) menjadi readP_to_S (R f) = run (f pure) . Dengan menggunakan return atau lebih tepatnya pure , readP_to_S menetapkan Result x Fail karena run terakhir akan bertemu. Jika tercapai, run akan berakhir dan kami akan mendapatkan daftar parsing kami.
> 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 " )] Di sini Anda melihat aliran dari readP_to_S ke hasil parsed.
-- (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)
-- ... Contoh Alternative untuk P memungkinkan kita untuk membagi aliran parser menjadi jalur kiri dan kanan. Ini berguna ketika input tidak dapat berjalan, satu, atau (lebih jarang) dua dari dua cara.
> readP_to_S ((get >>= a -> return a) <|> (get >> get >>= b -> return b)) " ABC "
[( ' A ' , " BC " ),( ' B ' , " C " )] Operator atau fungsi <|> memperkenalkan garpu dalam aliran parser. Parser akan melakukan perjalanan melalui jalur kiri dan kanan. Hasil akhirnya akan berisi semua parsing yang mungkin terjadi di sebelah kiri dan semua kemungkinan parsing yang menuju ke kanan. Jika kedua jalur gagal, maka seluruh parser gagal.
Catatan, dalam implementasi Parser Combinator lainnya, saat menggunakan operator <|> , parser akan ke kiri atau kanan tetapi tidak keduanya. Jika kiri berhasil, kanan diabaikan. Kanan hanya diproses jika sisi kiri gagal.
> readP_to_S ((get >>= a -> return [a]) <|> look <|> (get >> get >>= a -> return [a])) " ABC "
[( " ABC " , " ABC " ),( " A " , " BC " ),( " B " , " C " )] Anda dapat rantai operator <|> untuk namun banyak opsi atau alternatif yang ada. Parser akan mengembalikan kemungkinan parsing yang melibatkan masing -masing.
-- (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)) Inilah instance ReadP Monad. Perhatikan definisi untuk 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 "
[] Anda dapat menyebabkan seluruh jalur parser dibatalkan dengan menelepon fail . Karena READP tidak memberikan cara langsung untuk menghasilkan Result atau kasus Final , nilai pengembalian akan menjadi daftar kosong. Jika jalur yang gagal adalah satu -satunya jalur, maka seluruh hasilnya akan menjadi daftar kosong. Ingatlah bahwa ketika pertandingan run Fail , ia mengembalikan daftar kosong.
-- (c) The University of Glasgow 2002
instance Alternative P where
-- ...
-- fail disappears
Fail <|> p = p
p <|> Fail = p
-- ... Kembali ke instance P alternatif, Anda dapat melihat bagaimana kegagalan di kedua sisi (tetapi tidak keduanya) tidak akan gagal seluruh parser.
> readP_to_S (get >>= a -> get >>= b -> pfail >>= c -> return [a,b,c]) " ABC "
[] Alih -alih menggunakan fail , READP menyediakan pfail yang memungkinkan Anda untuk menghasilkan case Fail secara langsung.
GIFCURRY, editor video buatan Haskell untuk pembuat GIF, dikeluarkan untuk berbagai program yang berbeda. Untuk memastikan kompatibilitas, diperlukan nomor versi untuk setiap program yang dikeluarkannya. Salah satu program itu adalah 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 Di sini Anda melihat output dari convert --version . Bagaimana Anda bisa menguraikan ini untuk menangkap 6, 9, 10, dan 14?
Melihat output, kita tahu nomor versi adalah kumpulan angka yang dipisahkan oleh periode atau tanda hubung. Definisi ini juga mencakup tanggal sehingga kami akan memastikan bahwa dua angka pertama dipisahkan oleh suatu periode. Dengan begitu, jika mereka menempatkan tanggal sebelum nomor versi, kami tidak akan mendapatkan hasil yang salah.
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.Sebelum kita menyelami kode, inilah algoritma yang akan kita ikuti.
parseVersionNumber
:: [ String ]
-> ReadP [ String ]
parseVersionNumber
nums
= do
_ <- parseNotNumber
num <- parseNumber
let nums' = nums ++ [num]
parseSeparator nums' parseVersionNumber parseVersionNumber adalah kombinator parser utama yang mem -parsing string input untuk nomor versi. Ia menerima daftar string dan mengembalikan daftar string dalam konteks tipe data ReadP . Daftar string yang diterima bukanlah input yang diuraikan melainkan daftar angka yang ditemukan sejauh ini. Untuk panggilan fungsi pertama, daftarnya kosong karena belum diuraikan apa pun.
parseVersionNumber
nums Mulai dari atas, parseVersionNumber mengambil daftar string yang merupakan daftar angka saat ini yang ditemukan sejauh ini.
_ <- parseNotNumber parseNotNumber mengkonsumsi segala sesuatu yang bukan angka dari string input. Karena kami tidak tertarik dengan hasilnya, kami membuangnya ( _ <- ).
num <- parseNumber
let nums' = nums ++ [num]Selanjutnya kita mengkonsumsi semua yang merupakan angka dan kemudian menambahkannya ke daftar angka yang ditemukan sejauh ini.
parseSeparator nums' parseVersionNumber Setelah parseVersionNumber telah memproses nomor berikutnya, ia melewati daftar angka yang ditemukan dan dirinya sendiri ke 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 Di sini Anda melihat parseSeparator .
next <- look
case next of
" " -> return nums
(c : _) -> look memungkinkan kita untuk mendapatkan apa yang tersisa dari string input tanpa mengkonsumsinya. Jika tidak ada yang tersisa, itu mengembalikan angka yang ditemukan. Namun, jika ada sesuatu yang tersisa, itu menganalisis karakter pertama.
case c of
' . ' -> f nums
' - ' -> if length nums == 1 then f [] else f nums
_ -> if length nums == 1 then f [] else return nums Jika karakter berikutnya adalah suatu periode, hubungi parseVersionNumber lagi dengan daftar angka saat ini yang ditemukan. Jika ini berlari dan kami memiliki satu nomor, hubungi parseVersionNumber dengan daftar nomor kosong karena ini adalah tanggal. Jika ini berlari dan kami tidak memiliki satu nomor, hubungi parseVersionNumber dengan daftar nomor yang ditemukan sejauh ini. Kalau tidak, hubungi parseVersionNumber dengan daftar kosong jika kita memiliki tepat satu nomor atau mengembalikan nomor yang ditemukan jika kita tidak memiliki satu nomor.
parseNotNumber
:: ReadP String
parseNotNumber
=
munch ( not . isNumber) parseNotNumber menggunakan munch yang disediakan oleh ReadP . munch diberikan predikat (not . isNumber) yang mengembalikan benar untuk karakter apa pun yang bukan 0 hingga 9.
munch :: ( Char -> Bool ) -> ReadP String munch terus -menerus panggilan get jika karakter berikutnya dalam string input memenuhi predikat. Jika tidak, munch mengembalikan karakter yang melakukannya, jika ada. Karena hanya menggunakan get , Munch selalu berhasil.
Catatan, parseNumber mirip dengan parseNotNumber . Bukannya not . isNumber , predikatnya hanyalah isNumber .
parseNotNumber'
:: ReadP String
parseNotNumber'
=
many (satisfy ( not . isNumber)) Alih -alih menggunakan munch , Anda bisa menulis parseNotNumber seperti ini, menggunakan many dan satisfy - keduanya disediakan oleh Readp. Melihat tanda tangan tipe bagi many , ia menerima kombinator parser tunggal ( ReadP a ). Dalam hal ini, itu diberikan kombinator parser satisfy .
> readP_to_S (satisfy ( not . isNumber)) " a "
[( ' a ' , " " )]
> readP_to_S (satisfy ( not . isNumber)) " 1 "
[] satisfy membutuhkan predikat dan menggunakan get mengkonsumsi karakter berikutnya. Jika predikat yang diterima, pengembalian benar, satisfy pengembalian karakter. Kalau tidak, satisfy panggilan pfail dan gagal.
> readP_to_S (munch ( not . isNumber)) " abc123 "
[( " abc " , " 123 " )]
> readP_to_S (many (satisfy ( not . isNumber))) " abc123 "
[( " " , " abc123 " ),( " a " , " bc123 " ),( " ab " , " c123 " ),( " abc " , " 123 " )] Menggunakan many dapat memberi Anda hasil yang tidak diinginkan. Pada akhirnya, many yang memperkenalkan satu atau lebih kasus Result . Karena itu, many selalu berhasil.
> readP_to_S (many look) " abc123 "
-- Runs forever. many yang akan menjalankan parser Anda sampai gagal atau kehabisan input. Jika parser Anda tidak pernah gagal atau tidak pernah kehabisan input, many tidak akan pernah kembali.
> 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 ], " " )]Untuk setiap indeks dalam hasilnya, hasil parsed akan menjadi hasil dari menjalankan indeks parser pada seluruh input.
> 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 ], " " )] Inilah definisi alternatif bagi many . Di sisi kiri <|> , ia mengembalikan hasil parser saat ini. Di sisi kanan <|> , ia menjalankan parser, menambahkan hasil itu ke hasil parser saat ini, dan menyebut dirinya dengan hasil yang diperbarui. Ini memiliki efek tipe jumlah kumulatif di mana indeks i adalah hasil parser ditambahkan ke hasil parser pada i - 1 , i - 2 , ..., dan 1 .
Sekarang kami membangun parser, mari kita jalankan.
> 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. " )]Anda dapat melihatnya diekstraksi nomor versi dengan benar bahkan dengan tanggal yang datang sebelumnya.
Sekarang mari kita menguraikan sesuatu yang lebih rumit - file SRT.
Untuk rilis GIFCurry Six, saya perlu mengurai file SRT (Teks Subrip). File SRT berisi subtitle yang digunakan oleh program pemrosesan video untuk menampilkan teks di atas video. Biasanya teks ini adalah dialog film yang diterjemahkan ke dalam berbagai bahasa yang berbeda. Dengan menjaga teks terpisah dari video, hanya perlu ada satu video yang menghemat waktu, ruang penyimpanan, dan bandwidth. Perangkat lunak video dapat menukar teks tanpa harus menukar video. Bandingkan ini dengan pembakaran atau coding subtitle di mana teks menjadi bagian dari data gambar yang membentuk video. Dalam hal ini, Anda akan memerlukan video untuk setiap koleksi subtitle.
Video Dalam © Blender Foundation | www.sintel.org
Gifcurry dapat mengambil file SRT dan membakar subtitle untuk mengiris video Anda.
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?Di sini Anda melihat subtitle bahasa Inggris untuk Sintel (© Blender Foundation | www.sintel.org).
SRT mungkin merupakan yang paling mendasar dari semua format subtitle.
—Srt Subtitle | Matrosk
Format file SRT terdiri dari blok, satu untuk setiap subtitle, dipisahkan oleh garis kosong.
2Di bagian atas blok adalah indeks. Ini menentukan urutan subtitle. Semoga subtitle sudah beres dan semuanya memiliki indeks unik tetapi ini mungkin tidak terjadi.
01:04:13,000 --> 02:01:01,640 X1:167 X2:267 Y1:33 Y2:63Setelah indeks adalah waktu mulai, waktu akhir, dan satu set titik opsional yang menentukan persegi panjang, teks subtitle harus masuk.
01:04:13,000 Format cap waktu adalah hours:minutes:seconds,milliseconds .
Perhatikan koma alih -alih periode yang memisahkan detik dari milidetik.
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>.Bagian ketiga dan terakhir dari blok adalah teks subtitle. Ini dapat menjangkau beberapa baris dan ujung ketika ada garis kosong. Teks dapat mencakup tag pemformatan yang mengingatkan pada HTML.
parseSrt
:: ReadP [ SrtSubtitle ]
parseSrt
=
manyTill parseBlock (skipSpaces >> eof) parseSrt adalah kombinator parser utama yang menangani segalanya. Ini mem -parsing setiap blok hingga mencapai ujung file ( eof ) atau input. Untuk berada di sisi yang aman, mungkin ada jalan putih di antara blok terakhir dan ujung file. Untuk menangani ini, ini mem -parsing nol atau lebih karakter whitespace ( skipSpaces ) sebelum mem -parsing ujung file ( skipSpaces >> eof ). Jika masih ada input yang ditinggalkan pada saat eof tercapai, eof akan gagal dan ini tidak akan mengembalikan apa pun. Karena itu, penting bahwa parseBlock tidak meninggalkan apa pun selain Whitespace.
parseBlock
:: ReadP SrtSubtitle
parseBlock
= do
i <- parseIndex
(s, e) <- parseTimestamps
c <- parseCoordinates
t <- parseTextLines
return
SrtSubtitle
{ index = i
, start = s
, end = e
, coordinates = c
, taggedText = t
} Ketika kami pergi sebelumnya, sebuah blok terdiri dari indeks, cap waktu, mungkin beberapa koordinat, dan beberapa baris teks. Dalam versi parseBlock ini, Anda melihat gaya notasi yang lebih penting dengan sintaks rekaman.
parseBlock'
:: ReadP SrtSubtitle
parseBlock'
=
SrtSubtitle
<$> parseIndex
<*> parseStartTimestamp
<*> parseEndTimestamp
<*> parseCoordinates
<*> parseTextLines Inilah cara lain Anda bisa menulis parseBlock . Ini adalah gaya aplikasi. Pastikan untuk mendapatkan pesanan dengan benar. Sebagai contoh, saya bisa secara tidak sengaja mencampuradukkan stempel waktu mulai dan akhir.
parseIndex
:: ReadP Int
parseIndex
=
skipSpaces
>> readInt <$> parseNumber Di bagian atas blok adalah indeks. Di sini Anda melihat skipSpaces lagi. Setelah melewatkan whitespace, itu mem -parsing input untuk angka dan mengubahnya menjadi bilangan bulat yang sebenarnya.
readInt
:: String
-> Int
readInt
=
read readInt terlihat seperti ini.
> read " 123 " :: Int
123
> read " 1abc " :: Int
*** Exception : Prelude. read : no parse Biasanya menggunakan read secara langsung bisa berbahaya. read mungkin tidak dapat mengonversi input ke jenis yang ditentukan. Namun, parseNumber hanya akan mengembalikan 10 karakter digit numerik ( ['0'..'9'] ) jadi menggunakan read langsung menjadi aman.
Parsing cap waktu sedikit lebih terlibat daripada parsing indeks.
parseTimestamps
:: ReadP ( Timestamp , Timestamp )
parseTimestamps
= do
_ <- char ' n '
s <- parseTimestamp
_ <- skipSpaces
_ <- string " --> "
_ <- skipSpaces
e <- parseTimestamp
return (s, e)Ini adalah kombinator utama untuk parsing cap waktu.
char parses karakter yang Anda berikan atau gagal. Jika gagal maka parseTimestamps gagal, pada akhirnya menyebabkan parseSrt gagal sehingga harus ada karakter garis baru setelah indeks.
string seperti char kecuali bukan hanya satu karakter, itu mem -parsing string karakter yang Anda berikan atau gagal.
parseStartTimestamp
:: ReadP Timestamp
parseStartTimestamp
=
char ' n '
>> parseTimestamp parseTimestamps parses kedua cap waktu, tetapi untuk gaya aplikasi ( parseSrt' ), kita membutuhkan parser hanya untuk stempel waktu mulai.
parseEndTimestamp
:: ReadP Timestamp
parseEndTimestamp
=
skipSpaces
>> string " --> "
>> skipSpaces
>> parseTimestampIni mem -parsing segala sesuatu di antara cap waktu dan mengembalikan cap waktu.
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'
}Ini mem -parsing empat angka yang membentuk cap waktu. Tiga angka pertama dipisahkan oleh usus besar dan yang terakhir dipisahkan oleh koma. Namun, untuk lebih memaafkan, kami membiarkan kemungkinan ada periode alih -alih koma.
> readP_to_S (char ' . ' <|> char ' , ' ) " ... "
[( ' . ' , " .. " )]
> readP_to_S (char ' . ' <|> char ' , ' ) " ,.. "
[( ' , ' , " .. " )] Catatan, saat menggunakan char dengan <|> , hanya satu sisi yang dapat berhasil (dua char masuk, satu cuti char ) karena char mengkonsumsi satu karakter dan dua karakter tidak dapat menempati ruang yang sama.
Koordinat adalah bagian opsional dari blok tetapi jika dimasukkan, akan berada di jalur yang sama dengan cap waktu.
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 mengambil dua argumen. Argumen pertama dikembalikan jika argumen kedua, parser, gagal. Jadi jika parser koordinat gagal, parseCoordinates tidak akan mengembalikan Nothing . Dengan kata lain, koordinat parser gagal tidak menyebabkan seluruh parser gagal. Blok ini tidak akan memiliki Nothing untuk "bidang" coordinates .
parseCoordinate
:: Char
-> Int
-> ReadP String
parseCoordinate
c
n
= do
_ <- char ( Data.Char. toUpper c) <|> char ( Data.Char. toLower c)
_ <- string $ show n ++ " : "
parseNumber Parser ini memungkinkan label koordinat berada di huruf besar atau kecil. Misalnya, x1:1 X2:2 Y1:3 y2:4 akan berhasil.
Parsing teks adalah bagian yang paling terlibat karena pemformatan tag seperti HTML.
Parsing tag bisa menjadi tantangan - tanyakan saja pada siapa pun yang menguraikannya dengan ekspresi reguler. Untuk membuat ini lebih mudah bagi kami - dan bagi pengguna - kami akan menggunakan pendekatan tag sup. Parser akan mengizinkan tag bersarang yang tidak tertutup dan/atau salah. Ini juga akan memungkinkan tag apa pun dan bukan hanya b , u , i , dan font .
parseTextLines
:: ReadP [ TaggedText ]
parseTextLines
=
char ' n '
>> (getTaggedText <$> manyTill parseAny parseEndOfTextLines) Kami mulai dengan mencocokkan karakter baru. Setelah itu, kami functor peta atau fmap ( <$> ) getTaggedText atas karakter teks subtitle sampai kami mencapai ujung baris teks.
parseEndOfTextLines
:: ReadP ()
parseEndOfTextLines
=
void (string " nn " ) <|> eof Kami berhenti mengumpulkan karakter ( parseAny ) ketika kami mencapai dua karakter garis baru atau akhir file. Ini menandakan ujung blok.
getTaggedText
:: String
-> [ TaggedText ]
getTaggedText
s
=
fst
$ foldl
folder
( [] , [] )
parsed
where getTaggedText melipat melalui teks parsed dari kiri ke kanan, mengembalikan teks yang ditandai.
parsed
:: [ String ]
parsed
=
case readP_to_S (parseTaggedText [] ) s of
[] -> [s]
r @ (_ : _) -> ( fst . last ) r parsed mengembalikan daftar satu atau lebih string. Ia mencoba untuk menguraikan teks input untuk tag. Jika itu gagal, parsed mengembalikan string input di dalam daftar. Jika tidak, jika parseTaggedText berhasil, parse mengembalikan kemungkinan parsing terakhir ( (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) Saat folder bergerak dari kiri ke kanan, di atas string yang diuraikan, ia memeriksa apakah string saat ini adalah tag. Jika itu adalah tag, itu memperbarui set tag aktif saat ini ( t ). Kalau tidak, ia menambahkan sepotong teks lain yang ditandai yang terkait dengan set tag aktif.
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 memperbarui tags yang diberikan dengan menghapus atau menambahkan tag yang diberikan ( x ) tergantung pada apakah itu tag penutup atau pembuka. Jika tidak, itu hanya mengembalikan set tag yang dilewati. add akan menimpa tag yang ada jika tags sudah memiliki tag dengan nama yang sama. Anda dapat melihat ini di fungsi compare' yang diberikan.
Agar parser tetap sederhana, jika T pembuka ditemukan, T akan ditambahkan ke daftar tag atau menimpa T yang keluar jika sudah ada. Jika penutupan /T yang sesuai ditemukan, maka T dihapus dari daftar tag, jika ada. Tidak masalah jika ada dua atau lebih T berturut -turut, satu atau lebih T tanpa penutupan /T , dan /atau ada penutupan /T tanpa T pembukaan.
makeTag
:: String
-> Tag
makeTag
s
=
Tag
{ name = getTagName s
, attributes = getTagAttributes s
} makeTag merakit tag dari s yang diberikan. Setiap Tag memiliki nama dan nol atau lebih atribut.
parseTaggedText
:: [ String ]
-> ReadP [ String ]
parseTaggedText
strings
= do
s <- look
case s of
" " -> return strings
_ -> do
r <- munch1 ( /= ' < ' ) <++ parseClosingTag <++ parseOpeningTag
parseTaggedText $ strings ++ [r] parseTaggedText Mengembalikan string input yang dipecah menjadi potongan -potongan. Setiap bagian adalah teks yang terlampir dengan tag, tag penutup, atau tag pembuka. Setelah membagi sepotong, ia menambahkannya ke potongan -potongan lain dan memanggil dirinya sendiri lagi. Jika string input yang tersisa kosong, ia mengembalikan daftar string yang ditemukan.
> 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 " )] Operator <++ dibiarkan bias yang berarti bahwa jika sisi kiri berhasil, itu bahkan tidak akan repot dengan kanan. Ingatlah bahwa ketika kita menjalankan parser, kita mendapatkan daftar semua parsing yang mungkin. Semua parsing yang mungkin ini adalah hasil dari parser yang telah melakukan perjalanan melalui semua jalan yang mungkin. Dengan menggunakan <++ , kami menerima parsing yang mungkin dari jalur kiri dan dari jalur kanan jika dan hanya jika sisi kiri gagal. Jika Anda ingin semua parsing yang mungkin terjadi melalui sisi kiri dan kanan, Anda dapat menggunakan operator +++ yang disediakan oleh ReadP . +++ hanyalah <|> yang kami lihat di atas.
parseOpeningTag
:: ReadP String
parseOpeningTag
= do
_ <- char ' < '
t <- munch1 ( c -> c /= ' / ' && c /= ' > ' )
_ <- char ' > '
return $ " < " ++ t ++ " > "Tag pembuka adalah braket sudut pembuka, beberapa teks yang tidak termasuk slash maju, dan braket sudut penutupan langsung berikutnya.
parseClosingTag
:: ReadP String
parseClosingTag
= do
_ <- char ' < '
_ <- char ' / '
t <- munch1 ( /= ' > ' )
_ <- char ' > '
return $ " </ " ++ t ++ " > "Tag penutup adalah braket sudut pembuka, garis miring ke depan, beberapa teks, dan braket sudut penutupan langsung berikutnya.
getTagAttributes
:: String
-> [ TagAttribute ]
getTagAttributes
s
=
if isOpeningTag s
then
case readP_to_S (parseTagAttributes [] ) s of
[] -> []
(x : _) -> fst x
else
[] Tag pembuka dapat memiliki atribut. Misalnya, <font color="#101010"> . Setiap atribut adalah pasangan dua-tuple, nilai kunci. Dalam contoh di atas, color akan menjadi kunci dan #101010 akan menjadi nilainya.
getTagName
:: String
-> String
getTagName
s
=
case readP_to_S parseTagName s of
[] -> " "
(x : _) -> toLower' $ fst xIni mengembalikan nama tag dengan huruf kecil.
parseTagName
:: ReadP String
parseTagName
= do
_ <- char ' < '
_ <- munch ( == ' / ' )
_ <- skipSpaces
n <- munch1 ( c -> c /= ' ' && c /= ' > ' )
_ <- munch ( /= ' > ' )
_ <- char ' > '
return nNama tag adalah string pertama karakter non-whitespace setelah braket sudut pembukaan, kemungkinan slash ke depan, dan beberapa kemungkinan whitespace dan sebelum beberapa whitespace dan/atau braket sudut penutupan.
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 secara rekursif melewati string input, mengumpulkan pasangan nilai kunci. Pada awal tag ( < ), pertama -tama memangkas nama tag sebelum menangani atribut. Ini berhenti parsing untuk atribut ketika mencapai braket sudut penutup ( > ). Jika tag kebetulan memiliki atribut duplikat (berdasarkan kunci), add hanya akan memastikan yang terbaru tetap ada dalam daftar.
trimTagname
:: ReadP ()
trimTagname
=
char ' < '
>> skipSpaces
>> munch1 ( c -> c /= ' ' && c /= ' > ' )
>> return ()Ini memangkas atau membuang nama tag.
parseTagAttribute
:: ReadP TagAttribute
parseTagAttribute
= do
_ <- skipSpaces
k <- munch1 ( /= ' = ' )
_ <- string " = " "
v <- munch1 ( /= ' " ' )
_ <- char ' " '
_ <- skipSpaces
return (toLower' k, v)Kunci atribut adalah setiap string karakter non-whitespace sebelum tanda yang sama. Nilai atribut adalah karakter apa pun setelah tanda yang sama dan kutipan ganda dan sebelum kutipan ganda langsung berikutnya.
isTag
:: String
-> Bool
isTag
s
=
isOpeningTag s || isClosingTag sString adalah tag jika itu merupakan tag pembuka atau tag penutup.
isOpeningTag
:: String
-> Bool
isOpeningTag
s
=
isPresent $ readP_to_S parseOpeningTag sString adalah tag pembuka jika parser tag pembuka berhasil.
isClosingTag
:: String
-> Bool
isClosingTag
s
=
isPresent $ readP_to_S parseClosingTag sString adalah tag penutup jika parser tag penutup berhasil.
Sekarang kita telah mengumpulkan parser, mari kita coba.