Лучший способ написать гибкий модуль импортера

Пользователь может импортировать свои данные с других сайтов. Все, что ему нужно сделать, это ввести свое имя пользователя на иностранном сайте, и мы возьмем все фотографии и сохраним их в своей галерее. Некоторые из изображений необходимо преобразовать с помощью rMagick (вращающийся, водяной знак), который зависит от импортера (зависит от того, на каком веб-сайте пользователь выбирает импорт данных)

Мы обсуждаем самый сексуальный и самый гибкий способ сделать это. Мы используем несущую волну, но мы перейдем к скрепке, если она нам больше подходит.

Структура импортера

Текущая структура выглядит как (ее грубо псевдокод)

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

Мы боремся с решением, каково лучшее возвращение импортера.

Solution1:

Импортер возвращает массив строк с URL-адресами [ "http://..." , "http://..." , "http://..." ]. В этом массиве мы можем легко контактировать и передать несущую/скрепку на удаленные_нагрузки изображения. После этого мы запустим процессор для преобразования изображений, если нам нужно.

 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

это фактически возвращает массив [ "url_to_image" , "url_to_image" , "url_to_image" ]

Затем в Picture.after_create мы вызываем что-то, чтобы удалить водяной знак на этом изображении.

Solution2:

grab_pictures загружает каждое изображение в файл tempfile и преобразует его. он будет возвращать массив tempfiles [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]

Резюме

Результат будет таким же для пользователя, но внутри мы обнаруживаем два разных способа обработки данных.

Мы хотим сохранить логику там, где она принадлежит, и работать как можно более общие.

Можете ли вы, ребята, помочь нам выбрать правильный путь? Longterm мы хотим иметь около 15 разных импортеров.

Ответы

Ответ 1

У меня была аналогичная ситуация с этим в последнее время - я рекомендую массив строк по нескольким причинам:

  • Знакомство: как часто вы работаете с tempfiles? Как насчет других разработчиков в вашей команде? Насколько легко манипулировать строками и манипулировать временными файлами?

  • Гибкость: теперь вы хотите просто обработать изображение, но, возможно, в будущем вам нужно будет отслеживать идентификатор изображения для каждого изображения с внешнего сайта. Это тривиально с массивом строк. С массивом tempfiles это сложнее (насколько это зависит, но дело в том, что это будет сложнее). Это, конечно, относится и к другим, но еще неизвестным целям.

  • Скорость: быстрее и использует меньше дискового пространства для обработки массива строк, чем группа файлов. Это, возможно, небольшая проблема, но если вы будете залиты множеством фотографий одновременно, это может рассматриваться как зависящее от вашей среды.

В конечном счете, самое лучшее, что я могу сказать, это начать с строк, сделать несколько импортеров, а затем посмотреть, как они выглядят и чувствуют. Представьте, что вы менеджер проекта или клиент - начните делать странные, потенциально необоснованные требования к собранным данным. Насколько легко вам будет соответствовать этим требованиям с вашей текущей реализацией? Было бы проще, если бы вы использовали tempfiles?

Ответ 2

Я делаю это для аналогичного проекта, где мне нужно просматривать и получать информацию на разных сайтах. На каждом из этих сайтов я должен достичь той же цели, выполняя примерно те же действия, и они не имеют никакого курса по-разному.

Решение основано на основных принципах ООП:

Основной класс: обрабатывать операции высокого уровня, обрабатывать операции с базой данных, обрабатывать операции с изображениями, управлять ошибками

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

Ответ 3

Я решил аналогичную проблему... Мне пришлось импортировать из файла xls различные типы ресурсов, используя:

  • Класс импортера (ResourcesGroupsImporter).
  • Базовый класс сопоставления (ResourceMapper) Он выступает в качестве интерфейса для определенных картографов. Он имеет общие методы для всех ресурсов и повышает NotImplementedError, предлагая вам реализовать эти методы при добавлении нового типа ресурса.
  • Один сопоставитель по типу ресурсов (DetentionsPollMapper, FrontCycleMapper). Каждый из них реализует определенную логику для определенного ресурса.

Пример реализации:

Импортер...

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

Ответ 4

Помощник должен предоставить способ доступа к пиктограмме, как вы предпочитаете.

Однако сохранение "http://..." , "http://..." , "http://..." такого рода строк является недостатком безопасности.

Я предпочел бы хэш как это: domain_name = { "name_on_url.jpg" = > path_on_disk,...}

Чтобы обеспечить гибкость доступа.