R İle SiteMap İçeriğinin Derlenmesi ve URL Kontrolü

XML ve curl Paketleri İle XML-URL İşlemleri

R programlama dili ile ilgili son yazıyı Eylül, 2020'de yayınlamışım. İş yoğunluğu ve yine bununla ilintili olarak, ağırlıkla JavaScript ile çözüm üretmeye başlamam sebebiyle R ile aramdaki mesafe açılmış gibi hissediyorum. Bunu engellemek adına, hafta sonu birkaç işlemi yeniden oluşturmaya ve farklı paketlerle benzer eylemleri yinelemeye karar verdim.

AA

Daha önce R İle Sitemap (XML) Çözümlemesi başlıklı yazıda da değindiğim xsitemapGet yerine bir XML dosyasını indirmek, içeriğini okumak ve içeriğine bağlı olarak URL'leri nasıl kontrol edebileceğimize değinmek istiyorum. İlgili işlem özellikle farklı türlerdeki içerikleri kapsayan sitemapindex dosyaları için kolaylaştırıcı olacaktır.

<sitemapindex>
  <sitemap>
    <loc>https://domain.com/pages.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://domain.com/posts.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://domain.com/taxonomy.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://domain.com/images.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://domain.com/products.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://domain.com/videos.xml</loc>
  </sitemap>
</sitemapindex>

domain.com/sitemap.xml içeriğinin yukarıdaki gibi olduğunu düşünelim. Bu durumda ilgili loc kayıtları için ayrı ayrı işlemler yapmamız gerekir. Ancak, biz elimizdeki sitemap.xml adreslerini bir tablo içerisinde tutarak, içeriklerine bakmadan hepsi için ayrı tarama işlemleri gerçekleştirilmesini isteyebiliriz.

flowchart TD A[[sitemap.xml]] B([posts.xml]) C([pages.xml]) D([taxonomy.xml]) E([products.xml]) F([images.xml]) G([...more]) subgraph SiteMapIndex A:::main ==>|loc| B & C & D & E & F & G end classDef main fill:#aaa,stroke:#000,stroke-width:2px; subgraph SiteMap [URIs] B -->|loc| F1[...] & F2[...] C -->|loc| G1[...] & G2[...] D -->|loc| H1[...] & H2[...] E -->|loc| I1[...] & I2[...] F -->|loc| J1[...] & J2[...] G -->|loc| K1[...] & K2[...] end classDef sub fill:#ddd,stroke:#333; classDef child fill:#eee,stroke:#aaa; class B,C,D,E,F,G sub; class SiteMapIndex,SiteMap sub; class F1,F2,G1,G2,H1,H2,I1,I2,J1,J2,K1,K2 child;

XML ve curl İşlemleri

İlgili işlemleri XML ve curl paketleri aracılığı ile gerçekleştireceğim. curl paketi url() ve download.file() ile kıyaslandığında daha iyi performans göstermekte1. Bunun yanı sıra, https ve ftps desteği, gzip sıkıştırma, kimlik doğrulama gibi özellikler nedeniyle çoğu zaman önceliğim oluyor. Diğer yandan, curl paketi ile ilgili bilmediğim pek çok konu bulunmakta. Bu nedenle olabildiğince farklı şekilde ilgili paket içeriğinden faydalanmaya çalışıyorum.

İşlemleri 2 parça halinde ele alacağım. İlk bölüm XML dosyasına erişilmesi, içeriğinin okunması ve belirtilen formatta kaydedilmesini kapsarken, ikinci bölümde bir dosya içerisindeki URL'lerin okunması ve status testlerinin kontrolleri yer alacak.

any(grepl("curl", installed.packages()))
any(grepl("XML", installed.packages()))

library(curl)
library(XML)

İlgili paketlerin kontrol edilmesi ve yüklenmesi ile işlemlere başlayabiliriz. Vikram'ın paylaştığı kod parçacığı gibi çözümlerle de ilgili kontrol ve yükleme işlemlerini gerçekleştirebilirsiniz2.

Gönderilen URL'in uzantısını kontrol ederek temel bir doğrulama işlemi gerçekleştirebiliriz. Bu işlem için tools paketine ait file_ext() fonksiyonu kullanılabilirsiniz.

# url <- "https://domain.com/sitemap.xml"
getExtension <- tools::file_ext(sub("\\?.+", "", url))

Paket kullanmadan strsplit() veya benzer fonksiyonlarla işlem gerçekleştirebilirsiniz.

# url <- "https://domain.com/sitemap.xml"
getExtension <- function(url){ 
  ex <- strsplit(basename(url), split="\\.")[[1]]
  return(ex[-1])
}

curl paketinde bulunan curl_fetch_memory() bir URL'den belleğe, diske veya bir callback fonksiyonuna veri yazmak amacıyla kullanılabilir. Önceki kod örneğinde httr paketini kullanmıştım.

HTTP Status Kontrolü

İlk fonksiyon XML dosyasının çekilmesi HTTP Status, Content-type gibi temel doğrulamaları ve ardından XML içeriğinin xmlToDataFrame() ile tabloya (data frame) dönüştürülmesi işlemlerini kapsamakta.

