Rubyland has two gems that have occupied the web scraping spotlight for the past few years: Nokogiri and Mechanize. We spend an article on each of these before we put them into action with a practical example.
There are fancier terms around than web or screen scraping. Web harvesting and web data extraction pretty much tell you right away what’s going on. We can automate the extraction of data from web pages—and it’s not that complicated as well.
In a way, these tools allow you to imitate and automate human web browsing. You write a program that only extracts the sort of data that is of interest to you. Targeting specific data is almost as easy as using CSS selectors.
A few years ago I subscribed to some online video course that had like a million short videos but no option to download them in bulk. I had to go through every link on my own and do the dreaded ‘save as’ myself. It was sort of human web scraping—something that we often need to do when we lack the knowledge to automate that kind of stuff. The course itself was alright, but I didn’t use their services anymore after that. It was just too tedious.
Today, I wouldn’t care too much about such mind-melting UX. A scraper that would do the downloading for me would take me only a couple of minutes to throw together. No biggie!
Let me break it down real quick before we start. The whole thing can be condensed into a couple of steps. First we fetch a web page that has the desired data we need. Then we search through that page and identify the information we want to extract.
The final step is to target these bits, slice them if necessary, and decide how and where you want to store them. Well-written HTML is often key to making this process easy and enjoyable. For more involved extractions, it can be a pain if you have to deal with poorly structured markup.
What about APIs? Very good question. If you have access to a service with an API, there is often little need to write your own scraper. This approach is mostly for websites that don’t offer that sort of convenience. Without an API, this is often the only way to automate the extraction of information from websites.
You might ask, how does this scraping thing actually work? Without jumping into the deep end, the short answer is, by traversing tree data structures. Nokogiri builds these data structures from the documents you feed it and lets you target bits of interest for extraction. For example, CSS is a language written for tree traversal, for searching tree data structures, and we can make use of it for data extraction.
There are many approaches and solutions out there to play with. Rubyland has two gems that have occupied the spotlight for a number of years now. Many people still rely on Nokogiri and Mechanize for HTML scraping needs. Both have been tested and proven themselves to be easy to use while being highly capable. We will look at both of them. But before that, I’d like to take a moment to address the problem that we are going to solve at the end of this short introductory series.
Before you start scraping away, make sure you have the permission of the sites you are trying to access for data extraction. If the site has an API or RSS feed, for example, it might not only be easier to get that desired content, it might also be the legal option of choice.
Not everybody will appreciate it if you do massive scraping on their sites—understandably so. Get yourself educated on that particular site you are interested in, and don’t get yourself in trouble. Chances are low that you will inflict serious damage, but risking trouble unknowingly is not the way to go.
I needed to build a new podcast. The design was not where I wanted it to be, and I hated the way of publishing new posts. Damn WYSIWYGs! A little bit of context. About two years ago, I built the first version of my podcast. The idea was to play with Sinatra and build something super lightweight. I ran into a couple of unexpected issues since I tailor-made pretty much everything.
Coming from Rails, it was definitely an educational journey that I appreciate, but I quickly regretted not having used a static site that I could have deployed through GitHub via GitHub pages. Deploying new episodes and maintaining them lacked the simplicity that I was looking for. For a while, I decided that I had bigger fish to fry and focused on producing new podcast material instead.
This past summer I started to get serious and worked on a Middleman site that is hosted via GitHub pages. For season two of the show, I wanted something fresh. A new, simplified design, Markdown for publishing new episodes, and no fist fights with Heroku—heaven! The thing was that I had 139 episodes lying around that needed to be imported and converted first in order to work with Middleman.
For posts, Middleman uses .markdown files that have so called frontmatter for data—which replaces my database basically. Doing this transfer by hand is not an option for 139 episodes. That’s what computation is for. I needed to figure out a way to parse the HTML of my old website, scrape the relevant content, and transfer it to blog posts that I use for publishing new podcast episodes on Middleman.
Therefore, over the next three articles, I’m going to introduce you to the tools commonly used in Rubyland for such tasks. In the end, we’ll go over my solution to show you something practical as well.
Even if you are completely new to Ruby/Rails, chances are very good that you have already heard about this little gem. The name is dropped often and sticks with you easily. I'm not sure that many know that nokogiri is Japanese for “saw”.
It's a fitting name once you understand what the tool does. The creator of this gem is the lovely Tenderlove, Aaron Patterson. Nokogiri converts XML and HTML documents into a data structure—a tree data structure, to be more precise. The tool is fast and offers a nice interface as well. Overall, it’s a very potent library that takes care of a multitude of your HTML scraping needs.
You can use Nokogiri not only for parsing HTML; XML is fair game as well. It gives you the options of both XML path language and CSS interfaces to traverse the documents you load. XML path Language, or XPath for short, is a query language.
It allows us to select nodes from XML documents. CSS selectors are most likely more familiar to beginners. As with styles you write, CSS selectors make it fantastically easy to target specific sections of pages that are of interest for extraction. You just need to let Nokogiri know what you are after when you target a particular destination.
What we always need to start with is fetching the actual page we are interested in. We specify what kind of Nokogiri document we want to parse—XML or HTML for example:
Nokogiri::XML Nokogiri::HTML
require "nokogiri"
require "open-uri"
page = Nokogiri::XML(File.open("some.xml"))
page = Nokogiri::HTML(File.open("some.html"))Nokogiri:XML and Nokogiri:HTML can take IO objects or String objects. What happens above is straightforward. This opens and fetches the designated page using open-uri and then loads its structure, its XML or HTML into a new Nokogiri document. XML is not something beginners have to deal with very often.
Therefore, I’d recommend that we focus on HTML parsing for now. Why open-uri? This module from the Ruby Standard Library lets us grab the site without much fuss. Because IO objects are fair game, we can make easy use of open-uri.
Let’s put this into practice with a mini example:
require 'nokogiri'
require "open-uri"
url = 'http://betweenscreens.fm/'
page = Nokogiri::HTML(open(url))
header = page.at_css("h2.post-title")
title = header.text
puts "This is the raw header of the latest episode: #{header}"
puts "This is the title of the latest episode: #{title}"What we did here represents all the steps that are usually involved with web scraping—just at a micro level. We decide which URL we need and which site we need to fetch, and we load them into a new Nokogiri document. Then we open that page and target a specific section.
Here I only wanted to know the title of the latest episode. Using the at_css method and a CSS selector for h2.post-title was all I needed to target the extraction point. With this method we will only scrape this singular element, though. This gives us the whole selector—which is most of the time not exactly what we need. Therefore we extract only the inner text portion of this node via the text method. For comparison, you can check the output for both the header and the text below.
This is the raw title of the latest episode: <h2 class="post-title"><a href="episodes/142/">David Heinemeier Hansson</a></h2> This is the title of the latest episode: David Heinemeier Hansson
Although this example has very limited applications, it possesses all the ingredients, all the steps that you need to understand. I think it’s cool how simple this is. Because it might not be obvious from this example, I would like to point out how powerful this tool can be. Let’s see what else we can do with a Nokogiri script.
If you are a beginner and not sure how to target the HTML needed for this, I recommend that you search online to find out how to inspect the contents of websites in your browser. Basically, all major browsers make this process really easy these days.
On Chrome you just need to right-click on an element in the website and choose the inspect option. This will open a small window at the bottom of your browser which shows you something like an x-ray of the site’s DOM. It has many more options, and I would recommend spending some time on Google to educate yourself. This is time spent wisely!
The css method will give us not only a single element of choice but any element that matches the search criteria on the page. Pretty neat and straightforward!
require 'nokogiri'
require "open-uri"
url = 'http://betweenscreens.fm/'
page = Nokogiri::HTML(open(url))
headers = page.css("h2.post-title")
headers.each do |header|
puts "This is the raw title of the latest episode: #{header}"
end
headers.each do |header|
puts "This is the title of the latest episode: #{header.text}"
endThis is the raw title of the latest episode: <h2 class="post-title"><a href="episodes/142/">David Heinemeier Hansson</a></h2> This is the raw title of the latest episode: <h2 class="post-title"><a href="episodes/141/">Zach Holman</a></h2> This is the raw title of the latest episode: <h2 class="post-title"><a href="episodes/140/">Joel Glovier</a></h2> This is the raw title of the latest episode: <h2 class="post-title"><a href="episodes/139/">João Ferreira</a></h2> This is the raw title of the latest episode: <h2 class="post-title"><a href="episodes/138/">Corwin Harrell</a></h2> This is the raw title of the latest episode: <h2 class="post-title"><a href="episodes/137/">Roberto Machado</a></h2> This is the raw title of the latest episode: <h2 class="post-title"><a href="episodes/136/">James Edward Gray II</a></h2> This is the title of the latest episode: David Heinemeier Hansson This is the title of the latest episode: Zach Holman This is the title of the latest episode: Joel Glovier This is the title of the latest episode: João Ferreira This is the title of the latest episode: Corwin Harrell This is the title of the latest episode: Roberto Machado This is the title of the latest episode: James Edward Gray II
The only little difference in this example is that I iterate on the raw headers first. I also extracted its inner text with the text method. Nokogiri automatically stops at the end of the page and does not attempt to follow the pagination anywhere automatically.
Let’s say we want to have a bit more information, say the date and the subtitle for each episode. We can simply expand on the example above. It is a good idea anyway to take this step by step. Get a little piece working and add in more complexity along the way.
require 'nokogiri'
require "open-uri"
url = 'http://betweenscreens.fm/'
page = Nokogiri::HTML(open(url))
articles = page.css("article.index-article")
articles.each do |article|
header = article.at_css("h2.post-title")
date = article.at_css(".post-date")
subtitle = article.at_css(".topic-list")
puts "This is the raw header: #{header}"
puts "This is the raw date: #{date}"
puts "This is the raw subtitle: #{subtitle}\n\n"
puts "This is the text header: #{header.text}"
puts "This is the text date: #{date.text}"
puts "This is the text subtitle: #{subtitle.text}\n\n"
endThis is the raw header: <h2 class="post-title"><a href="episodes/142/">David Heinemeier Hansson</a></h2> This is the raw date: <span class="post-date">Oct 18 | 2016</span> This is the raw subtitle: <h3 class="topic-list">Rails community | Tone | Technical disagreements | Community policing | Ungratefulness | No assholes allowed | Basecamp | Open source persona | Aspirations | Guarding motivations | Dealing with audiences | Pressure | Honesty | Diverse opinions | Small talk</h3> This is the text header: David Heinemeier Hansson This is the text date: Oct 18 | 2016 This is the text subtitle: Rails community | Tone | Technical disagreements | Community policing | Ungratefulness | No assholes allowed | Basecamp | Open source persona | Aspirations | Guarding motivations | Dealing with audiences | Pressure | Honesty | Diverse opinions | Small talk This is the raw header: <h2 class="post-title"><a href="episodes/141/">Zach Holman</a></h2> This is the raw date: <span class="post-date">Oct 12 | 2016</span> This is the raw subtitle: <h3 class="topic-list">Getting Fired | Taboo | Transparency | Different Perspectives | Timing | Growth Stages | Employment & Dating | Managers | At-will Employment | Tech Industry | Europe | Low hanging Fruits | Performance Improvement Plans | Meeting Goals | Surprise Firings | Firing Fast | Mistakes | Company Culture | Communication</h3> This is the text header: Zach Holman This is the text date: Oct 12 | 2016 This is the text subtitle: Getting Fired | Taboo | Transparency | Different Perspectives | Timing | Growth Stages | Employment & Dating | Managers | At-will Employment | Tech Industry | Europe | Low hanging Fruits | Performance Improvement Plans | Meeting Goals | Surprise Firings | Firing Fast | Mistakes | Company Culture | Communication This is the raw header: <h2 class="post-title"><a href="episodes/140/">Joel Glovier</a></h2> This is the raw date: <span class="post-date">Oct 10 | 2016</span> This is the raw subtitle: <h3 class="topic-list">Digital Product Design | Product Design @ GitHub | Loving Design | Order & Chaos | Drawing | Web Design | HospitalRun | Diversity | Startup Culture | Improving Lives | CURE International | Ember | Offline First | Hospital Information System | Designers & Open Source</h3> This is the text header: Joel Glovier This is the text date: Oct 10 | 2016 This is the text subtitle: Digital Product Design | Product Design @ GitHub | Loving Design | Order & Chaos | Drawing | Web Design | HospitalRun | Diversity | Startup Culture | Improving Lives | CURE International | Ember | Offline First | Hospital Information System | Designers & Open Source This is the raw header: <h2 class="post-title"><a href="episodes/139/">João Ferreira</a></h2> This is the raw date: <span class="post-date">Aug 26 | 2015</span> This is the raw subtitle: <h3 class="topic-list">Masters @ Work | Subvisual | Deadlines | Design personality | Design problems | Team | Pushing envelopes | Delightful experiences | Perfecting details | Company values</h3> This is the text header: João Ferreira This is the text date: Aug 26 | 2015 This is the text subtitle: Masters @ Work | Subvisual | Deadlines | Design personality | Design problems | Team | Pushing envelopes | Delightful experiences | Perfecting details | Company values This is the raw header: <h2 class="post-title"><a href="episodes/138/">Corwin Harrell</a></h2> This is the raw date: <span class="post-date">Aug 06 | 2015</span> This is the raw subtitle: <h3 class="topic-list">Q&A | 01 | University | Graphic design | Design setup | Sublime | Atom | thoughtbot | Working location | Collaboration & pairing | Vim advocates | Daily routine | Standups | Clients | Coffee walks | Investment Fridays |</h3> This is the text header: Corwin Harrell This is the text date: Aug 06 | 2015 This is the text subtitle: Q&A | 01 | University | Graphic design | Design setup | Sublime | Atom | thoughtbot | Working location | Collaboration & pairing | Vim advocates | Daily routine | Standups | Clients | Coffee walks | Investment Fridays | This is the raw header: <h2 class="post-title"><a href="episodes/137/">Roberto Machado</a></h2> This is the raw date: <span class="post-date">Aug 03 | 2015</span> This is the raw subtitle: <h3 class="topic-list">CEO @ Subvisual | RubyConf Portugal | Creators School | Consultancy | Company role models | Group Buddies | Portuguese startup | Rebranding | Technologies used | JS frameworks | TDD & BDD | Startup mistakes | Culture of learning | Young entrepreneurs</h3> This is the text header: Roberto Machado This is the text date: Aug 03 | 2015 This is the text subtitle: CEO @ Subvisual | RubyConf Portugal | Creators School | Consultancy | Company role models | Group Buddies | Portuguese startup | Rebranding | Technologies used | JS frameworks | TDD & BDD | Startup mistakes | Culture of learning | Young entrepreneurs This is the raw header: <h2 class="post-title"><a href="episodes/136/">James Edward Gray II</a></h2> This is the raw date: <span class="post-date">Jul 30 | 2015</span> This is the raw subtitle: <h3 class="topic-list">Screencasting | Less Code | Reading code | Getting unstuck | Rails’s codebase | CodeNewbie | Small examples | Future plans | PeepCode | Frequency & pricing</h3> This is the text header: James Edward Gray II This is the text date: Jul 30 | 2015 This is the text subtitle: Screencasting | Less Code | Reading code | Getting unstuck | Rails’s codebase | CodeNewbie | Small examples | Future plans | PeepCode | Frequency & pricing
At this point, we already have some data to play with. We can structure or butcher it any way we like. The above should simply show what we have in a readable fashion. Of course we can dig deeper into each of these by using regular expressions with the text method.
We will look into this in a lot more in detail when we get to solving the actual podcast problem. It won’t be a class on regexp, but you will see some more of it in action—but no worries, not so much as to make your brain bleed.
What could be handy at this stage is extracting the href for the individual episodes as well. It couldn’t be any simpler.
require 'nokogiri'
require "open-uri"
url = 'http://betweenscreens.fm/'
page = Nokogiri::HTML(open(url))
articles = page.css("article.index-article")
articles.each do |article|
header = article.at_css("h2.post-title")
date = article.at_css(".post-date")
subtitle = article.at_css(".topic-list")
link = article.at_css("h2.post-title a")
podcast_url = "http://betweenscreens.fm/"
puts "This is the raw header: #{header}"
puts "This is the raw date: #{date}"
puts "This is the raw subtitle: #{subtitle}"
puts "This is the raw link: #{link}\n\n"
puts "This is the text header: #{header.text}"
puts "This is the text date: #{date.text}"
puts "This is the text subtitle: #{subtitle.text}"
puts "This is the raw link: #{podcast_url}#{link[:href]}\n\n"
endThe most important bits to pay attention to here are [:href] and podcast_url. If you tag on [:] you can simply extract an attribute from the targeted selector. I abstracted a little further, but you can see more clearly how it works below.
...
href = article.at_css("h2.post-title a")[:href]
...To get a complete and useful URL, I saved the root domain in a variable and constructed the full URL for each episode.
...
podcast_url = "http://betweenscreens.fm/"
puts "This is the raw link: #{podcast_url}#{link[:href]}\n\n"
...Let’s take a quick look at the output:
This is the raw header: <h2 class="post-title"><a href="episodes/143/">Jason Long</a></h2> This is the raw date: <span class="post-date">Oct 25 | 2016</span> This is the raw subtitle: <h3 class="topic-list">Open source | Empathy | Lower barriers | Learning tool | Design contributions | Git website | Branding | GitHub | Neovim | Tmux | Design love | Knowing audiences | Showing work | Dribbble | Progressions | Ideas</h3> This is the raw link: <a href="episodes/143/">Jason Long</a> This is the text header: Jason Long This is the text date: Oct 25 | 2016 This is the text subtitle: Open source | Empathy | Lower barriers | Learning tool | Design contributions | Git website | Branding | GitHub | Neovim | Tmux | Design love | Knowing audiences | Showing work | Dribbble | Progressions | Ideas This is the href: http://betweenscreens.fm/episodes/143/ This is the raw header: <h2 class="post-title"><a href="episodes/142/">David Heinemeier Hansson</a></h2> This is the raw date: <span class="post-date">Oct 18 | 2016</span> This is the raw subtitle: <h3 class="topic-list">Rails community | Tone | Technical disagreements | Community policing | Ungratefulness | No assholes allowed | Basecamp | Open source persona | Aspirations | Guarding motivations | Dealing with audiences | Pressure | Honesty | Diverse opinions | Small talk</h3> This is the raw link: <a href="episodes/142/">David Heinemeier Hansson</a> This is the text header: David Heinemeier Hansson This is the text date: Oct 18 | 2016 This is the text subtitle: Rails community | Tone | Technical disagreements | Community policing | Ungratefulness | No assholes allowed | Basecamp | Open source persona | Aspirations | Guarding motivations | Dealing with audiences | Pressure | Honesty | Diverse opinions | Small talk This is the href: http://betweenscreens.fm/episodes/142/ This is the raw header: <h2 class="post-title"><a href="episodes/141/">Zach Holman</a></h2> This is the raw date: <span class="post-date">Oct 12 | 2016</span> This is the raw subtitle: <h3 class="topic-list">Getting Fired | Taboo | Transparency | Different Perspectives | Timing | Growth Stages | Employment & Dating | Managers | At-will Employment | Tech Industry | Europe | Low hanging Fruits | Performance Improvement Plans | Meeting Goals | Surprise Firings | Firing Fast | Mistakes | Company Culture | Communication</h3> This is the raw link: <a href="episodes/141/">Zach Holman</a> This is the text header: Zach Holman This is the text date: Oct 12 | 2016 This is the text subtitle: Getting Fired | Taboo | Transparency | Different Perspectives | Timing | Growth Stages | Employment & Dating | Managers | At-will Employment | Tech Industry | Europe | Low hanging Fruits | Performance Improvement Plans | Meeting Goals | Surprise Firings | Firing Fast | Mistakes | Company Culture | Communication This is the href: http://betweenscreens.fm/episodes/141/ This is the raw header: <h2 class="post-title"><a href="episodes/140/">Joel Glovier</a></h2> This is the raw date: <span class="post-date">Oct 10 | 2016</span> This is the raw subtitle: <h3 class="topic-list">Digital Product Design | Product Design @ GitHub | Loving Design | Order & Chaos | Drawing | Web Design | HospitalRun | Diversity | Startup Culture | Improving Lives | CURE International | Ember | Offline First | Hospital Information System | Designers & Open Source</h3> This is the raw link: <a href="episodes/140/">Joel Glovier</a> This is the text header: Joel Glovier This is the text date: Oct 10 | 2016 This is the text subtitle: Digital Product Design | Product Design @ GitHub | Loving Design | Order & Chaos | Drawing | Web Design | HospitalRun | Diversity | Startup Culture | Improving Lives | CURE International | Ember | Offline First | Hospital Information System | Designers & Open Source This is the href: http://betweenscreens.fm/episodes/140/ This is the raw header: <h2 class="post-title"><a href="episodes/139/">João Ferreira</a></h2> This is the raw date: <span class="post-date">Aug 26 | 2015</span> This is the raw subtitle: <h3 class="topic-list">Masters @ Work | Subvisual | Deadlines | Design personality | Design problems | Team | Pushing envelopes | Delightful experiences | Perfecting details | Company values</h3> This is the raw link: <a href="episodes/139/">João Ferreira</a> This is the text header: João Ferreira This is the text date: Aug 26 | 2015 This is the text subtitle: Masters @ Work | Subvisual | Deadlines | Design personality | Design problems | Team | Pushing envelopes | Delightful experiences | Perfecting details | Company values This is the href: http://betweenscreens.fm/episodes/139/ This is the raw header: <h2 class="post-title"><a href="episodes/138/">Corwin Harrell</a></h2> This is the raw date: <span class="post-date">Aug 06 | 2015</span> This is the raw subtitle: <h3 class="topic-list">Q&A | 01 | University | Graphic design | Design setup | Sublime | Atom | thoughtbot | Working location | Collaboration & pairing | Vim advocates | Daily routine | Standups | Clients | Coffee walks | Investment Fridays |</h3> This is the raw link: <a href="episodes/138/">Corwin Harrell</a> This is the text header: Corwin Harrell This is the text date: Aug 06 | 2015 This is the text subtitle: Q&A | 01 | University | Graphic design | Design setup | Sublime | Atom | thoughtbot | Working location | Collaboration & pairing | Vim advocates | Daily routine | Standups | Clients | Coffee walks | Investment Fridays | This is the href: http://betweenscreens.fm/episodes/138/ This is the raw header: <h2 class="post-title"><a href="episodes/137/">Roberto Machado</a></h2> This is the raw date: <span class="post-date">Aug 03 | 2015</span> This is the raw subtitle: <h3 class="topic-list">CEO @ Subvisual | RubyConf Portugal | Creators School | Consultancy | Company role models | Group Buddies | Portuguese startup | Rebranding | Technologies used | JS frameworks | TDD & BDD | Startup mistakes | Culture of learning | Young entrepreneurs</h3> This is the raw link: <a href="episodes/137/">Roberto Machado</a> This is the text header: Roberto Machado This is the text date: Aug 03 | 2015 This is the text subtitle: CEO @ Subvisual | RubyConf Portugal | Creators School | Consultancy | Company role models | Group Buddies | Portuguese startup | Rebranding | Technologies used | JS frameworks | TDD & BDD | Startup mistakes | Culture of learning | Young entrepreneurs This is the href: http://betweenscreens.fm/episodes/137/
Neat, isn’t it? You can do the same to extract the [:class] of a selector.
require 'nokogiri'
require "open-uri"
url = 'http://betweenscreens.fm/'
page = Nokogiri::HTML(open(url))
body_classes = page.at_css("body")[:class]If that node has more than one class, you will get a list of all of them.
We are used to dealing with tree structures in CSS or even jQuery. It would be a pain if Nokogiri didn't offer a handy API to move within such trees.
require 'nokogiri'
require "open-uri"
url = 'http://betweenscreens.fm/'
page = Nokogiri::HTML(open(url))
header = page.at_css("h2.post-title")
header_children = page.at_css("h2.post-title").children
header_parent = page.at_css("h2.post-title").parent
header_prev_sibling = page.at_css("h2.post-title").previous_sibling
puts "#{header}\n\n"
puts "#{header_children}\n\n"
puts "#{header_parent}\n\n"
puts "#{header_prev_sibling}\n\n"#header
<h2 class="post-title"><a href="episodes/143/">Jason Long</a></h2>
#header_children
<a href="episodes/143/">Jason Long</a>
#header_parent
<article class="index-article">
<span class="post-date">Oct 25 | 2016</span><h2 class="post-title"><a href="episodes/143/">Jason Long</a></h2>
<h3 class="topic-list">Open source | Empathy | Lower barriers | Learning tool | Design contributions | Git website | Branding | GitHub | Neovim | Tmux | Design love | Knowing audiences | Showing work | Dribbble | Progressions | Ideas</h3>
<div class="soundcloud-player-small">
</div>
</article>
#header_previous_sibling
<span class="post-date">Oct 25 | 2016</span>As you can see for yourself, this is some pretty powerful stuff—especially when you see what .parent was able to collect in one go. Instead of defining a bunch of nodes by hand, you could collect them wholesale.
You can even chain them for more involved traversals. You can take this as complicated as you like, of course, but I would caution you to keep things simple. It can quickly get a little unwieldy and hard to understand. Remember, "Keep it simple, stupid!"
...
header_parent_parent = page.at_css("h2.post-title").parent.parent
header_prev_sibling_parent_children = page.at_css("h2.post-title").previous_sibling.parent.children
...require 'nokogiri'
require "open-uri"
url = 'http://betweenscreens.fm/'
page = Nokogiri::HTML(open(url))
header = page.at_css("h2.post-title")
header_prev_sibling_children = page.at_css("h2.post-title").previous_sibling.children
header_parent_parent = page.at_css("h2.post-title").parent.parent
header_prev_sibling_parent = page.at_css("h2.post-title").previous_sibling.parent
header_prev_sibling_parent_children = page.at_css("h2.post-title").previous_sibling.parent.children
puts "#{header}\n\n"
puts "#{header_prev_sibling_children}\n\n"
puts "#{header_parent_parent}\n\n"
puts "#{header_prev_sibling_parent}\n\n"
puts "#{header_prev_sibling_parent_children}\n\n"#header
<h2 class="post-title"><a href="episodes/143/">Jason Long</a></h2>
#header_previous_sibling_children
Oct 25 | 2016
#header_parent_parent
<li>
<article class="index-article">
<span class="post-date">Oct 25 | 2016</span><h2 class="post-title"><a href="episodes/143/">Jason Long</a></h2>
<h3 class="topic-list">Open source | Empathy | Lower barriers | Learning tool | Design contributions | Git website | Branding | GitHub | Neovim | Tmux | Design love | Knowing audiences | Showing work | Dribbble | Progressions | Ideas</h3>
<div class="soundcloud-player-small">
</div>
</article>
</li>
#header_previous_sibling_parent
<article class="index-article">
<span class="post-date">Oct 25 | 2016</span><h2 class="post-title"><a href="episodes/143/">Jason Long</a></h2>
<h3 class="topic-list">Open source | Empathy | Lower barriers | Learning tool | Design contributions | Git website | Branding | GitHub | Neovim | Tmux | Design love | Knowing audiences | Showing work | Dribbble | Progressions | Ideas</h3>
<div class="soundcloud-player-small">
</div>
</article>
#header_previous_sibling_parent_children
<span class="post-date">Oct 25 | 2016</span><h2 class="post-title"><a href="episodes/143/">Jason Long</a></h2>
<h3 class="topic-list">Open source | Empathy | Lower barriers | Learning tool | Design contributions | Git website | Branding | GitHub | Neovim | Tmux | Design love | Knowing audiences | Showing work | Dribbble | Progressions | Ideas</h3>
<div class="soundcloud-player-small">
</div>Nokogiri is not a huge library, but it has a lot to offer. I recommend you play with what you have learned thus far and expand your knowledge through its documentation when you hit a wall. But don’t get yourself into trouble!
This little intro should get you well on your way to understanding what you can do and how it works. I hope you will explore it a bit more on your own and have some fun with it. As you will find out on your own, it’s a rich tool that keeps on giving.
The Best Small Business Web Designs by DesignRush
/
2Create Modern Vue Apps Using Create-Vue and Vite
/
9Pros and Cons of Using WordPress
/
-1How to Fix the “There Has Been a Critical Error in Your Website” Error in WordPress
/
2How To Fix The “There Has Been A Critical Error in Your Website” Error in WordPress
How to Create a Privacy Policy Page in WordPress
/
8WordPress Website Maintenance Guide For Beginners
/
8How Long Does it Take to Learn JavaScript
/
1The Best Way to Deep Copy an Object in JavaScript
/
-2
4
-5Adding and Removing Elements From Arrays in JavaScript
/
-2Create a JavaScript AJAX Post Request: With and Without jQuery
/
85 Real-Life Uses for the JavaScript reduce() Method
/
-8How to Enable or Disable a Button With JavaScript: jQuery vs. Vanilla
/
4How to Enable or Disable a Button With JavaScript: jQuery vs Vanilla
/
-3Confirm Yes or No With JavaScript
/
-4How to Change the URL in JavaScript: Redirecting
/
-127 Best Tab and Accordion Widget Plugins for WordPress (Free & Premium)
/
1321 Best Tab and Accordion Widget Plugins for WordPress (Free & Premium)
/
-1130 HTML Best Practices for Beginners
/
-731 Best WordPress Calendar Plugins and Widgets (With 5 Free Plugins)
/
225 Ridiculously Impressive HTML5 Canvas Experiments
/
-4
1How to Implement Email Verification for New Members
/
3How to Create a Simple Web-Based Chat Application
/
730 Popular WordPress User Interface Elements
/
-3Top 18 Best Practices for Writing Super Readable Code
/
1Best Affiliate WooCommerce Plugins Compared
/
-1018 Best WordPress Star Rating Plugins
/
1510+ Best WordPress Twitter Widgets
/
220+ Best WordPress Booking and Reservation Plugins
/
5Working With Tables in React: Part Two
/
-2Best CSS Animations and Effects on CodeCanyon
/
-1730 CSS Best Practices for Beginners
/
4How to Create a Custom WordPress Plugin From Scratch
/
310 Best Responsive HTML5 Sliders for Images and Text… and 3 Free Options
/
-616 Best Tab and Accordion Widget Plugins for WordPress
/
-718 Best WordPress Membership Plugins and 5 Free Plugins
/
325 Best WooCommerce Plugins for Products, Pricing, Payments and More
/
210 Best WordPress Twitter Widgets
1 /
1112 Best Contact Form PHP Scripts for 2020
/
-220 Popular WordPress User Interface Elements
/
-410 Best WordPress Star Rating Plugins
/
-412 Best CSS Animations on CodeCanyon
/
-212 Best WordPress Booking and Reservation Plugins
/
120 Best WordPress Calendar Plugins and Widgets
/
-312 Elegant CSS Pricing Tables for Your Latest Web Project
/
824 Best WordPress Form Plugins for 2020
/
-2Getting Started With Django: Newly Updated Course
/
-1Create a Blog for Each Category or Department in Your WooCommerce Store
/
38 Best WordPress Booking and Reservation Plugins
/
-15Best Exit Popups for WordPress Compared
/
19Best Exit Popups for WordPress Compared
/
-311 Best Tab & Accordion WordPress Widgets & Plugins
/
-1012 Best Tab & Accordion WordPress Widgets & Plugins
1New Course: Practical React Fundamentals
/
2Short Course: Better Angular App Architecture With Modules
/
3Preview Our New Course on Angular Material
/
4Build Your Own CAPTCHA and Contact Form in PHP
/
-3Object-Oriented PHP With Classes and Objects
/
1Best Practices for ARIA Implementation
/
-5Accessible Apps: Barriers to Access and Getting Started With Accessibility
/
-4Dramatically Speed Up Your React Front-End App Using Lazy Loading
/
1715 Best Modern JavaScript Admin Templates for React, Angular, and Vue.js
/
-1715 Best Modern JavaScript Admin Templates for React, Angular and Vue.js
/
319 Best JavaScript Admin Templates for React, Angular, and Vue.js
/
3New Course: Build an App With JavaScript and the MEAN Stack
/
10Hands-on With ARIA: Accessibility Recipes for Web Apps
/
610 Best WordPress Facebook Widgets
13Hands-on With ARIA: Accessibility for eCommerce
/
6New eBooks Available for Subscribers
/
-8Hands-on With ARIA: Homepage Elements and Standard Navigation
/
11
-5Site Accessibility: Getting Started With ARIA
/
5How Secure Are Your JavaScript Open-Source Dependencies?
/
-3New Course: Secure Your WordPress Site With SSL
/
-2Testing Components in React Using Jest and Enzyme
/
-8Testing Components in React Using Jest: The Basics
/
-515 Best PHP Event Calendar and Booking Scripts
/
-3Create Interactive Gradient Animations Using Granim.js
/
11How to Build Complex, Large-Scale Vue.js Apps With Vuex
1 /
-1Examples of Dependency Injection in PHP With Symfony Components
/
-5Set Up Routing in PHP Applications Using the Symfony Routing Component
1 /
8A Beginner’s Guide to Regular Expressions in JavaScript
/
9Introduction to Popmotion: Custom Animation Scrubber
/
-6Introduction to Popmotion: Pointers and Physics
/
-10New Course: Connect to a Database With Laravel’s Eloquent ORM
/
-6How to Create a Custom Settings Panel in WooCommerce
/
-15Building the DOM faster: speculative parsing, async, defer and preload
1 /
420 Useful PHP Scripts Available on CodeCanyon
3 /
51Introduction to the Stimulus Framework
/
-12Single-Page React Applications With the React-Router and React-Transition-Group Modules
712 Best Contact Form PHP Scripts
1 /
-3Getting Started With the Mojs Animation Library: The ShapeSwirl and Stagger Modules
Getting Started With the Mojs Animation Library: The Shape Module
/
-1Getting Started With the Mojs Animation Library: The HTML Module
/
4Project Management Considerations for Your WordPress Project
/
-88 Things That Make Jest the Best React Testing Framework
/
6Creating an Image Editor Using CamanJS: Layers, Blend Modes, and Events
/
1New Short Course: Code a Front-End App With GraphQL and React
/
1Creating an Image Editor Using CamanJS: Applying Basic Filters
/
-12Creating an Image Editor Using CamanJS: Creating Custom Filters and Blend Modes
/
-1Modern Web Scraping With BeautifulSoup and Selenium
/
1Challenge: Create a To-Do List in React
1 /
-2Deploy PHP Web Applications Using Laravel Forge
/
-3Getting Started With the Mojs Animation Library: The Burst Module
/
4
-910 Things Men Can Do to Support Women in Tech
/
-2A Gentle Introduction to Higher-Order Components in React: Best Practices
/
-5A Gentle Introduction to HOC in React: Learn by Example
/
3A Gentle Introduction to Higher-Order Components in React
/
-6Creating Pretty Popup Messages Using SweetAlert2
/
7Creating Stylish and Responsive Progress Bars Using ProgressBar.js
/
1618 Best Contact Form PHP Scripts for 2022
/
-2How to Make a Real-Time Sports Application Using Node.js
/
-2Creating a Blogging App Using Angular & MongoDB: Delete Post
/
-17Set Up an OAuth2 Server Using Passport in Laravel
/
1Creating a Blogging App Using Angular & MongoDB: Edit Post
/
1Introduction to Mocking in Python
/
1Creating a Blogging App Using Angular & MongoDB: Show Post
/
10Creating a Blogging App Using Angular & MongoDB: Home
/
6
3Creating a Blogging App Using Angular & MongoDB: Login
/
12Creating Your First Angular App: Implement Routing
/
12Persisted WordPress Admin Notices: Part 4
/
-2Creating Your First Angular App: Components, Part 2
/
3Persisted WordPress Admin Notices: Part 3
/
4How Laravel Broadcasting Works
/
-1
7Persisted WordPress Admin Notices: Part 2
/
-2Create Your First Angular App: Storing and Accessing Data
/
6Persisted WordPress Admin Notices: Part 1
/
-1Error and Performance Monitoring for Web & Mobile Apps Using Raygun
/
6Using Luxon for Date and Time in JavaScript
7 /
-3How to Create an Audio Oscillator With the Web Audio API
/
-7How to Cache Using Redis in Django Applications
/
220 Essential WordPress Utilities to Manage Your Site
/
-6Introduction to API Calls With React and Axios
/
13Beginner’s Guide to Angular 4: HTTP
/
13Rapid Web Deployment for Laravel With GitHub, Linode, and RunCloud.io
/
8Beginners Guide to Angular 4: Routing
/
-10Beginner’s Guide to Angular 4: Services
/
1Beginner’s Guide to Angular 4: Components
/
1Creating a Drop-Down Menu for Mobile Pages
/
9Introduction to Forms in Angular 4: Writing Custom Form Validators
/
6Getting Started With Redux: Connecting Redux With React
/
14Getting Started With Redux: Learn by Example
/
14Getting Started With Redux: Why Redux?
/
11How to Auto Update WordPress Salts
/
-4How to Download Files in Python
/
-2
-19Eloquent Mutators and Accessors in Laravel
1 /
15
-11
1310 Best HTML5 Sliders for Images and Text
/
8Site Authentication in Node.js: User Signup
/
-2Creating a Task Manager App Using Ionic: Part 2
/
2Creating a Task Manager App Using Ionic: Part 1
/
1Introduction to Forms in Angular 4: Reactive Forms
/
2
4Introduction to Forms in Angular 4: Template-Driven Forms
/
424 Essential WordPress Utilities to Manage Your Site
/
-125 Essential WordPress Utilities to Manage Your Site
/
-10Get Rid of Bugs Quickly Using BugReplay
1 /
-16Manipulating HTML5 Canvas Using Konva: Part 1, Getting Started
/
610 Must-See Easy Digital Downloads Extensions for Your WordPress Site
/
-222 Best WordPress Booking and Reservation Plugins
/
-5Understanding ExpressJS Routing
/
-615 Best WordPress Star Rating Plugins
/
12Creating Your First Angular App: Basics
/
-1Inheritance and Extending Objects With JavaScript
/
-10Introduction to the CSS Grid Layout With Examples
1 /
4
5Performant Animations Using KUTE.js: Part 5, Easing Functions and Attributes
/
-6Performant Animations Using KUTE.js: Part 4, Animating Text
/
-10Performant Animations Using KUTE.js: Part 3, Animating SVG
/
-8New Course: Code a Quiz App With Vue.js
/
-2Performant Animations Using KUTE.js: Part 2, Animating CSS Properties
/
1
8Performant Animations Using KUTE.js: Part 1, Getting Started
/
110 Best Responsive HTML5 Sliders for Images and Text (Plus 3 Free Options)
/
-3Single-Page Applications With ngRoute and ngAnimate in AngularJS
/
-14Deferring Tasks in Laravel Using Queues
/
-3
2Site Authentication in Node.js: User Signup and Login
/
10Working With Tables in React, Part Two
/
-17Working With Tables in React, Part One
/
1How to Set Up a Scalable, E-Commerce-Ready WordPress Site Using ClusterCS
/
-2New Course on WordPress Conditional Tags
/
2
-11Building With Vue.js 2 and Firebase
6 /
11
-8
1Best Unique Bootstrap JavaScript Plugins
/
-6
-1Essential JavaScript Libraries and Frameworks You Should Know About
/
6
3Vue.js Crash Course: Create a Simple Blog Using Vue.js
/
-14Build a React App With a Laravel RESTful Back End: Part 1, Laravel 5.5 API
/
2API Authentication With Node.js
/
-4Beginner’s Guide to Angular: HTTP
/
8Beginner’s Guide to Angular: Routing
/
-4Beginners Guide to Angular: Routing
/
-4Beginner’s Guide to Angular: Services
/
2Beginner’s Guide to Angular: Components
/
1How to Create a Custom Authentication Guard in Laravel
/
5Learn Computer Science With JavaScript: Part 3, Loops
/
-3Build Web Applications Using Node.js
/
1Learn Computer Science With JavaScript: Part 4, Functions
/
5Learn Computer Science With JavaScript: Part 2, Conditionals
/
13Create Interactive Charts Using Plotly.js, Part 5: Pie and Gauge Charts
/
-8Create Interactive Charts Using Plotly.js, Part 4: Bubble and Dot Charts
/
15Create Interactive Charts Using Plotly.js, Part 3: Bar Charts
/
-10Awesome JavaScript Libraries and Frameworks You Should Know About
/
-6Create Interactive Charts Using Plotly.js, Part 2: Line Charts
/
-8Bulk Import a CSV File Into MongoDB Using Mongoose With Node.js
/
-5Build a To-Do API With Node, Express, and MongoDB
/
-8Getting Started With End-to-End Testing in Angular Using Protractor
/
11TypeScript for Beginners, Part 4: Classes
/
-15Object-Oriented Programming With JavaScript
/
410 Best Affiliate WooCommerce Plugins Compared
/
6Stateful vs. Stateless Functional Components in React
/
8Make Your JavaScript Code Robust With Flow
/
-2Build a To-Do API With Node and Restify
/
7Testing Components in Angular Using Jasmine: Part 2, Services
/
3Testing Components in Angular Using Jasmine: Part 1
/
-1Creating a Blogging App Using React, Part 6: Tags
/
-10React Crash Course for Beginners, Part 3
/
4React Crash Course for Beginners, Part 2
/
5React Crash Course for Beginners, Part 1
/
3Set Up a React Environment, Part 4
1 /
9Set Up a React Environment, Part 3
/
-9New Course: Get Started With Phoenix
/
-7Set Up a React Environment, Part 2
/
2Command Line Basics and Useful Tricks With the Terminal
/
8How to Create a Real-Time Feed Using Phoenix and React
/
-2Build a React App With a Laravel Back End: Part 2, React
/
2Build a React App With a Laravel RESTful Back End: Part 1, Laravel 9 API
/
-4Creating a Blogging App Using React, Part 5: Profile Page
/
-2Pagination in CodeIgniter: The Complete Guide
/
24JavaScript-Based Animations Using Anime.js, Part 4: Callbacks, Easings, and SVG
JavaScript-Based Animations Using Anime.js, Part 3: Values, Timeline, and Playback
Learn to Code With JavaScript: Part 1, The Basics
/
410 Elegant CSS Pricing Tables for Your Latest Web Project
/
-9Getting Started With the Flux Architecture in React
/
-2Getting Started With Matter.js: The Composites and Composite Modules
/
2Getting Started With Matter.js: The Engine and World Modules
/
1410 More Popular HTML5 Projects for You to Use and Study
/
-21Understand the Basics of Laravel Middleware
/
-1Iterating Fast With Django & Heroku
/
9Creating a Blogging App Using React, Part 4: Update & Delete Posts
/
5Creating a jQuery Plugin for Long Shadow Design
/
-12How to Register & Use Laravel Service Providers
2 /
-13Unit Testing in React: Shallow vs. Static Testing
/
4Creating a Blogging App Using React, Part 3: Add & Display Post
/
14Creating a Blogging App Using React, Part 2: User Sign-Up
20 /
-4Creating a Blogging App Using React, Part 1: User Sign-In
/
69 More Popular HTML5 Projects for You to Use and Study
/
15Creating a Grocery List Manager Using Angular, Part 2: Managing Items
/
89 Elegant CSS Pricing Tables for Your Latest Web Project
/
-3Dynamic Page Templates in WordPress, Part 3
/
-4Angular vs. React: 7 Key Features Compared
/
6Creating a Grocery List Manager Using Angular, Part 1: Add & Display Items
/
1New eBooks Available for Subscribers in June 2017
/
7Create Interactive Charts Using Plotly.js, Part 1: Getting Started
/
10The 5 Best IDEs for WordPress Development (And Why)
/
-233 Popular WordPress User Interface Elements
/
2New Course: How to Hack Your Own App
/
-11How to Install Yii on Windows or a Mac
/
3What Is a JavaScript Operator?
/
-7How to Register and Use Laravel Service Providers
/
9
waly Good blog post. I absolutely love this…