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

Те, кто как то связан с продажами на конкурентном рынке, наверняка знают, что мониторинг конкурентов является важной задачей. Результаты используются для совершенно различных целей — от изменения локальных политик ценообразования и ведения ассортимента до составления стратегических планов развития компании. Автор решил попрактиковаться в решении данной задачи и промониторить одного из крупных ритейлеров электроники в России, чьим регулярным клиентом автор является. Что из этого вышло —

Вместо введения


Сразу стоит сказать, что в статье не будет описаний методов социальной инженерии или общения с фирмами, предоставляющими услуги мониторинга. Также добавлю, что не будет и описания анализа мониторинга, только алгоритм сбора и некоторые трудности, с которыми пришлось столкнуться во время работы. Последнее время автор все чаще применяет R, и сбор данных решено было сделать с его использованием. К тому же все большую популярность набирают открытые данные (например вот, вот или вот) и навык работы с ними прямо из используемой среды будет полезен. Все действия носят демонстративных характер, и полученные дынные не были переданы никому.

Анализ сайта


Первое, что нужно сделать, изучить структуру сайта исследуемого конкурента. Начнем с контента. Есть несколько уровней товарного классификатора, и количество уровней для любого товара всегда одинаково. До списка товаров можно добраться через уровень 2 или уровень 3. Для дальнейшей работы было решено использовать уровень 2.
Следующим шагом было исследование исходного кода страницы со списком требуемых товаров и выяснение ее структуры. Каждый товар находится в отдельном HTML-контейнере. Тут нас ждет первая сложность — в коде страницы содержится информация только о первых 30 товарах в списке — понятно, что нас это не устраивает. Автор не понял как программно показать следующие 30 товаров, поэтому решено было изучить мобильную версию сайта. В мобильной версии внизу страницы есть ссылки на следующую и на последнюю страницы. Плюс — мобильная версия сайта гораздо менее «замусоренная» лишними ссылками и тэгами.

Пишем код


Изначально решено было разделить код на несколько составляющих:
1. Функция, которая собирает информацию с конкретной страницы. Принимает на вход URL конкретной страницы, на которой находится до 15 товаров (отличие мобильной версии). Функция возвращает data.frame содержащий информацию о наличии, наименовании, артикуле, количестве отзывов и ценах товаров.
Первая функция
getOnePageBooklet <- function(strURLsub="", curl=getCurlHandle()){
     # loading the required page
     html <- getURL(strURLsub, 
                    .encoding='UTF-8', 
                    curl=curl)
     # parsing html
     html.raw <- htmlTreeParse(
          html,
          useInternalNodes=T
     )
     # searching for SKU nodes
     html.parse.SKU <- xpathApply(html.raw, 
                                  path="//section[@class='b-product']", 
                                  fun=xmlValue)
     
     # some regex :)
     noT <- gsub(' ([0-9]+)\\s([0-9]+) ',' \\1\\2 ',unlist(html.parse.SKU))
     noT <- gsub(';',',',noT)
     noT <- gsub('\r\n',';',noT)
     noT <- trim(noT)
     noT <- gsub("(\\s;)+", " ", noT)
     noT <- gsub("^;\\s ", "", noT)
     noT <- gsub(";\\s+([0-9]+)\\s+;", "\\1", noT)
     noT <- gsub(" ; ", "", noT)
     noT <- gsub("Артикул ", "", noT)
     noT <- gsub("\\s+Купить;\\s*", "", noT)
     noT <- gsub("\\s+руб.;\\s*", "", noT)
     noT <- gsub(";\\s+", ";", noT)

     # text to list
     not.df <- strsplit(noT,';')

     # list to nice df
     tryCatch(
     not.df <- as.data.frame(matrix(unlist(not.df), 
                                     nrow = length(not.df), 
                                     byrow = T)), error=function(e) {print(strURLsub)}  )

}


2. Функция, которая, используя функцию 1, собирает информацию со всех страниц заданного уровня товарного классификатора. Основной смысл данной функции — найти номер последней страницы, пробежаться от первой до последней с помощью функции 1 и объединить полученные результаты в один data.frame. Результат функции — data.frame со всеми товарами данного уровня товарного классификатора.
Вторая функция
getOneBooklet <- function(strURLmain="", curl=getCurlHandle()){
     # data frane for the result
     df <- data.frame(inStock=character(), SKU=character(), Article=numeric(), Comment=numeric(), Price=numeric()) 
     
     # loading main subpage
     html <- getURL(strURLmain, 
                    .encoding='UTF-8', 
                    curl=curl)
     # parsing main subpage
     html.raw <- htmlTreeParse(
          html,
          useInternalNodes=T
     )
     # finding last subpage
     html.parse.pages <- xpathApply(html.raw, 
                                    path="//a[@class='page g-nouline']", 
                                    fun=xmlValue)
     if(length(html.parse.pages)==0){
          urlMax <- 1
     }else{
          urlMax <- as.numeric(unlist(html.parse.pages)[length(unlist(html.parse.pages))])          
     }

     # loop for all sybpages
     tryCatch(
     for(iPage in 1:urlMax){
          strToB <- paste0(strURLmain, '?pageNum=',iPage)
          df.inter <- getOnePageBooklet(strToB, curl)
          df <- rbind(df, df.inter)
     }, error=function(e) {print(iPage)})
     # write.table(df, paste0('D:\\', as.numeric(Sys.time()) ,'.csv'), sep=";")     
     df
}


3. Функция, которая, используя функцию 2, собирает информацию со всех имеющихся уровней товарного классификатора. Кроме того к полученным данным добавляются названия уровней классификатора. На всякий случай, когда целиком собрана информация по одной категории — результат сохраняется на диске.
Третья функция
getOneCity <- function(urlMain = "http://m.tramlu.ru", curl = getCurlHandle()){
     df.prices <- data.frame(inStock = character(), 
                             SKU = character(), 
                             Article = numeric(), 
                             Comment = numeric(), 
                             Price = numeric(), 
                             level1 = character(),
                             level2 = character())
     level1 <- getAllLinks(urlMain, curl)
     numLevel1 <- length(level1[,2])
     
     for (iLevel1 in 1:numLevel1){
          strURLsubmain <- paste0(urlMain, level1[iLevel1, 2])
          level2 <- getAllLinks(strURLsubmain, curl)
          numLevel2 <- length(level2[,2])
          
          for (iLevel2 in 1:numLevel2){
               strURLsku <- paste0(urlMain, level2[iLevel2,2])
               df.temp <- getOneBooklet(strURLsku, curl)
               df.temp$level1 <- level1[iLevel1,1]
               df.temp$level2 <- level2[iLevel2,1]
               
               df.prices <- rbind(df.prices, df.temp)
          }
          write.table(df.prices, paste0('D:\\', iLevel1 ,'.csv'), sep=";", quote = FALSE)          
     }
     df.prices
}


В дальнейшем, испугавшись молота бана, пришлось добавить еще функцию — использующую прокси. В последствии выяснилось, что банить никто даже не собирался, весь классификатор, с учетом тестирования скриптов, был собран без проблем, поэтому все ограничилось созданием data.frame с информацией по ста прокси, которые так и не использовались.
Получение списка proxy
getProxyAddress <- function(){
     htmlProxies <- getURL('http://www.google-proxy.net/', 
                           .encoding='UTF-8')
     #htmlProxies <- gsub('</td></tr>','  \n ', htmlProxies)
     htmlProxies <- gsub('\n','', htmlProxies)
     htmlProxies <- gsub('(</td><td>)|(</td></tr>)',' ; ', htmlProxies)
     # parsing main subpage
     htmlProxies.raw <- htmlTreeParse(
          htmlProxies,
          useInternalNodes=T
     )

     # finding last subpage
     html.parse.proxies <- xpathApply(htmlProxies.raw, 
                                    path="//tbody", 
                                    fun=xmlValue)
     html.parse.proxies<- gsub('( )+','', html.parse.proxies)
     final <- unlist(strsplit(as.character(html.parse.proxies),';'))
     final <- as.data.frame(matrix(final[1:800], 
                                    nrow = length(final)/8, 
                                    ncol = 8,
                                    byrow=T))
     #final <- gsub('( )+','', final)
     names(final) <- c('IP','Port','Code','Country','Proxy type','Google','Https','Last checked')
     sapply(final, as.character)
}


Использовать прокси можно вот так
Как использовать прокси
opts <- list(
     proxy         = "1.1.1.1", 
     proxyport     = "8080"
)
getURL("http://habrahabr.ru", .opts = opts)


Структуры всех составляющих кода похожи. Используются функции getURL из пакета RCurl, htmlTreeParse и xpathApply из пакета XML и несколько регулярных выражений.
Последней сложностью стало указание города, цены в котором мы хотим узнать. По умолчанию при загрузке данных выдавалась информация по ценам доставки по почте с неопределенным наличием товара. При заходе на сайт исследуемой компании возникает окно, которое предлагает выбрать город-месторасположение. Данная информация затем сохраняется в куки браузера и используется для показа цен и товаров в выбранном городе. Для того, чтобы R мог сохранять куки, необходимо задать свойства используемого соединения. Далее необходимо просто загрузить в R страницу, соответствующую интересующему городу.
Свойства соединения
agent    ="Mozilla/5.0"
curl = getCurlHandle()
curlSetOpt(cookiejar="cookies.txt",  
           useragent = agent, 
           followlocation = TRUE, 
           curl=curl)


Вместо заключения


Все, задав требуемые параметры, запускаем функцию 3, ждем около получаса и получаем список со всеми товарами и категориями исследуемого сайта. Получилось более 60 000 цен для города Москва. Результат представляется в виде data.frame и нескольких сохраненных файлов на диске.
Скрипт целиком находится на GitHub.

Спасибо за уделенное внимание.

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


  1. ilyaplot
    08.04.2015 01:07
    +2

    И какую информацию Вы хотели донести до читателя? Как сделать простой парсер на R?
    В чем преимущества данного решения и почему я должен использовать R для таких задач?
    Кстати, иногда удобнее собрать цены из yml. Единственная сложность — поиск самого yml файла.


    1. Dreamastiy Автор
      08.04.2015 09:15

      Да, как ни посмотри — это просто парсер на R.

      Использовать или не использовать — считаю, что это вопрос личных предпочтений. Лично мне кажется удобным, что нет необходимости писать парсер на другом языке, а затем результаты его работы импортировать в R.
      У меня и моих коллег R раньше не ассоциировался со сбором данных. В статье попытался показать, что данный инструмент можно использовать и для подобных целей.

      Про yml не знал, стоит попробовать, спасибо.


  1. dewil
    08.04.2015 01:23
    +1

    А кто сталкивался с мониторингом цен по чекам покупателей? Какие трудности? Интересно послушать.


    1. Mastseo
      08.04.2015 08:32

      Первая из трудностей, которая возникает — сбор чеков: либо самим рыться в урнах возле касс и магазинов (что, очевидно, просто непозволительно), либо каким-то образом мотивировать самих покупателей отправлять оригиналы чеков или сканы (менее предпочтительный вариант). Далее дело техники: отсканировать, распознать, сохранить, проанализировать.


      1. dewil
        08.04.2015 12:23

        Далее дело техники: отсканировать, распознать, сохранить, проанализировать.
        отсканировать лучше заменить на «получить от пользователей»


        1. Mastseo
          08.04.2015 12:43

          Если проанализировать бизнес-процессы, то чтобы существенно снизить нагрузку на, собственно, производство (если так можно выразиться) — совершенно с Вами согласен — нужно переложить процесс сканирования на сторону покупателя. Но при этом, теряется, контроль над этим этапом: повышается вероятность появления либо сканов слабого качества, не пригодных для обработки, либо вовсе отсутствия таковых. Конечно же, по-хорошему, можно попробовать смоделировать поток первичного материала (чеков) от покупателей и проанализировать процент «брака» от общего числа сканов, при условии что общее количество покупателей, присылающих сканы будет расти. И немаловажным также будет, определить при каких условиях будет расти это общее число покупателей. P.S Где-то я встречал забугорную компанию которая занимается обработкой чеков, найду обязательно скину ссылочку


          1. dewil
            08.04.2015 12:56

            будет очень интересно ссылку


    1. wildmandnd
      08.04.2015 08:51

      Вот эти ребята: itunes.apple.com/ru/app/billibox/id579337197?mt=8

      Непосильная сложность сканирования и парсинга чеков, дороговизна набора и ежедневного апдейта базы.


    1. Dreamastiy Автор
      08.04.2015 08:58

      Все зависит от задачи.
      Если необходимо разово промониторить цены в конкретном магазине — разумно самостоятельно пробежаться и записать цены с полок, во многих ситуациях этого достаточно. Если магазинов много и/или задача не разовая — тут уже нужно либо мотивировать покупателей присылать чеки самим (может обидеться конкурент), либо нанимать команду мониторщиков (которые ради денег могут прислать много несуществующих цен). Сейчас уже существуют компании, которые платят обычным людям за фотографии цен в магазинах. Затем эту информацию они продают компаниям.
      Другая задача — по чекам восстановить некоторые параметры продаж, и тут все гораздо сложней. В этом случае, для получения разумной точности, количество чеков, которые необходимо собрать просто огромно.


      1. dewil
        08.04.2015 10:43

        по первой части вашего коммента все понятно. есть реализации и довольно успешные.
        а по второй можно подробней?

        я так понимаю главная проблема, это оцифровка?


        1. Dreamastiy Автор
          08.04.2015 12:12

          Допустим Вы хотите исследовать пенетрацию (фактически это вероятность обнаружить товар в чеке) какого либо товара с заданной точностью. Нужно оценить количество чеков, необходимое для получения данной точности. Соответственно у нас есть 2 исхода — товар есть в чеке и товар в чеке отсутствует. Это биномиальное распределение и доверительный интервал будет иметь вид. Отсюда в зависимости от предполагаемого ассортимента исследуемого магазина и заданной точности можно получить необходимое количество чеков. Оно будет огромным.
          Проблема оцифровки отходит на второй план — собрать такое количество чеков очень сложно.


  1. Bangybug
    08.04.2015 05:55
    -2

    Очевидно, удобнее что-то спарсить на Javascript, который можно загнать прям на страницу магазина, загруженную в браузере.


  1. Graid
    08.04.2015 11:56

    Автор не понял как программно показать следующие 30 товаров, поэтому решено было изучить мобильную версию сайта.

    Для этого отлично подходит отладочная панель браузера.
    Вся нужная информация была здесь /catalogAdditional/93402?sort=5&viewType=4&pageNum=5