Extracting media text from newspaper’s websites is a very frequent task in webscraping. One advantage of these sites is that they tend to offer an RSS feed that contains a list of all the stories they have published, which we can then use to more efficiently scrape them.

Parsing RSS feeds requires we learn a slightly different data format: XML, or eXtensible Markup Language, which predates (but is similar to) JSON. Just like HTML, it uses a series of tags and a tree structure. We will use the xml2 and rvest packages to read data in XML format:

Let’s look at an example:

feed <- "http://www.spiegel.de/politik/index.rss"
library(xml2)
library(rvest)
rss <- read_xml(feed)
substr(as.character(rss), 1, 1000)
## [1] "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<rss xmlns:content=\"http://purl.org/rss/1.0/modules/content/\" version=\"2.0\">\n  <channel>\n    <title>SPIEGEL ONLINE - Politik</title>\n    <link>http://www.spiegel.de</link>\n    <description>Politik-Nachrichten aus Deutschland und den Brennpunkten in aller Welt. Aktuelle Reportagen, Analysen, Interviews.</description>\n    <language>de</language>\n    <pubDate>Mon, 30 Jul 2018 11:47:00 +0200</pubDate>\n    <lastBuildDate>Mon, 30 Jul 2018 11:47:00 +0200</lastBuildDate>\n    <image>\n      <title>SPIEGEL ONLINE</title>\n      <link>http://www.spiegel.de</link>\n      <url>http://www.spiegel.de/static/sys/logo_120x61.gif</url>\n    </image>\n    <item>\n      <title>Austritt aus der EU: Nur gut ein Viertel der Briten für harten Brexit</title>\n      <link>http://www.spiegel.de/politik/ausland/brexit-briten-offen-fuer-weiteres-referendum-ueber-eu-austritt-a-1220748.html#ref=rss</link>\n      <description>Nur noch acht Monate bis zum EU-Austritt, un"

Just like with HTML, we can extract specific nodes of the XML file using a combination of xml_nodes and xml_text

