Bir Sürecin Otomatize Edilmesi: Temel Gıda Fiyatlarının Takibi

Automation

Fiyat algısının kaybolduğu bir ortamda temel gıda fiyatlarının takibini otomatikleştirmek.

A. Uraz Akgül
2022-06-05

Mayıs 2022 verilerine göre Türkiye’de enflasyon TÜİK’e göre 73.5%; ENAG’a göre ise 160.76% oldu. Trading Economics verilerine göre dünyada 6., Avrupa’da 1. sıradayız.

R’da bir süreci otomatize etmeyi eğlenceli bir konu ile anlatmak isterdim ancak içinde bulunduğumuz ortam maalesef buna izin vermiyor.

Bu uygulamada, Cimri’den temel gıda kategorisini baz alarak aşağıdaki işlemleri yapacağız.

  1. Web kazıma ile istenen kategoride verilerin çekilmesi (web kazıma ile ilgili temel bilginizin olduğunu varsayacağım).

  2. Çekilen verilerin veri tabanına kaydedilmesi.

  3. Güncel veriler ile bir önceki zamana ait verilerin karşılaştırılması ve karşılaştırmanın kaydedilmesi.

  4. Tüm bu sürecin otomatik olarak yapılması için görev zamanlayıcının ayarlanması ve script’in belli bir frekansta çalıştırılması.

Cimri.com, farklı çevrimiçi alışveriş sitelerinde yer alan ürünleri listeleyen ve aralarında karşılaştırmalar yapan İstanbul, Türkiye merkezli web sitesidir. -Vikipedi

library(rvest) # web kazıma
library(DBI); library(RSQLite) # sqlite
library(taskscheduleR) # görev zamanlayıcı
library(openxlsx) # excel olarak kaydetmek için
library(tidyverse)
library(lubridate)
library(kableExtra) # zorunlu değil

Web kazıma ile istenen kategoride verilerin çekilmesi

Aşağıdaki link’i kullanarak verileri çekeceğiz.

url <- "https://www.cimri.com/market/temel-gida"

Temel gıda kategorisi 50 sayfadan oluşuyor. Eğer link’in yanına ?page=1 yazarsak bu ilk sayfa olduğu anlamına gelecek. Yani;

url <- "https://www.cimri.com/market/temel-gida?page=1"

İlk sayfada yer alan ürünleri ve fiyatları çekelim.

# ürün
item <- read_html(url) %>% 
  html_nodes("div.ProductCard_productName__35zi5") %>% 
  html_text()

# fiyat
price <- read_html(url) %>% 
  html_nodes("div.ProductCard_footer__Fc9OL span.ProductCard_price__10UHp") %>% 
  html_text()

df <- data.frame(
  Item = item,
  Price = price
)

İlk sayfada yer alan 32 ürünün ilk 10’u aşağıdadır.

Item Price
Biryağ 5 lt Ayçiçek Yağı 175,00 TL
Komili Riviera Sıcak Lezzetler 5 lt Zeytinyağı 328,06 TL
Yudum 5 lt Teneke Ayçiçek Yağı 192,50 TL
Bizim 5 lt Teneke Ayçiçek Yağı 189,90 TL
Vera 5 lt Pet Ayçiçek Yağı 145,00 TL
Yudum 1 lt Ayçiçek Yağı 42,90 TL
Yudum 18 lt Ayçiçek Yağı 739,95 TL
Sinangil 5 kg Un 52,90 TL
Torku Altın Ekin 5 lt Ayçiçek Yağı 175,00 TL
Ona 5 lt Ayçiçek Teneke Yağı 164,80 TL

İlk sayfayı çektiğimize göre artık diğer sayfaları da bir döngü ile çekebiliriz (total sayfa sayısı: 50). Son sayfa sayısını belirlemeyi dinamik hale getirebiliriz.

# son sayfa
lastPage <- read_html(url) %>% 
  html_nodes("div.Pagination_pagination__6kvLO li") %>% 
  html_text()

lastPage <- as.numeric(tail(lastPage[lastPage != ""], 1))

urls <- str_c(
  "https://www.cimri.com/market/temel-gida?page=",
  seq(1,lastPage,1)
) # tüm linkler

Döngüye geçebiliriz.

######### global #########

master <- data.frame()
time <- format(Sys.time(), "%Y-%m-%d %H:%M:%S")

##########################

