Functions and Iteration

wk10-d02-fun-iterations

Dr. D

Chico State
DATA 385 - Fall 2026

October 29, 2026

Setup

šŸŽ„ Functions (22 min) and Iteration (14 min) - code along in wk10-d02

library(tidyverse)
library(rvest)

This deck runs on saved copies of the gov.scot pages in notes/data/, so rendering does not request the site each time. The live code is shown, and the results come from the saved copies.

Checklist

  • robots.txt does not disallow these pages
  • the site’s content is under the Open Government Licence
  • the speeches are public statements
  • we only need four pages, saved once so we can iterate without excessive site requests

Function updates

The gov.scot pages were redesigned after the videos were made. The selectors in this deck are the current ones and differ from the video, and the functions have been updated.

  • html_node() in the video is html_element() here
  • html_nodes() in the video is html_elements() here
  • map_dfr() in the video is map() followed by list_rbind() here
  • str_c() with the . placeholder in the video is url_absolute() here

First Minister’s COVID speeches

šŸ Start with

Everything starts from a page that links to each of the First Minister’s speeches.

The gov.scot collection page listing the First Minister's speeches.

End with šŸ›‘

We want a table with one row per speech and six columns: title, date, location, abstract, text and url.

# A tibble: 218 Ɨ 6
   title                      date       location abstract text  url  
   <chr>                      <date>     <chr>    <chr>    <chr> <chr>
 1 Coronavirus (COVID-19) up… 2021-04-20 St Andr… Stateme… "Goo… http…
 2 Coronavirus (COVID-19) up… 2021-04-13 St Andr… Stateme… "Tha… http…
 3 Coronavirus (COVID-19) up… 2021-04-06 St Andr… Stateme… "Goo… http…
 4 Coronavirus (COVID-19) up… 2021-03-30 St Andr… Stateme… "Tha… http…
 5 Coronavirus (COVID-19) up… 2021-03-24 Scottis… Stateme… "Tha… http…
 6 Coronavirus (Covid-19) up… 2021-03-23 The Sco… Stateme… "Pre… http…
 7 Coronavirus (COVID-19) up… 2021-03-18 Scottis… Stateme… "Tha… http…
 8 Coronavirus (COVID-19) up… 2021-03-17 St Andr… Stateme… "\nG… http…
 9 Coronavirus (COVID-19) up… 2021-03-16 Scottis… Stateme… "Pre… http…
10 Coronavirus (COVID-19) up… 2021-03-15 St Andr… Stateme… "\nG… http…
11 Coronavirus (COVID-19) up… 2021-03-11 Scottis… Stateme… "I c… http…
12 Coronavirus (COVID-19) up… 2021-03-09 Scottis… Stateme… "Pre… http…
13 Coronavirus (COVID-19) up… 2021-03-05 Scottis… Parliam… "Hel… http…
14 Coronavirus (COVID-19) up… 2021-03-04 Scottis… Parliam… "I w… http…
15 Coronavirus (COVID-19) up… 2021-03-02 Scottis… Stateme… "Pre… http…
# ℹ 203 more rows

The collection page

www.gov.scot/collections/first-ministers-speeches

The links to each speech are not visible text. The URLs live in the page source, so we have to pull them out.

The collection page with the links to individual speeches highlighted.

A speech page

Each speech page has a title, an abstract, the date, the location, and then the text of the speech.

A speech page with the title, abstract, date, location and text highlighted.

Plan

  1. Scrape title, date, location, abstract, and text from a few COVID-19 speech pages to develop the code

  2. Write a function that scrapes title, date, location, abstract, and text from COVID-19 speech pages

  3. Scrape the urls of COVID-19 speeches from the main page

  4. Use this function to scrape from each individual COVID-19 speech from these urls and create a data frame with the columns title, date, location, abstract, text, and url

Scrape data from a few COVID-19 speech pages

Read page for 26 Oct speech

Copy the URL from the browser, save it as url, and use read_html() to read the page source into speech_page.

url <- "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-speech-26-october/"
speech_page <- read_html(url)
speech_page
{html_document}
<html dir="ltr" lang="en">
[1] <head>\n<meta http-equiv="Content-Type" content="text/html; cha ...
[2] <body>\n    <input type="hidden" id="site-root-path" value="/"> ...

The 26 October speech page.

Extract title

We only want one title, so we use html_element(). Then we ask for the text in it.

title <- speech_page |>
  html_element(".ds_page-header__title") |>
  html_text()

title
[1] "Coronavirus (COVID-19) update: First Minister's speech 26 October"

