रुबी वेब स्क्रेपिंग: एक व्यावहारिक गाइड
Advanced Data Extraction Specialist
TL;DR:
- रूबी की फेच-एंड-पार्स कहानी सेटअप के दो पंक्तियाँ हैं।
Net::HTTPमानक पुस्तकालय से plus Nokogiri सर्वर-निर्मित पृष्ठों को पूरी तरह से कवर करता है। - Nokogiri CSS चयनकर्ताओं को लेता है, इसलिए अधिकांश उदाहरण सीधे पोर्ट होते हैं।
doc.css("div.quote")वही व्यवहार करता है जैसा कि एक ब्राउज़र कंसोल में वही चयनकर्ता करता है। - स्क्रेपलेस यूनिवर्सल स्क्रेपिंग एपीआई JSON लिफाफा के साथ उत्तर देता है। मार्कअप
dataके अंदर आता है, इसलिए आप पहलेJSONको पार्स करते हैं और बाद में HTML। - फेरम डिब्बा से बाहर एक TLS ब्राउज़र एंडपॉइंट तक नहीं पहुँच सकता, और कारण एक गायब पंक्ति है। यह
hostnameसेट किए बिना अपना CDP सॉकेट बनाता है, इसलिए कोई SNI नहीं भेजा जाता है और SNI-निर्भर होस्ट हैंडशेक को अस्वीकार कर देता है। पृथक में पुष्टि की गई: समान सॉकेट, कोई SNI विफल होता है, SNIscrapeless.comके लिए एक प्रमाणपत्र के खिलाफ सफल होता है। - एक पंद्रह-पंक्ति पैच इसे काम करता है। SNI प्रदान करने पर, वही स्क्रिप्ट
title: Quotes to Scrapeऔरquotes on page: 10लौटाती है। - मुफ्त शुरुआत करें: स्क्रेपलेस डैशबोर्ड एक कुंजी जारी करता है जो नीचे दिए गए हर उदाहरण के साथ काम करती है।
आपको क्या चाहिए
नीचे सब कुछ रूबी 3.2.3 पर quotes.toscrape.com के खिलाफ चलाया गया, एक साइट जिसे स्क्रेपिंग प्रैक्टिस के लिए प्रकाशित किया गया था।
bash
gem install nokogiri ferrum --no-document
# nokogiri 1.19.4 · ferrum 0.17.2
Net::HTTP, URI और JSON मानक पुस्तकालय हैं, इसलिए असली निर्भरताएँ केवल पार्सर और बाद में ब्राउज़र ड्राइवर हैं।
आप एक संस्करण बैनर लिखने से पहले जानने लायक एक नामकरण विशेषता है: Nokogiri Nokogiri::VERSION को उजागर करता है, लेकिन फेरम कोई Ferrum::VERSION निरंतरता परिभाषित नहीं करता है। इसका संदर्भ लेना NameError: uninitialized constant Ferrum::VERSION उठाता है। इसके बजाय Gem.loaded_specs["ferrum"].version से संस्करण पढ़ें।
फेचिंग और पार्सिंग
ruby
require "net/http"
require "uri"
require "nokogiri"
uri = URI("https://quotes.toscrape.com/")
res = Net::HTTP.get_response(uri)
puts "http=#{res.code} bytes=#{res.body.bytesize}"
doc = Nokogiri::HTML(res.body)
quotes = doc.css("div.quote")
puts "quotes=#{quotes.size}"
puts "first_author=#{quotes.first.at_css('small.author').text}"
puts "first_text=#{quotes.first.at_css('span.text').text[0, 40]}"
text
http=200 bytes=11064
quotes=10
first_author=Albert Einstein
first_text=“The world as we have created it is a pr
दो आदतें तुरंत भुगतान करती हैं। css एक नोड सेट लौटाता है और at_css पहले मिलान या nil लौटाता है, इसलिए जब भी आप एक तत्व चाहते हैं - उसके लिए at_css को आगे बढ़ाएं — यह nil और एक खाली सेट के बीच का अंतर है जब एक क्षेत्र गायब है, और nil गलती के बिंदु पर जोर से विफल होता है।
बच्चा प्रश्नों को पंक्ति तक सीमित करें। quotes.first.at_css('span.text') उस नोड के अंदर खोजता है, जबकि एक लूप में doc.at_css की कॉल प्रत्येक पुनरावृत्ति पर पहले उद्धरण के पाठ को लौटाती है। यह एक विश्वसनीय दिखने का डेटा सेट बनाता है जहाँ हर पंक्ति वही मान लिए हुए है, जो एक क्रैश की तुलना में कहीं अधिक बुरा है।
Nokogiri का दस्तावेज़ीकरण XPath समकक्षों को कवर करता है यदि आप उन्हें पसंद करते हैं; doc.xpath("//div[@class='quote']") दूसरे बोली में वही क्वेरी है।
जहाँ प्लेन रूबी रुकता है
स्क्रिप्ट काम करती है क्योंकि लक्षित सर्वर-साइड को रेंडर करता है और अपने कॉलर को स्क्रीन नहीं करता है। दो स्थितियाँ इसे समाप्त करती हैं: सामग्री जो केवल JavaScript चलने के बाद मौजूद होती है, और साइटें जो एक नग्न HTTP क्लाइंट को एक बोट के रूप में मानती हैं। न तो एक रूबी की सीमा है - किसी भी भाषा में कोई HTTP क्लाइंट इनमें से कोई भी हल नहीं करता है।
यहाँ से वृद्धि अलग उपकरण हैं न कि एक दूसरे के बेहतर संस्करण, और प्लेन पथ सबसे तेज़ और सस्ता बना रहता है जहाँ यह काम करता है। यदि आपको यह जानने की आवश्यकता है कि एक ब्राउज़र क्या जोड़ता है, तो ब्राउज़र ऑटोमेशन स्पष्टीकरण सामान्य आकार को कवर करता है।
वृद्धि एक: यूनिवर्सल स्क्रेपिंग एपीआई
यह आपके कोड को एक सामान्य HTTP क्लाइंट बनाए रखता है और कठिनाई को सर्वर-साइड स्थानांतरित करता है।
ruby
require "net/http"
require "uri"
require "json"
require "nokogiri"
key = ENV.fetch("SCRAPELESS_API_KEY")
api = URI("https://api.scrapeless.com/api/v2/unlocker/request")
req = Net::HTTP::Post.new(api, "Content-Type" => "application/json", "x-api-token" => key)
req.body = JSON.dump(
actor: "unlocker.webunlocker",
input: { url: "https://quotes.toscrape.com/", js_render: false }
)
resp = Net::HTTP.start(api.hostname, api.port, use_ssl: true, read_timeout: 90) { |h| h.request(req) }
body = JSON.parse(resp.body)
puts "http=#{resp.code} envelope_keys=#{body.keys.join(',')}"
puts "quotes=#{Nokogiri::HTML(body['data']).css('div.quote').size}"
text
http=200 envelope_keys=code,data
quotes=10
लिफाफा आंतरिककरण का भाग है। resp.body JSON है; मार्कअप body["data"] पर रहता है। resp.body को सीधे Nokogiri::HTML को सौंपने से बिना मेल खाने वाले नोड्स वाला एक दस्तावेज़ प्राप्त होता है और कुछ नहीं उठाता - चयनकर्ता शांतिपूर्वक शून्य पंक्तियाँ लौटाते हैं। JSON को पार्स करें, data लें, फिर HTML को पार्स करें, और उस अनरैप को एक विधि में बनाए रखें बजाय इसके कि JSON.parse(...)["data"] को कोडबेस में बिखेर दें।
Net::HTTP.start(..., use_ssl: true) का उल्लेख करें न कि छोटे Net::HTTP.post का। एक https URI के खिलाफ use_ssl को छोड़ना एक सामान्य रूबी ठोकर है जो एक भ्रमित कनेक्शन रीसेट के रूप में उभरती है बजाय एक स्पष्ट त्रुटि के।
वृद्धि दो: एक असली ब्राउज़र, और एक जेम बग
फेरम रूबी का क्रोम डिव टूल्स प्रोटोकॉल ड्राइवर है, और यह एक ws_url: को स्वीकार करता है जिसे आप ने लॉन्च नहीं किया। एक TLS एंडपॉइंट की ओर इशारा करते हुए, यह विफल होता है:
text
OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0 peeraddr=172.67.74.62:443
state=error: sslv3 alert handshake failure
यह संदेश गलत लेयर का नाम देता है। यह कोई प्रमाणपत्र समस्या नहीं है, कोई सिफर समस्या नहीं है, या कोई प्रॉक्सी समस्या नहीं है।
वास्तविक कारण
फेरम 0.17.2 अपने CDP सॉकेट को lib/ferrum/client/web_socket.rb में इस तरह से बनाता है:
ruby
tcp = TCPSocket.new(uri.host, port)
ssl_context = OpenSSL::SSL::SSLContext.new
@sock = OpenSSL::SSL::SSLSocket.new(tcp, ssl_context)
@sock.sync_close = true
@sock.connect
OpenSSL::SSL::SSLSocket#hostname कभी असाइन नहीं होता, इसलिए रूबी कोई सर्वर नाम संकेत विस्तार नहीं भेजता। एक होस्ट जो एक पते से कई प्रमाणपत्र प्रदान करता है, एक नहीं चुन सकता, और हैंडशेक विफलता के साथ उत्तर देता है।
बीस पंक्तियाँ साधारण रूबी इसे अलग करती हैं - समान सॉकेट, एक पंक्ति भिन्न:
ruby
require "socket"
require "openssl"
HOST = "browser.scrapeless.com"
def attempt(sni)
tcp = TCPSocket.new(HOST, 443)
sock = OpenSSL::SSL::SSLSocket.new(tcp, OpenSSL::SSL::SSLContext.new)
sock.hostname = HOST if sni # the line ferrum omits
sock.sync_close = true
sock.connect
cn = sock.peer_cert.subject.to_a.find { |a| a[0] == "CN" }
result = "OK cert_cn=#{cn && cn[1]}"
sock.close
result
rescue => e
"FAIL #{e.class}: #{e.message.to_s[0, 70]}"
end
puts "without SNI: #{attempt(false)}"
puts "with SNI: #{attempt(true)}"
text
without SNI: FAIL OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0 peeraddr=172.67.74.62:443
with SNI: OK cert_cn=scrapeless.com
यही सम्पूर्ण बग है। कोई भी रूबी क्लाइंट जो hostname= को छोड़ देता है, उसे किसी भी SNI-निर्भर अंतिम बिंदु पर टकराता है, जो कि आजकल इनमें से अधिकांश हैं।
एक लाल हरिंग जिसका नाम लेना उचित है
फेरम में एक दूसरा संदिग्ध व्यवहार है, और यह कारण नहीं है। Ferrum::Browser::Process.parse_json_version आपके ws_url पर URI.join(url, "/json/version") चलाता है, जो पथ को बदल देता है और क्वेरी स्ट्रिंग को हटा देता है। एक wss:// URL जिसका पथ /api/v2/browser है और जिसकी क्वेरी आपके टोकन को ले जाती है, उसे दस्तावेज़ मूल के साथ /json/version पर फिर से लिखा जाता है, जिससे क्रेडेंशियल पूरी तरह से हट जाती है। वह फिर से लिखा गया पता यहाँ 404 page not found का उत्तर देता है।
यह गंभीर लगता है और ऐसा नहीं है: विधि JSON::ParserError को बचाती है, और एक 404 बॉडी मान्य JSON नहीं है, इसलिए विफलता निगल ली जाती है। केवल SNI को ठीक करना पर्याप्त है — नीचे का पैच रन उस प्रॉब पर अभी भी 404 करता है और वैसे भी काम करता है। यह जानना महत्वपूर्ण है ताकि आप इस पर एक अपराह्न न बिताएँ, जैसा कि इस लेख का पहला निदान किया गया था।
इसे काम करने के लिए बनाना
CDP सॉकेट आमतौर पर इस प्रकार के स्क्रेपर द्वारा खोले गए единिक TLS सॉकेट में से एक ही होता है, इसलिए निर्माण के समय नाम प्रदान करना पर्याप्त है:
ruby
require "ferrum"
require "openssl"
module SNIPatch
def connect
self.hostname = @ferrum_sni if @ferrum_sni && respond_to?(:hostname=)
super
end
end
class OpenSSL::SSL::SSLSocket
prepend SNIPatch
def ferrum_sni=(host)
@ferrum_sni = host
end
end
module SSLSocketFactoryPatch
def new(io, ctx = nil)
sock = super
sock.ferrum_sni = Thread.current[:ferrum_sni_host]
sock
end
end
class << OpenSSL::SSL::SSLSocket
prepend SSLSocketFactoryPatch
end
key = ENV.fetch("SCRAPELESS_API_KEY")
Thread.current[:ferrum_sni_host] = "browser.scrapeless.com"
browser = Ferrum::Browser.new(
ws_url: "wss://browser.scrapeless.com/api/v2/browser?token=#{key}",
timeout: 60,
process_timeout: 60
)
browser.go_to("https://quotes.toscrape.com/")
puts "title: #{browser.evaluate('document.title')}"
puts "quotes on page: #{browser.evaluate('document.querySelectorAll(".quote").length')}"
browser.quit
text
title: Quotes to Scrape
quotes on page: 10
दायरे पर दो नोट्स। पैच OpenSSL::SSL::SSLSocket के लिए वैश्विक है, जो एक एकल-उद्देश्य वाले स्क्रेपर में स्वीकार्य है और न तो कुछ ऐसा है जिसे किसी अन्य TLS सॉकेट को खोलने वाले Rails अनुप्रयोग में लोड किया जाए - वहाँ, इसे उस प्रक्रिया तक सीमित रखें जो ब्राउज़र को चलाती है। और अपस्ट्रीम सुधार जेम में एक ही पंक्ति है, इसलिए यह जांचें कि क्या 0.17.2 के बाद कोई रिलीज उस पर आई है या नहीं, इससे पहले कि आप अपना पैच ले जाएँ।
तीन के बीच चयन करना
| दृष्टिकोण | उपयोग कब करें | लागत |
|---|---|---|
Net::HTTP + नोकोगिरी |
सर्वर-जनित HTML, अनुमति देने वाला लक्षित | सबसे कम; एक जेम |
| यूनिवर्सल स्क्रेपिंग एपीआई | अवरुद्ध या चुनौती दी गई, कोई JS आवश्यक नहीं | एक HTTP कॉल, लिफाफा खोलने के लिए |
| फेरम + एक दूरस्थ ब्राउज़र | सामग्री को JavaScript निष्पादन की आवश्यकता है | सबसे अधिक; प्रति नौकरी एक सत्र, साथ ही SNI पैच |
सूची को नीचे की ओर काम करें न कि ऊपर। एक पृष्ठ जो एक ब्राउज़र की आवश्यकता प्रतीत होता है, अक्सर अपने डेटा को <script> टैग में एम्बेड करता है, और doc.at_css("script#__NEXT_DATA__") JSON.parse के साथ हर दिशा में एक ब्राउज़र सत्र को पराजित करता है।
निष्कर्ष
रूबी स्क्रेपिंग को अच्छी तरह से संभालती है, और मानक पुस्तकालय इससे अधिक है जितना लोग अपेक्षित करते हैं। Net::HTTP और नोकोगिरी सर्वर-जनित पृष्ठों को कवर करते हैं, इसके साथ at_css बनाम css और पंक्ति-स्कोप वाले चाइल्ड क्वेरीज़ जैसे दो विवरण सही निकासी को एक डेटासेट से अलग करते हैं जो ठीक दिखता है और ऐसा नहीं है।
ब्राउज़र पथ वह है जहाँ रूबी वर्तमान में अपने पड़ोसियों की तुलना में अधिक लागत लगाता है, और इसका कारण संकीर्ण है न कि मौलिक: एक जेम में एक असंरक्षित विशेषता, एक त्रुटि संदेश पैदा करती है जो SNI के बजाय TLS की ओर इंगित करती है। उपरोक्त अलगाव स्क्रिप्ट इसे बीस पंक्तियों में उत्तर देती है, और पैच इतना छोटा है कि उसे तब तक ले जाया जा सके जब तक अपस्ट्रीम वह एक-लाइन का सुधार शिप न करे।
रूबी के साथ स्क्रैप करने के लिए तैयार?
स्क्रापलेस डैशबोर्ड पर एक कुंजी बनाएँ और एक पृष्ठ के खिलाफ Net::HTTP उदाहरण चलाएँ जिसे आप पहले से इकट्ठा करते हैं। यदि यह वही लौटाता है जिसकी आप अपेक्षा करते हैं, तो आप समाप्त हैं - एक जेम, कोई ब्राउज़र नहीं। यूनिवर्सल स्क्रेपिंग एपीआई उन पृष्ठों को कवर करता है जहाँ यह नहीं करता है, और वर्तमान दरें मूल्य निर्धारण पृष्ठ पर हैं।
सामान्य प्रश्न
प्रश्न: नोकोगिरी या कोई वैकल्पिक पार्सर?
नोकोगिरी HTML के लिए डिफ़ॉल्ट है। यह CSS चयनकर्ताओं और XPath दोनों को लेता है, अच्छी तरह से प्रलेखित है, और दोषपूर्ण मार्कअप को संभालता है। हल्के शुद्ध-रूबी पार्सर मौजूद हैं और केवल तब विचार करने के योग्य हैं जब मूलभूत एक्सटेंशन आपके डिप्लॉयमेंट में एक समस्या हो।
प्रश्न: मेरा फेरम कनेक्शन SSL हैंडशेक त्रुटि के साथ क्यों विफल होता है?
क्योंकि फेरम 0.17.2 अपने OpenSSL::SSL::SSLSocket पर hostname सेट नहीं करता है, इसलिए कोई SNI भेजा नहीं जाता है और SNI-निर्भर अंतिम बिंदु एक प्रमाणपत्र का चयन नहीं कर सकता। उपरोक्त बीस-पंक्ति स्क्रिप्ट के साथ इसे अलगाव में पुन: उत्पन्न करें, फिर पैच लागू करें या अपस्ट्रीम सुधार की प्रतीक्षा करें।
प्रश्न: जब एपीआई कॉल स्पष्ट रूप से सफल हो गया हो, तो मेरा पार्सिंग कुछ क्यों नहीं लौटाता है?
आप लिफाफा पार्स कर रहे हैं। प्रतिक्रिया JSON है जिसमें मार्कअप data के अंदर है, इसलिए नोकोगिरी को JSON स्ट्रिंग मिलती है, कुछ भी नहीं मिलाती है, और कुछ भी उठाती नहीं है। पहले JSON पार्स करें और body["data"] को नोकोगिरी को सौंपें।
प्रश्न: क्या Ferrum::VERSION मौजूद है?
नहीं। यह NameError: uninitialized constant Ferrum::VERSION उत्पन्न करता है। जब आप संस्करण को लॉग करना चाहते हैं तो Gem.loaded_specs["ferrum"].version का उपयोग करें।
प्रश्न: क्या रूबी बड़े स्क्रेपिंग कार्यों के लिए एक उचित विकल्प है?
निष्कर्ष निकालने और पार्स करने के काम के लिए, हाँ — Nokogiri एक तेज़ मूल पार्सर है और थ्रेड्स IO-आधारित समवर्तीता को अच्छी तरह संभालते हैं। कमजोर क्षेत्र ब्राउज़र स्वचालन है, जहाँ चारों ओर की टूलिंग Python या Node की तुलना में पतली है, जैसा कि उपरोक्त SNI मुद्दा दर्शाता है। एक सामान्य विभाजन HTTP रास्ते के लिए Ruby और उन पृष्ठों के लिए एक होस्टेड ब्राउज़र है जिन्हें एक की आवश्यकता होती है।
स्क्रैपलेस में, हम केवल सार्वजनिक रूप से उपलब्ध डेटा का उपयोग करते हैं, जबकि लागू कानूनों, विनियमों और वेबसाइट गोपनीयता नीतियों का सख्ती से अनुपालन करते हैं। इस ब्लॉग में सामग्री केवल प्रदर्शन उद्देश्यों के लिए है और इसमें कोई अवैध या उल्लंघन करने वाली गतिविधियों को शामिल नहीं किया गया है। हम इस ब्लॉग या तृतीय-पक्ष लिंक से जानकारी के उपयोग के लिए सभी देयता को कोई गारंटी नहीं देते हैं और सभी देयता का खुलासा करते हैं। किसी भी स्क्रैपिंग गतिविधियों में संलग्न होने से पहले, अपने कानूनी सलाहकार से परामर्श करें और लक्ष्य वेबसाइट की सेवा की शर्तों की समीक्षा करें या आवश्यक अनुमतियाँ प्राप्त करें।



