Как ограбить магазин среди бела дня: Часть 1

В один прекрасный момент потребовалось одну БД наполнить тестовыми данными, причём достаточным количеством, чтобы можно было использовать для тестирований различных, и чтобы смысл несло. А приложение таково, что содержит сведения о различных бытовых товарах, да с характеристиками и ценами. Вот и решено было, как принято, ограбить тяжким честным трудом один известный сайт, позиционирующий себя как сервис поиск, выбора и покупки товаров. Сразу оговорюсь, что распространять контент цели не было. Ко всему прочему, основной профиль компании, чей магазин решили ограбить, и замешаной не так давно в курьёзных случаях с раскрытием в том числе приватной информации, является поиск и сбор данных, в том числе из публичных источников. И даже есть у них на то специализированная вакансия.

Итак, умыв руки, приступим.

Задача не сложная, и не требующая особых затрат машинного времени, тем более, что мы не собираемся брать магазин штурмом, а будем всё делать предельно тихо, чтобы никого не побеспокоить, так что Ruby — отличный вариант. В ходе ограбления нам понадобятся некоторые инструменты библиотеки, а также некоторые устройства дополнительные программы.

Определим, что же мы хотим. Объять необъятное отложим на попозже, и ограничим список товаров холодильниками, телевизорами и стиральными машинами. Информацию о товаре ограничим названием, брендом, средней ценой по нашему дефолтному региону, и несколькими дополнительными параметрами разных типов.
Зайдя с главной страницы, можно заметить, что каждому из этих разделов соответствует два числовых параметра — CAT_ID и hid. Не станем задумываться, зачем они нам нужны, а просто примем за данное и используем как ключ к входной двери.

def parse
   {'Холодильники' => ['107500', '90594'],
      'Стиральные машины' => ['109012', '90566'],
      'Телевизоры' => ['108206', '90639']
    }.each do |category, catid_hid|
      products = parse_category category, catid_hid[0], catid_hid[1]
    end
end


Будем продвигаться шаг за шагом, определяя метод за методом, пока у нас не будет целостного плана.

Идём дальше, и смотрим, какую полезую информацию можно взять со страницы категории товара. Видно, что тут нет полного списка товаров категории, а он нам будет нужен, поэтому берём ссылку «Посмотреть все модели» и используем её.
Не утруждаем себя мыслями о назначении оставшихся параметров XML'ного гуру, поняв, что для всех категорий они одинаковы.
Итак, перед нами многостраничный каталог моделей определённой категории.

def parse_category category, catid, hid
  parse_model_page category, "http://market.yandex.ru/guru.xml?CMD=-RR=9,0,0,0-VIS=160-CAT_ID=#{catid}-EXC=1-PG=10&hid=#{hid}"
end


Очевидно, что теперь уже придётся разбирать HTML и смотреть на отдельные ссылки.
Определим метод, скачивающий страницу и сразу разбирающий её на элементы.

def page_get page_url
  html = HTTParty.get(page_url).body
  Nokogiri::HTML(html)
end


Заметьте, мы использовали два интересных инструмента — HTTParty, наиболее популярный HTTP клиент для Ruby, и Nokogiri, наиболее популярный парсер XML/HTML. Использование более прогрессивного Excon имело бы смысл, но он обладает рядом недостатоков, затрудняющих его использование в контексте данного ограбления.

Итак, мы знаем, что у нас на странице есть некий список моделей, а также по меньшей мере ссылка на следующую страницу.

def parse_model_page category, page_url
  page = page_get page_url

  models = page.search('.b-offers__desc')
  
  models.each do |model|
    parse_model category, model
  end

  next_page = page.search('.b-pager__next').first[:href] rescue nil
  parse_model_page category, ('http://market.yandex.ru' + next_page) if next_page
end


Что же мы тут делаем? Во-первых, среди полученных элементов страницы ищем те, что соответствуют блоку информации о модели с помощью метода search и передавая в него CSS селектор. В качестве результата имеем список блоков с информацией о моделях. В цикле вызываем метод разбора информации о моделях, который определим позже.

Далее, ищем ссылку на следующую страницу так же с помощью CSS селектора. Тут нужно быть аккуратным, ведь рано или поздно мы дойдём и до последней страницы, на которой ссылка «следующая» будет присутствовать, но у неё будет другой класс, 'b-pager__inactive', и наш метод first для выборки бросит исключение, которое мы поймаем и инициализируем next_page nil'ом.
Рекурсивно вызовем этот же метод для разбора следующей страницы. Если есть предположение, что страниц будет слишком много, имеет смысл получить их все и вызывать в цикле.

Определим, что мы делаем дальше, а именно одну из самых интересных вещей, как то разбор общих данных о модели.

def parse_model category, model
  short_spec = model.search('p.b-offers__spec').text

  model_url = model.search('a.b-offers__name').first[:href] rescue nil
  return if model_url.nil?
  full_spec_url = model_url.gsub 'model.xml', 'model-spec.xml'
  spec = page_get('http://market.yandex.ru' + full_spec_url)

  name = spec.search('.b-page-title').text
  brand_name = spec.search('a.b-breadcrumbs__link')[-1].text
  
  price = mean_price model_url
  
  features, full_features = parse_features(category, spec)

  return if Product.first :name => name

  product = Product.new({:category => category, :brand => brand,
   :name => name, :specifications => short_spec,
   :url => ('http://market.yandex.ru'+model_url),
   :features => features, :mean_price => price})
  logger.error product.errors.inspect unless product.save
end


Здесь мы со страницы модели берём краткую информацию о ней, а ко всему прочему берём ссылку на полную информацию о модели, которую тоже хотим ограбить методом parse_features, и ссылку на страницу со средней ценой, которую ограбим с помощью метода mean_price.
В случае, если такой продукт у нас уже в базе есть, пропускаем, в противном случае — сохраняем!

Ну, и напоследок определим один из оставшихся методов для получения средней цены.

def mean_price model_url
  model_page = agent.get('http://market.yandex.ru' + model_url)
  model_page.search('.b-model-prices__avg .b-prices__num').text
end


На сегодня всё, а во второй части я расскажу о том, как для разных категорий товара у нашего магазина используются разные наименования одних и тех же характеристик, о том, как грабить данные по шаблону, о том, как магазин защищается с помощью каптчи от недобросовестных веб-роботов, и что случайно определяет наш честный труд в ту же категорию, и как магазину доказать, что это не так, заходя с разных сторон IP, при этом по-прежнему грабя среднюю цену по нужному нам дефолтному региону.
Спасибо за внимание!
  • +7
  • 24 августа 2011, 03:15
  • philpirj
  • 0

Комментарии (3)

RSS свернуть / развернуть
+
-1
> с помощью каптчи
антикапча?
>заходя с разных IP
Какова производительность?
avatar

mir

  • 24 августа 2011, 09:18
+
-1
Антикаптча получается несколько хуже по производительности (получение нового IP — ~15 секунд, разгадка капчи — около 20ти с учётом погрешностей), да ещё и тянет копеечку. Про антикаптчу писал тут, но там разгадывалась recaptcha, а переделывать показалось труднее, чем переключать IP.
avatar

philpirj

  • 24 августа 2011, 11:46
+
0
И дороже, ведь разгадывают реальные люди, которые хотят есть, а основу Tor составляют добровольцы.
avatar

philpirj

  • 17 октября 2011, 02:33

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.
Блоги, Ruby, Как ограбить магазин среди бела дня: Часть 1