getURL <- function(url) {

  getExtension <- function(url){ 
    ex <- strsplit(basename(url), split="\\.")[[1]]
    return(ex[-1])
  }

  if(!is.na(url)
     && !is.na(getExtension(url))
     && getExtension(url) == 'xml') {
    get <- curl_fetch_memory(url)
    if(get$status_code == '200' &&
       !is.null(get$content) &&
       grepl("xml", get$type)) {
      xmlData <- xmlParse(rawToChar(get$content), encoding = "UTF-8")
      # rootNodeList <- xmlToList(xmlData)
      rootNodeDF <- xmlToDataFrame(xmlData)
      rootNodeDF$xmlFileName <- parseURI(url)$path
      cat("...")
      return(rootNodeDF)
    }
  }
}

İkinci fonksiyon ise tabloya dönüştürdüğümüz verinin bir dosya olarak kaydedilmesi görevine sahip.

saveXMLFile <- function(
  url,
  extension = ".csv",
  fileName = format(Sys.time(), "%y-%m-%d_%H-%M-%S"),
  workDir = "/Users/user/Desktop/",
  preX = "sitemap_",
  fileDir = "sitemaps") {

    res <- getURL(url)
    pageData <- lapply(res$loc, getURL)
    pages <- do.call(rbind.data.frame, pageData)

    dataXML <- if(any(ncol(pages) > 2)) pages else res

    fullPath <- paste0(workDir, fileDir)
    if(!dir.exists(fullPath)) dir.create(file.path(fullPath))
    setwd(file.path(fullPath))

    file <- paste0(preX, fileName, extension)

    switch(
      extension, 
      ".csv" = {
        write.csv(dataXML, file)
      }, ".xlsx" = {
        library(xlsx)
        write.xlsx(dataXML, file, sheetName = "Links")
      }, {
        saveRDS(dataXML, file)
      }
    )
}

Evet, fonksiyonlar da hazır olduğuna göre belirlediğimiz bir XML'in URL ile ilgili işlemimizi bağlatabiliriz.

siteMapURL <- "https://domain.com/sitemap.xml"
saveXMLFile(siteMapURL)

XML dosyalarının sayısına ve içeriklerine bağlı olarak işlemin tamamlanma süresi elbette değişkenlik gösterecektir3. İlgili dosyanın oluşturulması ile birlikte aşağıdaki örneğe benzer bir CSV (veya belirttiğimiz diğer uzantıya göre) dosya oluşturulacaktır4.

"","loc","priority","lastmod","changefreq","xmlFileName"
"1","https://domain.com","1.00","2021-03-19T10:53:46+03:00","always","/pages.xml"
"2","https://domain.com/about","0.50","2021-03-19T10:53:46+03:00","monthly","/pages.xml"

Peki, sitemap dosyalarından elde ettiğimiz bu URL'lerin HTTP durum kodlarını nasıl kontrol edebiliriz?

getURL() fonksiyonuna URL kontrolünü ekleyebiliriz.

getURL <- function(url) {
  getExtension <- function(url){ 
    ex <- strsplit(basename(url), split="\\.")[[1]]
    return(ex[-1])
  }

  if(!is.na(url)
     && !is.na(getExtension(url))
     && getExtension(url) == 'xml') {
      get <- curl_fetch_memory(url)
      if(get$status_code == '200' &&
       !is.null(get$content) &&
       grepl("xml", get$type)) {
        xmlData <- xmlParse(rawToChar(get$content), encoding = "UTF-8")
        # rootNodeList <- xmlToList(xmlData)
        rootNodeDF <- xmlToDataFrame(xmlData)

        rootNodeDF$status <- NA
        rootNodeDFwStatus <- rootNodeDF

        data <- list()

        success <- function(res){
          # cat(res$url, res$status, "\n")
          data <<- c(data, list(res))
        }
        failure <- function(msg){
          cat("Request failed!", msg, "\n")
        }

        cat("Please wait...","\n")

        getURIs <- function(URIs) {
          pool <- new_pool()
          for(i in URIs) {
            cat("> ", i, "\n")
            curl_fetch_multi(i, done = success, fail = failure, pool = pool)
          }
          multi_run(pool = pool)
        }

        getStatus <- lapply(rootNodeDFwStatus$loc, getURIs)

        for (j in 1:length(data)){
          w <- which(rootNodeDFwStatus$loc == data[[j]]$url)
          if(any(w)){
            rootNodeDFwStatus[w,]$status = data[[j]]$status_code
          }
        }

        rootNodeDFwStatus$xmlFileName <- parseURI(url)$path
        return(rootNodeDFwStatus)
    }
  }
}

URL kontrolleri için de yine curl paketi içerisinde bulunan curl_fetch_multi() isteklere dönen yanıtları callback ile işleyebilmemizi sağlar5. Elbette bu işlemi curl() ya da curl_fetch_stream() ile de gerçekleştirebilirdik6. Ancak, yazının başında da belirttiğim gibi özellikle farklı fonksiyonlar kullanmaya özen göstermeye çalıştım7.

Yukarıda yer alan kod parçacığının tamamını ayrıca GitHub üzerinden görüntüleyebilir ve indirebilirsiniz.