The title of the speech page highlighted with SelectorGadget.

Extract date

The date comes out as a character string. We want R to know it is a date, so we convert it with dmy(), which reads day, month, year.

speech_page |>
  html_element("#sg-meta__publication-date") |>
  html_text()
[1] "26 October 2020"
date <- speech_page |>
  html_element("#sg-meta__publication-date") |>
  html_text() |>
  dmy()

date
[1] "2020-10-26"

The publication date highlighted with SelectorGadget.

Extract location

Same pattern: start with speech_page, get the element, get the text.

location <- speech_page |>
  html_element(".ds_metadata__item:last-child .ds_metadata__value") |>
  html_text()

location
[1] "St Andrew's House, Edinburgh"

The location highlighted with SelectorGadget.

Extract abstract

The abstract is the short description right under the title. Same pattern again.

abstract <- speech_page |>
  html_element("p.ds_leader") |>
  html_text()

abstract
[1] "Statement given by First Minister Nicola Sturgeon at a media briefing in St Andrew's House on Monday 26 October 2020."

The abstract highlighted with SelectorGadget.

Extract text

The text is many paragraphs, so this time we want every match: html_elements(), plural. We store it as a list so all the paragraphs fit in one cell of our data frame.

text <- speech_page |>
  html_elements("#preamble p") |>
  html_text() |>
  list()

text
[[1]]
 [1] " Good afternoon, and thanks for joining us. I want to start with the usual daily report on the COVID statistics."                                                                                                                                                                                                                                                                                                                   
 [2] "The total number of positive cases reported yesterday was 1,122."                                                                                                                                                                                                                                                                                                                                                                   
 [3] "This represents 7.1% of the total number of tests carried out. 428 of the new cases were in Greater Glasgow and Clyde, 274 in Lanarkshire, 105 in Lothian and 97 in Ayrshire and Arran. "                                                                                                                                                                                                                                           
 [4] "The remaining cases were spread across the mainland health board regions. "                                                                                                                                                                                                                                                                                                                                                         
 [5] "The total number of confirmed cases is now 57,874."                                                                                                                                                                                                                                                                                                                                                                                 
 [6] "I can also confirm that 1,152 people are in hospital – that is an increase of 36 from yesterday"                                                                                                                                                                                                                                                                                                                                    
 [7] "90 people are in intensive care, which is four more than yesterday."                                                                                                                                                                                                                                                                                                                                                                
 [8] "And I regret to say that in the last 24 hours, one further death has been registered of a patient who first tested positive over the previous 28 days.  It is important though to remember that registration offices tend not to be open as normal over the weekend so the Sunday and Monday figures are often lower."                                                                                                              
 [9] "We also reported 11 deaths on Saturday, and one yesterday.  So since the last briefing on Friday, 13 additional deaths have been registered. That takes the total number of deaths, under this measurement, to 2,701."                                                                                                                                                                                                              
