什么是这种情况下最好的Ruby类设计/模式?

时间:2013-08-31 14:13:29

标签: mysql ruby oop

我目前使用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个零售商)。

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

1 个答案:

答案 0 :(得分:1)

就像@James伍德沃德所说,你将要为每个零售商创建一个班级。我要发布的模式有三个部分:

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

    这一步非常简单。您已经有SiteSiteDetail课程。您可以保留它们以存储您从数据库中的网站获取的数据。

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

    1. 在数据库中保留数据
    2. 从网站上抓取数据
    3. 我们将在第二步中创建新类来处理抓取责任。现在,您可以删除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
      3. 但是,Ruby没有抽象类。它的功能是鸭子打字和模块混合。这意味着我们将使用这种非常相似的模式:

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

          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中的类,并为您需要抓取的700个网站中的每个网站实现get_product_urlsget_product_details。不幸的是,这是模式的繁琐部分:没有真正的方法可以为所有700个站点编写不同的抓取算法。

          第3步:运行每个刮板

          最后一步是创建擦除网站的cron作业。

          every :day, at: '12:00am' do
            ExampleScraper.new.get_data
            FooBarScraper.new.get_data
            # + 698 more lines
          end