headlines <- xml_nodes(rss, 'title')
(headlines <- xml_text(headlines))
##  [1] "SPIEGEL ONLINE - Politik"                                                                                  
##  [2] "SPIEGEL ONLINE"                                                                                            
##  [3] "Austritt aus der EU: Nur gut ein Viertel der Briten für harten Brexit"                                     
##  [4] "CDU-Ministerpräsident für Abschiebungen: \"Auch nach Syrien, wenn die Voraussetzungen dafür gegeben sind\""
##  [5] "Russland-Ermittlungen: Trump wirft Mueller Interessenkonflikte vor"                                        
##  [6] "\"Viel Feind, viel Ehr\": Innenminister Matteo Salvini irritiert mit faschistischem Zitat"                 
##  [7] "Wahlen \"weder frei noch fair\": USA kündigen Sanktionen gegen Kambodscha an"                              
##  [8] "Die Lage am Montag: Die schwierige Reise der Andrea Nahles"                                                
##  [9] "Newsblog: Das war Der Morgen @SPIEGELONLINE am 30.7.2018"                                                  
## [10] "Blockade des US-Haushalts: Trump droht im Streit über Einwanderung mit \"Shutdown\""                       
## [11] "Treffen zwischen Sulzberger und US-Präsident: \"New York Times\"-Verleger warnt Trump"                     
## [12] "Rechtliche Gleichstellung: Pride and Prejudice"                                                            
## [13] "Recep Tayyip Erdogan: Kritik an Staatsbesuchplänen in Deutschland"                                         
## [14] "#MeTwo: \"Unsere Gesellschaft ist keine Monokultur\""                                                      
## [15] "\"Spaltung der Gesellschaft\": Initiativen lehnen Nominierung für Preis ab - weil Seehofer Schirmherr ist" 
## [16] "Wahlergebnis: Kambodschas Dauer-Ministerpräsident bleibt offenbar im Amt"                                  
## [17] "Proteste in Brasilien: Zehntausende fordern Lulas Freilassung"                                             
## [18] "Mugabe zu Wahlen in Simbabwe: \"Nicht für die stimmen, die mich schikaniert haben\""                       
## [19] "Iran: Vor dem Hitzschlag"                                                                                  
## [20] "Bootsunglück: Türkische Flüchtlinge vor Lesbos ertrunken"                                                  
## [21] "Israels Nationalgesetz: Arabischer Knesset-Abgeordneter legt Mandat nieder"                                
## [22] "Streit über festgesetzten US-Pastor: Erdogan droht Trump mit Ende der Partnerschaft"
urls <- xml_nodes(rss, 'link')
(urls <- xml_text(urls))
##  [1] "http://www.spiegel.de"                                                                                                                         
##  [2] "http://www.spiegel.de"                                                                                                                         
##  [3] "http://www.spiegel.de/politik/ausland/brexit-briten-offen-fuer-weiteres-referendum-ueber-eu-austritt-a-1220748.html#ref=rss"                   
##  [4] "http://www.spiegel.de/politik/ausland/reiner-haseloff-fuer-abschiebungen-in-buergerkriegsland-syrien-a-1220732.html#ref=rss"                   
##  [5] "http://www.spiegel.de/politik/ausland/donald-trump-wirft-robert-mueller-interessenskonflikte-vor-a-1220735.html#ref=rss"                       
##  [6] "http://www.spiegel.de/politik/ausland/matteo-salvini-innenminister-italiens-zitiert-diktator-mussolini-a-1220727.html#ref=rss"                 
##  [7] "http://www.spiegel.de/politik/ausland/kambodscha-usa-kuendigen-sanktionen-gegen-hun-sen-regierung-an-a-1220722.html#ref=rss"                   
##  [8] "http://www.spiegel.de/politik/deutschland/news-andrea-nahles-donald-trump-klimawandel-bayreuth-mario-draghi-a-1219750.html#ref=rss"            
##  [9] "http://www.spiegel.de/politik/deutschland/nachrichten-am-morgen-die-news-in-echtzeit-a-1220379.html#ref=rss"                                   
## [10] "http://www.spiegel.de/politik/ausland/donald-trump-droht-im-streit-um-einwanderung-mit-shutdown-a-1220719.html#ref=rss"                        
## [11] "http://www.spiegel.de/politik/ausland/donald-trump-new-york-times-verleger-a-g-sulzberger-warnt-us-praesidenten-a-1220716.html#ref=rss"        
## [12] "http://www.spiegel.de/politik/deutschland/ehe-fuer-alle-mehr-als-ein-jahr-nach-einfuehrung-gibt-es-noch-diskriminierung-a-1220701.html#ref=rss"
## [13] "http://www.spiegel.de/politik/deutschland/recep-tayyip-erdogan-kritik-an-staatsbesuchplaenen-in-deutschland-a-1220714.html#ref=rss"            
## [14] "http://www.spiegel.de/video/metwo-initiator-ali-can-ueber-hashtag-gegen-diskriminierung-video-99019588.html#ref=rss"                           
## [15] "http://www.spiegel.de/politik/deutschland/berlin-initiativen-lehnen-nominierung-fuer-preis-ab-wegen-horst-seehofer-a-1220708.html#ref=rss"     
## [16] "http://www.spiegel.de/politik/ausland/kambodscha-regierungspartei-hat-umstrittene-wahl-gewonnen-a-1220707.html#ref=rss"                        
## [17] "http://www.spiegel.de/politik/ausland/brasilien-luiz-inacio-lula-da-silva-tausende-demonstrieren-fuer-freilassung-a-1220697.html#ref=rss"      
## [18] "http://www.spiegel.de/politik/ausland/simbabwe-robert-mugabe-meldet-sich-zu-wort-habe-blutbad-verhindern-wollen-a-1220687.html#ref=rss"        
## [19] "http://www.spiegel.de/politik/ausland/iran-hitzewarnung-fuer-das-regime-in-teheran-a-1219998.html#ref=rss"                                     
## [20] "http://www.spiegel.de/politik/ausland/tuerkei-fluechtlinge-vor-lesbos-ertrunken-angeblich-guelen-anhaenger-a-1220679.html#ref=rss"             
## [21] "http://www.spiegel.de/politik/ausland/israel-arabischer-knesset-abgeordneter-suheir-bahlul-legt-mandat-nieder-protest-a-1220681.html#ref=rss"  
## [22] "http://www.spiegel.de/politik/ausland/tuerkei-recep-tayyip-erdogan-droht-usa-mit-ende-der-partnerschaft-a-1220677.html#ref=rss"

Once we have the article URLs, we could go page by page, looking at their internal structure, and then scraping it. However, some packages exist that already compile a set of scrapers that generally work with any type of newspaper website – one of these is boilerpipeR. It uses a combination of machine learning and heuristics to develop functions that should work for any newspaper website. Let’s see how it works in this case:

library(boilerpipeR)
# read first URL -- note that all text needs to be into a single character vector
text <- readLines(urls[3])
## Warning in readLines(urls[3]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/brexit-briten-offen-fuer-weiteres-
## referendum-ueber-eu-austritt-a-1220748.html#ref=rss'
text <- paste(text, collapse="\n")
# now let's try to parse it..
main_text <- ArticleExtractor(text)
cat(main_text)
## 1. Brexit nicht zwingend
## Ihr Bericht enthält einen entscheidenden Fehler. Trotz Artikel 50 Einreichung MUSS Grossbritannien die EU nicht zwingend im März verlassen. Als Mitglied der „People‘s Vote“ Bewegung weiß ich, das bei einem erneuten [...]
## Ihr Bericht enthält einen entscheidenden Fehler. Trotz Artikel 50 Einreichung MUSS Grossbritannien die EU nicht zwingend im März verlassen. Als Mitglied der „People‘s Vote“ Bewegung weiß ich, das bei einem erneuten Referendum auch wieder „Verbleib in der EU“ auf dem Zettel stehen kann, falls ein bestimmter Deal oder No-Deal abgelehnt werden.
## Riven heute, 11:39 Uhr
## 2. @ 1
## Und ich als Jurist sage, dass der Austritt zwingend ist. Art. 50 EUV ist da recht eindeutig: "Die Verträge finden auf den betroffenen Staat ab dem Tag des Inkrafttretens des Austrittsabkommens oder andernfalls zwei Jahre [...]
## Und ich als Jurist sage, dass der Austritt zwingend ist. Art. 50 EUV ist da recht eindeutig: "Die Verträge finden auf den betroffenen Staat ab dem Tag des Inkrafttretens des Austrittsabkommens oder andernfalls zwei Jahre nach der in Absatz 2 genannten Mitteilung keine Anwendung mehr, es sei denn, der Europäische Rat beschließt im Einvernehmen mit dem betroffenen Mitgliedstaat einstimmig, diese Frist zu verlängern." Und im Absatz 5: "Ein Staat, der aus der Union ausgetreten ist und erneut Mitglied werden möchte, muss dies nach dem Verfahren des Artikels 49 beantragen." Die Briten WERDEN ausscheiden. Das ist das, was sie sich immer gewünscht haben. Viel Spaß in der Zukunft, sich gegen USA, China, Russland UND die EU global durchzusetzen.
## ttvtt heute, 11:40 Uhr
## 3. komplexe Wirtschaftszusammenhänge
## 50 % wollen über die ausgehandelte Brexit-Übereinkunft in einem weiteren Referendum abzustimmen. Was soll so ein Referendum bringen? Die Übereinkunft wird so komplex sein, dass wohl die Tragweite nur von wirklichen Fachleuten [...]
## 50 % wollen über die ausgehandelte Brexit-Übereinkunft in einem weiteren Referendum abzustimmen. Was soll so ein Referendum bringen? Die Übereinkunft wird so komplex sein, dass wohl die Tragweite nur von wirklichen Fachleuten verstanden wird. Und darüber soll dann die Hausfrau Klein-Britney und der Taxifahrer Brain-George abstimmen?
## RedOrc heute, 11:42 Uhr
## 4. Radio Eriwan: Im Prinzip ja, aber..
## Klar kann man ein Referendum abhalten, genau wie man auch ein Referendum gegen die Hitzewelle initiieren könnte - beides hätte ungefähr die gleichen Auswirkungen. Der Rücktritt vom Rücktritt (Brexit) ist zwar prinzipiell [...]
## Zitat von nadworksIhr Bericht enthält einen entscheidenden Fehler. Trotz Artikel 50 Einreichung MUSS Grossbritannien die EU nicht zwingend im März verlassen. Als Mitglied der „People‘s Vote“ Bewegung weiß ich, das bei einem erneuten Referendum auch wieder „Verbleib in der EU“ auf dem Zettel stehen kann, falls ein bestimmter Deal oder No-Deal abgelehnt werden.
## Klar kann man ein Referendum abhalten, genau wie man auch ein Referendum gegen die Hitzewelle initiieren könnte - beides hätte ungefähr die gleichen Auswirkungen. Der Rücktritt vom Rücktritt (Brexit) ist zwar prinzipiell möglich, aber nur wenn alle anderen restlichen EU-Mitglieder dem zustimmen - und das hätte nur dann überhaupt einen Hauch von Chance wenn GB die ganzen Privilegien und Sonderlocken, die es im Laufe der Jahre so erhalten hat, aufgibt und den Euro einführt.  Wie wahrscheinlich ist das Szenario?   (Wurde im übrigen schon elendig oft durchgekaut)
## stadtmusikant123 heute, 11:43 Uhr
## 5. Es komt wie es kommt
## Nun, 50 % wollen einen Brexit. Den werden sie auch bekommen.  Wie der Brexit ausfällt steht in den Sternen. Beeinflussen kann der "Mann auf der Straße" das Ergebnis nicht mehr. Rechtlich gibt das das [...]
## Nun, 50 % wollen einen Brexit. Den werden sie auch bekommen.  Wie der Brexit ausfällt steht in den Sternen. Beeinflussen kann der "Mann auf der Straße" das Ergebnis nicht mehr. Rechtlich gibt das das Austrittsverfahren nicht her, und der Zeitablauf ebenfalls nicht. Und dann sind da noch all die anderen Mitgieder, die den Briten ihre Extra-Würste so uneingeschränkt gönnen.  Ausgelöst ist er, und rückgängig machen ist rechtlich und politisch mehr wie nur unwahrscheinlich.  Denn angenommen, die EU-Mitglieder würden einer Rücknahme des Brexit zustimmen, dann wären die , bei den anderen Mitgliedern so beliebten  britischen Extra-Würste manifestiert, wer glaubt also an so was?