[10] "That reminds us again of how dangerous this virus can be and I want to send my condolences to everyone who has lost someone."                                                                                                                                                                                                                                                                                                       
[11] "I am joined by the Cabinet Secretary for Health who will say a few words about the Scottish Government’s revised testing strategy and about preparations for winter."                                                                                                                                                                                                                                                               
[12] "Firstly, I will give a brief preview of the week ahead, before updating you on one particular issue."                                                                                                                                                                                                                                                                                                                               
[13] "On Friday, the Scottish Government published our strategic framework for tackling COVID. The framework includes the proposed levels that will apply to different parts of the country, depending on the prevalence of the virus."                                                                                                                                                                                                   
[14] "As you recall on Friday we published our strategic framework for tackling COVID."                                                                                                                                                                                                                                                                                                                                                   
[15] "That framework included the proposed levels that will apply to different parts of the country, depending on prevalence of the virus."                                                                                                                                                                                                                                                                                               
[16] "Over the weekend, we've discussed the framework with partners and considered further public health advice. These discussions are continuing as are discussions with local councils."                                                                                                                                                                                                                                                
[17] "There are likely to be some clarifications to the detail of the measures we set out on Friday and so we will publish a revised version, ahead of the parliamentary debate that will take place tomorrow."                                                                                                                                                                                                                           
[18] " And of course we will keep the detail of the levels under ongoing review as the pandemic develops"                                                                                                                                                                                                                                                                                                                                 
[19] "I will also set out more detail ahead of tomorrow's debate on the factors that will guide decisions on which levels apply to different parts of the country."                                                                                                                                                                                                                                                                       
[20] "And assuming Parliament agrees the draft framework tomorrow, Ministers will decide later in the week on advice from the national incident management team, and our chief advisors and in consultation with local councils, what levels will initially apply in what parts of the country from Monday November 2."                                                                                                                   
[21] "And I would remind you that in broad terms, the current restrictions in place in the central belt are equivalent to the proposed level three. And those in the rest of the country, broadly equivalent to level two. So for many places there may be no immediate change."                                                                                                                                                          
[22] "For an area to come down a level, we want to see a fall in the prevalence of the virus be sustained, not just seen on one or two days."                                                                                                                                                                                                                                                                                             
[23] "And that I think is important and giving business stability but also in giving people confidence that we are not going to take unnecessary risks when we start to lift restrictions."                                                                                                                                                                                                                                               
[24] "So the next few days will see some important decisions which of course will have an impact on all of us."                                                                                                                                                                                                                                                                                                                           
[25] "I think it is important for context for all of us to remember, given how tough all of this is, that these decisions are one that Scotland is facing in common with countries across these islands, across Europe, and of course, across the world. Many countries are facing having to impose or re-impose tough restrictions, especially on hospitality, as a second wave of COVID takes hold."                                    
[26] "The purpose of adopting tough restrictions just over two weeks ago here was to try to curb the increase in COVID cases that we were seeing before the virus ran further out of control."                                                                                                                                                                                                                                            
[27] "The daily figures I have been reporting recently as I indicated on Friday  suggest that these restrictions are starting to have an effect. We do think that the increase in case numbers is slowing but it's not yet in decline, which is why we can't be complacent, but we should take encouragement from the daily numbers at the moment because they suggest that the sacrifices that everybody is making are starting to work."
[28] "Once the new five levels approach is adopted and once people get more familiar with it, which I know will take a bit of time, I hope people will be better able to understand, on an ongoing basis the spread of COVID in their particular area, and also what measures are necessary to tackle it."                                                                                                                                
[29] "Hopefully that in turn will underline a key point. The best way of moving to a lower level of restrictions – and of living more freely – is to have a lower level of transmission. And the best way we have of driving transmission lower – and keeping it low – is for all of us to stick to the rules that are in place at any given time. That is of course a collective responsibility for all of us."                          
[30] "The second point I want to highlight is that two more walk-through testing sites have opened in the last few days."                                                                                                                                                                                                                                                                                                                 
[31] "On Thursday, a new site opened in the centre of Greenock in Inverclyde, and on Saturday, one opened at the Highland Council headquarters in Inverness."                                                                                                                                                                                                                                                                             
[32] "That means that in total we have 11 walk-through testing sites now open to meet the needs of local communities across Scotland and to try to make access to testing easier for people."                                                                                                                                                                                                                                             
[33] "I want to take this opportunity to thank everyone involved in setting up these sites and all those who are helping to run them."                                                                                                                                                                                                                                                                                                    
[34] "This walk-through centres are only part of our testing infrastructure but they are an important way to make testing more accessible in Scotland’s towns and cities. So we will continue to work with partners – including the UK Government -  to establish more of them in the weeks and months ahead.."                                                                                                                           
[35] "It is possible to book a test at a walk-through site by going to the NHS Inform website. Please remember that though they are walk-through sites, you should seek to walk, cycle or drive to them, but do not use public transport to travel to any testing centre."                                                                                                                                                                
[36] "If you do not have a car - and cannot walk or cycle - please book a home testing kit through NHS Inform."                                                                                                                                                                                                                                                                                                                           
[37] "The reason for that, is to ensure you do not transmit the virus on the way to or from the testing centre."                                                                                                                                                                                                                                                                                                                          
[38] "I would remind everybody again that if you experience an of the symptoms of COVID it is really important to self-isolate immediately and take steps to get tested."                                                                                                                                                                                                                                                                 
[39] "I want to end, as usual, by stressing the other key rules and guidelines."                                                                                                                                                                                                                                                                                                                                                          
[40] "If you live in Lothian, Lanarkshire, Forth Valley, Ayrshire and Arran and Greater Glasgow and Clyde, you should not travel outside those areas unless you have a clear need to do so."                                                                                                                                                                                                                                              
[41] "People living in other parts of Scotland should not travel to these areas unless necessary."                                                                                                                                                                                                                                                                                                                                        
[42] "None of us should be visiting each other’s homes just now, except for very specific purposes such as childcare or looking after a vulnerable person."                                                                                                                                                                                                                                                                               
[43] "And when we do meet people from other households - outdoors, or at a cafƩ - the maximum group size is 6, from a maximum of two households."                                                                                                                                                                                                                                                                                         
[44] "Avoid car-sharing if you can. Work from home if you can. And remember to download the Protect Scotland app, if you haven’t already done so."                                                                                                                                                                                                                                                                                        
[45] "And finally, remember the FACTS – the five rules which will help to remind you how to do the right thing."                                                                                                                                                                                                                                                                                                                          
[46] "If we all stick to these rules then hopefully the encouraging signs we are seeing in the figures that we are reporting every day will not just continue but accelerate, and a slowing of the increase over the weeks to come will become a decline in the number of daily new cases."                                                                                                                                               
[47] "Thank you all very much."                                                                                                                                                                                                                                                                                                                                                                                                           

