Ruby Magic-এর এই সংস্করণে, আমরা রুবিতে ফাইল স্ট্রিমিং সম্পর্কে শিখব, IO ক্লাস কীভাবে ফাইল পূর্ণভাবে মেমোরিতে লোড না করেই পড়ে, এবং কীভাবে এটি বাইট বাফার করে লাইন প্রতি লাইন ফাইল পড়ে। চলুন ডুব দিই!
"স্লার্পিং" এবং ফাইল স্ট্রিমিং
রুবির File.read মেথড একটি ফাইল পড়ে এবং এর পুরো কন্টেন্ট রিটার্ন করে।
irb> content = File.read("log/production.log")
=> "I, [2018-06-27T16:45:02.843719 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"\\/articles\\\" for 127.0.0.1 at 2018-06-27 16:45:02 +0200\\nI, [2018-06-27T16:45:02.846719 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Processing by ArticlesController#index as HTML\\nI, [2018-06-27T16:45:02.848212 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Rendering articles/index.html.erb within layouts/application\\nD, [2018-06-27T16:45:02.850020 #9098] DEBUG -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Article Load (0.3ms) SELECT \"articles\".* FROM \"articles\"\\nI, [2018-06-27T16:45:02.850901 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Rendered articles/index.html.erb within layouts/application (1.7ms)\\nI, [2018-06-27T16:45:02.851633 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Completed 200 OK in 5ms (Views: 3.4ms | ActiveRecord: 0.3ms)\\n"
অন্তরঙ্গভাবে, এটি ফাইলটি ওপেন করে, এর কন্টেন্ট পড়ে, ফাইলটি বন্ধ করে, এবং কন্টেন্টকে একটি সিঙ্গল স্ট্রিং হিসেবে রিটার্ন করে। ফাইলের কন্টেন্ট "স্লার্প" করে একবারে, এটি মেমোরিতে থাকে যতক্ষণ না রুবির গার্বেজ কালেক্টর দ্বারা ক্লিন আপ করা হয়।
উদাহরণস্বরূপ, ধরা যাক আমরা একটি ফাইলের সব ক্যারেক্টার আপারকেস করতে চাই এবং অন্য একটি ফাইলে লেখতে চাই। File.read ব্যবহার করে, আমরা কন্টেন্ট পেতে পারি, ফলাফল স্ট্রিংয়ে String#upcase কল করতে পারি, এবং আপারকেসড স্ট্রিংটি File.write-এ পাস করতে পারি।
irb> upcased = File.read("log/production.log").upcase
=> "I, [2018-06-27T16:45:02.843719 #9098] INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] STARTED GET \"\\/ARTICLES\\\" FOR 127.0.0.1 AT 2018-06-27 16:45:02 +0200\\nI, [2018-06-27T16:45:02.846719 #9098] INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] PROCESSING BY ARTICLESCONTROLLER#INDEX AS HTML\\nI, [2018-06-27T16:45:02.848212 #9098] INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] RENDERING ARTICLES/INDEX.HTML.ERB WITHIN LAYOUTS/APPLICATION\\nD, [2018-06-27T16:45:02.850020 #9098] DEBUG -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] ARTICLE LOAD (0.3MS) SELECT \"ARTICLES\".* FROM \"ARTICLES\"\\nI, [2018-06-27T16:45:02.850901 #9098] INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] RENDERED ARTICLES/INDEX.HTML.ERB WITHIN LAYOUTS/APPLICATION (1.7MS)\\nI, [2018-06-27T16:45:02.851633 #9098] INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] COMPLETED 200 OK IN 5MS (VIEWS: 3.4MS | ACTIVERECORD: 0.3MS)\\n"
irb> File.write("log/upcased.log", upcased)
=> 896
যদিও এটি ছোট ফাইলের জন্য কাজ করে, পুরো ফাইল মেমোরিতে পড়া বড় ফাইলের সাথে কাজ করার সময় সমস্যা সৃষ্টি করতে পারে। উদাহরণস্বরূপ, যখন একটি ১৪-গিগাবাইট লগ ফাইল পার্স করা হয়, পুরো ফাইল একবারে পড়া একটি ব্যয়বহুল অপারেশন হবে। ফাইলের কন্টেন্ট মেমোরিতে রাখা হয়, তাই অ্যাপের মেমোরি ফুটপ্রিন্ট উল্লেখযোগ্যভাবে বড় হয়। এটি শেষ পর্যন্ত মেমোরি সুয়াপিং এবং ওএস দ্বারা অ্যাপের প্রসেস কিল করার দিকে নিয়েতে পারে।
শুভভাবে, রুবি File.foreach ব্যবহার করে ফাইল লাইন বাই লাইন পড়ার অনুমতি দেয়। ফাইলের পুরো কন্টেন্ট একবারে পড়ার বদলে, এটি পাস করা ব্লকটি প্রতিটি লাইনের জন্য এক্সিকিউট করবে।
এর ফলাফল enumারেবল, তাই এটি প্রতিটি লাইনের জন্য একটি ব্লক yield করে, বা যদি কোনো ব্লক পাস না করা হয়, একটি Enumerator অবজেক্ট রিটার্ন করে। এটি বড় ফাইল পড়ার অনুমতি দেয় তাদের সকল কন্টেন্ট একসাথে মেমোরিতে লোড করা ছাড়া।
irb> File.foreach("log/production.log") { |line| p line }
"I, [2018-06-27T16:45:02.843719 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"\\/articles\\\" for 127.0.0.1 at 2018-06-27 16:45:02 +0200\\n"
"I, [2018-06-27T16:45:02.846719 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Processing by ArticlesController#index as HTML\\n"
"I, [2018-06-27T16:45:02.848212 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Rendering articles/index.html.erb within layouts/application\\n"
"D, [2018-06-27T16:45:02.850020 #9098] DEBUG -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Article Load (0.3ms) SELECT \"articles\".* FROM \"articles\"\\n"
"I, [2018-06-27T16:45:02.850901 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Rendered articles/index.html.erb within layouts/application (1.7ms)\\n"
"I, [2018-06-27T16:45:02.851633 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Completed 200 OK in 5ms (Views: 3.4ms | ActiveRecord: 0.3ms)\\n"
একটি পুরো ফাইল আপারকেস করতে, আমরা ইনপুট ফাইল থেকে লাইন বাই লাইন পড়ি, আপারকেস করি, এবং আউটপুট ফাইলে অ্যাপেন্ড করি।
irb> File.open("upcased.log", "a") do |output|
irb* File.foreach("production.log") { |line| output.write(line.upcase) }
irb> end
=> nil
তাহলে, পুরো ফাইল প্রথমে পড়া ছাড়া লাইন বাই লাইন পড়া কীভাবে কাজ করে? সেটা বুঝতে, আমাদের ফাইল পড়ার চারদিকের কিছু লেয়ার খুলতে হবে। চলুন রুবির IO ক্লাসে নজদিক থেকে তাকাই।
I/O এবং রুবির IO ক্লাস
যদিও File.read এবং File.foreach আছে, File ক্লাসের ডকুমেন্টেশন সেগুলো লিস্ট করে না। বরং, আপনি File ক্লাস ডকুমেন্টেশনে ফাইল পড়া বা লেখার কোনো মেথড খুঁজে পাবেন না, কারণ সেগুলো প্যারেন্ট IO ক্লাস থেকে ইনহেরিট করা।
I/O
একটি I/O ডিভাইস হলো একটি ডিভাইস যা কম্পিউটারের থেকে বা কম্পিউটারে ডেটা ট্রান্সফার করে, উদাহরণস্বরূপ কীবোর্ড, ডিসপ্লে এবং হার্ড ড্রাইভ। এটি ইনপুট/আউটপুট, বা I/O, ডেটা স্ট্রিম পড়ে বা প্রোডিউস করে পারফর্ম করে।
হার্ড ড্রাইভ থেকে ফাইল পড়া এবং লেখা সবচেয়ে সাধারণ I/O যেটি আপনি মুখোমুখি হবেন। অন্যান্য ধরনের I/O شامل সকেট কমিউনিকেশন, আপনার টার্মিনালে লগ আউটপুট এবং আপনার কীবোর্ড থেকে ইনপুট।
রুবিতে IO ক্লাস সকল ইনপুট এবং আউটপুট হ্যান্ডল করে যেন ফাইল পড়া এবং লেখা। কারণ ফাইল পড়া অন্য যেকোনো I/O স্ট্রিম পড়ার থেকে আলাদা নয়, File ক্লাস সরাসরি IO.read এবং IO.foreach এর মত মেথড ইনহেরিট করে।
irb> IO.foreach("log/production.log") { |line| p line }
"I, [2018-06-27T16:45:02.843719 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"\\/articles\\\" for 127.0.0.1 at 2018-06-27 16:45:02 +0200\\n"
"I, [2018-06-27T16:45:02.846719 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Processing by ArticlesController#index as HTML\\n"
"I, [2018-06-27T16:45:02.848212 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Rendering articles/index.html.erb within layouts/application\\n"
"D, [2018-06-27T16:45:02.850020 #9098] DEBUG -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Article Load (0.3ms) SELECT \"articles\".* FROM \"articles\"\\n"
"I, [2018-06-27T16:45:02.850901 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Rendered articles/index.html.erb within layouts/application (1.7ms)\\n"
"I, [2018-06-27T16:45:02.851633 #9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Completed 200 OK in 5ms (Views: 3.4ms | ActiveRecord: 0.3ms)\\n"
File.foreach IO.foreach-এর সমতুল্য, তাই IO ক্লাস ভার্সনটি ব্যবহার করা যায় একই ফলাফল পেতে যা আমরা আগে পেয়েছিলাম।
কার্নেলের মাধ্যমে I/O স্ট্রিম পড়া
অন্তরঙ্গভাবে, রুবির IO ক্লাসের পড়া এবং লেখার ক্ষমতা কার্নেল সিস্টেম কলের চারদিকে অ্যাবস্ট্রাকশন উপর ভিত্তি করে। অপারেটিং সিস্টেমের কার্নেল I/O ডিভাইস থেকে পড়া এবং লেখার যত্ন নেয়।
ফাইল খোলা
IO.sysopen একটি ফাইল খোলে কার্নেলকে ফাইল টেবিলে ফাইলের একটি রেফারেন্স রাখতে বলে এবং প্রসেসের ফাইল ডেস্ক্রিপ্টর টেবিলে একটি ফাইল ডেস্ক্রিপ্টর তৈরি করে।
ফাইল ডেস্ক্রিপ্টর এবং ফাইল টেবিল
একটি ফাইল খোলা একটি ফাইল ডেস্ক্রিপ্টর রিটার্ন করে — একটি ইন্টিজার যা I/O রিসোর্স এক্সেস করতে ব্যবহৃত হয়।
প্রতিটি প্রসেসের নিজস্ব ফাইল ডেস্ক্রিপ্টর টেবিল থাকে ফাইল ডেস্ক্রিপ্টরগুলো মেমোরিতে রাখতে, এবং প্রতিটি ডেস্ক্রিপ্টর সিস্টেম-ব্যাপী ফাইল টেবিল-এ একটি এন্ট্রি নির্দেশ করে।
একটি I/O রিসোর্স থেকে পড়তে বা লিখতে, প্রসেস ফাইল ডেস্ক্রিপ্টরটি কার্নেলে একটি সিস্টেম কল দিয়ে পাস করে। কার্নেল তারপর প্রসেসের نمایند্যে ফাইলটি এক্সেস করে, কারণ প্রসেসগুলোর ফাইল টেবিলে এক্সেস নেই।
ফাইল খোলা নয় তাদের কন্টেন্ট মেমোরিতে রাখে, কিন্তু ফাইল ডেস্ক্রিপ্টর টেবিলটি ফুল হয়ে যেতে পারে, তাই ফাইল খোলার পর সবসময় বন্ধ করা একটি ভালো অভ্যাস। File.open র্যাপ করা মেথডগুলো যেন File.read এটি অটোমেটিক্যালি করে, সেইসাথে ব্লক নেওয়া মেথডগুলোও।
এই উদাহরণে, আমরা এক ধাপ возраত যাব IO.sysopen মেথডটি সরাসরি কল করে। একটি ফাইলনেম পাস করলে, মেথডটি একটি ফাইল ডেস্ক্রিপ্টর তৈরি করে যা আমরা পরে খোলা ফাইল রেফারেন্স করতে পারি।
irb> IO.sysopen("log/production.log")
=> 9
IO ইনস্ট্যান্স তৈরি করতে রুবি পড়তে এবং লিখতে পারবে, আমরা ফাইল ডেস্ক্রিপ্টরটি IO.new-এ পাস করি।
irb> file_descriptor = IO.sysopen("log/production.log")
=> 9
irb> io = IO.new(file_descriptor)
=> #
একটি I/O স্ট্রিম বন্ধ করতে এবং ফাইল টেবিল থেকে ফাইলের রেফারেন্স সরাতে, আমরা IO ইনস্ট্যান্সে IO#close কল করি।
irb> io.close
=> nil
বাইট পড়া এবং কার্সর সরানো
IO#sysread একটি IO অবজেক্ট থেকে বাইটের সংখ্যা পড়ে।
irb> io.sysread(64)
=> " [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"\\/articles\\\" "
এই উদাহরণে আমরা আগে তৈরি করা IO ইনস্ট্যান্সটি ব্যবহার করলাম ফাইল ডেস্ক্রিপ্টর ইন্টিজার IO.new-এ পাস করে। এটি ফাইল থেকে প্রথম ৬৪ বাইট পড়ে এবং রিটার্ন করে IO#sysread ৬৪ আর্গুমেন্ট হিসেবে কল করে।
irb> io.sysread(64)
=> "for 127.0.0.1 at 2018-06-27 16:45:02 +0200\\nI, [2018-06-27T16:45:"
প্রথমবার আমরা ফাইল থেকে বাইট রিকোয়েস্ট করলে, কার্সরটি অটোমেটিক্যালি সরিয়ে দেয়া হয়, তাই একই ইনস্ট্যান্সে আবার IO#sysread কল করলে ফাইলের পরবর্তী ৬৪ বাইট প্রোডিউস হবে।
কার্সর সরানো
IO.sysseek ম্যানুয়ালি ফাইলে একটি লোকেশনে কার্সর সরায়।
irb> io.sysseek(32)
=> 32
irb> io.sysread(64)
=> "9098] INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started "
irb> io.sysseek(0)
=> 0
irb> io.sysread(64)
=> " [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"\\/articles\\\" "
এই উদাহরণে, আমরা পজিশন ৩২-এ সরিয়ে, তারপর IO#sysread ব্যবহার করে ৬৪ বাইট পড়ি। IO.sysseek আবার ০ দিয়ে কল করলে, আমরা ফাইলের শুরুতে ফিরে যাই, আমাদের প্রথম ৬৪ বাইট আবার পড়তে দেয়।
ফাইল লাইন বাই লাইন পড়া
এখন, আমরা জানি IO ক্লাসের কনভেনিয়েন্স মেথডগুলো কীভাবে I/O স্ট্রিম খোলে, সেগুলো থেকে বাইট পড়ে এবং কীভাবে সেগুলো কার্সর পজিশন সরায়।
IO.foreach এবং IO#gets এর মত মেথডগুলো বাইটের সংখ্যা অনুযায়ী না হয়ে লাইন বাই লাইন রিকোয়েস্ট করতে পারে। পরবর্তী নিউলাইন খুঁজে বের করে সেই পজিশন পর্যন্ত সব বাইট নেওয়ার কোনো পারফরম্যান্ট উপায় নেই, তাই রুবি ফাইলের কন্টেন্ট স্প্লিট করার যত্ন নেয়।
class MyIO
def initialize(filename)
fd = IO.sysopen(filename)
@io = IO.new(fd)
end
def each(&block)
line = ""
while (c = @io.sysread(1)) != $/
line << c
end
block.call(line)
each(&block)
rescue EOFError
@io.close
end
end
এই ইমপ্লিমেন্টেশনে, #each মেথড ফাইল থেকে IO#sysread ব্যবহার করে একবারে এক বাইট নেয়, যতক্ষণ না বাইটটি $/, যা নিউলাইন ইন্ডিকেট করে। যখন এটি একটি নিউলাইন খুঁজে পায়, এটি বাইট নেওয়া বন্ধ করে এবং পাস করা ব্লককে সেই লাইন সহ কল করে।
এই সমাধান কাজ করে কিন্তু অক্ষম কারণ এটি ফাইলের প্রতিটি বাইটের জন্য IO.sysread কল করে।
ফাইল কন্টেন্ট বাফার করা
রুবি এই কাজটি কীভাবে করে সেটা বুঝতে স্মার্ট, ফাইলের কন্টেন্টের একটি ইন্টারনাল বাফার রাখে। ফাইলটি একবারে এক বাইট পড়ার বদলে, এটি ৫১২ বাইট একবারে নেয় এবং চেক করে রিটার্নড বাইটগুলোর মধ্যে কোনো নিউলাইন আছে কিনা। যদি থাকে, এটি নিউলাইনের আগের পোর্শনটি রিটার্ন করে এবং বাকিটাকে মেমোরিতে বাফার হিসেবে রাখে। যদি বাফারে নিউলাইন না থাকে, এটি নিউলাইন পাওয়া পর্যন্ত ৫১২ বাইট আরও ফেচ করে।
class MyIO
def initialize(filename)
fd = IO.sysopen(filename)
@io = IO.new(fd)
@buffer = ""
end
def each(&block)
@buffer << @io.sysread(512) until @buffer.include?($/)
line, @buffer = @buffer.split($/, 2)
block.call(line)
each(&block)
rescue EOFError
@io.close
end
end
এই উদাহরণে, #each মেথড ইন্টারনাল @buffer ভেরিয়েবলে ৫১২ বাইটের চান্কে বাইট যোগ করে যতক্ষণ না @buffer ভেরিয়েবলে একটি নিউলাইন থাকে। যখন সেটি হয়, এটি বাফারকে প্রথম নিউলাইন দিয়ে স্প্লিট করে। প্রথম পোর্শন হলো line, এবং দ্বিতীয় পোর্শন হলো নতুন বাফার।
পাস করা ব্লকটি তারপর লাইনসহ কল করা হয় এবং অবশিষ্ট @buffer পরবর্তী লুপের জন্য রাখা হয়।
ফাইলের কন্টেন্ট বাফার করে, I/O কলের সংখ্যা কমানো হয় যখন ফাইলকে লজিক্যাল চান্কে ভাগ করা হয়।
ফাইল স্ট্রিমিং
সারাংশে, ফাইল স্ট্রিমিং কাজ করে অপারেটিং সিস্টেমের কার্নেলকে একটি ফাইল খোলতে বলে, তারপর সেটি থেকে বাইট বাইট করে পড়ে। রুবিতে ফাইল লাইন প্রতি লাইন পড়ার সময়, ডেটা ফাইল থেকে ৫১২ বাইট একবারে নেওয়া হয় এবং তারপর "লাইন"গুলোর মধ্যে স্প্লিট করা হয়।
এটি রুবিতে I/O এবং ফাইল স্ট্রিমিং-এর আমাদের ওভারভিউ সমাপ্ত করে। আমরা জানতে চাই এই আর্টিকেল সম্পর্কে আপনি কী ভাবেন, অথবা আপনার কোনো প্রশ্ন আছে কিনা। আমরা সবসময় টপিক ইনভেস্টিগেট এবং এক্সপ্লেইন করার জন্য তত্পর, তাই যদি রুবিতে কোনো জাদুই জিনিস থাকে যা আপনি পড়তে চান, @AppSignal-এ আমাদের জানাতে সংকোচ করবেন না!