ต้องการแยกวิเคราะห์อะไร? ไม่เคยได้ยินเรื่อง "ตัวแยกวิเคราะห์"? ต้องการเรียนรู้ Haskell บ้างไหม? สุดยอด! ด้านล่างคือทุกสิ่งที่คุณต้องการเพื่อลุกขึ้นและแยกวิเคราะห์ด้วยตัวแยกวิเคราะห์ Haskell จากที่นี่คุณสามารถลองจัดการกับรูปแบบการทำให้เป็นอนุกรมข้อมูลลึกลับ, ส่วนหน้าคอมไพเลอร์, ภาษาเฉพาะโดเมน - คุณตั้งชื่อมัน!
รวมอยู่ในคู่มือนี้เป็นสองโปรแกรมสาธิต
version-number-parser วิเคราะห์ไฟล์สำหรับหมายเลขเวอร์ชัน srt-file-parser วิเคราะห์ไฟล์สำหรับคำบรรยาย SRT อย่าลังเลที่จะลองใช้ไฟล์ที่พบใน test-input/
ดาวน์โหลด Haskell Tool Stack จากนั้นเรียกใช้สิ่งต่อไปนี้
git clone https://github.com/lettier/parsing-with-haskell-parser-combinators
cd parsing-with-haskell-parser-combinators
stack buildหากใช้ Cabal คุณสามารถเรียกใช้สิ่งต่อไปนี้
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.txtหากต้องการลองใช้ตัวแยกวิเคราะห์ไฟล์ SRT ให้เรียกใช้สิ่งต่อไปนี้
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.txtหากต้องการลองใช้ตัวแยกวิเคราะห์ไฟล์ SRT ให้เรียกใช้สิ่งต่อไปนี้
cd parsing-with-haskell-parser-combinators
.cabal-sandbox/bin/srt-file-parser
What is the SRT file path ?
test-input/subtitles.srtหนึ่งในวิธีที่ดีกว่าในการเรียนรู้เกี่ยวกับกลยุทธ์การแยกวิเคราะห์ Parser Combinator คือการดูการใช้งานหนึ่ง
ตัวแยกวิเคราะห์ที่สร้างขึ้นโดยใช้ combinators นั้นตรงไปตรงมาเพื่อสร้างอ่านได้โมดูลที่มีโครงสร้างดีและบำรุงรักษาได้ง่าย
- Parser Combinator - Wikipedia
ลองมาดูภายใต้ฝากระโปรงของ READP ซึ่งเป็นไลบรารี Combinator Parser ที่พบในฐาน เนื่องจากอยู่ในฐานคุณควรมีมันอยู่แล้ว
หมายเหตุคุณอาจต้องการลอง Parsec หลังจากทำความคุ้นเคยกับ READP มันก็เป็นไลบรารี combinator parser ที่คนอื่นชอบ readp เป็นโบนัสเพิ่มเติมรวมอยู่ในไลบรารีบูตของ GHC เป็น GHC เวอร์ชัน 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 เราจะเริ่มต้นด้วยประเภทข้อมูล P a In P a ขึ้นอยู่กับคุณ (ผู้ใช้ Library) และสามารถเป็นสิ่งที่คุณต้องการได้ คอมไพเลอร์สร้างอินสแตนซ์ functor โดยอัตโนมัติและมีอินสแตนซ์ที่เขียนด้วยมือสำหรับการสมัคร, monad, MonadFail และทางเลือก
หมายเหตุสำหรับข้อมูลเพิ่มเติมเกี่ยวกับ functors ผู้สมัครและ monads เช็คเอาต์คู่มือง่าย ๆ ของคุณสำหรับ monads ผู้สมัครและ functors
P เป็นประเภทผลรวมที่มีห้ากรณี
Get ใช้อักขระตัวเดียวจากสตริงอินพุตและส่งคืน P ใหม่Look ยอมรับการซ้ำซ้อนของสตริงอินพุตและส่งคืน P ใหม่Fail ระบุว่าตัวแยกวิเคราะห์เสร็จสิ้นโดยไม่มีผลลัพธ์Result ถือการแยกวิเคราะห์ที่เป็นไปได้และกรณี P อื่นFinal เป็นรายการของสองเท่ากับ องค์ประกอบ tuple แรกคือการแยกวิเคราะห์ที่เป็นไปได้ของอินพุตและองค์ประกอบ tuple ที่สองคือส่วนที่เหลือของสตริงอินพุตที่ไม่ได้ใช้โดย 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 มันทำทั้งหมดของการยกหนักในขณะที่มันวิ่งซ้ำผ่านตัวแยกวิเคราะห์ทั้งหมดที่เราเห็นข้างต้น คุณจะเห็นว่าต้องใช้ 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 ในสตริงอินพุต) และส่วนที่เหลือของสตริงอินพุต sLook จะเรียกตัวเองด้วย P ใหม่ (ส่งคืนโดยผ่านฟังก์ชั่น f ใน Look f , สตริงอินพุต s ) และสตริงอินพุต สังเกตว่า Look ไม่ได้ใช้อักขระใด ๆ จากสตริงอินพุตอย่าง GetResult มันจะประกอบสอง tuple-ประกอบด้วยผลลัพธ์ที่แยกวิเคราะห์และสิ่งที่เหลืออยู่ของสตริงอินพุต-และเตรียมสิ่งนี้ให้กับผลลัพธ์ของการเรียกซ้ำที่เรียกใช้กับเคส P อื่นและสตริงอินพุตFinal run ใช้ส่งคืนรายการของสอง tuples ที่มีผลลัพธ์ที่แยกวิเคราะห์และของที่เหลือสตริงอินพุตrun จะส่งคืนรายการที่ว่างเปล่า ตัวอย่างเช่นหากเคส Fail run จะส่งคืนรายการที่ว่างเปล่า > run ( Get ( a -> Get ( b -> Result [a,b] Fail ))) " 12345 "
[( " 12 " , " 345 " )] READP ไม่ได้เปิดเผย run แต่ถ้าเป็นเช่นนั้นคุณสามารถเรียกได้ว่าแบบนี้ ทั้งสอง Get s กิน '1' และ '2' ทิ้งไว้ข้างหลัง "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 คุณสามารถรวมผลลัพธ์ที่แยกวิเคราะห์ไว้ในรายการสุดท้ายของสอง tuples
-- (c) The University of Glasgow 2002
readP_to_S :: ReadP a -> ReadS a
-- readP_to_S :: ReadP a -> String -> [(a,String)]
readP_to_S ( R f) = run (f return ) ในขณะที่ READP ไม่เปิดเผย run โดยตรง แต่จะเปิดเผยผ่าน readP_to_S readP_to_S แนะนำ newtype ที่เรียกว่า ReadP readP_to_S ยอมรับ ReadP a , สตริงและส่งคืนรายการของสอง tuples
-- (c) The University of Glasgow 2002
newtype ReadP a = R ( forall b . ( a -> P b ) -> P b ) นี่คือคำจำกัดความของ ReadP a มีอินสแตนซ์สำหรับ functor, การสมัคร, monad, MonadFail , ทางเลือกและ MonadPlus ตัวสร้าง R ใช้ฟังก์ชั่นที่ใช้ฟังก์ชันอื่นและส่งคืน P ฟังก์ชั่นที่ได้รับการยอมรับจะใช้ทุกสิ่งที่คุณ a และส่งคืน P
-- (c) The University of Glasgow 2002
readP_to_S ( R f) = run (f return ) โปรดจำไว้ว่า P เป็นประเภทของ Monad และ 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)) นี่คือคำจำกัดความอินสแตนซ์ Functor สำหรับ ReadP
> readP_to_S ( fmap toLower get) " ABC "
[( ' a ' , " BC " )]
> readP_to_S (toLower <$> get) " ABC "
[( ' a ' , " BC " )] สิ่งนี้ช่วยให้เราทำอะไรแบบนี้ได้ fmap Functor แมป toLower เหนือ functor get ซึ่งเท่ากับ R Get โปรดจำไว้ว่าประเภทของ Get คือ (Char -> P a) -> P a ซึ่ง ReadP constructor ( 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)) ที่นี่คุณจะเห็นคำจำกัดความ Functor ที่เขียนใหม่สำหรับตัวอย่าง fmap toLower get ตัวอย่าง
เมื่อมองด้านบน readP_to_S กลับมา [('a',"BC")] ได้อย่างไรเมื่อเราใช้ Get ที่ไม่สิ้นสุด 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)
-- ... อินสแตนซ์ทาง Alternative สำหรับ P ช่วยให้เราสามารถแยกการไหลของตัวแยกวิเคราะห์ออกเป็นเส้นทางซ้ายและขวา สิ่งนี้มีประโยชน์เมื่ออินพุตไม่สามารถไปได้หนึ่งหรือ (ไม่ค่อย) สองในสองวิธี
> readP_to_S ((get >>= a -> return a) <|> (get >> get >>= b -> return b)) " ABC "
[( ' A ' , " BC " ),( ' B ' , " C " )] ตัวดำเนินการ <|> ฟังก์ชั่นแนะนำส้อมในการไหลของตัวแยกวิเคราะห์ ตัวแยกวิเคราะห์จะเดินทางผ่านเส้นทางซ้ายและขวา ผลลัพธ์ที่ได้จะมีการแยกวิเคราะห์ที่เป็นไปได้ทั้งหมดที่ไปทางซ้ายและการแยกวิเคราะห์ที่เป็นไปได้ทั้งหมดที่ถูกต้อง หากเส้นทางทั้งสองล้มเหลวดังนั้นตัวแยกวิเคราะห์ทั้งหมดก็ล้มเหลว
หมายเหตุในการใช้งานตัวแยกวิเคราะห์อื่น ๆ เมื่อใช้ตัวดำเนินการ <|> ตัวแยกวิเคราะห์จะไปทางซ้ายหรือขวา แต่ไม่ใช่ทั้งสองอย่าง หากฝ่ายซ้ายประสบความสำเร็จทางด้านขวาจะถูกละเว้น ด้านขวาจะถูกประมวลผลเฉพาะในกรณีที่ด้านซ้ายล้มเหลว
> 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 "
[] แทนที่จะใช้ fail READP จะให้ pfail ซึ่งช่วยให้คุณสามารถสร้างเคส Fail ได้โดยตรง
GIFCURRY ตัวแก้ไขวิดีโอที่สร้างขึ้นสำหรับ Haskell สำหรับผู้ผลิต GIF, เชลล์ออกไปยังโปรแกรมต่าง ๆ ที่แตกต่างกัน เพื่อให้แน่ใจว่าเข้ากันได้จึงจำเป็นต้องใช้หมายเลขเวอร์ชันสำหรับแต่ละโปรแกรมที่ใช้งานได้ หนึ่งในโปรแกรมเหล่านั้นคือ 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 ใช้ munch ที่ ReadP จัดเตรียมไว้ munch จะได้รับ predicate (not . isNumber) ซึ่งส่งคืนจริงสำหรับตัวละครใด ๆ ที่ไม่ได้เป็น 0 ถึง 9
munch :: ( Char -> Bool ) -> ReadP String munch เรียกอย่างต่อเนื่อง get หากอักขระถัดไปในสตริงอินพุตเป็นไปตามเพรดิเคต หากไม่เป็นเช่นนั้น munch จะส่งคืนอักขระที่ทำถ้ามี เนื่องจากมันใช้เพียง get เท่านั้น Munch จึงประสบความสำเร็จเสมอ
หมายเหตุ parseNumber คล้ายกับ parseNotNumber แทนที่จะ not . isNumber , predicate เป็นเพียง isNumber
parseNotNumber'
:: ReadP String
parseNotNumber'
=
many (satisfy ( not . isNumber)) แทนที่จะใช้ munch คุณสามารถเขียน parseNotNumber เช่นนี้โดยใช้ many และ satisfy - ทั้งสองอย่างที่ Readp จัดเตรียมไว้ เมื่อดูที่ลายเซ็นประเภทสำหรับ many มันยอมรับตัวแยกวิเคราะห์ตัวแยกวิเคราะห์เดียว ( ReadP a ) ในกรณีนี้มันจะได้รับ satisfy parser combinator
> readP_to_S (satisfy ( not . isNumber)) " a "
[( ' a ' , " " )]
> readP_to_S (satisfy ( not . isNumber)) " 1 "
[] satisfy ต้องใช้เพรดิเคตและใช้ get บริโภคอักขระต่อไป หากคำกริยาที่ได้รับการยอมรับจะส่งคืนจริงให้ satisfy สนองต่อตัวละคร มิฉะนั้น satisfy การโทร pfail และล้มเหลว
> 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. " )]คุณสามารถเห็นว่ามันแยกหมายเลขเวอร์ชันอย่างถูกต้องแม้จะมีวันที่มาก่อน
ตอนนี้เรามาวิเคราะห์สิ่งที่ซับซ้อนกว่ากัน - ไฟล์ SRT
สำหรับการเปิดตัว 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 ) เป็นศูนย์หรือมากกว่าก่อนที่จะแยกวิเคราะห์จุดสิ้นสุดของไฟล์ ( skipSpaces >> eof ) หากยังคงมีอินพุตที่เหลืออยู่ในเวลาที่ 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)นี่คือ combinator หลักสำหรับการแยกวิเคราะห์การประทับเวลา
char แยกวิเคราะห์ตัวละครที่คุณให้หรือมันล้มเหลว หากมันล้มเหลว parseTimestamps ล้มเหลวในที่สุดก็ทำให้ parseSrt ล้มเหลวดังนั้นจะต้องมีอักขระใหม่หลังจากดัชนี
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 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 ใช้สองอาร์กิวเมนต์ อาร์กิวเมนต์แรกจะถูกส่งคืนหากอาร์กิวเมนต์ที่สองตัวแยกวิเคราะห์ล้มเหลว ดังนั้นหากพิกัด Parser ล้มเหลว parseCoordinates จะ Nothing อีกวิธีหนึ่งการแยกวิเคราะห์ความล้มเหลวของพิกัดไม่ได้ทำให้ตัวแยกวิเคราะห์ทั้งหมดล้มเหลว บล็อกนี้จะ Nothing สำหรับ coordinates "ฟิลด์"
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) เราเริ่มต้นด้วยการจับคู่กับตัวละครใหม่ หลังจากนั้นเรา functor map หรือ fmap ( <$> ) getTaggedText เหนืออักขระข้อความคำบรรยายจนกว่าเราจะถึงจุดสิ้นสุดของบรรทัดข้อความ
parseEndOfTextLines
:: ReadP ()
parseEndOfTextLines
=
void (string " nn " ) <|> eof เราหยุดรวบรวมอักขระ ( 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 อัปเดต tags ที่กำหนดโดยการลบหรือเพิ่มแท็กที่กำหนด ( x ) ขึ้นอยู่กับว่ามันเป็นปิดหรือเปิดแท็ก ถ้าไม่ใช่มันก็จะส่งคืนชุดแท็กที่ผ่าน add จะเขียนทับแท็กที่มีอยู่ถ้า tags มีแท็กแล้วมีชื่อเดียวกัน คุณสามารถเห็นสิ่งนี้ได้ในฟังก์ชั่น compare'
เพื่อให้ตัวแยกวิเคราะห์ง่ายขึ้นหากพบแท็กเปิด T จะเพิ่ม T ลงในรายการแท็กหรือเขียน T ออกที่ออกหากมีอยู่แล้ว หากพบการปิด /T ที่สอดคล้องกัน T จะถูกลบออกจากรายการแท็กหากมีอยู่ ไม่สำคัญว่าจะมีสองหรือมากกว่า T S ในแถวหนึ่งหรือมากกว่าหนึ่ง T s โดยไม่ต้องปิด /T และ /หรือมีการปิด /T โดยไม่ต้องเปิด T
makeTag
:: String
-> Tag
makeTag
s
=
Tag
{ name = getTagName s
, attributes = getTagAttributes s
} makeTag ประกอบแท็กจาก s ที่กำหนด แต่ละ Tag มีชื่อและแอตทริบิวต์เป็นศูนย์หรือมากกว่า
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"> แต่ละแอตทริบิวต์เป็นคู่สองค่าคีย์-ค่า ในตัวอย่างข้างต้น 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 หลังจากวงเล็บมุมเปิด, slash ไปข้างหน้าที่เป็นไปได้และช่องว่างที่เป็นไปได้บางอย่างและก่อนหน้าช่องว่างและ/หรือวงเล็บมุมปิด
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)คีย์แอตทริบิวต์คือสตริงของอักขระที่ไม่ใช่ Whitespace ก่อนที่จะมีเครื่องหมายเท่ากัน ค่าแอตทริบิวต์คืออักขระใด ๆ หลังจากเครื่องหมายเท่ากันและใบเสนอราคาสองเท่าและก่อนที่จะใบเสนอราคาสองครั้งถัดไป
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สตริงเป็นแท็กปิดหากตัวแยกวิเคราะห์แท็กปิดสำเร็จ
ตอนนี้เราได้รวบรวมตัวแยกวิเคราะห์มาลองดูสิ