') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - adwinsky/skyscraper: DSL for scrapping content from remote and local websites · GitHub
Skip to content

Repository files navigation

Installation

Skyscraper installation is simple, just run:

gem install skyscraper

or add following entry to your gemfile:

gem "skyscraper"

if you want to use it in your rails project.

Finding nodes by CSS Selectors

>> Skyscraper::fetch("http://rubyonrails.org").first("title").text# => "Ruby on Rails"
>> Skyscraper::fetch("http://rubyonrails.org").first(".copyright p").text# => "\\"Rails\\", \\"Ruby on Rails\\", and the Rails logo are registered trademarks of David Heinemeier Hansson. All rights reserved."

You can use this thanks to Nokogiri#css method.

Reading HTML attributes

>> Skyscraper::fetch("http://rubyonrails.org").first(".announce").class# => "announce"
>> Skyscraper::fetch("http://rubyonrails.org").first("img").height# => "112"
>> Skyscraper::fetch("http://rubyonrails.org").first(".copyright").style# => "margin-top: 20px"

Notice!

Skyscraper::Node::Base#class method is overriden, to access original class method, please call Skyscraper::Node::Base#original_class

You can find list of all available methods in Reading attributes Section

Using Skyscraper as included module

Fetch content from multiple pages and store it in the active record database is a common problem. You can do this quick, using Skyscraper as included module.

classSampleincludeSkyscrapersettingslimit: 10,delay: {after: 5,time: 1},encoding: "utf-8"pages["http://google.com","https://github.com","http://rubyonrails.org"]# pages method also accepts blocks as argument, then you can use Skyscraper::fetch method inside to get list of pages from website more dynamicallyfield:html,"html",:htmlfield:title,"title"do |node|
"'#{node.text}'"endfield:first_link,"body"do |node|
"'#{node.first("a").href}'"endfield:first_image,"img",:download# field method takes following arguments: # field_name => name that the record will have in the results table# selector => css selector of fetching element, so it can even looks like "tag #id.some_class"# optionally symbol with the node method or block, if nothing is provided, text method on the node is firedafter_eachdo |result|
page=Page.newpage.title=result[:title]page.html=result[:html]page.first_link=result[:first_link]page.first_image_path=results[:first_image]page.saveendafter_alldoputs"Job done"endendSample.new.fetch#this will run above code applying provided callbacks and returns array with results

You will find more details in Including section.

Traversing

Traversing through Skyscraper nodes is very similar to the way jQuery provides.

>> Skyscraper::fetch("https://github.com").first(".top-nav").find("li").map(&:html)# => ["<a href="https://github.com/plans">Signup and Pricing</a>", "<a href="https://github.com/explore">Explore GitHub</a>", "<a href="https://github.com/features">Features</a>", "<a href="https://github.com/blog">Blog</a>", "<a href="https://github.com/login">Login</a>"]

Of course you can write the same code in the easier way:

>> Skyscraper::fetch("https://github.com").find(".top-nav li").map(&:html)# => ["<a href=\\"https://github.com/plans\\">Signup and Pricing</a>", "<a href=\\"https://github.com/explore\\">Explore GitHub</a>", "<a href=\\"https://github.com/features\\">Features</a>", "<a href=\\"https://github.com/blog\\">Blog</a>", "<a href=\\"https://github.com/login\\">Login</a>"]

or even:

>> Skyscraper::fetch("https://github.com").find(".top-nav li a").map(&:content)# => ["<a href=\\"https://github.com/plans\\">Signup and Pricing</a>", "<a href=\\"https://github.com/explore\\">Explore GitHub</a>", "<a href=\\"https://github.com/feature\s\">Features</a>", "<a href=\\"https://github.com/blog\\">Blog</a>", "<a href=\\"https://github.com/login\\">Login</a>"]

Read more about traversing in Traversing section

Following

You can quickly follow node element if it have href attribute:

>> Skyscraper::fetch("https://github.com").first(".top-nav li a").follow.first("title").html# => "Plans &amp; Pricing · GitHub"

This example visits first menu item from github.com page, and then fetch title of it.

Downloading

When node element have src or href attribute, you can easily download it:

>> Skyscraper::fetch("http://rubyonrails.org").first(".message img").download# => "/tmp/skyscraper/1/rails.png"

You can either provide download path and new file name in arguments. Default path is also available to set in configuration.

>> Skyscraper::fetch("http://rubyonrails.org").first(".message img").download(path: "/tmp/test/:sequence/:file_name")# => "/tmp/test/1/rails.png"
>> Skyscraper::fetch("http://rubyonrails.org").first(".message img").download(path: "/tmp/test/:sequence/:file_name")# => "/tmp/test/2/rails.png"
>> Skyscraper::fetch("http://rubyonrails.org").first(".message img").download(path: "/tmp/test/my_file.png")# => "/tmp/test/my_file.png"
>> Skyscraper.config.download_path="/tmp/test/my_path_from_config/:file_name"# => "/tmp/test/my_path_from_config/:file_name"
>> Skyscraper::fetch("http://rubyonrails.org").first(".message img").download# => "/tmp/test/my_path_from_config/rails.png"

#download method returns path to saved file.

Submiting

You are also able to submit forms (currently only with POST method), by doing following:

>> node=Skyscraper::fetch("http://www.balticplaza.eu/kontakt").first("#new_inquiry")# => #<Skyscraper::Node:0x9f43d88 @element=#<Nokogiri::XML::Element:0x4fa1ece name="form" ...
>> submited_page=node.submit(:"inquiry[name]"=>"Example name")# => #<Skyscraper::Node:0x9e82f84 @element=[#<Nokogiri::XML::Element:0x4f41920 name="html" ...
>> submited_page.first("#inquiry_name").value# => "Example name"

Configuration

Please visit Configuration section to get all details of Skyscraper configuration.

Testing

Please consider that you can fetch not only remote sites but also local files. This can be very helpful when you prefer TDD coding.

Other topics

Requirements

Skyscraper requires ruby in > 1.9 version. It's also depending on Nokogiri, Open-Uri, Uri and Actionpack libraries.

What is consider to be added?

  • Fetch reattempt on errors
  • Testing mode - downloading only small amount of records, and showing how they would look in database
  • Ruby < 1.9 versions support
  • Redis, ActiveRecord cache and storage
  • Ruby on Rails generators

Please don't hesitate to post me a comment about above or other functionality that might be added.

Contributors

Here I will post list of contributors, which helps to created documentation and create bug fixes.

About

DSL for scrapping content from remote and local websites

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages