Ruby Web Scraping
Ruby web scraping usually takes two gems: HTTParty (or Faraday, or the built-in Net::HTTP) to download a page, and Nokogiri to parse it with CSS selectors or XPath. For pages that need JavaScript, Ferrum, Selenium or the Playwright client drive a real browser from Ruby, and the same code runs inside a Rails background job. ProxyField residential proxies work with each of these gems over HTTP from $1.95/GB; start with the Easy Trial: 1 GB for $2.95.
- residential IPs
- 412M+
- from, traffic never expires
- $1.95/GB
- Easy Trial
- 1 GB for $2.95
On this page 9 sections
The Ruby scraping toolkit
| Gem | What it does | Version (RubyGems, 2026-10-23) | License |
|---|---|---|---|
| Nokogiri | Parses HTML and XML; CSS selectors and XPath | 1.19.4 | MIT |
| HTTParty | Simple HTTP client with proxy options | 0.24.3 | MIT |
| Faraday | HTTP client with middleware (retries, logging) | 2.15.0 | MIT |
| Net::HTTP | HTTP client in Ruby's standard library | ships with Ruby | Ruby license |
| Mechanize | Browser-like client: cookies, forms, following links | 2.14.1 | MIT |
| Ferrum | Drives Chrome over the DevTools protocol | 0.19.1 | MIT |
| selenium-webdriver | Selenium's official Ruby bindings | 4.51.0 | Apache-2.0 |
| playwright-ruby-client | Ruby client for Playwright | 1.64.0 | MIT |
| Kimurai | Scraping framework on top of these | 2.2.0 | MIT |
For most static pages, HTTParty plus Nokogiri is all you need.
Scrape a static page with HTTParty and Nokogiri
gem install httparty nokogirirequire "httparty"
require "nokogiri"
url = "https://www.scrapethissite.com/pages/simple/"
response = HTTParty.get(url, headers: { "User-Agent" => "Mozilla/5.0" }, timeout: 30)
raise "HTTP #{response.code}" unless response.code == 200
doc = Nokogiri::HTML(response.body)
countries = doc.css("div.country").map do |card|
{
name: card.at_css("h3.country-name").text.strip,
capital: card.at_css("span.country-capital").text.strip,
population: card.at_css("span.country-population").text.to_i
}
end
puts countries.size, countries.firstThe page is a practice sandbox with 250 countries, each in a div.country. css returns every match and at_css the first; xpath and at_xpath are the XPath versions. Read an attribute with node["href"], and make relative links absolute with URI.join(url, href).to_s.
Scrape multiple pages
When the page number is a URL parameter, count up until a page comes back empty. On the sandbox's hockey table it is ?page_num=N:
require "csv"
teams, page = [], 1
loop do
res = HTTParty.get("https://www.scrapethissite.com/pages/forms/",
query: { page_num: page }, timeout: 30)
rows = Nokogiri::HTML(res.body).css("tr.team")
break if rows.empty?
rows.each do |tr|
teams << [tr.at_css("td.name").text.strip, tr.at_css("td.year").text.to_i, tr.at_css("td.wins").text.to_i]
end
page += 1
sleep 1
end
CSV.open("teams.csv", "w") do |csv|
csv << %w[team year wins]
teams.each { |row| csv << row }
endWhere the site has a "Next" link instead, read doc.at_css("a[rel=next]")&.[]("href") and stop when it is nil. Mechanize does the same with page.link_with(text: "Next")&.click, and keeps cookies between pages.
Errors, retries and rate limits
Unattended Ruby scrapers fail in three predictable ways: timeouts, blocks and layout changes.
- Retries with backoff: retry
Net::OpenTimeout,Net::ReadTimeoutand 429 or 5xx responses up to 3 times, sleeping 2, 4 and 8 seconds. With Faraday, thefaraday-retrymiddleware does this for you. - Honor
Retry-Afteron 429 responses, and keep a delay per host. - Fail loudly on
nil:at_cssreturnsnilwhen a selector matches nothing, and.textonnilraisesNoMethodError. Check forniland log the URL, so a layout change stops the run instead of writing empty rows.
Add a proxy
HTTParty takes the proxy as four options. The ProxyField gateway is gate.proxyfield.io:7000; <login>-country-us-city-new_york asks for an exit IP in New York. Region, session and session time go in the same username: -region-<region>, -session-<id> and -ttl-<seconds> (30 to 129,600 seconds, 20 minutes by default), as in <login>-country-us-region-new_york-city-new_york-session-he9f2qga-ttl-600; every part after the login is optional.
response = HTTParty.get("https://api.ipify.org",
http_proxyaddr: "gate.proxyfield.io",
http_proxyport: 7000,
http_proxyuser: "<login>-country-us-city-new_york",
http_proxypass: "<password>",
timeout: 30)
puts response.body # the IP the site seesThe same with Faraday and with Net::HTTP:
require "faraday"
conn = Faraday.new(proxy: "http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000")
puts conn.get("https://api.ipify.org").body
require "net/http"
uri = URI("https://api.ipify.org")
Net::HTTP.start(uri.host, uri.port, "gate.proxyfield.io", 7000,
"<login>-country-us-city-new_york", "<password>", use_ssl: true) do |http|
puts http.get(uri.request_uri).body
endThe gateway can give each request a new IP, or keep one IP for a sticky session from 30 s to 36 h, for multi-page flows that need the same visitor. Which IP type to use for which site: proxies for web scraping.
Scrape JavaScript-rendered pages with Ruby
Nokogiri does not run JavaScript. If the data is missing from response.body but visible in the browser, first look for the JSON request the page makes (DevTools > Network > Fetch/XHR) and call it with HTTParty. If you need the rendered page, drive a browser.
Ferrum controls Chrome directly, without a separate driver:
require "ferrum"
browser = Ferrum::Browser.new(timeout: 30, proxy: {
host: "gate.proxyfield.io", port: 7000,
user: "<login>-country-us-city-new_york", password: "<password>"
})
browser.go_to("https://example.com/js-page")
html = browser.body # the HTML after scripts ran
browser.quit
puts Nokogiri::HTML(html).at_css("h1")&.textPlaywright in Ruby goes through the playwright-ruby-client gem, which uses the Playwright driver from npm (npm install playwright and npx playwright install chromium first):
require "playwright"
Playwright.create(playwright_cli_executable_path: "npx playwright") do |playwright|
playwright.chromium.launch(headless: true, proxy: {
server: "http://gate.proxyfield.io:7000",
username: "<login>-country-us-city-new_york", password: "<password>"
}) do |browser|
page = browser.new_page
page.goto("https://example.com/js-page")
page.wait_for_selector("h1")
puts page.text_content("h1")
end
endSelenium supports Ruby through the official selenium-webdriver gem. Chrome's --proxy-server flag takes no username or password, so with Selenium the simplest setup is an IP allowlist: add your server's IP to your account's allowlist, then pass --proxy-server=http://gate.proxyfield.io:<port>, where <port> is the port you set up in the dashboard for that IP. More on authenticated proxies in Selenium: Selenium proxy setup.
Browsers download every script, image and font, so they are slower and use more proxy traffic per page than plain HTTP; render only the pages that need it. The general approach is in how to scrape dynamic web pages.
Web scraping in Ruby on Rails
Never scrape inside a web request: a slow site would hold up your users. Put the scraper in a job and schedule it.
# app/jobs/scrape_countries_job.rb
class ScrapeCountriesJob < ApplicationJob
queue_as :default
retry_on Net::ReadTimeout, wait: :polynomially_longer, attempts: 3
def perform
res = HTTParty.get("https://www.scrapethissite.com/pages/simple/", timeout: 30,
http_proxyaddr: "gate.proxyfield.io", http_proxyport: 7000,
http_proxyuser: ENV["PROXY_USER"], http_proxypass: ENV["PROXY_PASS"])
Nokogiri::HTML(res.body).css("div.country").each do |card|
Country.upsert({ name: card.at_css("h3.country-name").text.strip,
population: card.at_css("span.country-population").text.to_i },
unique_by: :name)
end
end
endupsert with a unique index keeps reruns from duplicating rows. Schedule the job with your queue's recurring tasks: Solid Queue, the default queue backend in Rails 8, reads them from config/recurring.yml. Keep the proxy credentials in Rails credentials or environment variables.
Is Ruby a good choice for web scraping?
Yes, for small and medium jobs, and especially when the rest of your app is Ruby: Nokogiri is fast and mature, and HTTParty and Faraday make requests short to write. Python has the larger scraping ecosystem (Scrapy, more tutorials, more parsers), and Node.js has the most browser tooling; for very high page counts per server, compiled languages such as Rust or Go use less memory. Comparisons by language: choosing a language for web scraping. Other guides in this series: web scraping in Rust, how to build a web scraper.
Running Ruby scrapers at volume? ProxyField residential proxies: 412M+ residential IPs in 195+ countries, unlimited concurrent threads, pay per GB with no subscription. Start with the Easy Trial (1 GB); traffic never expires, and all purchases are final.
Ruby Web Scraping FAQ
How do I scrape a website with Ruby?
ProxyField residential proxies, from $1.95/GB, go into HTTParty as four options, and the scrape itself is short: fetch the page with HTTParty, parse it with Nokogiri::HTML(response.body), select elements with css or at_css, read .text.strip or node["href"], and write the rows with Ruby's CSV library. Loop over the pages with a short sleep, and route HTTParty through the rotating residential proxies once one IP is not enough.
Is Ruby good for web scraping?
Yes for most jobs: Nokogiri is fast and mature, and HTTParty or Faraday keep requests short. Python has the bigger scraping ecosystem and more tutorials, so Ruby makes most sense when the rest of your stack is Ruby or Rails.
Which Ruby gem is best for web scraping?
Nokogiri for parsing, paired with HTTParty or Faraday for requests; Mechanize when you need cookies, forms and link-following; Ferrum or the Playwright client for JavaScript pages. Kimurai bundles these into a scraping framework.
Does Nokogiri execute JavaScript?
No. Nokogiri parses the HTML it is given and never runs scripts. For script-built pages, call the JSON request the page makes, or render it with Ferrum, Playwright or Selenium and pass the resulting HTML to Nokogiri.
How do you scrape JavaScript-rendered pages with Ruby?
Drive a real browser: Ferrum controls Chrome over the DevTools protocol, the playwright-ruby-client gem runs Playwright, and selenium-webdriver runs Selenium. Wait for the element you need, then read the page's HTML or the element's text.
How do I use Playwright in Ruby?
Install the playwright-ruby-client gem and the Playwright driver from npm, then call Playwright.create(playwright_cli_executable_path: "npx playwright") and launch Chromium inside the block. The launch options take a proxy hash with server, username and password.
Does Selenium support Ruby?
Yes. The selenium-webdriver gem is one of Selenium's official language bindings, and it drives Chrome, Firefox, Edge and Safari. Chrome's --proxy-server flag takes no credentials, so pair it with an IP allowlist or an authenticating setup.
How do you scrape multiple pages with Ruby?
Loop over the page parameter, such as ?page_num=1, 2, 3, until a page returns no items, or follow the rel=next link until it disappears. Sleep between requests and reuse one connection where you can.
How do I use a proxy for Ruby web scraping?
Pass http_proxyaddr, http_proxyport, http_proxyuser and http_proxypass to HTTParty, or a proxy URL to Faraday.new(proxy: ...). Net::HTTP takes the proxy host, port, user and password as arguments to Net::HTTP.start.
Can I scrape websites in Ruby on Rails?
Yes, in a background job rather than in a controller, so a slow site never holds up a web request. Use Active Job with retries, store rows with upsert on a unique key, and schedule the job with your queue's recurring tasks.
Show 1 more questionShow fewer questions
How should Ruby scrapers handle errors, retries and rate limits?
Retry timeouts and 429 or 5xx responses a few times with growing waits, honor Retry-After, and never retry a 403 in a tight loop. Treat a nil from at_css as a layout change and stop with a clear log line.
Related pages
Easy Trial: 1 GB for $2.95.
Try for the price of 1 GB, no subscription, traffic never expires.