Once we have prototype code, the last step is to generalize using a loop that will iterate over URLs.

articles <- list()
for (i in 1:length(urls)){

    message(i, " of ", length(urls))
    text <- paste(readLines(urls[i]), collapse="\n")
    main_text <- ArticleExtractor(text)
    articles[[i]] <- data.frame(
        url = urls[i],
        headline = headlines[i],
        text = main_text,
        stringsAsFactors=F)

}
## 1 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de'
## 2 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de'
## 3 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/brexit-briten-offen-fuer-weiteres-
## referendum-ueber-eu-austritt-a-1220748.html#ref=rss'
## 4 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/reiner-haseloff-fuer-abschiebungen-in-
## buergerkriegsland-syrien-a-1220732.html#ref=rss'
## 5 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/donald-trump-wirft-robert-mueller-
## interessenskonflikte-vor-a-1220735.html#ref=rss'
## 6 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/matteo-salvini-innenminister-italiens-
## zitiert-diktator-mussolini-a-1220727.html#ref=rss'
## 7 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/kambodscha-usa-kuendigen-sanktionen-gegen-
## hun-sen-regierung-an-a-1220722.html#ref=rss'
## 8 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/deutschland/news-andrea-nahles-donald-trump-
## klimawandel-bayreuth-mario-draghi-a-1219750.html#ref=rss'
## 9 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/deutschland/nachrichten-am-morgen-die-news-in-
## echtzeit-a-1220379.html#ref=rss'
## 10 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/donald-trump-droht-im-streit-um-
## einwanderung-mit-shutdown-a-1220719.html#ref=rss'
## 11 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/donald-trump-new-york-times-verleger-a-g-
## sulzberger-warnt-us-praesidenten-a-1220716.html#ref=rss'
## 12 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/deutschland/ehe-fuer-alle-mehr-als-ein-jahr-nach-
## einfuehrung-gibt-es-noch-diskriminierung-a-1220701.html#ref=rss'
## 13 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/deutschland/recep-tayyip-erdogan-kritik-an-
## staatsbesuchplaenen-in-deutschland-a-1220714.html#ref=rss'
## 14 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/video/metwo-initiator-ali-can-ueber-hashtag-gegen-
## diskriminierung-video-99019588.html#ref=rss'
## 15 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/deutschland/berlin-initiativen-lehnen-nominierung-
## fuer-preis-ab-wegen-horst-seehofer-a-1220708.html#ref=rss'
## 16 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/kambodscha-regierungspartei-hat-umstrittene-
## wahl-gewonnen-a-1220707.html#ref=rss'
## 17 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/brasilien-luiz-inacio-lula-da-silva-
## tausende-demonstrieren-fuer-freilassung-a-1220697.html#ref=rss'
## 18 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/simbabwe-robert-mugabe-meldet-sich-zu-wort-
## habe-blutbad-verhindern-wollen-a-1220687.html#ref=rss'
## 19 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/iran-hitzewarnung-fuer-das-regime-in-
## teheran-a-1219998.html#ref=rss'
## 20 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/tuerkei-fluechtlinge-vor-lesbos-ertrunken-
## angeblich-guelen-anhaenger-a-1220679.html#ref=rss'
## 21 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/israel-arabischer-knesset-abgeordneter-
## suheir-bahlul-legt-mandat-nieder-protest-a-1220681.html#ref=rss'
## 22 of 22
## Warning in readLines(urls[i]): incomplete final line found on 'http://
## www.spiegel.de/politik/ausland/tuerkei-recep-tayyip-erdogan-droht-usa-mit-
## ende-der-partnerschaft-a-1220677.html#ref=rss'
articles <- do.call(rbind, articles)