for(i in 1:2){ # 1:2 yazdım ancak siz 1:lastPage yazabilirsiniz
  
  thepage <- read_html(urls[i]) # sayfa okundu
  
  # ürün
  item <- thepage %>% 
    html_nodes("div.ProductCard_productName__35zi5") %>% 
    html_text()
  
  # fiyat
  price <- thepage %>% 
    html_nodes("div.ProductCard_footer__Fc9OL span.ProductCard_price__10UHp") %>% 
    html_text()
  
  # ürün ve fiyatın birleştirilmesi
  tbl <- data.frame(
    Item = item,
    Price = price
  )
  
  # birleştirilen tablonun globalde yaratılan master ile birleştirilmesi
  master <- master %>% 
    bind_rows(tbl)
  
  # sürekli istek göndermemek için sistem bir süre (ör: 3 saniye) uyutulabilir
  Sys.sleep(time = 3)
  
  if(i == 2){ # 2 yerine lastPage yazabilirsiniz
    
    # eğer i değişkeni son sayfaya eşitse ki döngü bitecek;
    # globalde yaratılan time değişkeni master veri çerçevesine eklenecek.
    # time değişkeni zamanın sabit olması için globalde yaratıldı.
    
    master$Time <- time
    
  }
  
}

Varsayımsal olarak 50; şimdilik 2 sayfanın tamamını çektik. Son 10 ürüne ve fiyatına bakalım.

Item Price Time
55 Duru 1 kg Karabuğday Greçka 67,80 TL 2022-06-05 18:40:24
56 Nestle 1927 2.5 kg Kuvertür Sütlü Çikolata 215,13 TL 2022-06-05 18:40:24
57 Sinangil 8 kg Un 148,47 TL 2022-06-05 18:40:24
58 Özgün 5 lt Naturel Sızma Zeytinyağı 306,90 TL 2022-06-05 18:40:24
59 Pakmaya 100 gr Aktif Kuru Hamur Mayası 6,49 TL 2022-06-05 18:40:24
60 Yudum 1 lt Egemden Riviera Zeytinyağı 59,39 TL 2022-06-05 18:40:24
61 Tmo Teneke Ayçiçek Yağı 5 lt 198,00 TL 2022-06-05 18:40:24
62 Amasya Un 50 kg Çeşitlik Unu 639,00 TL 2022-06-05 18:40:24
63 İndomie 80 Gr Gurme Acı Ve Baharatlı Hazır Noodle 4,13 TL 2022-06-05 18:40:24
64 Olin 18 lt Ayçiçek Yağı 673,41 TL 2022-06-05 18:40:24

Çekilen verilerin veri tabanına kaydedilmesi

Çektiğimiz verileri veri tabanına kaydetme vakti. Bunun için SQLite’ı kullanacağız. Bu uygulama için tercih etme nedenim, herhangi bir yazılım/sunucu kurulumuna ihtiyacımız olmayacak. Bunun yanında sade ve basit bir yapısı vardır.

SQLite için DBI ve RSQLite paketini; veri tabanını (geçici değil; sürekli) yaratmak için aşağıdaki komutu kullanacağız. Dosya yolu neredeyse oraya kaydedecektir.

# dosya yolu
# getwd()

myDB <- dbConnect(SQLite(), "marketDB.sqlite") # Dosya yolunu adres göstermenizi tavsiye ederim.
# Ör: "C:/.../marketDB.sqlite"

Verileri veri tabanına kaydedelim. Bu işlemi belli bir frekansta ya da uygulamamızda olduğu gibi saat başı yapacağımız zaman yeni gelen verilerin veri tabanındaki tablonun üzerine yazmaması önemli olacak. Bunun için append parametresi TRUE olarak belirlendi.

dbWriteTable(myDB, "master", master, append = TRUE)

Veri tabanına kaydettiğimiz verileri SQL sorgusu ile çekelim.

mastertbl <- dbGetQuery(myDB, "SELECT * FROM master")
Item Price Time
Biryağ 5 lt Ayçiçek Yağı 175,00 TL 2022-06-05 18:24:16
Komili Riviera Sıcak Lezzetler 5 lt Zeytinyağı 328,06 TL 2022-06-05 18:24:16
Yudum 5 lt Teneke Ayçiçek Yağı 192,50 TL 2022-06-05 18:24:16
Bizim 5 lt Teneke Ayçiçek Yağı 189,90 TL 2022-06-05 18:24:16
Vera 5 lt Pet Ayçiçek Yağı 145,00 TL 2022-06-05 18:24:16
Yudum 1 lt Ayçiçek Yağı 42,90 TL 2022-06-05 18:24:16
Yudum 18 lt Ayçiçek Yağı 739,95 TL 2022-06-05 18:24:16
Sinangil 5 kg Un 52,90 TL 2022-06-05 18:24:16
Torku Altın Ekin 5 lt Ayçiçek Yağı 175,00 TL 2022-06-05 18:24:16
Ona 5 lt Ayçiçek Teneke Yağı 164,80 TL 2022-06-05 18:24:16

