• ✓ individuelle SEO-Beratung & -Betreuung
  • ✓ keine Abnahmeverpflichtung
  • ✓ zertifizierte SEO-Experten
  • ✓ transparente & faire Arbeitsweise
  • ✓ nachhaltige SEO-Strategien
SEO-Monitoring mit Crawlabgleichen in R – 2020 Version

SEO-Monitoring mit Crawlabgleichen in R – 2020 Version

Inhaltsverzeichnis

Bekanntlich ist es sinnvoll eine Website während der Umsetzung des SEO-Konzepts sowie im Rahmen der Betreuung genaustens im Auge zu behalten. Eine der mächtigsten Instrumente um immer auf dem neusten Stand zu sein, ist das zeitversetzte Crawlen und Abgleichen der Website basierend auf der URL Basis. Um den regelmäßigen Zeitaufwand dafür auf ein Minimum zu reduzieren, werden wir in diesem Beitrag alles vom Crawling bis zum Output-Excel automatisieren! Anders ausgedrückt, das Einzige was zu tun ist neben der initialen Einrichtung, ist die intellektuelle Analyse des Ausgabe-Excel.

Da unser letzter Blog-Beitrag zu diesem Thema schon einige Zeit her ist und wir uns bei [Get:Traction] natürlich auch ständig weiterentwickeln, ist es an der Zeit, mal wieder ein Update zu lancieren! Optimierung ist schließlich der Inbegriff unserer Kernkompetenz.

Der Output – Was kommt dabei raus?

Um abzuklären, ob der Crawlabgleich etwas für Dich ist, ist es sinnvoll zuerst einmal einen Blick auf den Output zu werfen:

Das Endergebnis ist nach wie vor eine Excel Datei, welche in einem Reiter für jede Metrik die Unterschiede zum vorherigen Crawl aufzeichnet. Dabei sind folgende Punkte abgedeckt:

  • Neue URLs
  • Nicht mehr verlinkte URLs
  • Änderungen bei URLs, die in beiden Crawls vorhanden sind:
    • Status Codes
    • Canonical-Anweisungen
    • Meta Robots
    • H1
    • Titles
    • Descriptions
    • Content Types
    • Klicktiefe
    • Dateigröße
SEO-Monitoring: Excel Datei mit Reitern für jede Metrik

Übersicht der Arbeitsschritte

Grundsätzlich haben wir 3 Schritte während des gesamten Prozesses:

– Wir crawlen die Website in Zeitabständen

– Wir vergleichen die Crawls miteinander

– Wir analysieren das Endergebnis

SEO Monitoring mit Screaming Frog, R und Excel

Crawlen der Website

Für diesen Arbeitsschritt benötigen wir die Screaming-Frog SEO Spider.

Erstellen der Ordnerstruktur und Einrichtung der Crawl-Jobs im Aufgabenplaner

Um unseren Export nach beliebigen Zeitabständen zu automatisieren und damit regelmäßig viel Zeit zu sparen, legen wir eine standardisierte Ordnerstruktur an und erstellen im Windows Aufgabenplaner Jobs für das Crawling.

Ordnerstruktur

Initial benötigen wir 4 Ordner, die alle im gleichen Verzeichnis abzulegen sind:

Ordnerstruktur

Die Struktur sieht folgendermaßen aus:

  • 01_crawl_abgleiche
    • /[Kunde]
      • Hier kommen die vom R-Skript generierten XLSX rein.
  • 02_sf_exports
    • /[Kunde]
      • /[yyyy.mm.dd.hh.mm.ss]
        • Hier legt der Screaming Frog automatisch seine Exporte ab. Zum einen den „internal_all.csv“. Zum anderen – um im Nachhinein im Detail nachsehen zu können – den gesamten Crawl als „.seospider“.
  • 03_sf_configs
    • Hier liegen die Screaming-Frog Konfigurationsdateien ab (sofern vorhanden).
  • 04_R
    • /[R-Projekt]
      • Hier liegt das „.R“-Projekt, welches aus den letzten beiden Crawls für eine Website den Abgleich bastelt und diesen in „01_crawlabgleiche/[Kunde]/“ abglegt.

Alle fett geschriebenen Ordner in der obigen Darstellung müssen einmal initial angelegt werden. Der Rest wird automatisch von unserem Skript und dem SF generiert.

Windows Job für Crawls

  1. Erstellen der 4 initialen Ordner.
  2. Erstellen eines Ordners für jeden Kunden unter 02_sf_exports.
  3. Wenn Screaming-Frog Konfigurationsdateien für die vorgesehenen Crawls vorhanden sind, diese im Ordner „03_sf_configs“ ablegen.
  4. Öffne die „Aufgabenplanung“ über die Windows-Suche.
  5. Es ist empfehlenswert einen neuen Ordner für die Crawlings sowie die R-Jobs in der Aufgabenplanung zu erstellen:
