R в качестве инструмента мониторинга цен

в 12:47, , рубрики: competitors, R, открытые данные, Программирование, Юлмарт лучше всех, метки: , ,

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

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

Сразу стоит сказать, что в статье не будет описаний методов социальной инженерии или общения с фирмами, предоставляющими услуги мониторинга. Также добавлю, что не будет и описания анализа мониторинга, только алгоритм сбора и некоторые трудности, с которыми пришлось столкнуться во время работы. Последнее время автор все чаще применяет 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('rn',';',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('https://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.

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

Автор: Dreamastiy

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js