Loading repository data…
Loading repository data…
vifreefly / repository
Write web scrapers in Ruby using a clean, AI-assisted DSL. Kimurai uses AI to figure out where the data lives, then caches the selectors and scrapes with pure Ruby. Get the intelligence of an LLM without the per-request latency or token costs.
Write web scrapers in Ruby using a clean, AI-assisted DSL. Kimurai uses AI to figure out where the data lives, then caches the selectors and scrapes with pure Ruby. Get the intelligence of an LLM without the per-request latency or token costs:
# google_spider.rb
require 'kimurai'
class GoogleSpider < Kimurai::Base
@start_urls = ['https://www.google.com/search?q=web+scraping+ai']
@delay = 1
def parse(response, url:, data: {})
results = extract(response) do
array :organic_results do
object do
string :title
string :snippet
string :url
end
end
array :sponsored_results do
object do
string :title
string :snippet
string :url
end
end
array :people_also_search_for, of: :string
string :next_page_link
number :current_page_number
end
save_to 'google_results.json', results, format: :json
if results[:next_page_link] && results[:current_page_number] < 3
request_to :parse, url: absolute_url(results[:next_page_link], base: url)
end
end
end
GoogleSpider.crawl!
How it works:
extract sends the HTML + your schema to an LLMgoogle_spider.jsonPrefer writing your own selectors? Kimurai works great as a traditional scraper too — with headless antidetect Chromium, Firefox, or simple HTTP requests:
# github_spider.rb
require 'kimurai'
class GithubSpider < Kimurai::Base
@engine = :chrome
@start_urls = ["https://github.com/search?q=ruby+web+scraping&type=repositories"]
@delay = 3..5
def parse(response, url:, data: {})
response.xpath("//div[@data-testid='results-list']//div[contains(@class, 'search-title')]/a").each do |a|
request_to :parse_repo_page, url: absolute_url(a[:href], base: url)
end
if next_page = response.at_xpath("//a[@rel='next']")
request_to :parse, url: absolute_url(next_page[:href], base: url)
end
end
def parse_repo_page(response, url:, data: {})
item = {}
item[:owner] = response.xpath("//a[@rel='author']").text.squish
item[:repo_name] = response.xpath("//strong[@itemprop='name']").text.squish
item[:repo_url] = url
item[:description] = response.xpath("//div[h2[text()='About']]/p").text.squish
item[:tags] = response.xpath("//div/a[contains(@title, 'Topic')]").map { |a| a.text.squish }
item[:watch_count] = response.xpath("//div/h3[text()='Watchers']/following-sibling::div[1]/a/strong").text.squish
item[:star_count] = response.xpath("//div/h3[text()='Stars']/following-sibling::div[1]/a/strong").text.squish
item[:fork_count] = response.xpath("//div/h3[text()='Forks']/following-sibling::div[1]/a/strong").text.squish
item[:last_commit] = response.xpath("//div[@data-testid='latest-commit-details']//relative-time/text()").text.squish
save_to "results.json", item, format: :pretty_json
end
end
GithubSpider.crawl!
$ ruby github_spider.rb
I, [2025-12-16 12:15:48] INFO -- github_spider: Spider: started: github_spider
I, [2025-12-16 12:15:48] INFO -- github_spider: Browser: started get request to: https://github.com/search?q=ruby+web+scraping&type=repositories
I, [2025-12-16 12:16:01] INFO -- github_spider: Browser: finished get request to: https://github.com/search?q=ruby+web+scraping&type=repositories
I, [2025-12-16 12:16:01] INFO -- github_spider: Info: visits: requests: 1, responses: 1
I, [2025-12-16 12:16:01] INFO -- github_spider: Browser: started get request to: https://github.com/sparklemotion/mechanize
I, [2025-12-16 12:16:06] INFO -- github_spider: Browser: finished get request to: https://github.com/sparklemotion/mechanize
I, [2025-12-16 12:16:06] INFO -- github_spider: Info: visits: requests: 2, responses: 2
I, [2025-12-16 12:16:06] INFO -- github_spider: Browser: started get request to: https://github.com/jaimeiniesta/metainspector
I, [2025-12-16 12:16:11] INFO -- github_spider: Browser: finished get request to: https://github.com/jaimeiniesta/metainspector
I, [2025-12-16 12:16:11] INFO -- github_spider: Info: visits: requests: 3, responses: 3
I, [2025-12-16 12:16:11] INFO -- github_spider: Browser: started get request to: https://github.com/Germey/AwesomeWebScraping
I, [2025-12-16 12:16:13] INFO -- github_spider: Browser: finished get request to: https://github.com/Germey/AwesomeWebScraping
I, [2025-12-16 12:16:13] INFO -- github_spider: Info: visits: requests: 4, responses: 4
I, [2025-12-16 12:16:13] INFO -- github_spider: Browser: started get request to: https://github.com/vifreefly/kimuraframework
I, [2025-12-16 12:16:17] INFO -- github_spider: Browser: finished get request to: https://github.com/vifreefly/kimuraframework
...
[
{
"owner": "sparklemotion",
"repo_name": "mechanize",
"repo_url": "https://github.com/sparklemotion/mechanize",
"description": "Mechanize is a ruby library that makes automated web interaction easy.",
"tags": ["ruby", "web", "scraping"],
"watch_count": "79",
"star_count": "4.4k",
"fork_count": "480",
"last_commit": "Sep 30, 2025",
"position": 1
},
{
"owner": "jaimeiniesta",
"repo_name": "metainspector",
"repo_url": "https://github.com/jaimeiniesta/metainspector",
"description": "Ruby gem for web scraping purposes. It scrapes a given URL, and returns you its title, meta description, meta keywords, links, images...",
"tags": [],
"watch_count": "20",
"star_count": "1k",
"fork_count": "166",
"last_commit": "Oct 8, 2025",
"position": 2
},
{
"owner": "Germey",
"repo_name": "AwesomeWebScraping",
"repo_url": "https://github.com/Germey/AwesomeWebScraping",
"description": "List of libraries, tools and APIs for web scraping and data processing.",
"tags": ["javascript", "ruby", "python", "golang", "php", "awesome", "captcha", "proxy", "web-scraping", "aswsome-list"],
"watch_count": "5",
"star_count": "253",
"fork_count": "33",
"last_commit": "Apr 5, 2024",
"position": 3
},
{
"owner": "vifreefly",
"repo_name": "kimuraframework",
"repo_url": "https://github.com/vifreefly/kimuraframework",
"description": "Kimurai is a modern web scraping framework written in Ruby which works out of box with Headless Chromium/Firefox, PhantomJS, or simple HTTP requests and allows to scrape and interact with JavaScript rendered websites",
"tags": ["crawler", "scraper", "scrapy", "headless-chrome", "kimurai"],
"watch_count": "28",
"star_count": "1k",
"fork_count": "158",
"last_commit": "Dec 12, 2025",
"position": 4
},
// ...
{
"owner": "citixenken",
"repo_name": "web_scraping_with_ruby",
"repo_url": "https://github.com/citixenken/web_scraping_with_ruby",
"description": "",
"tags": [],
"watch_count": "1",
"star_count": "0",
"fork_count": "0",
"last_commit": "Aug 29, 2022",
"position": 118
}
]
Okay, that was easy. How about JavaScript rendered websites with dynamic HTML? Let's scrape a page with infinite scroll:
# infinite_scroll_spider.rb
require 'kimurai'
class InfiniteScrollSpider < Kimurai::Base
@engine = :chrome
@start_urls = ["https://infinite-scroll.com/demo/full-page/"]
def parse(response, url:, data: {})
posts_headers_path = "//article/h2"
count = response.xpath(posts_headers_path).count
loop do
browser.execute_script("window.scrollBy(0,10000)") ; sleep 2
response = browser.current_response
new_count = response.xpath(posts_headers_path).count
if count == new_count
logger.info "> Pagination is done" and break
else
count = new_count
logger.info "> Continue scrolling, current posts count is #{count}..."
end
end
posts_headers = response.xpath(posts_headers_path).map(&:text)
logger.info "> All posts from page: #{posts_headers.join('; ')}"
end
end
InfiniteScrollSpider.crawl!
$ ruby infinite_scroll_spider.rb
I, [2025-12-16 12:47:05] INFO -- infinite_scroll_spider: Spider: started: infinite_scroll_spider
I, [2025-12-16 12:47:05] INFO -- infinite_scroll_spider: Browser: started get request to: https://infinite-scroll.com/demo/full-page/
I, [2025-12-16 12:47:09] INFO -- infinite_scroll_spider: Browser: finished get request to: https://infinite-scroll.com/demo/full-page/
I, [2025-12-16 12:47:09] INFO -- infinite_scroll_spider: Info: visits: requests: 1, responses: 1
I, [2025-12-16 12:47:11] INFO -- infinite_scroll_spider: > Continue scrolling, current posts count is 5...
I, [2025-12-16 12:47:13] INFO -- infinite_scroll_spider: > Continue scrolling, current posts count is 9...
I, [2025-12-16 12:47:15] INFO -- infinite_scroll_spider: > Continue scrolling, current posts count is 11...
I, [2025-12-16 12:47:17] INFO -- infinite_scroll_spider: > Continue scrolling, current posts count is 13...
I, [2025-12-16 12:47:19] INFO -- infinite_scroll_spider: > Continue scrolling, current posts count is 15...
I, [2025-12-16 12:47:21] INFO -- infinite_scroll_spider: > Pagination is done
I, [2025-12-16 12:47:21] INFO -- infinite_scroll_spider: > All posts from page:
1a - Infinite Scroll full page demo;
1b - RGB Schemes logo in Computer Arts;
2a - RGB Schemes logo;
2b - Masonry gets horizontalOrder;
2c - Every vector 2016;
3a - Logo Pizza delivered;
3b - Some CodePens;
3c - 365daysofmusic.com;
3d - Holograms;
4a - Huebee: 1-click color picker;
4b - Word is Flickity is good;
Flickity v2 released: groupCells, adaptiveHeight, parallax;
New tech gets chatter; Isotope v3 released: stagger in, IE8 out;
Packery v2 released
I, [2025-12-16 12:47:21] INFO -- infinite_scroll_spider: Browser: driver selenium_chrome has been destroyed
I, [2025-12-16 12:47:21] INFO -- infinite_scroll_spider: Spider: stopped: {spider_name: "infinite_scroll_spider", status: :completed, error: nil, environment: "development", start_time: 2025-12-16 12:47:05.372053 +0300, stop_time: 2025-12-16 12:47:21.505078 +0300, running_time: "16s", visits: {requests: 1, responses: 1}, items: {sent: 0, processed: 0}, events: {requests_errors: {}, drop_items_errors: {}, custom: {}}}
Configure your LLM provider to start using AI extraction. The extract method is powered by Nukitori:
# github_spider_ai.rb
require 'kimurai'
Kimurai.configure do |config|
config.default_model = "gemini-3-flash-preview" # OpenAI, Anthropic, Gemini, local LLMs, etc.
config.gemini_api_key = ENV["GEMINI_API_KEY"]
end
class GithubSpider < Kimurai::Base
@engine = :chrome
@start_urls = ["https://github.com/search?q=ruby+web+scraping&type=repositories"]
@delay = 3..5
def parse(response, url:, data: {})
data = extract(response) do
string :next_page_url, description: "Next page path url"
array :repos do
object do
string :name
string :url
string :description
string :stars
string :language
array :tags, of: :string
end
end
end
save_to "results.json", data[:repos], format: :json