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