为什么我的 Ruby 函数网页只抓取第一页而不抓取分页页面?

Why is my Ruby function web scraping the first page only and not the paginated pages?

我正在使用 nokogiri 在经销商网站的大约 14 个页面上抓取所有车辆,我遇到的错误是我的代码 运行 仅在第一页上抓取了 14 次。我的代码有什么问题?

正如您从输出中看到的那样,一遍又一遍地抓取相同的车辆,而不是下一页中的一组新车辆。

Ruby 版本: 2.6.2

scraper.rb :

    require 'nokogiri'
    require 'httparty'
    require 'byebug'
    
    def scraper
        url = "https://www.example.com/new-vehicles/"
        unparsed_page = HTTParty.get(url)
        parsed_page = Nokogiri::HTML(unparsed_page)
        vehicles = Array.new
        vehicle_listings = parsed_page.css("//div[@class='vehicle list-view new-vehicle publish']") #20 cars
        page = 1
        per_page = vehicle_listings.count  #20
        total = parsed_page.css('span.count').text.to_i #281
        last_page = (total.to_f / per_page.to_f).ceil #14
        while page <= last_page
            pagination_url = "https://www.example.com/new-vehicles/#action=im_ajax_call&perform=get_results&page=#{page}"
            pagination_unparsed_page = HTTParty.get(pagination_url)
            puts pagination_url
            puts "Page: #{page}"
            puts ''
            pagination_parsed_page = Nokogiri::HTML(pagination_unparsed_page)
            pagination_vehicle_listings = pagination_parsed_page.css("//div[@class='vehicle list-view new-vehicle publish']") #20 cars
            pagination_vehicle_listings.each do |vehicle_listing|
                vehicle = {
                    title: vehicle_listing.css('h2')&.text&.gsub("New", '').gsub("2021", '').gsub("With", '').gsub("Navigation", ''),
                    price: vehicle_listing.css('span.price')[0]&.text&.delete('^0-9').to_i,
                    stock_number: vehicle_listing.css('.stock-label')&.text.gsub("Stock #: ", ''),
                    exterior_color: vehicle_listing.css('span.detail-content')[3]&.text,
                    interior_color: vehicle_listing.css('span.detail-content')[4]&.text&.delete('0-9').gsub('MPG', 'unavailable')
                }
                vehicles << vehicle
                    puts "Added #{vehicle[:stock_number]}"
                    puts ""
                end
                page += 1
        end
    byebug
    end
        
scraper

输出:

Page: 1

Added vehicle with stock#: 218864

Added vehicle with stock#: 218865

Added vehicle with stock#: 218604

https://www.example.com/new-vehicles/#action=im_ajax_call&perform=get_results&page=2

Page: 2

Added vehicle with stock#: 218864

Added vehicle with stock#: 218865

Added vehicle with stock#: 218604

虽然看起来数据是通过遵循 link 的 GET 请求加载的,但实际上是通过 jQuery 的 POST 请求加载的。 您可以通过打开浏览器开发人员工具的“网络”选项卡来验证这一点。

如果您在浏览器中输入 url,则会发出一个请求:

  • 对初始内容的 GET 请求

如果您随后在车辆列表的分页导航中单击 link,则

  • URL 已更新以包含页面。请注意,它不是 query (?),而是 fragment (#) 的一部分,它永远不会发送到服务器但留在客户端。
  • 请求页面的内容是通过 POST 请求(通过 jQuery)获取的,然后该响应被合并到内容中,看起来它是通过 POST 请求获取的=65=].

如果你想模仿这个POST请求,那就有点复杂了:

它包含一个 nonce,您可以在您必须发送的初始内容(搜索 ajax_nonce)中找到它(因此获取初始内容,抓取随机数,然后将其与 POST 请求一起发送)

这是一个带有 CURL(替换随机数)的示例,它会将第 2 页的列表返回为 JSON(即使响应 header 说 Content-Type: text/html; charset=UTF-8

curl -X POST https://www.pacificbmw.com/
     -H "Content-Type: application/x-www-form-urlencoded" 
     -d "action=im_ajax_call&perform=get_results&page=2&_nonce=e085e144a19b3b493c7aab9aff479722&_post_id=6&_referer=/new-vehicles/" 

这将 return

{"results":"
<table class=\ "results_table\">\n
    <tbody>\t\n\n
        <tr class=\ "hidden-xs\">\n
            <td colspan=\ "7\">\n
                <!-- VEHICLE BLOCK -->\n
                <div class=\ "vehicle list-view new
    ....

您需要将其解析为 JSON,然后提取 results 密钥并使用 nokogiri 再次解析,然后抓取列表。

更新:

作为替代方案,您还可以使用支持和执行 javascript(Selenium + Headless Chrome 或类似)

的抓取工具

像这样你可以模仿浏览器和用户,你可以触发点击表单输入。

这篇文章是一个简短的介绍:https://russmorley.net/tutorial/selenium/2019/03/07/Scrape.html

我能够通过使用 Watir 和无头 chrome 浏览器来抓取我需要的数据,该浏览器允许 ajax/javascript 代码在抓取页面之前 运行。

    require 'nokogiri'
    require 'httparty'
    require 'byebug'
    require 'watir'
    
    def scraper
        url = "https://www.example.com/new-vehicles/"
        unparsed_page = HTTParty.get(url)
        parsed_page = Nokogiri::HTML(unparsed_page)
        vehicles = Array.new
        vehicle_listings = parsed_page.css("//div[@class='vehicle list-view new-vehicle publish']") #20 cars
        page = 1
        per_page = vehicle_listings.count  #20
        total = parsed_page.css('span.count').text.to_i #281
        last_page = (total.to_f / per_page.to_f).ceil #14
        
        # Create instance of headless chrome called browser
        browser = Watir::Browser.new :chrome, headless: true
        
        while page <= last_page
            pagination_url = "https://www.example.com/new-vehicles/#action=im_ajax_call&perform=get_results&page=#{page}"
            browser.goto(pagination_url)
            pagination_unparsed_page = Nokogiri::HTML(browser.html)
            puts pagination_url
            puts "Page: #{page}"
            puts ''
            pagination_parsed_page = pagination_unparsed_page
            pagination_vehicle_listings = pagination_parsed_page.css("//div[@class='vehicle list-view new-vehicle publish']") #20 cars
            pagination_vehicle_listings.each do |vehicle_listing|
                vehicle = {
                    stock_number: vehicle_listing.css('.stock-label')&.text.gsub("Stock #: ", '')
                }
                vehicles << vehicle
                    puts "Added #{vehicle[:stock_number]}"
                    puts ""
                end
                page += 1
        end
        # Be sure to close the browser
        browser.close
    byebug
    end

scraper