编写灵活导入器模块的最佳方法

时间:2015-08-12 22:09:08

标签: ruby-on-rails ruby

用户可以从其他网站导入他的数据。他需要做的只是在国外网站上输入他的用户名,我们将抓住所有图片并将其保存到自己的图库中。有些图片需要使用rMagick(旋转,水印)进行转换,这取决于导入器(取决于用户选择从哪个网站导入数据)

我们正在讨论最性感和最灵活的方式。我们正在使用carrierwave,但如果它适合我​​们,我们将改为回形针。

进口商结构

当前结构看起来像(大致是伪代码)

module Importer
  class Website1
    def grab_pictures
    end
  end

  class Website2
    def grab_pictures
    end
  end
end

class ImporterJob        
  def perform(user, type, foreign_username)
    pictures = Importer::type.grab_pictures(foreign_username)

    pictures.each do |picture|
      user.pictures.create picture 
    end
  end
end

我们为这个决定而苦苦挣扎,是进口商的最佳回报。

解决方法1:

导入程序返回带有网址的字符串数组 [" http:// ...."," http:// ...。&# 34;," http://..."]。 我们可以轻松地循环并告诉carrierwave / paperclip remote_download图像。之后,如果需要,我们将运行处理器来转换图片。

 def get_picture_urls username
  pictures = []
  page = get_html(username)
  page.scan(/\/p\/\d{4}-\d{2}\/#{username}\/[\w\d]{32}-thumb.jpg/).each do |path|
    pictures << path
  end
  pictures.uniq.collect{|x| "http://www.somewebsite.com/#{x.gsub(/medium|thumb/, "big")}"}
end

这实际上会返回一个数组[&#34; url_to_image&#34;,&#34; url_to_image&#34;,&#34; url_to_image&#34;]

然后在Picture.after_create中,我们调用了一些东西来移除该Image上的Watermark。

溶液2:

grab_pictures正在将每张图片下载到一个tempfile并对其进行转换。它将返回一个临时文件数组 [tempfile,tempfile,tempfile]

代码是:

def read_pictures username
  pictures = []
  page = get_html(username)
  page.scan(/\/p\/\d{4}-\d{2}\/#{username}\/[a-z0-9]{32}-thumb.jpg/).each do |path|
    pictures << path
  end
  pictures.uniq.map { |pic_url| remove_logo(pic_url) }
end

def remove_logo pic_url
    big = Magick::Image.from_blob(@agent.get(pic_url.gsub(/medium.jpg|thumb.jpg/, 'big.jpg')).body).first
    # ... do some transformation and watermarking
    file = Tempfile.new(['tempfile', '.jpg'])
    result.write(file.path)
    file
  end

这实际上返回了[Tempfile,Tempfile,Tempfile]

的数组

摘要

结果对用户来说是一样的 - 但在内部我们发现了两种不同的数据处理方式。

我们希望将逻辑保留在它所属的位置并尽可能地通用。

你们能帮我们选择正确的方法吗?长期我们希望有大约15个不同的进口商。

4 个答案:

答案 0 :(得分:4)

我最近遇到了类似的情况 - 我推荐一系列字符串有几个原因:

  1. 熟悉:您多久使用一次临时文件?您团队中的其他开发人员呢?操纵字符串与操纵临时文件有多容易?

  2. 灵活性:现在您只想处理图片,但将来您可能需要跟踪外部网站上每张图片的图片ID。这对于一系列字符串来说是微不足道的。使用一系列临时文件,它更难(只需多少,但事实是它会更难)。当然,这也适用于其他尚未知的目标。

  3. 速度:它比一组文件更快,使用更少的磁盘空间来处理字符串数组。这可能是一个小问题,但如果你同时充斥着很多照片,那么根据你的环境可能需要考虑。

  4. 最终,我能说的最好的事情是从字符串开始,制作一些进口商,然后看看它的外观和感觉。假装您是项目经理或客户 - 开始对您收集的数据提出奇怪的,可能不合理的要求。您目前的实施方式满足这些要求有多容易?如果你使用临时文件会更容易吗?

答案 1 :(得分:2)

我这样做是为了一个类似的项目,我必须在不同的网站上浏览和获取信息。在每个网站上,我必须通过执行大致相同的操作来达到相同的目标,并且它们的结构不同,所有结构都不同。

该解决方案的灵感来自OOP的基本原则:

主类:处理高级操作,处理数据库操作,处理图像操作,管理错误

class MainClass

  def import
    # Main method, prepare the download and loop through each images
    log_in
    go_to_images_page
    images = get_list_of_images
    images.each do |url|
      begin
        image_record = download_image url
        transform_image image_record
      rescue
        manage_error
      end
    end
    display_logs
    send_emails
  end

  def download_image(url)
    # Once the specific class returned the images url, this common method
    # Is responsible for downloading and creating database record
    record = Image.new picture: url 
    record.save!
    record
  end  

  def transform_image(record)
    # Transformation is common so this method sits in the main class
    record.watermark!
  end   

  # ... the same for all commom methods (manage_error, display_logs, ...)

end

特定类(每个目标网站一个):处理低级别的操作并将数据返回到主类。这个类必须具有的唯一交互是网站,这意味着没有数据库访问和尽可能没有错误管理(不要被你的设计卡住;)

注意:在我的设计中,我只是从MainClass继承,但如果您愿意,可以使用模块包含。

class Target1Site < MainClass
  def log_in
    # Perform specific action in website to log the use in
    visit '/log_in'
    fill_in :user_name, with: ENV['user_name']
    ...
  end

  def go_to_images_page
    # Go to specific url
    visit '/account/gallery'
  end

  def get_list_of_images
    # Use specific css paths
    images = all :css, 'div#image-listing img'
    images.collect{|i| i['src']}
  end

  # ...

end

答案 2 :(得分:2)

我解决了类似的问题......我不得不使用以下方法从xls文件导入不同的资源类型:

  • 导入器类(ResourcesGroupsImporter)。
  • 基本映射器类(ResourceMapper)它充当特定映射器的接口。它为所有资源提供了通用方法,并且引发NotImplementedError,鼓励您在添加新资源类型时实施这些方法。
  • 按资源类型划分的一个映射器(DetentionsPollMapperFrontCycleMapper)。每一个都为特定资源实现特定逻辑。

实施示例:

进口商......

class ResourcesGroupsImporter
  attr_reader :group
  attr_reader :mappers

  def initialize(_source, _resources_group)
    @group = _resources_group
    @source = _source
    @xls = Roo::Spreadsheet.open(@source.path, extension: :xlsx)
    @mappers = Resource::RESOURCEABLE_CLASSES.map { |klass| resource_mapper(klass) }
  end

  def import
    ActiveRecord::Base.transaction do
      self.mappers.each { |mapper| create_resource(mapper) }
      relate_source_with_group unless self.has_errors?
      raise ActiveRecord::Rollback if self.has_errors?
    end
  end

  def has_errors?
    !self.mappers.select { |mapper| mapper.has_errors? }.empty?
  end

  private

  def resource_mapper(_class)
    "#{_class}Mapper".constantize.new(@xls, @group)
  end

  def create_resource(_mapper)
    return unless _mapper.resource

    _mapper.load_resource_attributes
    _mapper.resource.complete
    _mapper.resource.force_validation = true

    if _mapper.resource.save
      create_resource_items(_mapper)
    else
      _mapper.load_general_errors
    end
  end

  def create_resource_items(_mapper)
    _mapper.set_items_sheet
    columns = _mapper.get_items_columns

    @xls.each_with_index(columns) do |data, index|
      next if data == columns
      break if data.values.compact.size.zero?
      item = _mapper.build_resource_item(data)
      _mapper.add_detail_errors(index, item.errors.messages) unless item.save
    end
  end

  def relate_source_with_group
    @group.reload
    @group.source = @source
    @group.save!
  end
end

界面......

class ResourceMapper
  attr_reader :general_errors
  attr_reader :detailed_errors
  attr_reader :resource

  def initialize(_xls, _resource_group)
    @xls = _xls
    @resource = _resource_group.resourceable_by_class_type(resource_class)
  end

  def resource_class
    raise_implementation_error
  end

  def items_sheet_number
    raise_implementation_error
  end

  def load_resource_attributes
    raise_implementation_error
  end

  def get_items_columns
    raise_implementation_error
  end

  def build_resource_item(_xls_item_data)
    resource_items.build(_xls_item_data)
  end

  def raise_implementation_error
    raise NotImplementedError.new("#{caller[0]} method not implemented on inherited class")
  end

  def has_errors?
    !self.general_errors.nil? || !self.detailed_errors.nil?
  end

  def resource_items
    self.resource.items
  end

  def human_resource_name
    resource_class.model_name.human
  end

  def human_resource_attr(_attr)
    resource_class.human_attribute_name(_attr)
  end

  def human_resource_item_attr(_attr)
    "#{resource_class}Item".constantize.human_attribute_name(_attr)
  end

  def load_general_errors
    @general_errors = self.resource.errors.messages
  end

  def add_detail_errors(_xls_row_idx, _error)
    @detailed_errors ||= []
    @detailed_errors << [ _xls_row_idx+1, _error ]
  end

  def set_items_sheet
    @xls.default_sheet = items_sheet
  end

  def general_sheet
    sheet(0)
  end

  def items_sheet
    sheet(self.items_sheet_number)
  end

  def sheet(_idx)
    @xls.sheets[_idx]
  end

  def general_cell(_col, _row)
    @xls.cell(_col, _row, general_sheet)
  end
end

特定的映射器类型......

class DetentionsPollMapper < ResourceMapper
  def items_sheet_number
    6
  end

  def resource_class
    DetentionsPoll
  end

  def load_resource_attributes
    self.resource.crew = general_cell("N", 3)
    self.resource.supervisor = general_cell("N", 4)
  end

  def get_items_columns
    {
      issue: "Problema identificado",
      creation_date: "Fecha",
      workers_count: "N° Trabajadores esperando",
      detention_hours_string: "HH Detención",
      lost_hours: "HH perdidas",
      observations: "Observación"
    }
  end

  def build_resource_item(_xls_item_data)
    activity = self.resource.activity_by_name(_xls_item_data[:issue])

    data = {
      creation_date: _xls_item_data[:creation_date],
      workers_count: _xls_item_data[:workers_count],
      detention_hours_string: _xls_item_data[:detention_hours_string],
      lost_hours: _xls_item_data[:lost_hours],
      observations: _xls_item_data[:observations],
      activity_id: !!activity ? activity.id : nil
    }

    resource_items.build(data)
  end
end

class FrontCycleMapper < ResourceMapper
  def items_sheet_number
    8
  end

  def resource_class
    FrontCycle
  end

  def load_resource_attributes
    self.resource.front = general_cell("S", 3)
  end

  def get_items_columns
    {
      task: "Tarea",
      start_time_string: "Hora",
      task_type: "Tipo de Tarea",
      description: "Descripción"
    }
  end

  def build_resource_item(_xls_item_data)
    activity = self.resource.activity_by_name_and_category(
      _xls_item_data[:task], _xls_item_data[:task_type])

    data = {
      description: _xls_item_data[:description],
      start_time_string: _xls_item_data[:start_time_string],
      activity_id: !!activity ? activity.id : nil
    }

    resource_items.build(data)
  end
end

答案 3 :(得分:2)

帮助者必须提供一种方法来访问pict。

但是保存“http:// ...”,“http:// ...”,“http:// ...”这种字符串,缺乏安全性。

我更喜欢像这样的哈希:domain_name = {“name_on_url.jpg”=&gt; path_on_disk,...}

确保访问的灵活性。