İşlemler bittikten sonra aşağıdaki kod ile veri tabanından çıkılabilir. Başta yazılan kod (aşağıdaki yorum satırı) ile tekrar bağlanacaktır.

dbDisconnect(myDB)
# myDB <- dbConnect(SQLite(), "marketDB.sqlite")

Güncel veriler ile bir önceki zamana ait verilerin karşılaştırılması ve karşılaştırmanın kaydedilmesi

Veri tabanından alınan tablonun time sütunu character formatında olacağı için aşağıdaki gibi zaman formatına dönüştürülmelidir.

mastertbl$Time <- as.POSIXct(mastertbl$Time)

Bizim bu tabloda iki zamana ihtiyacımız olacak: En güncel zaman ve bir önceki.

# En güncel zaman
maxTime <- mastertbl[ymd_hms(mastertbl$Time)==max(ymd_hms(mastertbl$Time)),]
# Bir önceki zaman
maxTime2 <- mastertbl[ymd_hms(mastertbl$Time)!=max(ymd_hms(mastertbl$Time)),]
maxTime2 <- maxTime2[ymd_hms(maxTime2$Time)==max(ymd_hms(maxTime2$Time)),]

İstediğimiz verileri alıp birleştirdikten sonra bazı düzenlemeler yapacağız. Ama öncesinde şunu not düşmekte fayda var: Bu süreci ilk defa başlattığımız zaman elimizde bir tane zaman olacak. Yani, herhangi bir karşılaştırma olmayacak. Hata almamak ve süreci devam ettirebilmek için bazı koşullara ihtiyaç olacaktır.

Aşağıdaki kodu çalıştırmadan önce maxTime2 değişkeninin sıfırdan büyük olup olmadığına bakmamız gerekir. Bakmaz isek hata alırız.

if(nrow(maxTime2) > 0){
  
  comparetbl <- maxTime %>% 
  bind_rows(maxTime2) %>% 
  group_by(Time) %>% 
  mutate(ID = cur_group_id(),
         ID  = if_else(ID == 1, "Before", "After")) %>% 
  pivot_wider(!Time, names_from = "ID", values_from = "Price") %>% 
  mutate(
    # Virgülün nokta yapılması ve TL'nin kaldırılması; numeric'e dönüştürülmesi
    After = as.numeric(gsub("\\,",".",gsub(" TL","",After))),
    Before = as.numeric(gsub("\\,",".",gsub(" TL","",Before))),
    # Öncesi ve sonrası arasındaki fiyat farkı
    Diff = After - Before
  ) %>% 
  filter(Diff != 0) # farkı sıfır olmayanlar filtrelendi
  
}

Eğer son filtreden sonra veri çerçevesi 0’dan büyük ise bir excel dosyası ile kaydedeceğiz. Öncesinde ise bir koşul koymak gerekiyor. Eğer comparetbl tablosu var ise sıfırdan büyük olup olmadığı koşuluna geçmelidir.

if(exists("comparetbl")){
  
  if(nrow(comparetbl) > 0){
    
    write.xlsx(comparetbl, "PriceTracker.xlsx")
    
  }
  
}

Görev zamanlayıcının ayarlanması ve script’in belli bir frekansta çalıştırılması

Görev zamanlayıcı PC’den ayarlanabileceği gibi taskscheduleR paketi ile de yapılabilir.

taskscheduler_create(
  
  taskname = "OrnekScript", # görev adı
  rscript = "OrnekScript.R", # çalıştırılacak olan script;
  # dosya yolu belirtilmeli ("C:/.../OrnekScript.R")
  schedule = "HOURLY", # saatlik
  starttime = "18:00", # manuel belirlenen saat ile başlayacak; Ör: 18:00
  modifier = 1 # 1 saat ara ile
  
)

