রুবি শুধু একটি ফান ল্যাঙ্গুয়েজ নন, এটি একটি এক্সিলেন্ট স্ট্যান্ডার্ড লাইব্রেরিও সাথে আসে। যেগুলোর কিছু সেটা ज्यादा পরিচিত নন, এবং почти লুকানো জেম। আজ গেস্ট রাইটার মাইকেল কোল হাইলাইট করছেন একটি ফেভারিট: StringScanner।
রুবির লুকানো জেম: StringScanner
থার্ড পার্টি জেম ইনস্টল করা ছাড়া চুক্তা দূর যাওয়া যায়, ডেটা স্ট্রাকচার যেমন OpenStruct এবং Set থেকে CSV পার্সিং এবং বেঞ্চмар্কিং পর্যন্ত। যাইহোক, রুবির স্ট্যান্ডার্ড ইনস্টলেশনে কিছু কম পরিচিত লাইব্রেরি আছে যা খুব উপকারী হতে পারে, उनमें से একটি হল StringScanner যা ডকুমেন্টেশনের অনুযায়ী "স্ট্রিংসে লেক্সিক্যাল স্ক্যানিং অপারেশন প্রদান করে"।
স্ক্যানিং এবং পার্সিং
তাহলে "লেক্সিক্যাল স্ক্যানিং" এর অর্থ কী সঠিকভাবে? এসেনশিয়ালি এটি একটি ইনপুট স্ট্রিং নিয়ে সার্থক তথ্যের টুকরো এক্সট্রাক্ট করার প্রক্রিয়াকে বর্ণনা করে, কিছু নিয়ম অনুসরণ করে। উদাহরণস্বরূপ, এটি একটি কম্পাইলারের প্রথম স্টেজে দেখা যায় যা একটি এক্সপ্রেশন যেমন 2 + 1 ইনপুট হিসেবে নেয় এবং নিচের টোকেন সিকোয়েন্সে রূপান্তরিত করে:
[{ number: "1" }, {operator: "+"}, { number: "1"}]
লেক্সিক্যাল স্ক্যানারস সাধারণত ফাইনাইট-স্টেট অটোমেটা হিসেবে ইমপ্লিমেন্ট করা হয় এবং সেসব জেনারেট করার জন্য বেশ কিছু পরিচিত টুল আছে (যেমন ANTLR বা Ragel)।
তবে,有時候 আমাদের পার্সিং নিডস उतने জটিল নন, এবং রেগুলার এক্সপ্রেশন-বেসড StringScanner এর মত একটি সিম্পল লাইব্রেরি এই পরিস্থিতিতে খুব হ্যান্ডি হতে পারে। এটি একটি所謂 স্ক্যান পয়েন্টার-এর লোকেশন মনে রাখে কাজ করে যা কিছুই নন বরং স্ট্রিং-এ একটি ইন্ডেক্স। স্ক্যানিং প্রসেস তারপর স্ক্যান পয়েন্টারের ঠিক পরে থাকা কোডকে প্রদত্ত এক্সপ্রেশন সাথে ম্যাচ করার চেষ্টা করে। ম্যাচিং অপারেশনের পাশাপাশি, StringScanner স্ক্যান পয়েন্টার সরানোর (স্ট্রিং-এর মধ্য দিয়ে ফরওয়ার্ড বা ব্যাকওয়ার্ড যাওয়া), আহেড লুকিং (স্ক্যান পয়েন্টার মডিফাই না করে পরবর্তীটা কী সেটা দেখা), এবং বের করার মেথডও প্রদান করে আমরা স্ট্রিং-এ কোথায় আছি (লাইন বা পুরো স্ট্রিং-এর শুরু বা শেষ)।
Rails লগ পার্স করা
পর্যাপ্ত থিওরি, আসুন StringScanner একশনে দেখি। নিচের উদাহরণটি একটি Rails-র লগ এন্ট্রি নেবে নিচের মত,
log_entry = <<EOS Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900 Processing by HomeController#index as HTML Rendered text template within layouts/application (0.0ms) Rendered layouts/_assets.html.erb (2.0ms) Rendered layouts/_top.html.erb (2.6ms) Rendered layouts/_about.html.erb (0.3ms) Rendered layouts/_google_analytics.html.erb (0.4ms) Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms) EOS
এবং এটিকে নিচের হ্যাশে পার্স করবে:
{ method: "GET", path: "/" ip: "127.0.0.1", timestamp: "2017-08-20 20:53:10 +0900", success: true, response_code: "200", duration: "79ms", }
NB: যদিও এটি StringScanner-এর জন্য একটি ভালো উদাহরণ, একটি রিয়েল অ্যাপ্লিকেশন Lograge এবং এর JSON লগ ফরম্যাটার ব্যবহার করলে ভালো হবে।
StringScanner ব্যবহার করতে, আমাদের প্রথমে এটি রিকোয়ার করতে হবে:
require 'strscan'
এতে পর, আমরা একটি নতুন ইনস্ট্যান্স ইনিশিয়ালাইজ করতে পারি লগ এন্ট্রি আর্গুমেন্ট হিসেবে কন্সট্রাক্টরে পাস করে। একই সাথে, আমরা একটি খালি হ্যাশও ডিফাইন করব আমাদের পার্সিং এফর্টের ফলাফল ধারণ করতে:
scanner = StringScanner.new(log_entry) log = {}
আমরা এখন স্ক্যানারের pos মেথড ব্যবহার করতে পারি আমাদের স্ক্যান পয়েন্টারের বর্তমান লোকেশন পেতে। এক্সপেক্টেডলি, রেজাল্ট 0, স্ট্রিং-এর প্রথম ক্যারেক্টার:
scanner.pos #=> 0
এটি ভিজ্যুয়ালাইজ করি যাতে প্রসেসটি ফলো করা সহজ হয়:
Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900 ^ ... Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
স্ক্যানারের স্টেট আরও ইনট্রাস্পেকশনের জন্য আমরা beginning_of_line? এবং eos? ব্যবহার করতে পারি কনফার্ম করতে যে স্ক্যান পয়েন্টার বর্তমানে লাইনের শুরুতে আছে এবং আমরা এখনো আমাদের ইনপুট ফুলি কনসিউম করিনি:
scanner.beginning_of_line? #=> true scanner.eos? #=> false
আমরা যেটি প্রথমে এক্সট্রাক্ট করতে চাই সেটি হলো HTTP রিকোয়েস্ট মেথড, যেটি "Started" শব্দের ঠিক পরে একটি স্পেস দিয়ে পাওয়া যায়। আমরা স্ক্যানারের উপযুক্ত নামকৃত skip মেথড ব্যবহার করতে পারি স্ক্যান পয়েন্টার এডভান্স করার জন্য, যেটি ইগনোর করা ক্যারেক্টের সংখ্যা রিটার্ন করে, আমাদের কেসে ৮। এছাড়াও আমরা matched? ব্যবহার করতে পারি কনফার্ম করতে সবকিছু এক্সপেক্টেডলি কাজ করেছে কিনা:
scanner.skip(/Started /) #=> 8 scanner.matched? #=> true
স্ক্যান পয়েন্টার এখন রিকোয়েস্ট মেথডের ঠিক আগে আছে:
Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900 ^ ... Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
এখন আমরা scan_until ব্যবহার করে আসল ভ্যালু এক্সট্রাক্ট করতে পারি, যেটি পুরো রেগুলার এক্সপ্রেশন ম্যাচ রিটার্ন করে। যেহেতু রিকোয়েস্ট মেথড সকল অপারকেসে, আমরা একটি সিম্পল ক্যারেক্টার ক্লাস এবং + অপারেটর ব্যবহার করতে পারি যা এক বা একাধিক ক্যারেক্টার ম্যাচ করে:
log[:method] = scanner.scan_until(/[A-Z]+/) #=> "GET"
এই অপারেশনের পর স্ক্যান পয়েন্টার "GET" শব্দের শেষ "T"-তে থাকবে।
Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900 ^ ... Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
রিকোয়েস্টেড পাথ এক্সট্রাক্ট করতে, আমাদের একটি স্পেস স্কিপ করতে হবে এবং তারপর ডবল কোটের ভিতরের সব কিছু এক্সট্রাক্ট করতে হবে। এটাকে করার অনেক পদ্ধতি আছে, একটি হলো ক্যাপচার গ্রুপ (রেগুলার এক্সপ্রেশনের वो অংশ যা প্যারেনথেসিসে থাকা, অর্থাৎ (.+)) যা এক বা একাধিক যেকোনো ক্যারেক্টার ম্যাচ করে:
scanner.scan(/\s"(.+)"/) #=> " \"/\""
তবে, আমরা এই scan অপারেশনের রিটার্ন ভ্যালু সরাসরি ব্যবহার করব না, বরং captures ব্যবহার করব প্রথম ক্যাপচার গ্রুপের ভ্যালু পেতে:
log[:path] = scanner.captures.first #=> "/"
আমরা সফলভাবে পাথ এক্সট্রাক্ট করলাম এবং স্ক্যান পয়েন্টার এখন ক্লোজিং ডবল কোটে আছে:
Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900 ^ ... Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
লগ থেকে IP অ্যাড্রেস পার্স করতে, আমরা একবার আবার skip ব্যবহার করব "for" স্ট্রিংকে স্পেস দিয়ে ঘেরা হয়ে ইগনোর করতে এবং তারপর scan_until ব্যবহার করব এক বা একাধিক নন-হোয়াইটস্পেস ক্যারেক্টার ম্যাচ করার জন্য (\s হোয়াইটস্পেস রিপ্রেজেন্ট করে ক্যারেক্টার ক্লাস হিসেবে এবং [^\s] ইটস 네가েশন):
scanner.skip(/ for /) #=> 5 log[:ip] = scanner.scan_until(/[^\s]+/) #=> "127.0.0.1"
আপনি বলতে পারেন স্ক্যান পয়েন্টার এখন কোথায় থাকবে? এক মিনিট চিন্তা করুন এবং তারপর আপনার উত্তর সলিউশনের সাথে কমপেয়ার করুন:
Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900 ^ ... Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
টাইমস্ট্যাম্প পার্স করা এখন আপনার কাছে ফ্যামিলিয়ার লাগতে পারে। আমরা পুরানো বিশ্বস্ত skip ব্যবহার করব লিটারেল স্ট্রিং " at " ইগনোর করতে এবং তারপর scan_until ব্যবহার করব করেন্ট লাইনের শেষ পর্যন্ত পড়তে, যেটি রেগুলার এক্সপ্রেশনে $ দ্বারা রিপ্রেজেন্ট করা হয়:
scanner.skip(/ at /) #=> 4 log[:timestamp] = scanner.scan_until(/$/) #=> "2017-08-20 20:53:10 +0900"
আমরা যে পরবর্তী ইনফরমেশনে আগ্রহী সেটি হলো লাস্ট লাইনে HTTP স্ট্যাটাস কোড, তাই আমরা skip_until ব্যবহার করব আমাদের পুরো পথে "Completed" শব্দের পরে স্পেস পর্যন্ত নিয়ে যেতে।
scanner.skip_until(/Completed /) #=> 296
নাম অনুযায়ী এটি scan_until-এর মত কাজ করে কিন্তু ম্যাচড স্ট্রিং রিটার্ন করার বদলে স্কিপ করা ক্যারেক্টের সংখ্যা রিটার্ন করে। এতে স্ক্যান পয়েন্টার HTTP রেসপন্স কোডের ঠিক সামনে থাকে যেটি আমরা খুঁজছি।
Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900 ... Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms) ^
এখন HTTP রেসপন্স কোড স্ক্যান করার আগে, কি কুল হতো যদি আমরা বলতে পারতাম HTTP রেসপন্স কোড সাকসেস ডেনোট করে (এই উদাহরণের জন্য 2xx রেঞ্জে যেকোনো কোড) বা ফেইলার (অন্য সব রেঞ্জ)? এটা অর্জন করতে আমরা peek ব্যবহার করব পরবর্তী ক্যারেক্টার দেখতে, স্ক্যান পয়েন্টার ছাড়াই মুভ করেই।
log[:success] = scanner.peek(1) == "2" #=> true
এখন আমরা scan ব্যবহার করব পরবর্তী তিন ক্যারেক্টার পড়তে, যেটি রেগুলার এক্সপ্রেশন /\d{3}/ দ্বারা রিপ্রেজেন্ট করা হয়:
log[:response_code] = scanner.scan(/\d{3}/) #=> "200"
একবার আবার স্ক্যান পয়েন্টার আগে ম্যাচ করা রেগুলার এক্সপ্রেশনের শেষে থাকবে:
Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900 ... Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms) ^
আমরা লগ এন্ট্রি থেকে যে লাস্ট ইনফরমেশন এক্সট্রাক্ট করতে চাই সেটি হলো এক্সিকিউশন টাইম মিলিসেকেন্ডে, যেটি অর্জন করা যায় skip করে স্ট্রিং " OK in " ওভার এবং তারপর পড়া সব কিছু পর্যন্ত যেতে লিটারেল স্ট্রিং "ms" পর্যন্ত।
scanner.skip(/ OK in /) #=> 7 log[:duration] = scanner.scan_until(/ms/) #=> "79ms"
এই লাস্ট বিট যোগ করার সাথে, আমরা চাওয়া হ্যাশটি পেলাম।
{ method: "GET", path: "/" ip: "127.0.0.1", timestamp: "2017-08-20 20:53:10 +0900", success: true, response_code: "200", duration: "79ms", }
সারসংক্ষেপ
রুবির StringScanner একটি সুন্দর মিডিল গ্রাউন্ড আক্যুপাই করে সিম্পল রেগুলার এক্সপ্রেশন এবং একটি ফুল-ব্লোন লেক্সারের মধ্যে। এটি জটিল স্ক্যানিং এবং পার্সিং নিডসের জন্য বেস্ট চয়েস নন। কিন্তু এর সরল প্রকৃতি এটি বেসিক রেগুলার এক্সপ্রেশন জ্ঞানযুক্ত সবার জন্য সহজ করে দেয় ইনপুট স্ট্রিং থেকে ইনফরমেশন এক্সট্রাক্ট করতে এবং আমি এটিকে সফলভাবে প্রোডাকশন কোডে ব্যবহার করেছি অতীতে। আশা করি আপনি এই লুকানো জেম আবিষ্কার করবেন।
PS: আপনি কী মনে করেন লুকানো জেম আমরা পরবর্তীতে হাইলাইট করব সেটা জানান!