The speech text highlighted with SelectorGadget.

Put it all in a data frame

A tibble with one row, because it is one speech, and six columns. The url is the input we started with.

(oct_26_speech <- tibble(
  title    = title,
  date     = date,
  location = location,
  abstract = abstract,
  text     = text,
  url      = url)
)
# A tibble: 1 Ɨ 6
  title                       date       location abstract text  url  
  <chr>                       <date>     <chr>    <chr>    <lis> <chr>
1 Coronavirus (COVID-19) upd… 2020-10-26 St Andr… Stateme… <chr> http…

The 26 October speech page.

Read page for 23 Oct speech

Let’s try another one, with the URL for the 23rd.

url <- "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-speech-23-october/"
speech_page <- read_html(url)
speech_page
{html_document}
<html dir="ltr" lang="en">
[1] <head>\n<meta http-equiv="Content-Type" content="text/html; cha ...
[2] <body>\n    <input type="hidden" id="site-root-path" value="/"> ...

Extract components of 23 Oct speech

We saved over speech_page, then ran exactly the same code.

title <- speech_page |>
  html_element(".ds_page-header__title") |>
  html_text()

date <- speech_page |>
  html_element("#sg-meta__publication-date") |>
  html_text() |>
  dmy()

location <- speech_page |>
  html_element(".ds_metadata__item:last-child .ds_metadata__value") |>
  html_text()

abstract <- speech_page |>
  html_element("p.ds_leader") |>
  html_text()

text <- speech_page |>
  html_elements("#preamble p") |>
  html_text() |>
  list()

Put it all in a data frame

(oct_23_speech <- tibble(
  title    = title,
  date     = date,
  location = location,
  abstract = abstract,
  text     = text,
  url      = url)
)
# A tibble: 1 Ɨ 6
  title                       date       location abstract text  url  
  <chr>                       <date>     <chr>    <chr>    <lis> <chr>
1 Coronavirus (COVID-19) upd… 2020-10-23 St Andr… Stateme… <chr> http…

The 23 October speech page.

Functions

This is getting tiring…

When should you write a function?

A meme: functionalize all the things.

When you’ve copied and pasted a block of code more than twice.

How many copies?

In the video, a search of the collection page for ā€œcoronavirus covid-19ā€ found 142 hits. That is a lot of copying and pasting.

A browser search for covid-19 on the collection page showing the number of hits.

Why functions?

  • Automate common tasks in a more powerful and general way than copy-and-pasting:
    • Give your function an evocative name that makes your code easier to understand
    • As requirements change, only need to update code in one place, instead of many
    • Eliminate chance of making incidental mistakes when you copy and paste (i.e. updating a variable name in one place, but not in another)
  • Down the line: Improve your reach as a data scientist by writing functions (and packages!) that others use

What changes from page to page?

Assuming that the page structure is the same for each speech page, how many ā€œthingsā€ do you need to know for each speech page to scrape the data we want from it?

url_23_oct <- "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-speech-23-october/"
speech_page <- read_html(url_23_oct)

title <- speech_page |>
  html_element(".ds_page-header__title") |>
  html_text()

date <- speech_page |>
  html_element("#sg-meta__publication-date") |>
  html_text() |>
  dmy()

location <- speech_page |>
  html_element(".ds_metadata__item:last-child .ds_metadata__value") |>
  html_text()

abstract <- speech_page |>
  html_element("p.ds_leader") |>
  html_text()

text <- speech_page |>
  html_elements("#preamble p") |>
  html_text() |>
  list()

tibble(
  title = title, date = date, location = location,
  abstract = abstract, text = text, url = url_23_oct
)

Only one thing changes from page to page: the url. Everything else is exactly the same.

