Computer >> कंप्यूटर ट्यूटोरियल >  >> प्रोग्रामिंग >> Ruby

जटिल रेगेक्स को सरल बनाएं: एक सरल, पठनीय पार्सर से बदलें

कबूल करने का समय: मुझे नियमित अभिव्यक्तियों के साथ काम करना विशेष रूप से पसंद नहीं है। जबकि मैं हर समय उनका उपयोग करता हूँ, /^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

यह कोड मेरे नियमित-अभिव्यक्ति-आधारित दृष्टिकोण से केवल थोड़ा लंबा है लेकिन बहुत अधिक सीधा है।

अंतिम विचार

शायद वहाँ एक नियमित अभिव्यक्ति है जो मेरे उपयोग के मामले के साथ ठीक काम करेगी। यदि इतिहास कोई मार्गदर्शक है, तो यह शायद इतना सरल है कि मुझे मूर्ख जैसा दिखाए। :)

लेकिन मैंने इस छोटे पार्सर को लिखने का मौका वास्तव में आनंद लिया। इसने मुझे रेगेक्स दृष्टिकोण के साथ जिस खाई में था उससे बाहर निकाला। एक अच्छे बोनस के रूप में, मुझे परिणामी कोड में उस कोड की तुलना में बहुत अधिक विश्वास है जो जटिल नियमित अभिव्यक्तियों पर आधारित है।

  1. जावास्क्रिप्ट रेगुलर एक्सप्रेशन मॉडिफायर्स को उदाहरण सहित समझाएं जावास्क्रिप्ट रेगुलर एक्सप्रेशन मॉडिफायर्स को उदाहरण सहित समझाएं

    परिचय जावास्क्रिप्ट रेगुलर एक्सप्रेशन मॉडिफायर्स (फ्लैग्स) रेगुलर एक्सप्रेशन का वैकल्पिक भाग होते हैं जो केस-इनसेंसिटिव और ग्लोबल सर्च करने की अनुमति देते हैं। मॉडिफायर्स को एक साथ जोड़ा भी जा सकता है। मॉडिफायर्स मॉडिफायरविवरण gग्लोबल मिलान सक्षम करता है - पहले मिलान पर रुकने के बजाय सभी मिलान

  1. C++ में उदाहरण के साथ एक्सप्रेशन ट्री C++ में उदाहरण के साथ एक्सप्रेशन ट्री

    एक एक्सप्रेशन ट्री एक विशेष प्रकार का बाइनरी ट्री है जिसमें ट्री का प्रत्येक नोड या तो एक ऑपरेटर या ऑपरेंड से बना होता है। लीफ नोड्स ट्री के एक ऑपरेंड को दर्शाते हैं। नॉन-लीफ नोड्स ट्री के एक ऑपरेटर को दर्शाते हैं। उदाहरण: इनफिक्स एक्सप्रेशन प्राप्त करने के लिए जिसे आसानी से हल किया जा सकता है, हम

  1. रूबी पार्सिंग में मास्टर करें: स्क्रैच से अपना खुद का पार्सर बनाएं रूबी पार्सिंग में मास्टर करें: स्क्रैच से अपना खुद का पार्सर बनाएं

    पार्सिंग स्ट्रिंग्स के एक समूह को समझने और उन्हें किसी ऐसी चीज़ में बदलने की कला है जिसे हम समझ सकें। आप रेगुलर एक्सप्रेशन्स का उपयोग कर सकते हैं, लेकिन वे हमेशा इस काम के लिए उपयुक्त नहीं होते। उदाहरण के लिए, यह सामान्य ज्ञान है कि HTML को रेगुलर एक्सप्रेशन्स से पार्स करना शायद एक अच्छा विचार नहीं