هل تحتاج إلى تحليل شيء ما؟ لم تسمع أبداً عن "مشمع محلل"؟ تبحث لتعلم بعض هاسكل؟ مذهل! فيما يلي كل ما ستحتاجه للاستيقاظ والتحليل مع مجموعات Haskell Parser. من هنا ، يمكنك محاولة معالجة تنسيقات تسلسل البيانات الباطنية ، والأطراف الأمامية للمترجم ، ولغات محددة للمجال - سمها ما شئت!
المدرجة في هذا الدليل هما برنامجان تجريبيان.
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 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 ، هي النظر في تنفيذ واحدة.
المحللون المصممون باستخدام المجموعات التوسيمية واضح ومباشر لبناء وقابل للقراءة والمعيار والمنظم جيدًا ويمكن الحفاظ عليه بسهولة.
- Parser Combinator - Wikipedia
دعونا نلقي نظرة تحت غطاء محرك السيارة ، مكتبة Combinator المحلل الموجودة في القاعدة. لأنه في القاعدة ، يجب أن يكون لديك بالفعل.
ملاحظة ، قد ترغب في تجربة Parsec بعد التعرف على Readp. إنها أيضًا مكتبة Combinator Parser التي يفضلها الآخرون إعادة إرسالها. كمكافأة إضافية ، يتم تضمينه في مكتبات التمهيد في 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 إن a in P a متروك لك (مستخدم المكتبة) ويمكن أن يكون ما تريد. يقوم المترجم بإنشاء مثيل Functor تلقائيًا وهناك حالات مكتوبة يدويًا للتطبيق والموناد MonadFail والبديل.
لاحظ ، لمعرفة المزيد عن المسلسلات ، والمطبات ، والموناد ، الخروج من دليلك السهل إلى Monads و Applicities و Functors.
P هو نوع مجموع مع خمس حالات.
Get استهلاك حرف واحد من سلسلة الإدخال وإرجاع P جديد.Look يقبل مكررة من سلسلة الإدخال ويعيد P جديد.Fail إلى الانتهاء من المحلل دون نتيجة.Result تحمل تحليل محتمل وحالة P أخرى.Final هو قائمة من tuples. عنصر 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 هو قلب محلل القراءة. إنه يقوم بكل الرفع الثقيل لأنه يمر بشكل متكرر من خلال جميع الحالات المحللات التي رأيناها أعلاه. يمكنك أن ترى أن الأمر يتطلب 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 ، s الإدخال وسلسلة الإدخال. لاحظ كيف لا تستهلك Look أي أحرف من سلسلة الإدخال مثل Get .Result ، فإنها تجمع ثنائية-تحتوي على النتيجة المحسورة وما تبقى من سلسلة الإدخال-ويقوم بتقديم هذا إلى نتيجة مكالمة متكررة تعمل مع حالة P أخرى وسلسلة الإدخال.Final ، run يعيد قائمة من الثنائية التي تحتوي على نتائج مستوحاة وبقايا سلسلة الإدخال.run بإرجاع قائمة فارغة. على سبيل المثال ، إذا كانت الحالة Fail ، فسيقوم run بإرجاع قائمة فارغة. > run ( Get ( a -> Get ( b -> Result [a,b] Fail ))) " 12345 "
[( " 12 " , " 345 " )] لا تعرض Readp run ولكن إذا حدث ذلك ، فيمكنك تسميته هكذا. Get الاثنان '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 ، يمكنك تضمين نتيجة محسورة في القائمة النهائية للثانين.
-- (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 ، وسلسلة ، وإرجاع قائمة من اثنين.
-- (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 Maps toLower على get the functor الذي يساوي R Get . أذكر أن نوع 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)) هنا ترى تعريف Functor إعادة كتابته لـ fmap toLower get GESE.
إذا نظرنا أعلاه ، كيف عاد 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 " )] يقدم المشغل أو الدالة <|> شوكة في تدفق المحلل. سوف يسافر المحلل عبر كل من المسارات اليمنى واليسرى. ستحتوي النتيجة النهائية على جميع التحليلات المحتملة التي ذهبت إلى اليسار وجميع التحلات الممكنة التي سارت على اليمين. إذا فشل كلا المسارين ، فإن المحلل بالكامل يفشل.
لاحظ ، في تطبيقات 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 . لاحظ تعريف 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 ، ستكون قيمة الإرجاع قائمة فارغة. إذا كان المسار الفاشل هو المسار الوحيد ، فستكون النتيجة بأكملها قائمة فارغة. أذكر أنه عند Fail run المباريات ، فإنه يعيد قائمة فارغة.
-- (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 المسند (not . isNumber) الذي يعيد صحيح لأي حرف ليس من 0 إلى 9.
munch :: ( Char -> Bool ) -> ReadP String munch المكالمات المستمرة get إذا كان الحرف التالي في سلسلة الإدخال يفي بالمسند. إذا لم يحدث ذلك ، munch يعيد الشخصيات التي فعلت ، إن وجدت. لأنه يستخدم فقط get ، ينجح مونش دائمًا.
لاحظ أن parseNumber يشبه parseNotNumber . بدلا من not . isNumber ، المسند هو مجرد isNumber .
parseNotNumber'
:: ReadP String
parseNotNumber'
=
many (satisfy ( not . isNumber)) بدلاً من استخدام munch ، يمكنك كتابة parseNotNumber مثل هذا ، باستخدام many satisfy - والتي توفرها Readp منها. بالنظر إلى توقيع النوع بالنسبة many ، فإنه يقبل مجموعة محلل واحد ( ReadP a ). في هذه الحالة ، يتم إعطاؤه satisfy Combinator Parser.
> 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 (النص الفرعي). تحتوي ملفات 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 هو أبسط تنسيقات الترجمة.
- الترجمة الفرعية | 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 سيعود فقط أحرف الأرقام العددية العشرة ( ['0'..'9'] ) ، لذا فإن استخدام read مباشرة يصبح آمنًا.
تحليل الطابع الزمني أكثر مشاركة بقليل من تحليل الفهرس.
parseTimestamps
:: ReadP ( Timestamp , Timestamp )
parseTimestamps
= do
_ <- char ' n '
s <- parseTimestamp
_ <- skipSpaces
_ <- string " --> "
_ <- skipSpaces
e <- parseTimestamp
return (s, e)هذا هو المشابك الرئيسي لتحليل الطابع الزمني.
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 encl ، إجازة 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 . بعبارة أخرى ، لا يتسبب فشل المحلل المحلل في فشل المحلل بالكامل. لن تحتوي هذه الكتلة على 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 أو 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 ) بناءً على ما إذا كانت علامة إغلاق أو فتح. إذا لم يكن الأمر كذلك ، فإنه يعيد فقط مجموعة العلامات التي تم تمريرها. Will Will Will will add علامة موجودة إذا كانت tags بالفعل علامة على نفس الاسم. يمكنك رؤية هذا في وظيفة compare' المعطاة.
للحفاظ على المحلل البسيط ، إذا تم العثور على علامة فتح T ، يتم إضافة T إلى قائمة العلامات أو الكتابة فوق T exiting إذا كانت موجودة بالفعل. إذا تم العثور على إغلاق /T المقابل ، فسيتم إزالة T من قائمة العلامات ، إذا كانت موجودة. لا يهم ما إذا كان هناك اثنين أو أكثر على التوالي ، واحد أو أكثر T T دون إغلاق /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اسم العلامة هو أول سلسلة من أحرف غير وايتيس بمساحة بعد قوس زاوية الفتح ، وعمق مائل محتمل للأمام ، وبعض المسافة البيضاء الممكنة وقبل بعض المسافة البيضاء و/أو قوس الزاوية الختامية.
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)مفتاح السمة هو أي سلسلة من أحرف غير وايتيسبيس قبل العلامة المتساوية. قيمة السمة هي أي أحرف بعد العلامة المتساوية والاقتباس المزدوج وقبل الاقتباس المزدوج الفوري التالي.
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السلسلة هي علامة إغلاق إذا نجح محلل الختام.
الآن بعد أن قمنا بتجميع المحلل ، دعنا نجربه.