3

我目前有这个类,用于使用 Nokogiri 从单个零售商网站上抓取产品。XPath、CSS 路径详细信息存储在 MySQL 中。

ActiveRecord::Base.establish_connection( 
  :adapter => "mysql2",
  ...
)

class Site < ActiveRecord::Base
  has_many :site_details

  def create_product_links
    # http://www.example.com
    p = Nokogiri::HTML(open(url))
    p.xpath(total_products_path).each {|lnk| SiteDetail.find_or_create_by(url: url + "/" + lnk['href'], site_id: self.id)}
  end    
end

class SiteDetail < ActiveRecord::Base
  belongs_to :site   

  def get_product_data
    # http://www.example.com
    p = Nokogiri::HTML(open(url))
    title = p.css(site.title_path).text
    price = p.css(site.price_path).text
    description = p.css(site.description_path).text
    update_attributes!(title: title, price: price, description: description)
  end 
end

# Execution
@s = Site.first
@s.site_details.get_product_data

将来我会添加更多站点(大约 700 个)。每个站点都有不同的页面结构。所以get_product_data方法不能按原样使用。我可能不得不用来case or if statement跳转和执行相关代码。很快,这个类变得相当粗壮和丑陋(700 个零售商)。

适合这种情况的最佳设计方法是什么?

4

1 回答 1

1

就像@James Woodward 所说,您将要为每个零售商创建一个类。我要发布的模式包含三个部分:

  1. 几个ActiveRecord实现通用接口的类,用于存储要从每个站点记录的数据
  2. 700 个不同的类,一个用于您要抓取的每个站点。这些类实现了抓取网站的算法,但不知道如何将信息存储在数据库中。为此,它们依赖于步骤 1 中的通用接口。
  3. 最后一个类将它们联系在一起,运行您在步骤 2 中编写的每个抓取算法。

第 1 步:ActiveRecord接口

这一步非常简单。你已经有一个SiteSiteDetail类。您可以保留它们以将您从网站上抓取的数据存储在数据库中。

你告诉SiteSiteDetail类如何从网站上抓取数据。我认为这是不合适的。现在你已经赋予类两个职责:

  1. 将数据持久化到数据库中
  2. 从网站上抓取数据

我们将在第二步创建新的类来处理抓取责任。现在,您可以删除SiteSiteDetail类,以便它们仅充当数据库记录:

class Site < ActiveRecord::Base
  has_many :site_details
end

class SiteDetail < ActiveRecord::Base
  belongs_to :site
end

第 2 步:实施刮板

现在,我们将创建处理抓取责任的新类。如果这是一种支持抽象类或接口(如 Java 或 C#)的语言,我们将这样进行:

  1. 创建一个IScraperAbstractScraper界面来处理抓取网站的常见任务。
  2. FooScraper为您要抓取的每个站点实现一个不同的类,每个站点都继承AbstractScraper或实现IScraper.

不过,Ruby 没有抽象类。它所拥有的是鸭子类型和模块混合。这意味着我们将使用这种非常相似的模式:

  1. 创建一个SiteScraper模块来处理抓取网站的常见任务。该模块将假定扩展它的类具有可以调用的某些方法。
  2. FooScraper为您要抓取的每个站点实现一个不同的类,每个类都混合在SiteScraper模块中并实现模块期望的方法。

它看起来像这样:

module SiteScraper
  # Assumes that classes including the module
  # have get_products and get_product_details methods
  #
  # The get_product_urls method should return a list
  # of the URLs to visit to get scraped data
  #
  # The get_product_details the URL of the product to
  # scape as a string and return a SiteDetail with data
  # scraped from the given URL 
  def get_data
    site = Site.new
    product_urls = get_product_urls

    for product_url in product_urls
      site_detail = get_product_details product_url
      site_detail.site = site
      site_detail.save
    end
  end
end 

class ExampleScraper
  include 'SiteScraper'

  def get_product_urls
    urls = []
    p = Nokogiri::HTML(open('www.example.com/products'))
    p.xpath('//products').each {|lnk| urls.push lnk}
    return urls
  end

  def get_product_details(product_url)
    p = Nokogiri::HTML(open(product_url))
    title = p.css('//title').text
    price = p.css('//price').text
    description = p.css('//description').text

    site_detail = SiteDetail.new
    site_detail.title = title
    site_detail.price = price
    site_detail.description = description
    return site_detail
  end
end

class FooBarScraper
  include 'SiteScraper'

  def get_product_urls
    urls = []
    p = Nokogiri::HTML(open('www.foobar.com/foobars'))
    p.xpath('//foo/bar').each {|lnk| urls.push lnk}
    return urls
  end

  def get_product_details(product_url)
    p = Nokogiri::HTML(open(product_url))
    title = p.css('//foo').text
    price = p.css('//bar').text
    description = p.css('//foo/bar/iption').text

    site_detail = SiteDetail.new
    site_detail.title = title
    site_detail.price = price
    site_detail.description = description
    return site_detail
  end
end

...等等,创建一个混合SiteScraper并实现的类,get_product_urlsget_product_details为您需要抓取的 700 个网站中的每个网站创建一个类。不幸的是,这是模式中乏味的部分:没有真正的方法可以为所有 700 个站点编写不同的抓取算法。

第 3 步:运行每个 Scraper

最后一步是创建抓取站点的 cron 作业。

every :day, at: '12:00am' do
  ExampleScraper.new.get_data
  FooBarScraper.new.get_data
  # + 698 more lines
end
于 2013-09-15T21:05:11.370 回答