Turn your code into a function

  • Pick a short but informative name, preferably a verb.
scrape_speech <-

Turn your code into a function

  • Pick a short but evocative name, preferably a verb.
  • List inputs, or arguments, to the function inside function. If we had more the call would look like function(x, y, z).
scrape_speech <- function(x){

}

Turn your code into a function

  • Pick a short but informative name, preferably a verb.
  • List inputs, or arguments, to the function inside function. If we had more the call would look like function(x, y, z).
  • Place the code you have developed in body of the function, a { block that immediately follows function(...).
scrape_speech <- function(url){

  # code we developed earlier to scrape info
  # on single speech goes here

}

scrape_speech()

The argument url is used in only two places: to read the page, and to store in the output table.

scrape_speech <- function(url) {

  speech_page <- read_html(url)

  title <- speech_page |>
    html_element(".ds_page-header__title") |>
    html_text()

  date <- speech_page |>
    html_element("#sg-meta__publication-date") |>
    html_text() |>
    dmy()

  location <- speech_page |>
    html_element(".ds_metadata__item:last-child .ds_metadata__value") |>
    html_text()

  abstract <- speech_page |>
    html_element("p.ds_leader") |>
    html_text()

  text <- speech_page |>
    html_elements("#preamble p") |>
    html_text() |>
    list()

  tibble(
    title = title, date = date, location = location,
    abstract = abstract, text = text, url = url
  )
}

Function in action

The only thing that changed is the input.

scrape_speech(url = "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-speech-26-october/") |>
  glimpse()
Rows: 1
Columns: 6
$ title    <chr> "Coronavirus (COVID-19) update: First Minister's sp…
$ date     <date> 2020-10-26
$ location <chr> "St Andrew's House, Edinburgh"
$ abstract <chr> "Statement given by First Minister Nicola Sturgeon…
$ text     <list> <" Good afternoon, and thanks for joining us. I wan…
$ url      <chr> "https://www.gov.scot/publications/coronavirus-covi…

Function in action

scrape_speech(url = "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-speech-23-october/") |>
  glimpse()
Rows: 1
Columns: 6
$ title    <chr> "Coronavirus (COVID-19) update: First Minister's sp…
$ date     <date> 2020-10-23
$ location <chr> "St Andrew's House, Edinburgh"
$ abstract <chr> "Statement given by First Minister Nicola Sturgeon…
$ text     <list> <" Good afternoon everyone. Thank you very much for…
$ url      <chr> "https://www.gov.scot/publications/coronavirus-covi…

Function in action

scrape_speech(url = "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-speech-22-october/") |>
  glimpse()
Rows: 1
Columns: 6
$ title    <chr> "Coronavirus (COVID-19) update: First Minister's sp…
$ date     <date> 2020-10-22
$ location <chr> "St Andrew's House, Edinburgh"
$ abstract <chr> "Statement given by First Minister Nicola Sturgeon…
$ text     <list> <" Good afternoon, let me start as usual with the d…
$ url      <chr> "https://www.gov.scot/publications/coronavirus-covi…

Writing functions

What goes in / what comes out?

  • Functions take inputs, defined in the function definition. Inputs are separated by commas and go inside the parentheses after function. The code that says what to do with them goes in the curly braces.
function([inputs separated by commas]){
  # what to do with those inputs
}
  • By default they return the last value computed in the function
scrape_page <- function(x){
  # do bunch of stuff with the input...

  # return a tibble
  tibble(...)
}
  • You can define more outputs to be returned in a list as well as nice print methods (but we won’t go there for now…)

What does add_2() return?

What is going on here?

add_2 <- function(x){
  x + 2
  1000
}
add_2(3)
[1] 1000
add_2(10)
[1] 1000

The calculation happens, but the last thing in the function is 1000, so that is what comes back.

Naming functions

ā€œThere are only two hard things in Computer Science: cache invalidation and naming things.ā€ - Phil Karlton

Naming functions: guidelines

  • Names should be short but clearly evoke what the function does
  • Names should be verbs, not nouns
  • Multi-word names should be separated by underscores (snake_case as opposed to camelCase)
  • A family of functions should be named similarly (scrape_page(), scrape_speech() OR str_remove(), str_replace() etc.)
  • Avoid overwriting existing (especially widely used) functions
# JUST DON'T 
mean <- function(x){
  x * 3
}

Scraping all of the speeches

šŸ Start with

We have a function that scrapes one speech, given its URL. Now we need a list of URLs.

The gov.scot collection page listing the First Minister's speeches.

End with šŸ›‘

# A tibble: 218 Ɨ 6
   title                      date       location abstract text  url  
   <chr>                      <date>     <chr>    <chr>    <chr> <chr>
 1 Coronavirus (COVID-19) up… 2021-04-20 St Andr… Stateme… "Goo… http…
 2 Coronavirus (COVID-19) up… 2021-04-13 St Andr… Stateme… "Tha… http…
 3 Coronavirus (COVID-19) up… 2021-04-06 St Andr… Stateme… "Goo… http…
 4 Coronavirus (COVID-19) up… 2021-03-30 St Andr… Stateme… "Tha… http…
 5 Coronavirus (COVID-19) up… 2021-03-24 Scottis… Stateme… "Tha… http…
 6 Coronavirus (Covid-19) up… 2021-03-23 The Sco… Stateme… "Pre… http…
 7 Coronavirus (COVID-19) up… 2021-03-18 Scottis… Stateme… "Tha… http…
 8 Coronavirus (COVID-19) up… 2021-03-17 St Andr… Stateme… "\nG… http…
 9 Coronavirus (COVID-19) up… 2021-03-16 Scottis… Stateme… "Pre… http…
10 Coronavirus (COVID-19) up… 2021-03-15 St Andr… Stateme… "\nG… http…
11 Coronavirus (COVID-19) up… 2021-03-11 Scottis… Stateme… "I c… http…
12 Coronavirus (COVID-19) up… 2021-03-09 Scottis… Stateme… "Pre… http…
13 Coronavirus (COVID-19) up… 2021-03-05 Scottis… Parliam… "Hel… http…
14 Coronavirus (COVID-19) up… 2021-03-04 Scottis… Parliam… "I w… http…
15 Coronavirus (COVID-19) up… 2021-03-02 Scottis… Stateme… "Pre… http…
# ℹ 203 more rows

Define scrape_speech()

scrape_speech <- function(url) {

  speech_page <- read_html(url)

  title <- speech_page |>
    html_element(".ds_page-header__title") |>
    html_text()

  date <- speech_page |>
    html_element("#sg-meta__publication-date") |>
    html_text() |>
    dmy()

  location <- speech_page |>
    html_element(".ds_metadata__item:last-child .ds_metadata__value") |>
    html_text()

  abstract <- speech_page |>
    html_element("p.ds_leader") |>
    html_text()

  text <- speech_page |>
    html_elements("#preamble p") |>
    html_text() |>
    list()

  tibble(
    title = title, date = date, location = location,
    abstract = abstract, text = text, url = url
  )
}

Use scrape_speech()

url_26_oct <- "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-speech-26-october/"
scrape_speech(url = url_26_oct)

url_23_oct <- "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-speech-23-october/"
scrape_speech(url = url_23_oct)
# A tibble: 1 Ɨ 6
  title                       date       location abstract text  url  
  <chr>                       <date>     <chr>    <chr>    <lis> <chr>
1 Coronavirus (COVID-19) upd… 2020-10-26 St Andr… Stateme… <chr> http…
# A tibble: 1 Ɨ 6
  title                       date       location abstract text  url  
  <chr>                       <date>     <chr>    <chr>    <lis> <chr>
1 Coronavirus (COVID-19) upd… 2020-10-23 St Andr… Stateme… <chr> http…

Inputs

Inputs

You now have a function that will scrape the relevant info on speeches given the URL of the page of the speech. Where can we get a list of URLs of each of the speeches?

So far we have clicked on these links by hand. We want to pull the URLs hidden under the links out of the page source.

The collection page with links to the individual speeches.

All URLs

Read the page that lists all the speeches, then grab the elements that hold the links.

This time we do not want the text. We want an attribute of the element: href, the hyperlink reference, which is where the URL is saved in the HTML.

all_speeches_page <- read_html("https://www.gov.scot/collections/first-ministers-speeches/")
all_speeches_page |>
  html_elements(".collections-list a") |>
  html_attr("href") |>
  head(10)
 [1] "/publications/the-roles-and-functions-of-scotlands-law-officers-first-ministers-statement/"          
 [2] "/publications/preventing-violence-against-women-for-an-equally-safe-scotland-first-ministers-speech/"
 [3] "/publications/programme-for-government-2026-to-2031-first-ministers-speech/"                         
 [4] "/publications/welcome-to-scotland-reception-first-ministers-speech-21-july/"                         
 [5] "/publications/quality-meat-scotland-breakfast-first-ministers-speech-18-june-2026/"                  
 [6] "/publications/prosper-forum-first-ministers-speech-4-june-2026/"                                     
 [7] "/publications/ambitious-for-scotland-first-ministers-speech/"                                        
 [8] "/publications/cbi-annual-lunch-first-ministers-speech-22-may-2026/"                                  
 [9] "/publications/first-minister-nomination-acceptance-speech/"                                          
[10] "/publications/eradicating-child-poverty-first-ministers-speech/"                                     

COVID-19 URLs fragments

These are relative links, so the start of the address is missing. The speeches we want all have covid-19 in the address. str_subset() keeps only the strings that contain a pattern. It is like filter(), but for a vector.

all_speeches_page |>
  html_elements(".collections-list a") |>
  html_attr("href") |>
  str_subset("covid-19") |>
  head(10)
 [1] "/publications/coronavirus-covid-19-update-first-ministers-speech-tuesday-22-february-2022/"
 [2] "/publications/coronavirus-covid-19-update-first-ministers-statement-8-february-2022/"      
 [3] "/publications/coronavirus-covid-19-update-first-ministers-statement-1-february-2022/"      
 [4] "/publications/coronavirus-covid-19-update-first-ministers-statement-25-january-2022/"      
 [5] "/publications/coronavirus-covid-19-update-first-ministers-statement-18-january-2022/"      
 [6] "/publications/coronavirus-covid-19-update-first-ministers-statement-11-january-2022/"      
 [7] "/publications/coronavirus-covid-19-update-first-ministers-statement-5-january-2022/"       
 [8] "/publications/coronavirus-covid-19-update-first-ministers-statement-21-december-2021/"     
 [9] "/publications/coronavirus-covid-19-update-first-ministers-speech-17-december-2021/"        
[10] "/publications/coronavirus-covid-19-update-first-ministers-statement-14-december-2021/"     

COVID-19 URLs

We still need the beginning of each address. url_absolute() adds the site’s address to each relative link.

The video builds the full address with str_c() and the . placeholder. The native pipe |> does not have the ., so we use url_absolute() instead.

all_speeches_page |>
  html_elements(".collections-list a") |>
  html_attr("href") |>
  str_subset("covid-19") |>
  xml2::url_absolute("https://www.gov.scot") |>
  head(10)
 [1] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-speech-tuesday-22-february-2022/"
 [2] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-8-february-2022/"      
 [3] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-1-february-2022/"      
 [4] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-25-january-2022/"      
 [5] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-18-january-2022/"      
 [6] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-11-january-2022/"      
 [7] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-5-january-2022/"       
 [8] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-21-december-2021/"     
 [9] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-speech-17-december-2021/"        
[10] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-14-december-2021/"     

Save COVID-19 URLs

We will use covid_speech_urls for the rest of the lesson.

covid_speech_urls <- all_speeches_page |>
  html_elements(".collections-list a") |>
  html_attr("href") |>
  str_subset("covid-19") |>
  xml2::url_absolute("https://www.gov.scot")

head(covid_speech_urls, 10)
 [1] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-speech-tuesday-22-february-2022/"
 [2] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-8-february-2022/"      
 [3] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-1-february-2022/"      
 [4] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-25-january-2022/"      
 [5] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-18-january-2022/"      
 [6] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-11-january-2022/"      
 [7] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-5-january-2022/"       
 [8] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-21-december-2021/"     
 [9] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-speech-17-december-2021/"        
[10] "https://www.gov.scot/publications/coronavirus-covid-19-update-first-ministers-statement-14-december-2021/"     

Iteration

Define the task

  • Goal: Scrape info on all COVID-19 speeches of the First Minister
  • So far:
scrape_speech(covid_speech_urls[1])
scrape_speech(covid_speech_urls[2])
scrape_speech(covid_speech_urls[3])
  • What else do we need to do?
    • Run the scrape_speech() function on all COVID-19 speech links
    • Combine the resulting data frames from each run into one giant data frame

Iteration

How can we tell R to apply the scrape_speech() function to each link in covid_speech_urls?

  • Option 1: Write a for loop, i.e. explicitly tell R to visit a link, apply the function, store the result, then visit the next link, apply the function, append the result to the stored result from the previous link, and so on and so forth.

  • Option 2: Map the function to each element in the list of links, and let R take care of the storing and appending of results.

How does mapping work?

Suppose we have exam 1 and exam 2 scores of 4 students stored in a list…

exam_scores <- list(
  exam1 = c(80, 90, 70, 50),
  exam2 = c(85, 83, 45, 60)
)

…and we find the mean score in each exam. The first argument is what we map over, and the second is the function.

map(exam_scores, mean)
$exam1
[1] 72.5

$exam2
[1] 68.25

map_something

…and suppose we want the results as a numeric (double) vector

map_dbl(exam_scores, mean)
exam1 exam2 
72.50 68.25 

…or as a character string

map_chr(exam_scores, mean)
Error in `map_chr()`:
ℹ In index: 1.
ℹ With name: exam1.
Caused by error:
! Can't coerce from a number to a string.

map_something variants

Functions for looping over an object and returning a value (of a specific type):

  • map() - returns a list
  • map_lgl() - returns a logical vector
  • map_int() - returns a integer vector
  • map_dbl() - returns a double vector
  • map_chr() - returns a character vector
  • map_df() / map_dfr() - return a data frame by row binding. These are superseded: use map() followed by list_rbind().
  • map_dfc() - returns a data frame by column binding. Superseded: use map() followed by list_cbind().
  • …

Go to each page, scrape speech

  • Map the scrape_speech() function
  • to each element of covid_speech_urls
  • and return a data frame by row binding

Save the result, because this takes a while to run.

covid_speeches <- map(covid_speech_urls, scrape_speech) |>
  list_rbind()

The result

covid_speeches |>
  print(n = 15)
# A tibble: 218 Ɨ 6
   title                      date       location abstract text  url  
   <chr>                      <date>     <chr>    <chr>    <chr> <chr>
 1 Coronavirus (COVID-19) up… 2021-04-20 St Andr… Stateme… "Goo… http…
 2 Coronavirus (COVID-19) up… 2021-04-13 St Andr… Stateme… "Tha… http…
 3 Coronavirus (COVID-19) up… 2021-04-06 St Andr… Stateme… "Goo… http…
 4 Coronavirus (COVID-19) up… 2021-03-30 St Andr… Stateme… "Tha… http…
 5 Coronavirus (COVID-19) up… 2021-03-24 Scottis… Stateme… "Tha… http…
 6 Coronavirus (Covid-19) up… 2021-03-23 The Sco… Stateme… "Pre… http…
 7 Coronavirus (COVID-19) up… 2021-03-18 Scottis… Stateme… "Tha… http…
 8 Coronavirus (COVID-19) up… 2021-03-17 St Andr… Stateme… "\nG… http…
 9 Coronavirus (COVID-19) up… 2021-03-16 Scottis… Stateme… "Pre… http…
10 Coronavirus (COVID-19) up… 2021-03-15 St Andr… Stateme… "\nG… http…
11 Coronavirus (COVID-19) up… 2021-03-11 Scottis… Stateme… "I c… http…
12 Coronavirus (COVID-19) up… 2021-03-09 Scottis… Stateme… "Pre… http…
13 Coronavirus (COVID-19) up… 2021-03-05 Scottis… Parliam… "Hel… http…
14 Coronavirus (COVID-19) up… 2021-03-04 Scottis… Parliam… "I w… http…
15 Coronavirus (COVID-19) up… 2021-03-02 Scottis… Stateme… "Pre… http…
# ℹ 203 more rows

What could go wrong?

covid_speeches <- map(covid_speech_urls, scrape_speech) |>
  list_rbind()
  • This will take a while to run
  • If you get HTTP Error 429 (Too many requests) the server is telling you to slow down. Modify your function to add a random wait (sleep) time between hitting each link.
scrape_speech <- function(url){

  # Sleep for randomly generated number of seconds
  # Generated from a uniform distribution between 0 and 1
  Sys.sleep(runif(1))

  # Rest of your function code goes here...
}

This is a way of being nice to the server. In AE08 we pause between every request from the start, not only after an error. The polite package is a more principled way to do it.

Acknowledgements & Credits

Note

This page adapts material from Data Science in a Box (Unit 2, Decks 21 and 22: ā€œFunctionsā€ and ā€œIterationā€) by Mine Ƈetinkaya-Rundel, licensed under CC BY-SA 4.0. Source: tidyverse/datascience-box and tidyverse/datascience-box. Modified: combined the two decks and converted them from xaringan to Quarto revealjs; updated the gov.scot selectors for the redesigned site and run the examples on saved copies of the pages; replaced %>%, html_node(), html_nodes() and map_dfr() with |>, html_element(), html_elements() and map() followed by list_rbind(); replaced str_c() with the . placeholder with url_absolute(); dropped the unused DT package and the library(lubridate) call; named the elements of the exam_scores list.