Of course, some times this standardized code will not work with specific websites. In those cases, it’s easier to just develop our own code. Here I show you an example written by one of the students in my lab, Anthony Ramos.

The following two blocks of code to the following: 1) download the home page of the Spiegel in html format, 2) extract the URLs of all articles linked in the home page, and then download those, and finally 3) parse the html code in those pages.

scrapeSpiegelOnline <- function(path) {

  html <- download.file("http://www.spiegel.de/",
    destfile=path)
  doc <-read_html(path)

  #get main articles
  title=html_nodes(doc,".article-title a")
  titles = xml_attr(title,"title")
  title_links = xml_attr(title,"href")

  title_links <- ifelse( grepl("https?://", title_links),
    title_links, paste0("http://www.spiegel.de", title_links) )

  df <- data.frame(title=titles, url=title_links, 
                   time=as.character(Sys.time()), stringsAsFactors=F)
  df <- df[!is.na(df$url),]
  df <- df[!duplicated(df$url),]
  
  return(df)
}


scrapeSpiegelArticle <- function(url, path) {
  
  html <- download.file(url, destfile=path, quiet=TRUE)

  article <- read_html(path)
  article_intro <- html_text(html_nodes(article,".headline-intro"))[1]
  article_title <- html_text(html_nodes(article,".headline"))[1]
  article_title <- paste(article_intro,article_title,sep=": ")
  
  date <- html_text(html_nodes(article,".article-function-date"))[1]
  date <- gsub("\t|\n|\r", "", gsub("\r", "", date))
  
  content <- html_text(html_nodes(article,"p"))
  summary <- content[1]
  
  content <- paste(content, collapse="\n\n")
  content <- gsub("^ *| *$", "", gsub("\n|\t|\r", "", content))
  
  summary <- gsub("^ *| *$", "", gsub("\n", "", summary))
  summary <- gsub(" {2,}", " ", summary)
  
  comments <- grep("insgesamt (.*) Beiträge",html_text(html_nodes(article, "span")),value=TRUE)
  comments <- gsub("^ *", "", gsub("\r|\n|\t", "", comments))
  
  
  article_df <- data.frame(url=url, date=date, summary=summary,
                           headline=article_title, text=content,
                           comments=ifelse(length(comments)==0, NA, comments),
                           stringsAsFactors=F)
                           
  return(article_df)

}
today <- Sys.Date()

# filename for homepage, in html and csv format
homepage <- paste0("home-", today, ".html")
homepagecsv <- paste0("articles-", today, ".csv")

# folder where home articles will be stored
artfolder <- paste0("home-articles")
try(dir.create(artfolder, showWarnings=FALSE))

# scraping homepage and saving .html to "homepage" file
headlines <- scrapeSpiegelOnline(path=homepage)

# scraping 10 first articles linked in the homepage
articles <- list()
for (i in 1:10){
    message(headlines$title[i])
  artname <- paste0(artfolder, "/", 
        gsub("/", "_", gsub("http://www.spiegel.de/", "", headlines$url[i])) )
    # download html file and parse it
  error <- tryCatch(
        articles[[i]] <- scrapeSpiegelArticle(
            url = headlines$url[i], path=artname),
        error=function(e) e)
    if (inherits(error, "error")){ message("Error")}
    Sys.sleep(1)
}
## Austritt aus der EU: Nur gut ein Viertel der Briten für harten Brexit
## Handelsstreit und Brexit: Britischer Notenbankchef warnt vor Protektionismus
## Russland-Ermittlungen: Trump wirft Mueller Interessenkonflikte vor
## Neue US-Strategie der Bundesregierung: "Nicht der Klügere gibt nach"
## CDU-Ministerpräsident für Abschiebungen: "Auch nach Syrien, wenn die Voraussetzungen dafür gegeben sind"
## Ärger um Seehofers mangelnden Arbeitseifer: Minister mit begrenzter Laufzeit
## Abschlussbericht von Malaysias Regierung: Rätsel von Flug MH370 bleibt ungelöst
## Hitzewelle: Weinlese beginnt so früh wie nie zuvor
## Reaktionen auf getöteten Eisbären: "Die Eisbären-Heimat ist kein verdammter Touristenort"
## Seit 65 Jahren verheiratet: Wie Marlene und Werner Hüfner die ewige Liebe fanden
articles <- do.call(rbind, articles)

# merging
headlines <- merge(headlines, articles, by="url", all.x=TRUE, sort=FALSE)

# writing to disk
write.csv(headlines, file=homepagecsv, row.names=FALSE)