कबूल करने का समय: मुझे नियमित अभिव्यक्तियों के साथ काम करना विशेष रूप से पसंद नहीं है। जबकि मैं हर समय उनका उपयोग करता हूँ, /^foo.*$/ से अधिक जटिल कुछ भी मुझे रुककर सोचने पर मजबूर कर देता है। जबकि मुझे यकीन है कि ऐसे लोग हैं जो \A(?=\w{6,10}\z)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3}) जैसे एक्सप्रेशंस को एक नज़र में समझ सकते हैं, लेकिन मुझे कई मिनट गूगल करने पड़ते हैं और यह मुझे नाखुश कर देता है। यह रूबी पढ़ने से काफी अलग है।
यदि आप उत्सुक हैं, तो उपरोक्त उदाहरण रेगेक्स लुकअहेड्स पर इस लेख से लिया गया है।
स्थिति
हनीबैजर में मैं वर्तमान में हमारे खोज UI को बेहतर बनाने पर काम कर रहा हूँ। कई खोज प्रणालियों की तरह, हमारी एक सरल क्वेरी भाषा का उपयोग करती है। मेरे परिवर्तनों से पहले, यदि आप एक कस्टम डेट रेंज खोजना चाहते थे, तो आपको मैन्युअली इस तरह एक क्वेरी टाइप करनी पड़ती थी:
occurred:[2017-06-12T16:10:00Z TO 2017-06-12T17:10:00Z]
आउच!
नए खोज UI में, हम यह पता लगाना चाहते हैं कि आप कब डेट-संबंधी क्वेरी टाइप करना शुरू करते हैं और एक सहायक डेटपिकर पॉप अप करते हैं। और निश्चित रूप से, डेटपिकर तो सिर्फ शुरुआत है। अंततः हम संदर्भ-संवेदनशील संकेतों का विस्तार करके अधिक प्रकार के खोज शब्दों को कवर करेंगे। यहाँ कुछ उदाहरण हैं:
assigned:jane@email.com context.user.id=100 resolved:false ignored:false occurred:[ params.article.title:"Starr's parser post" foo:'ba
मुझे इन स्ट्रिंग्स को इस तरह टोकनाइज़ करने की आवश्यकता है कि:
- व्हाइटस्पेस टोकन्स को अलग करता है, सिवाय जब ' ', " " या [] से घिरा हो
- अनकोटेड व्हाइटस्पेस अपने आप में एक टोकन है
- मैं इनपुट स्ट्रिंग को सटीक रूप से पुनः बनाने के लिए
tokens.join("")चला सकता हूँ
उदाहरण के लिए:
tokenize(%[params.article.title:"Starr's parser post" foo:'ba]) => ["params.article.title:\"Starr's parser post\"", " ", "foo:'ba"]
एक नियमित अभिव्यक्ति का उपयोग करना
मेरा पहला विचार एक वैध टोकन कैसा दिखना चाहिए इसे परिभाषित करने के लिए एक कैप्चरिंग नियमित अभिव्यक्ति का उपयोग करना था, फिर स्ट्रिंग को टोकन्स में विभाजित करने के लिए String#split का उपयोग करना। यह वास्तव में एक बहुत अच्छी ट्रिक है:
# रेगेक्स में पैरेंथेसिस का मतलब है कि सेपरेटर सरणी में जोड़ा जाएगा
"foo bar baz".split(/(foo|bar|baz)/) => ["", "foo", " ", "bar", " ", "baz"]
यह शुरू में आशाजनक लग रहा था, अजीब खाली स्ट्रिंग्स के बावजूद। लेकिन मेरा वास्तविक-विश्व नियमित अभिव्यक्ति बहुत अधिक जटिल था। मेरा पहला ड्राफ्ट ऐसा दिखता था:
/
( # कैप्चर ग्रुप ताकि split मिलान और गैर-मिलान स्ट्रिंग्स को शामिल करे
(?:
# कुंजी का पहला वर्ण, जो है
(?!\s)[^:\s"'\[]{1} # ..कोई भी वैध "कुंजी" वर्ण जो व्हाइटस्पेस से पहले नहीं है
|^[^:\s"'\[]{0,1} # ..या लाइन की शुरुआत में कोई भी वैध "कुंजी" वर्ण
)
[^:\s"'\[]* # शेष "कुंजी" वर्ण
: # एक कोलन
(?:
# "मान" वर्ण, जो हैं
'[^']+' # ..सिंगल कोट्स से घिरा कुछ भी
| "[^"]+" # ..या डबल कोट्स से घिरा कुछ भी
| \[\S+\sTO\s\S+\] # ..या [x TO y] जैसा कुछ भी
| [^\s"'\[]+ # ..या व्हाइटस्पेस या विशेष वर्ण न रखने वाला कोई भी स्ट्रिंग
)
)
/xi
इसके साथ काम करने से मुझे एक डूबता हुआ एहसास हुआ। हर बार जब मुझे एक एज केस मिलता, मुझे नियमित अभिव्यक्ति में संशोधन करना पड़ता, जिससे यह और भी जटिल हो जाता। इसके अलावा, इसे जावास्क्रिप्ट के साथ-साथ रूबी में भी काम करने की आवश्यकता थी, इसलिए नकारात्मक लुकबिहाइंड जैसी कुछ विशेषताएं उपलब्ध नहीं थीं।
...यह लगभग उसी समय था जब इस सब की बेतुकापन ने मुझे मारा। मैं जिस नियमित अभिव्यक्ति दृष्टिकोण का उपयोग कर रहा था, वह स्क्रैच से एक साधारण पार्सर लिखने की तुलना में बहुत अधिक जटिल था।
पार्सर की शारीरिक रचना
मैं कोई विशेषज्ञ नहीं हूँ, लेकिन सरल पार्सर सरल होते हैं। वे सब करते हैं:
- एक स्ट्रिंग के माध्यम से चरित्र दर चरित्र आगे बढ़ें
- प्रत्येक वर्ण को एक बफर में जोड़ें
- जब एक टोकन-अलग करने वाली स्थिति का सामना होता है, तो बफर को एक सरणी में सहेजें और इसे खाली करें।
यह जानकर, हम एक सरल पार्सर सेट अप कर सकते हैं जो स्ट्रिंग्स को व्हाइटस्पेस द्वारा विभाजित करता है। यह लगभग "foo bar".split(/(\s+)/) के बराबर है।
class Parser
WHITESPACE = /\s/
NON_WHITESPACE = /\S/
def initialize
@buffer = []
@output = []
end
def parse(text)
text.each_char do |c|
case c
when WHITESPACE
flush if previous.match(NON_WHITESPACE)
@buffer << c
else
flush if previous.match(WHITESPACE)
@buffer << c
end
end
flush
@output
end
protected
def flush
if @buffer.any?
@output << @buffer.join("")
@buffer = []
end
end
def previous
@buffer.last || ""
end
end
puts Parser.new().parse("foo bar baz").inspect
# आउटपुट ["foo", " ", "bar", " ", "baz"]
यह मेरी इच्छा की दिशा में एक कदम है, लेकिन इसमें कोट्स और ब्रैकेट्स के लिए समर्थन गायब है। सौभाग्य से, इसे जोड़ने में केवल कुछ लाइन्स कोड लगते हैं:
def parse(text)
surround = nil
text.each_char do |c|
case c
when WHITESPACE
flush if previous.match(NON_WHITESPACE) && !surround
@buffer << c
when '"', "'"
@buffer << c if !surround
surround = c
elsif surround == c
flush
surround = nil
end
when "["
@buffer << c
surround = c if !surround
when "]"
@buffer << c if surround == "["
flush
surround = nil
end
else
flush() if previous().match(WHITESPACE) && !surround
@buffer << c
end
end
flush
@output
end
यह कोड मेरे नियमित-अभिव्यक्ति-आधारित दृष्टिकोण से केवल थोड़ा लंबा है लेकिन बहुत अधिक सीधा है।
अंतिम विचार
शायद वहाँ एक नियमित अभिव्यक्ति है जो मेरे उपयोग के मामले के साथ ठीक काम करेगी। यदि इतिहास कोई मार्गदर्शक है, तो यह शायद इतना सरल है कि मुझे मूर्ख जैसा दिखाए। :)
लेकिन मैंने इस छोटे पार्सर को लिखने का मौका वास्तव में आनंद लिया। इसने मुझे रेगेक्स दृष्टिकोण के साथ जिस खाई में था उससे बाहर निकाला। एक अच्छे बोनस के रूप में, मुझे परिणामी कोड में उस कोड की तुलना में बहुत अधिक विश्वास है जो जटिल नियमित अभिव्यक्तियों पर आधारित है।