स्वीकारोक्ति का समय:मुझे विशेष रूप से नियमित अभिव्यक्तियों के साथ काम करना पसंद नहीं है। जबकि मैं हर समय उनका उपयोग करता हूं, /^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:[email protected] 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
का उपयोग करें स्ट्रिंग को टोकन में विभाजित करने के लिए। यह वास्तव में एक बहुत अच्छी चाल है:
# The parens in the regexp mean that the separator is added to the array
"foo bar baz".split(/(foo|bar|baz)/)
=> ["", "foo", " ", "bar", " ", "baz"]
अजीब खाली-तार के बावजूद, यह शुरुआत में आशाजनक लग रहा था। लेकिन मेरी वास्तविक दुनिया की नियमित अभिव्यक्ति कहीं अधिक जटिल थी। मेरा पहला ड्राफ्ट इस तरह दिखता था:
/
( # Capture group is so split will include matching and non-matching strings
(?: # The first character of the key, which is
(?!\s)[^:\s"'\[]{1} # ..any valid "key" char not preceeded by whitespace
|^[^:\s"'\[]{0,1} # ..or any valid "key" char at beginning of line
)
[^:\s"'\[]* # The rest of the "key" chars
: # a colon
(?: # The "value" chars, which are
'[^']+' # ..anything surrounded by single quotes
| "[^"]+" # ..or anything surrounded by double quotes
| \[\S+\sTO\s\S+\] # ..or anything like [x TO y]
| [^\s"'\[]+ # ..or any string not containing whitespace or special chars
)
)
/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
# Outputs ["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
यह कोड मेरे नियमित-अभिव्यक्ति-आधारित दृष्टिकोण से थोड़ा ही लंबा है, लेकिन बहुत अधिक सीधा है।
विभाजन विचार
वहाँ शायद एक नियमित अभिव्यक्ति है जो मेरे उपयोग के मामले में ठीक काम करेगी। अगर इतिहास कोई मार्गदर्शक है, तो शायद यह इतना आसान है कि मुझे मूर्ख बना दिया जाए। :)पी>
लेकिन मुझे वास्तव में इस छोटे से पार्सर को लिखने का मौका मिला। इसने मुझे उस रट से बाहर कर दिया जिसमें मैं रेगेक्स दृष्टिकोण के साथ था। एक अच्छे बोनस के रूप में, मैं परिणामी कोड में बहुत अधिक आश्वस्त हूं, जितना कि मैं कभी भी उस कोड के साथ हूं जो जटिल नियमित अभिव्यक्तियों पर आधारित है।