Neuen Ordner erstellen
  1. Navigiere in den neu erstellten Ordner
  2. Erstelle mit Rechtsklick -> „Neue Aufgabe erstellen“ eine neue Aufgabe
  3. Im Dialogfeld müsst ihr dem Job nun einen Namen geben (Hier empfiehlt es sich die Namensgebung zu standardisieren und das Intervall mit einzubeziehen z.B. „1month_kunde“. Unter dem Reiter „Trigger“ könnt ihr das gewünschte Intervall und die Ausführungstage festlegen. Wir crawlen bspw. wöchentlich. Das sähe dann so aus:Navigiere in den neu erstellten Ordner
  4. Erstelle mit Rechtsklick -> „Neue Aufgabe erstellen“ eine neue Aufgabe
  5. Im Dialogfeld müsst ihr dem Job nun einen Namen geben (Hier empfiehlt es sich die Namensgebung zu standardisieren und das Intervall mit einzubeziehen z.B. „1month_kunde“. Unter dem Reiter „Trigger“ könnt ihr das gewünschte Intervall und die Ausführungstage festlegen. Wir crawlen bspw. wöchentlich. Das sähe dann so aus:
SEO Monitoring: Trigger bearbeiten
  1. Im Reiter Aktion legen wir nun den Job folgendermaßen an:
  • Kopiert euch den Pfad der „ScreamingFrog CLI“ mittels „Shift-Rechtsklick“ -> „Als Pfad kopieren“
  • („C:\Program Files (x86)\Screaming Frog SEO Spider\ScreamingFrogSEOSpiderCli.exe“)
  • Klickt auf „Neu“ und tragt den Kopierten Pfad im Feld „Programm/Skript“ ein
  • Kopiert nun in das Feld „Argumente hinzufügen“ folgenden Text:

–project-name Kunden_Crawlabgleiche –task-name 1month_tue_1200_kunde –crawl „https://www.beispiel.de/“ –config „C:\03_sf_configs\kunde.seospiderconfig“ –headless –output-folder „C:\02_sf_exports\kunde“ –timestamped-output –save-crawl –export-tabs Internal%3AAll –export-format csv

  • Ersetzt nun alle fett markierten Stellen im Text durch eure eigenen Angaben.
    • Kunden_Crawlabgleiche ist der Projektname im Screaming-Frog. Kann dort angelegt werden. (Optionaler Parameter)
    • 1_month_tue_1200_[kunde] ist der Name des Tasks im Screaming-Frog.
    • Die URL ist die zu crawlende website
    • Die beiden Pfade geben einmal den Ort der zu nutzenden SF-Config für den Crawl an und den Output Folder. Hier muss der Pfad in den jeweiligen Kunden Ordner unter /02_sf_exports rein.
  • Eine Dokumentation zu den genutzten Parametern findet sich hier.

Das Endergebnis sieht dann so aus:

Endergebnis

Jetzt könnt Ihr auf OK klicken und den Job speichern. Damit haben wir unseren automatisierten Crawl!

Abgleichen der Websites

Für diesen Arbeitsschritt benötigen wir R Studio. Hier eine kurze Anleitung und Erklärung zur Installation.

Anlegen des R-Skripts und Erstellen von Windows-Jobs

Anlegen des R-Skripts

  • Ladet euch den Ordner hier von unserem Git-Hub herunter.
  • Hier befinden sich 2 Dateien („website_change_monitor.R“ und „FUNS.R“)
  • Öffnet R Studio und legt ein neues R-Project im Ordner /04_R/ an.
  • Kopiert jetzt die beiden Dateien aus dem Download in euren Projektordner („/04_R/[Projektname]/“)

Jetzt müssen wir nur noch 2 Pfade im R Skript anpassen und schon haben wir alles für unseren Windows Job vorbereitet!

Navigiert hierzu in R Studio in eurem Projekt in die Datei „website_change_monitor.R“ und passt folgende Variablen an:

Variablen anpassen

PATH_TO_XLSX_EXPORTS ist der Pfad in euren /01_crawl_abgleiche Ordner.

PATH_TO_SF_EXPORTS ist der Pfad in euren /02_sf_exports Ordner.

(Tipp: Um auf der sicheren Seite zu sein könnt ihr „C:/“ eingeben und dann mit TAB und den Pfeiltasten bis in den jeweiligen Ordner navigieren)

Speichert das Skript jetzt mit „STRG & S“.

Damit das Skript funktioniert müsst ihr ein paar R Packages installieren. Das geht ganz einfach indem ihr am oberen Rand in R Studio auf den hints „install“ durchklickt. Sollten die Meldungen nicht aufpoppen, könnt ihr alle Packages folgendermaßen installieren:

install.packages(„[package]“)

Als Beispiel einmal:

install.packages(„tidyverse“)

Die Namen aller benötigten Packages findet ihr unter #1 Libraries (alle die mit „library()“ geladen werden) im Skript.Die Namen aller benötigten Packages findet ihr unter #1 Libraries (alle die mit „library()“ geladen werden) im Skript.

Anlegen des Windows-Jobs für das R-Skript

  1. Legt euch wieder im Aufgabenplaner einen neuen Ordner für R an.
  2. Erstellt hier eine neue Aufgabe mit Namen und Trigger. Der Trigger sollte natürlich nach dem ausführen der Crawl Jobs gesetzt werden.
  3. Gebt im Feld „Programm/Skript“ den Pfad zu eurer R-Installation ein, ungefähr so: „C:\Program Files\R\R-3.6.1\bin\Rscript.exe“
  4. Im Feld „Argumente hinzufügen“ geben wir nun den Dateinamen des R-Skripts an: „website_change_monitor.R“
  5. Im Feld „Starten in“ geben wir den Pfad des Ordners an, in dem sich das Skript befindet: „/04_R/[Projektname]/“
  6. Jetzt können wir den Job speichern!
Job speichern

Das wars! Jetzt haben wir einen völlig automatisierten Crawlabgleich. Zur Einrichtung eines weiteren Abgleichs, müsst ihr nur einen Ordner für den neuen Kunden in 02_sf_exports erstellen und einen Crawl-Job im Windows Aufgabenplaner erstellen. Das Skript registriert den neuen Kunden von alleine. Eure Output-Excels findet ihr nun im Ordner /01_Crawl_Abgleiche/.

Das Skript in allen Einzelheiten

Libraries

Hier werden die zum Ausführen des Skripts benötigten R-Packages geladen. Um diese beim Ausführen des Skripts nutzen zu können, müsst ihr diese vorher installieren (Beschrieben in Punkt Anlegen des R-Skripts).

# Libraries -------------------------------------------
options(java.parameters = "-Xmx8g")
library(tidyverse)
library(xlsx)
library(slackr)
library(lubridate)
source("FUNS.R")

Vars

Hier werden die Pfade für die Imports und Exports festgelegt. Zusätzlich legen wir hier unseren Slack API Key als Variable (Auskommentiert da nicht essenziell) an, um erfolgreich ausgeführte Scripts sowie Fehlermeldungen in unseren Scripts- Slack Channel automatisiert zu pushen! Damit hat man also auch ein Warnsystem implementiert. Die Variable „#TESTING_WEBSITE“ ist standardmäßig auskommentiert mit einem #. Sollte man Adhoc nur eine einzige Website vergleichen wollen, kann man hier das # entfernen und in die Gänsefüßchen die Website schreiben. Dabei einfach den Namen des Ordners verwenden, wie ihr ihn im sf_exports Verzeichnis erstellt habt.

# VARS ------------------------------------------------
PATH_TO_XLSX_EXPORTS <- "C:/01_crawl_abgleiche"
PATH_TO_SF_EXPORTS <- "C:/02_sf_exports"
#TESTING_WEBSITE <- ""
#SLACK_API_KEY <- ""

Slack

Hier wird dem „slackR“ Package die nötige Information mitgegeben. Den API Key, in welchen Channel soll gepusht werden? Unter welchem Namen sollen die Nachrichten dort auftauchen?

Die Sektion ist im download-Skript ebenfalls auskommentiert, da die Nutzung von SlackR natürlich nicht obligatorisch ist.

# Slack -----------------------------------------------
#slackr_setup(api_token = SLACK_API_KEY,
#             channel = "#scripts",
#             username = "[R] Crawl-Abgleich",
#             echo = FALSE)

Get Files

Find all CSV’s

Hier werden alle Dateien mit .csv-Endung rekursiv aus dem in der Variable PATH_TO_SF_EXPORTS enthaltenen Pfad aufgelistet.

# Get-files -------------------------------------------
## Find all CSVs
files <- list.files(PATH_TO_SF_EXPORTS,
                    full.names = TRUE,
                    recursive = TRUE,
                    pattern = "\\.csv")

Split paths

Nun splitten wir die Pfade der aufgelisteten Dateien in dem Dataframe „df_file_paths“ in einzelne Spalten auf.

## Split paths
df_file_paths <- tibble(path = files) %>%
  splitstackshape::cSplit(splitCols = "path",
                          drop = FALSE,
                          sep = "/",
                          direction = "wide",
                          type.convert = FALSE) %>%
  as_tibble()

Extract website, datetime

Hier determinieren wir, welche Teile der in „df_file_paths“ enthaltenen Pfade welches Element sind und geben den entsprechenden Spalten die Namen.

## Extract website, datetime
select_cols <- c(1, ncol(df_file_paths) - 2, ncol(df_file_paths) - 1)
df_file_paths <- df_file_paths[ , select_cols]
names(df_file_paths) <- c("path", "website", "datetime")

Convert to date

Hier konvertieren wir das bisher als Character zu verstehende Datum in der Spalte „datetime“ in ein richtiges Datums-Datenformat. Dazu nutze ich die äußerst nutzvolle Library Lubridate.

## Convert to date
df_file_paths <- df_file_paths %>%
  mutate(datetime = ymd_hms(datetime),
         date = date(datetime))

Check if two Crawls exist

Wir gruppieren nach der Spalte „website“ und zählen, ob die Anzahl an Elementen pro Gruppe mehr als 1 beträgt. Anschließend lösen wir die Gruppierung wieder auf.

## Check if two crawls exist
df_file_paths <- df_file_paths %>%
  group_by(website) %>%
  filter(n() > 1) %>%
  ungroup()

Get last two crawls

Wir gruppieren abermals nach „website“ und fügen dem Dataframe die Spalte „date_rank“ hinzu. Dieser wird mithilfe der rank() Funktion absteigend durch die Spalte „datetime“ berechnet. Abschliessend filtern wir auf Einträge mit „date_rank“ 1 & 2 und lösen die Gruppierung wieder auf.

## Get last two crawls
df_file_paths <- df_file_paths %>%
  group_by(website) %>%
  mutate(date_rank = rank(desc(date))) %>%
  filter(date_rank %in% c(1, 2)) %>%
  ungroup()

Get Website for iteration

Um für jede Website einen einzelnen Crawl Abgleich bauen zu können, müssen wir jede Website als einzelnes Objekt einer Liste betrachten und die gleichen Aktionen für jedes Objekt in dieser Liste iterativ durchführen. Hierzu bedienen wir uns eines einfachen for()-loops. Diese Liste bereiten wir nun mithilfe der unique-Funktion vor. Diese gibt uns, nach Einspeisung der Spalte „websites“ aus „df_file_paths“, einen Vector ohne doppelte Werte zurück. Sprich – wir haben eine Liste aller unserer Websites, die als Iterationsobjekt benutzt werden können.

## Get websites for iteration
websites <- unique(df_file_paths$website)

Filter for testing

Bevor wir mit dem eigentlichen Abgleich loslegen, gucken wir hier nach, ob die vorher angesprochene Variable „TESTING_WEBSITE“ besteht und wenn ja, filtern nur auf die dort enthaltene Website.

## Filter for testing
if (exists("TESTING_WEBSITE")) websites <- TESTING_WEBSITE

Compare Crawls

Wir iterieren über unseren „websites“ vector und führen für jede enthaltene Website folgende Aktionen aus:

Am Anfang des Loops reduzieren wir das in dieser Iteration behandelte Datenset auf die Daten einer einzigen Website und setzen temporäre Variablen für das neue und alte Crawldatum. Wir geben eine Message in der Console aus und lesen beide Crawls, mithilfe der in „FUNS.R“ definierten Funktion „read_crawl“, ein. Dazu fügen wir eine Spalte mit neuem/altem Datum an.

for (WEBSITE in websites) {
  try_res <- tryCatch({
    df_website_file_paths <- df_file_paths %>%
      filter(website == WEBSITE)
    CRAWL_NEW_DATE <- max(df_website_file_paths$date)
    CRAWL_OLD_DATE <- min(df_website_file_paths$date)
    message("\n\n[", WEBSITE, "] ", strrep("*", 40), "\n\n~ Read crawls\n")
    crawl_new <- read_crawl(df_website_file_paths[df_website_file_paths$date == CRAWL_NEW_DATE, "path"][[1,1]]) %>%
      mutate(date = CRAWL_NEW_DATE)
    crawl_old <-read_crawl(df_website_file_paths[df_website_file_paths$date == CRAWL_OLD_DATE, "path"][[1,1]]) %>%
      mutate(date = CRAWL_OLD_DATE)

Create website-dir if not exists

Der Kommentar ist ziemlich treffend – wir kreieren (wenn nicht vorhanden) ein Verzeichnis mithilfe des Iterationsobjektes sowie des Pfads aus PATH_TO_XLSX_EXPORTS.

## Create website-dir if not exists
    dir.create(file.path(PATH_TO_XLSX_EXPORTS, WEBSITE),
               showWarnings = FALSE)

Get new (linked) URLs

Abermals eine Message in die Console. Hier filtern wir innerhalb eines Anti-Joins vom neuen auf den alten Crawl, auf URLs, in dessen „content“ Spalte „html“ vorkommt oder die „status“ Spalte dem String „Connection Timeout“ entspricht.

Dabei bleiben nur noch die im neuen Crawl enthaltenen URLs zurück, die im alten nicht mehr existieren. Wir wählen per „select“ aus, welche Spalten wir haben wollen und konvertieren das ganze in ein Dataframe, da die XLSX Library kein Tibble versteht.

    ## Get new (linked) URLs ---------------------------
    message("~ Get new (linked) URLs\n")
    new_urls <- crawl_new %>%
      filter(str_detect(content, "html") | status == "Connection Timeout") %>%
      anti_join(crawl_old %>%
                  filter(str_detect(content, "html") | status == "Connection Timeout"),
                by = "address") %>%
      mutate(is_canonical = (address == canonical_link_element_1 | canonical_link_element_1 == "")) %>%
      select(date,
             address,
             status_code,
             status,
             indexability_status,
             meta_robots_1,
             canonical_link_element_1,
             title_1,
             meta_description_1,
             is_canonical) %>%
      mutate(date = as.character(date)) %>%
      as.data.frame()
# convert to DataFrame as xlsx:: cannot handle tibble()

Get no longer linked/ deleted URLs

Message in die Console. Gleiches Vorgehen wie bei Punkt 4.5.2 Nur hier Anti-joinen wir vom alten auf den Neuen Crawl.

## Get not longer / deleted URLs -------------------
    message("~ Get not longer / deleted URLs\n")
    no_longer_linked_urls <- crawl_old %>%
      filter(str_detect(content, "html") | status == "Connection Timeout") %>%
      anti_join(crawl_new %>%
                  filter(str_detect(content, "html") | status == "Connection Timeout"),
                by = "address")

Get Status Code of not linked/deleted URLs

Message. Hier prüfen wir den Status Code der nicht mehr verlinkten Seiten, da wir ihn ja über den Crawl nicht kennen. Der Grund ist, dass wir prüfen wollen, ob eine Seite korrekterweise nicht mehr verlinkt ist. Antwortet die URL mit „404 Not Found“, ergibt es vollkommen Sinn, sie nicht mehr zu verlinken. Antwortet die Seite hingegen mit „200 OK“, stellt sich die Frage, warum sie nicht mehr verlinkt ist, wenn sie doch erreichbar ist. Im Hintergrund rennt hier ein parallelisierter Status-Code-Fetcher los. Je nachdem, wie viele URLs nicht mehr verlinkt sind, kann die Abfrage durchaus länger dauern. Auf der Console seht ihr aber die gerade abgefragten URLs, sodass ihr zumindest einen groben Überblick habt, dass sich noch etwas tut und dass das Skript nicht hängen geblieben ist. (Anhand dieses Punktes ist unter anderem erkennbar, dass es sinnvoll ist, die Crawls jeweils mit gleicher Konfiguration durchzuführen).

    ## get Status Code of not linked / deleted URLs
    message("~ Get Status Code of not linked / deleted URLs\n")
    urls <- no_longer_linked_urls$address
    if (length(urls) > 0) {
      r <- fetch_multi_urls(urls)
      r_df <- tibble(
        address = unlist(lapply(r, `[[`, "url")),
        status_code_current = unlist(lapply(r, `[[`, "status_code"))
      )
      no_longer_linked_urls <- no_longer_linked_urls %>%
        left_join(r_df, by = "address") %>%
        select(date,
               address,
               status_code,
               status,
               indexability_status,
               status_code_current,
               meta_robots_1,
               canonical_link_element_1,
               title_1,
               meta_description_1) %>%
        rename(status_code_old = status_code) %>%
        mutate(date = as.character(date)) %>%
        as.data.frame()
    }

Get identical URLs

Was wohl 😉. Wir filtern auf HTML URLs beider Crawls und machen uns eines inner-Joins habhaft, um die identischen URLs des Abgleichs zu erhalten. Dazu fügen wir im Datenset Spalten für jedes Detail einer URL an, welche angeben, ob sich der entsprechende Wert geändert hat.

    message("~ Get identical URLs\n")
    identical_urls <- crawl_new %>%
      filter(str_detect(content, "html")) %>%
      inner_join(crawl_old %>%
                   filter(str_detect(content, "html")),
                 by = "address",
                 suffix = c("_new", "_old")) %>%
      mutate(change_status_code = status_code_new != status_code_old,
             change_canonical = canonical_link_element_1_new != canonical_link_element_1_old,
             change_index = meta_robots_1_new != meta_robots_1_old,
             change_title = title_1_new != title_1_old,
             change_description = meta_description_1_new != meta_description_1_old,
             change_content_type = content_new != content_old,
             change_crawl_depth = crawl_depth_new != crawl_depth_old,
             change_h1 = h1_1_new != h1_1_old,
             change_word_count = word_count_new != word_count_old,
             change_indexability = indexability_new != indexability_old,
             change_indexability_status = indexability_status_old != indexability_status_new,
             change_orphan_pages = (is.na(crawl_depth_old) & !is.na(crawl_depth_new)) | (!is.na(crawl_depth_old) & is.na(crawl_depth_new)))

Get identical URL with changes

Da wir nun das Datenset an URLs definiert haben, welches in beiden Crawls enthalten ist, können wir nun die einzelnen Bestandteile dieses Datensets betrachten und vergleichen.

Verwaiste Seiten:

Hier definieren wir, je nach dem, was in unserer Spalte change_orphan_pages, die wir in Punkt 4.5.5 gebildet haben, steht, ob es sich um eine nicht mehr oder neu verwaiste Seite handelt. Dies speichern wir als Dataframe, um es später in der Excel abbilden zu können.

Dieses Vorgehen bleibt für jede Metrik das gleiche. Darum werde ich hier nicht auf jeden Unterpunkt eingehen.

   ## Get identical URLs with changes -----------------
    message("~ Get identical URLs with changes\n")
    ## orphane pages
    message("\tOrphan pages")
    change_orphan_pages <- identical_urls %>%
      filter(change_orphan_pages == TRUE) %>%
      mutate(change = case_when(!is.na(crawl_depth_old) & is.na(crawl_depth_new) ~ "neu verwaist",
                                is.na(crawl_depth_old) & !is.na(crawl_depth_new) ~ "nicht mehr verwaist")) %>%
      select(change,
             address,
             status_code_old,
             status_code_new,
             status_old,
             status_new,
             title_1_old,
             title_1_new) %>%
      as.data.frame()

Get biggest resources

Hier sortieren wir bei Images, JavaScript und CSS, absteigend nach der Größe (size_bytes) und speichern diese als DataFrame.

    ## Get biggest ressources --------------------------
    message("~ Get biggest ressources\n")
    ## images
    message("\tImages")
    top_image <- crawl_new  %>%
      filter(str_detect(content, "image")) %>%
      select(address, size_bytes) %>%
      arrange(desc(size_bytes)) %>%
      as.data.frame()
    ## JavaScript
    message("\tJavaScript")
    top_js <- crawl_new  %>%
      filter(str_detect(content, "javascript")) %>%
      select(address, size_bytes) %>%
      arrange(desc(size_bytes)) %>%
      as.data.frame()
    ## CSS
    message("\tCSS\n")
    top_css <- crawl_new  %>%
      filter(str_detect(content, "css")) %>%
      select(address, size_bytes) %>%
      arrange(desc(size_bytes)) %>%
      as.data.frame()

Create Excel

Hier nutzen wir die Library XLSX, um eine Arbeitsmappe zu erstellen und diese mit unseren sheets in der „sheet_order“ variable und den im Loop erstellten Data-Frames zu befüllen. Mit saveWorkbook() wird dem Export der Pfad übergeben und der Crawlabgleich, mit neuem und altem Datum im Namen, im entsprechenden Ordner der Website abgespeichert.

    ## Create Excel ------------------------------------
    message("~ Write Excel for: [ ", WEBSITE, " ] ", strrep("*", 40), "\n")
    ## set order of sheets here
    sheet_order <- c("new_urls",
                     "no_longer_linked_urls",
                     "change_orphan_pages",
                     "change_status_code",
                     "change_index",
                     "change_canonical",
                     # "change_indexability",
                     # "change_indexability_status",
                     "change_title",
                     "change_description",
                     "change_h1",
                     "change_word_count",
                     "change_content_type",
                     "change_crawl_depth",
                     "top_image",
                     "top_js",
                     "top_css")
    wb <- createWorkbook()
    for (sheet in sheet_order) {
      if (nrow(eval(parse(text = sheet))) > 0)
        add_sheets(wb, sheet)
    }
    saveWorkbook(wb, file.path(PATH_TO_XLSX_EXPORTS,
                               WEBSITE,
                               paste0(CRAWL_NEW_DATE,
                                      "__",
                                      CRAWL_OLD_DATE,
                                      "__",
                                      WEBSITE,
                                      ".xlsx")))

Send Slack notification (Fail, Success)

Zu guter Letzt unsere Slack-Notification. Hier wird eine Message aus dem momentanen Iterationsobjekt und einer vorgefertigten Nachricht zusammengebaut und mithilfe der vorher erstellten Variablen und des Packages SlackR in den Channel gepostet. Alle Slack-Inhalte sind im download-Skript auskommentiert, da die Nutzung von „SlackR“ rein optional ist.

Damit ist der For-Loop abgeschlossen und das Skript erstellt die Crawlabgleiche aller vorhandenen Websites.

Viel Spaß beim Abgleichen! Wenn ihr Fragen oder Anregungen habt, gerne kommentieren oder mir direkt eine Mail schreiben 🙂

get:traction Mitarbeiter David Mayer

David Meyer

Analyst

Nachdem ich meine berufliche Karriere in der Schweiz mit einer Ausbildung als Applikationsentwickler (EFZ) und einem damit verbundenen einjährigen Praktikum im Bereich Digital Analytics bei der Coop Genossenschaft begann, wollte ich mehr über die ganze Thematik „Online-Marketing“ wissen. So kam ich nach einem Umzug in meine Heimatstadt Berlin auf [get:traction], welche mir die Möglichkeit bot, mich in meinen Interessengebieten innerhalb eines Jahres enorm zu entwickeln. Heute schaffen Datenwissenschaften im Allgemeinen & komplexe Analysen im SEO-Bereich die Verbindung zwischen meiner Affinität zur Informatik und dem Online-Marketing. Als Analyst bei [get:traction] ist es mein Anliegen, unseren Kunden durch mein analytisches Denken und meine informationstechnische Fachkompetenz den größtmöglichen Mehrwert zu bieten.

4.9/5 - (30 votes)

23 Kommentare:

Mikesagt:

Hi,
I’m running in an error called:
NA does not exist in the current working directory.

# Setup -------------------------------------------------------------------
# Hier den Pfad zu den CSV-Exporten eintragen
path_to_crawls <- "C:/Users/customerweb/Documents/Websiteaenderungen/Crawls/KUNDE_2017-12-01_internal_all"
# Hier den Pfad eintragen, unter dem die Excel-Dateien gespeichert werden soll
path_to_export <- "C:\Users/customerweb/Documents/Websiteaenderungen/Exports"

I assume, that the path contains a mistake? Is that right?

Patrick Lürwersagt:

Hi Mike,
that’s right. You have a backslash in your second path (C:\Users/). If you change it, it should work.
Best regards,
Patrick

Mikesagt:

Thanks for your answer. Sorry that was a short mistake from my site. The error also exists with the right backslash.

Patrick Lürwersagt:

Hi Mike,
that may be a stupid question, but are there two crawls in the folder? I.e., in your first comment you specified the path_to_crawls with "C:/Users/customerweb/Documents/Websiteaenderungen/Crawls/KUNDE_2017-12-01_internal_all". I did not notice the first time, but it seems to me that you are providing a file name instead of a folder.
Suppose the two crawls KUNDE_2017-12-01_internal_all.csv and KUNDE_2017-12-08_internal_all.csv you would like to compare are located in your folder "C:/Users/customerweb/Documents/Websiteaenderungen/Crawls/ you have to assign the path_to_crawls-variable as follows: path_to_crawls <- "C:/Users/customerweb/Documents/Websiteaenderungen/Crawls/.
If that is not the case let me know.

Davesagt:

Sehr cooles Skript. Bei einem Vergleich von 2 relativ großen Crawls (200.000 URLs) crashed mir allerdings R irgendwie immer mit einem "garbage collection overheat". Hast du dafür zufällig irgendeinen Tipp?

Patrick Lürwersagt:

Servus Dave,
cool, dass dir das Skript gefällt. Blöd, dass R stirbt. Der Fehler ist mir noch nicht untergekommen. Ich würde jetzt aber ad hoc darauf tippen, dass dein RAM vollläuft und die garbage collection nicht hinterherkommt, neuen Speicherplatz frei zu machen. Das ist jetzt aber wirklich nur eine Vermutung.
Wie viel RAM steht R denn zur Verfügung (in der Console memory.limit() ausführen)? Tritt der Fehler denn auch auf, wenn du kleinere Crawls miteinander vergleichst?
Sollte es wirklich daran liegen, dass dein RAM vollläuft hift eigentlich nur mehr RAM. Oder das Skript müsste so umgeschrieben werden, dass die Crawls in einer Datenbank liegen und nicht im RAM zum Abgleich vorgehalten werden.
Tut mir Leid, dass ich dir da nicht wirklich weiter helfen kann.
LG

Davesagt:

Bei Crawls unter 10.000 URLs läuft alles super. Das Einlesen größerer Crawls ist übrigens auch kein Problem, es hängt am Ende dabei jedoch immer beim xlsx Schreiben (in der Global Environment sind die eingelesenen Variablen auch betrachtbar).
Memory war bisher anscheinend laut memory.limit() auf 16000 mb begrenzt, hab das jetzt mal mit dem Befehl auf 50000 erhöht.
Es müsste ja eigentlich entweder am generellen Ram, Java (ist eigentlich in der 64 bit Version installiert) oder dem Excel written/xlsx Befehl was gemacht werden können. Werde diese Woche dafür ein bisschen rumspielen mit der ganzen Sache.
Nochmal danke für das Skript und deine Antwort 🙂

Stefansagt:

Vielen Dank für das tolle Skript – wenn ich es nur zum Laufen bringen könnte 🙁
Direkt am Anfang erhalte ich Meldungen zu irgendwelchen Konflikten. Ich kann damit leider gar nichts anfangen ….
> library(tidyverse)
── Attaching packages ─────────────────────────────────────────────── tidyverse 1.2.1 ──
✔ ggplot2 3.2.1 ✔ purrr 0.3.3
✔ tibble 2.1.3 ✔ dplyr 0.8.3
✔ tidyr 1.0.0 ✔ stringr 1.4.0
✔ readr 1.3.1 ✔ forcats 0.4.0
── Conflicts ────────────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag() masks stats::lag()
> library(lubridate)
Attaching package: ‘lubridate’
The following object is masked from ‘package:base’:
date
> library(stringr)
> library(xlsx)
Unable to find any JVMs matching version "(null)".
No Java runtime present, try –request to install.
Error: package or namespace load failed for ‘xlsx’:
.onLoad failed in loadNamespace() for 'rJava', details:
call: dyn.load(file, DLLpath = DLLpath, …)
error: unable to load shared object '/Library/Frameworks/R.framework/Versions/3.6/Resources/library/rJava/libs/rJava.so':
dlopen(/Library/Frameworks/R.framework/Versions/3.6/Resources/library/rJava/libs/rJava.so, 6): Library not loaded: /Library/Java/JavaVirtualMachines/jdk-11.0.1.jdk/Contents/Home/lib/server/libjvm.dylib
Referenced from: /Library/Frameworks/R.framework/Versions/3.6/Resources/library/rJava/libs/rJava.so
Reason: image not found
In addition: Warning message:
In system("/usr/libexec/java_home", intern = TRUE) :
running command '/usr/libexec/java_home' had status 1
> library(httr)
Mittendrin dann weiteres Meldungen in rot:
See spec(…) for full column specifications.
Warning messages:
1: Missing column names filled in: 'X3' [3], 'X8' [8], 'X11' [11], 'X14' [14], 'X16' [16], 'X18' [18], 'X20' [20], 'X22' [22], 'X24' [24], 'X25' [25], 'X26' [26], 'X27' [27], 'X28' [28], 'X29' [29], 'X30' [30], 'X31' [31], 'X35' [35], 'X43' [43], 'X44' [44], 'X45' [45], 'X46' [46], 'X47' [47]
Error in stri_detect_regex(string, pattern, negate = negate, opts_regex = opts(pattern)) :
argument `str` should be a character vector (or an object coercible to)
Naja und dann am Ende noch
Error in eval(lhs, parent, parent) : object 'internal_all_html' not found
Habe R und R Studio frisch installiert. Kannst du mir Tipps geben, was da eventuell schief läuft?
Danke!

Davidsagt:

Hallo Stefan,
vielen Dank für deinen Kommentar!
Meine erste Einschätzung wäre, dass Java nicht installiert ist.
Um das herauszufinden:
1. Windows Taste > cmd
2. `Java -version` ausführen.
Entweder wird dir nun (wenn korrekt installiert) die aktuell installierte Java Version angezeigt oder du erhältst eine Fehlermeldung. – In diesem Fall solltest du Java installieren: https://www.java.com/de/download/
LG

Stefansagt:

Hey David, danke dir für die Einschätzung. Java ist aber auf meinem Mac installiert 🙁

Stefansagt:

upps, mein Kommentar war nicht ganz präzise wie sich nach weiterer Recherche rausstellt. Ich musste das Java Development Kit noch auf dem Mac installieren. Juhu, damit sind zumindest die beiden Fehlerzeilen "Unable to find any JVMs matching version "(null)".
No Java runtime present, try –request to install." nicht mehr da … Leider immernoch weit entfernt davon, das Skript zum Laufen zu bringen

Davidsagt:

Um die nächsten Errors anzugehen, wurden alle Libraries die im Script genutzt werden,
library(tidyverse)
library(lubridate)
library(stringr)
library(xlsx)
library(httr)
, auch vorher beispielsweise für xlsx mittels: `install.packages("xlsx")` installiert?
Des weiteren haben sich im Screaming Frog die Exporte geändert. Die erste Zeile muss beim Einlesen nun nicht mehr übersprungen werden also sollte das Argument `, skip = 1` beim Einlesen der Crawls in der Funktion `read_csv` herausgenommen werden.

Stefansagt:

Das ist auch ein wertvoller Tipp! Hmm, also anscheinend sind die nicht installiert. Na dann mache ich mich mal da ran … Danke abermals

Stefansagt:

Hmm, so langsam werden die Fehler tatsächlich weniger.
> library(tidyverse)
Fehler: package or namespace load failed for ‘tidyverse’ in loadNamespace(i, c(lib.loc, .libPaths()), versionCheck = vI[[i]]):
es gibt kein Paket namens ‘scales’
Woran kann das denn jetzt schon wieder liegen?

Patrick Lürwersagt:

Moin, das liegt daran, dass die Library scales nicht installiert ist. Kleiner Tipp, Fehlermeldungen kann man meist recht gut googlen. 😉 LG

Sandrasagt:

Hallo und vielen Dank für die tolle Anleitung 🙂
Bei mir läuft das Skript leider auch nicht und ich weiß absolut nicht, woran das liegen könnte.
skip = 1 habe ich bereits wie oben beschrieben aus dem Code entfernt.
Nach dem Punkt "Einlesen der Crawls" kommt bei mir die folgende Fehlermeldung:
Parsed with column specification:
cols(
`Internal – All` = col_character()
)
Warnung: 159 parsing failures.
row col expected actual file
1 — 1 columns 43 columns 'C:/Users/Kunde/Documents/Kunde1/Crawls//kunde_2019-11-19_internal_all.csv'
2 — 1 columns 43 columns 'C:/Users/Kunde/Documents/Kunde1/Crawls//kunde_2019-11-19_internal_all.csv'
3 — 1 columns 43 columns 'C:/Users/Kunde/Documents/Kunde1/Crawls//kunde_2019-11-19_internal_all.csv'
… … ……… ………. …………………………………………………………………………….
See problems(…) for more details.
Parsed with column specification:
cols(
`Internal – All` = col_character()
)
Warnung: 161 parsing failures.
row col expected actual file
1 — 1 columns 43 columns 'C:/Users/Kunde/Documents/Kunde1/Crawls//kunde_2019-11-27_internal_all.csv'
2 — 1 columns 43 columns 'C:/Users/Kunde/Documents/Kunde1/Crawls//kunde_2019-11-27_internal_all.csv'
3 — 1 columns 43 columns 'C:/Users/Kunde/Documents/Kunde1/Crawls//kunde_2019-11-27_internal_all.csv'
… … ……… ………. …………………………………………………………………………….
See problems(…) for more details.
Dann folgt nach dem Punkt "Ein bisschen aufräumen" die folgende Fehlermeldung:
Fehler in stri_detect_regex(string, pattern, negate = negate, opts_regex = opts(pattern)) :
argument `str` should be a character vector (or an object coercible to)
Zum Schluss bekomme ich am Ende nur die folgende Info:
Fehler in eval(lhs, parent, parent) :
Objekt 'internal_all_html' nicht gefunden
Ich kann mir vorstellen, dass es am Einlesen der Dokumente liegen könnte, aber ich verstehe nicht, welches Problem es hierbei gibt.
Könnt ihr mir evtl. Tipps geben, woran das liegen könnte?
Vielen Dank und liebe Grüße
Sandra

Patrick Lürwersagt:

Hi Sandra,
> cols(
Internal – All = col_character()
)
das sieht für mich so aus, dass du einen alten Crawl-Export laden willst, der in der ersten Zeile noch die Angabe über die Art des Crawls enhält. Entweder den Frosch updaten und noch mal exportieren oder das skip = 1 zum Überspringen der ersten Zeil wieder einfügen.
LG

Sandrasagt:

Hallo Patrick,
vielen Dank für Deine Antwort.
Ich habe beides versucht, wobei ich das Gefühl habe, dass ich durch das Screaming-Frog-Update der Lösung näher komme.
Nun (mit der neuen Version des Screaming Frogs, ohne Einfügen von skip = 1) wird mir nach "Einlesen der Crawls" die folgende Fehlermeldung angezeigt:
Parsed with column specification:
cols(
.default = col_double(),
Address = col_character(),
Content = col_character(),
Status = col_character(),
Indexability = col_character(),
`Indexability Status` = col_character(),
`Title 1` = col_character(),
`Meta Description 1` = col_character(),
`Meta Keyword 1` = col_character(),
`H1-1` = col_character(),
`H2-1` = col_character(),
`H2-2` = col_character(),
`Meta Robots 1` = col_character(),
`X-Robots-Tag 1` = col_logical(),
`Meta Refresh 1` = col_logical(),
`Canonical Link Element 1` = col_character(),
`rel="next" 1` = col_logical(),
`rel="prev" 1` = col_logical(),
`HTTP rel="next" 1` = col_logical(),
`HTTP rel="prev" 1` = col_logical(),
`Link Score` = col_logical()
# … with 5 more columns
)
See spec(…) for full column specifications.
Parsed with column specification:
cols(
.default = col_double(),
Address = col_character(),
Content = col_character(),
Status = col_character(),
Indexability = col_character(),
`Indexability Status` = col_character(),
`Title 1` = col_character(),
`Meta Description 1` = col_character(),
`Meta Keyword 1` = col_character(),
`H1-1` = col_character(),
`H2-1` = col_character(),
`H2-2` = col_character(),
`Meta Robots 1` = col_character(),
`X-Robots-Tag 1` = col_logical(),
`Meta Refresh 1` = col_logical(),
`Canonical Link Element 1` = col_character(),
`rel="next" 1` = col_logical(),
`rel="prev" 1` = col_logical(),
`HTTP rel="next" 1` = col_logical(),
`HTTP rel="prev" 1` = col_logical(),
`Link Score` = col_logical()
# … with 5 more columns
)
See spec(…) for full column specifications.
Das Skript endet dann mit der Fehlermeldung:
Fehler: Objekt 'size_new' nicht gefunden
Kann das an der Bezeichnung der "size"-Spalte liegen?
Mir ist schon aufgefallen, dass die alte Spalte "size" im neuen Screaming Frog jetzt "Size (bytes)" heißt. Im Skript ist aber nur von "size" die Rede.
Ich habe schon versucht, den Code entsprechend anzupassen, komme aber mit den Klammern und Leerzeichen in der neuen Bezeichnung nicht zurecht. Wenn ich im Code "size" in "size_(bytes)" ändere, kommen lauter weitere Fehlermeldungen.
Könnt ihr mir nochmal weiterhelfen?
Vielen Dank und liebe Grüße
Sandra

Jonsagt:

Wirklich sehr cool! Unendlich viel Liebe für diese Arbeitserleichterung. Genau so etwas hab ich immer gesucht, sind die Kunden doch selten so gut einem alle Änderungen zuverlässig mitzuteilen.
Für neue Nutzer wäre es praktisch, auf GitHub eine leicht abgeänderte Version (mit Zeile 1, mit umbenannter "Size (bytes)"-Spalte) hochzuladen, um auch für die aktuellen ScreamingFrog-Exporte zu passen.

Patrick Lürwersagt:

Servus Jon,
danke, danke. 🙂 Ja, die Anpassung muss ich mal angehen. Ich schieb es nur immer vor mir her, weil ich das Skript mittlerweile weiter aufgebohrt habe und eigentlich eine neuere Version bereitstellen könnte. Nur muss ich dann wieder was schreiben und so liegt es gerade halt leider liegen….

Hallo David,
danke für die Anleitung. Habe sie soweit durchgearbeitet.
In Schritt 9 bei der Aufgabenverwaltung scheint euer CMS die doppelten „–“ durch einfache „-“ ersetzt zu haben. Hab da die Scheduling Funktion von SF verwendet und dann die Werte entsprechend eurer Vorgabe geändert. „–project-name“ scheint SF nicht zu verstehen.
Nachdem ich alles eingestellt habe und das Crawling etc. funktionieren, bekomme ich bei RStudio folgenden Fehler:
Nur bei R habe ich ein Problem:
„Attache Paket: ‚lubridate‘
The following object is masked from ‚package:base‘:
date
Fehler in matrix(NA_character_, nrow = nrow(indt), ncol = Ncol) :
ungültiger ’ncol‘ Wert (zu groß oder NA)
Ruft auf: %>% … -> -> lapply -> FUN -> matrix
Zusätzlich: Warnmeldungen:
1: In max(unlist(lapply(SetUp, function(y) y[[„Mat“]][, 2]), use.names = FALSE)) :
kein nicht-fehlendes Argument für max; gebe -Inf zurück
2: In matrix(NA_character_, nrow = nrow(indt), ncol = Ncol) :
NAs introduced by coercion to integer range
Ausführung angehalten“

Java neuinstalliert etc., aber leider kommt der Fehler noch immer.
Hast du vielleicht einen Tipp für mich?
Danke vielmals.
LG
Sercan

Hallo David,

danke für die Anleitung. Habe sie soweit durchgearbeitet.
In Schritt 9 bei der Aufgabenverwaltung scheint euer CMS die doppelten „–“ durch einfache „-“ ersetzt zu haben. Hab da die Scheduling Funktion von SF verwendet und dann die Werte entsprechend eurer Vorgabe geändert. „–project-name“ scheint SF nicht zu verstehen.

Nachdem ich alles eingestellt habe und das Crawling etc. funktionieren, bekomme ich bei RStudio folgenden Fehler:

Nur bei R habe ich ein Problem:

„Attache Paket: ‚lubridate‘

The following object is masked from ‚package:base‘:

date

Fehler in matrix(NA_character_, nrow = nrow(indt), ncol = Ncol) :
ungültiger ’ncol‘ Wert (zu groß oder NA)
Ruft auf: %>% … -> -> lapply -> FUN -> matrix
Zusätzlich: Warnmeldungen:
1: In max(unlist(lapply(SetUp, function(y) y[[„Mat“]][, 2]), use.names = FALSE)) :
kein nicht-fehlendes Argument für max; gebe -Inf zurück
2: In matrix(NA_character_, nrow = nrow(indt), ncol = Ncol) :
NAs introduced by coercion to integer range
Ausführung angehalten“


Java neuinstalliert etc., aber leider kommt der Fehler noch immer.

Hast du vielleicht einen Tipp für mich?

Danke vielmals.

LG

Sercan

Hallo David,
danke für die Anleitung. Habe sie soweit durchgearbeitet.
In Schritt 9 bei der Aufgabenverwaltung scheint euer CMS die doppelten „–“ durch einfache „-“ ersetzt zu haben. Hab da die Scheduling Funktion von SF verwendet und dann die Werte entsprechend eurer Vorgabe geändert. „–project-name“ scheint SF nicht zu verstehen.
Nachdem ich alles eingestellt habe und das Crawling etc. funktionieren, bekomme ich bei RStudio folgenden Fehler:
Nur bei R habe ich ein Problem:
„Attache Paket: ‚lubridate‘
The following object is masked from ‚package:base‘:
date
Fehler in matrix(NA_character_, nrow = nrow(indt), ncol = Ncol) :
ungültiger ’ncol‘ Wert (zu groß oder NA)
Ruft auf: %>% … -> -> lapply -> FUN -> matrix
Zusätzlich: Warnmeldungen:
1: In max(unlist(lapply(SetUp, function(y) y[[„Mat“]][, 2]), use.names = FALSE)) :
kein nicht-fehlendes Argument für max; gebe -Inf zurück
2: In matrix(NA_character_, nrow = nrow(indt), ncol = Ncol) :
NAs introduced by coercion to integer range
Ausführung angehalten“

Java neuinstalliert etc., aber leider kommt der Fehler noch immer.
Hast du vielleicht einen Tipp für mich?
Danke vielmals.
LG
Sercan

David Meyersagt:

Hi Sercan
Dieser Fehler ist typisch, wenn die CSV-Dateien nicht richtig eingelesen/gefunden werden konnten.
Sprich: Das Skript an der Stelle #Split File Paths keine Pfade zum Splitten hat, da die Variable „files“ leer ist.
Am besten einmal sicherstellen, dass die Pfade zu deinen CSV Dateien unter # VARS richtig angegeben sind. Sonst gerne nochmal melden 🙂
LG
David


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert