Лучший способ написать гибкий модуль импортера
Пользователь может импортировать свои данные с других сайтов. Все, что ему нужно сделать, это ввести свое имя пользователя на иностранном сайте, и мы возьмем все фотографии и сохраним их в своей галерее. Некоторые из изображений необходимо преобразовать с помощью 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,...}
Чтобы обеспечить гибкость доступа.