Ayarlar kod ile yapılabileceği gibi Addins ile de yapılabilir. R Studio’da Addins’e tıkladıktan sonra Schedule R Scripts on Windows’a tıklanır. Açılan ekran ile ayarlar daha kolay bir şekilde yapılabilir.

Görev zamanlayıcı çalıştıktan sonra taskname’e verdiğiniz isim ile birlikte bir log dosyası atabilir. Burada olası uyarı ve hataları görebilirsiniz.

Sürecin otomatize edilmesi bitti. Ürünlere ait fiyatlar veri tabanında kayıtlı ve 1 adet de karşılaştırma dosyası bulunmaktadır. Buna bir de görsel ekleyebilirsiniz. Bu kısmı size bırakıyorum :)

BONUS

İş hayatında Outlook kullanıldığı ve bu tip süreçleri birçok konuda entegre ettiğim için Outlook ile nasıl mail atılır bunu göstermek istiyorum.

library(RDCOMClient) # outlook mail
library(xtable) # html ile tablo oluşturmak için

if(nrow(comparetbl) > 0){
  
  x <- head(comparetbl)
  y <- print(xtable(x), type="html", print.results=FALSE)
  body <- paste0("<html>", y, "</html>")
  
  OutApp <- COMCreate("Outlook.Application")
  outMail = OutApp$CreateItem(0)
  outMail[["To"]] = "mailingidecegiadres@sirket.com"
  outMail[["subject"]] = paste0(nrow(comparetbl)," Adet Ürünün Fiyatında Değişiklik Var!")
  outMail[["Attachments"]]$Add(paste0("PriceTracker.xlsx"))
  outMail[["HTMLbody"]] = body
  outMail$Send()
}

GitHub hesabımdan örnek bir script’e ulaşabilirsiniz.

OrnekScript.R:

library(rvest)
library(DBI); library(RSQLite)
library(openxlsx)
library(tidyverse)
library(lubridate)

url <- "https://www.cimri.com/market/temel-gida?page=1"

lastPage <- read_html(url) %>% 
  html_nodes("div.Pagination_pagination__6kvLO li") %>% 
  html_text()
lastPage <- as.numeric(tail(lastPage[lastPage != ""], 1))

urls <- str_c(
  "https://www.cimri.com/market/temel-gida?page=",
  seq(1,lastPage,1)
)

master <- data.frame()
time <- format(Sys.time(), "%Y-%m-%d %H:%M:%S")

for(i in 1:2){
  
  thepage <- read_html(urls[i])
  
  item <- thepage %>% 
    html_nodes("div.ProductCard_productName__35zi5") %>% 
    html_text()
  
  price <- thepage %>% 
    html_nodes("div.ProductCard_footer__Fc9OL span.ProductCard_price__10UHp") %>% 
    html_text()
  
  tbl <- data.frame(
    Item = item,
    Price = price
  )
  
  master <- master %>% 
    bind_rows(tbl)
  
  Sys.sleep(time = 3)
  
  if(i == 2){
    
    master$Time <- time
    
  }
  
}

myDB <- dbConnect(SQLite(), "marketDB.sqlite")
dbWriteTable(myDB, "master", master, append = TRUE)
mastertbl <- dbGetQuery(myDB, "SELECT * FROM master")
dbDisconnect(myDB)

mastertbl$Time <- as.POSIXct(mastertbl$Time)
maxTime <- mastertbl[ymd_hms(mastertbl$Time)==max(ymd_hms(mastertbl$Time)),]
maxTime2 <- mastertbl[ymd_hms(mastertbl$Time)!=max(ymd_hms(mastertbl$Time)),]
maxTime2 <- maxTime2[ymd_hms(maxTime2$Time)==max(ymd_hms(maxTime2$Time)),]

if(nrow(maxTime2) > 0){
  
  comparetbl <- maxTime %>% 
    bind_rows(maxTime2) %>% 
    group_by(Time) %>% 
    mutate(ID = cur_group_id(),
           ID  = if_else(ID == 1, "Before", "After")) %>% 
    pivot_wider(!Time, names_from = "ID", values_from = "Price") %>% 
    mutate(
      After = as.numeric(gsub("\\,",".",gsub(" TL","",After))),
      Before = as.numeric(gsub("\\,",".",gsub(" TL","",Before))),
      Diff = After - Before
    ) %>% 
    filter(Diff != 0)
  
}

if(exists("comparetbl")){
  
  if(nrow(comparetbl) > 0){
    
    write.xlsx(comparetbl, "PriceTracker.xlsx")
    
  }
  
}