There are times when we need to scrape some data from a website to use in our analyses. In a perfect world, the provider of the data would provide a csv or json download but let’s face it… we do not live in a perfect world and often data that is posted to the internet is done by people who really do not care about subseqent use by others (else they would have made it easy to use rather than just showing it).

Here is a quick tutorial on one method I use to do this.

Necessary Resources

For this example, I’m going to use the rvest and dplyr libraries to scrape a USDA page for county FIPS codes1 for all the counties in Virginia.

if( !("dplyr" %in% installed.packages()) ) {
  install.packages("tidyverse")
}
if( !("rvest" %in% installed.packages() )  ) {
  install.packages("rvest")
}

library( rvest )
Loading required package: xml2
library( tidyverse ) 
── Attaching packages ─────────────────────────────────────── tidyverse 1.3.0 ──
✓ ggplot2 3.3.2     ✓ purrr   0.3.4
✓ tibble  3.0.4     ✓ dplyr   1.0.2
✓ tidyr   1.1.2     ✓ stringr 1.4.0
✓ readr   1.3.1     ✓ forcats 0.5.0
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
x dplyr::filter()         masks stats::filter()
x readr::guess_encoding() masks rvest::guess_encoding()
x dplyr::lag()            masks stats::lag()
x purrr::pluck()          masks rvest::pluck()

To start, grab the URL of the page you are intending to scrape. This one is a pretty straight forward structure.

USDA Website

url <- "https://www.nrcs.usda.gov/wps/portal/nrcs/detail/?cid=nrcs143_013697" 
page <- read_html(url)
page
{html_document}
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
[1] <head>\n<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">\n< ...
[2] <body style="">\t\t \t\t           \r\n<a href="#actualContent" accesskey ...

Now we can use some tidy methods to get to the components. We will go through the nodes and grab the “body” component then show the children in the body.

page %>% 
  html_node("body") %>%
  html_children()
{xml_nodeset (2)}
[1] <a href="#actualContent" accesskey="c" style="position:absolute;color:#9B ...
[2] <table id="wptheme_pageArea" summary="This table is used for page layout" ...

The page has broken up into compoennts, of which we can search. To figure out what parts we are going to grab data from, we need to look at the raw code in the document. Your browser can do this and you’ll have to manually look through the components. Here is what this page looks like.

HTML Content

Here you notice that the table that contains the data we are interested in looking at has a class equal to data. We can grab all the table components from the page and look to see which one we are interested in.

page %>% 
  xml_find_all(".//table") 
{xml_nodeset (26)}
 [1] <table id="wptheme_pageArea" summary="This table is used for page layout ...
 [2] <table class="layoutRow" cellpadding="0" cellspacing="0" width="100%"><t ...
 [3] <table summary="This table is used for page layout" class="themetable" b ...
 [4] <table class="layoutRow" cellpadding="0" cellspacing="0" width="100%"><t ...
 [5] <table summary="This table is used for page layout" width="100%" border= ...
 [6] <table class="layoutRow" cellpadding="0" cellspacing="0" width="100%"><t ...
 [7] <table summary="This table is used for page layout" class="themetable" b ...
 [8] <table summary="This table is used for page layout" width="100%" border= ...
 [9] <table class="layoutRow" cellpadding="0" cellspacing="0" width="100%"><t ...
[10] <table summary="This table is used for page layout" class="themetable" b ...
[11] <table summary="This table is used for page layout" width="100%" border= ...
[12] <table summary="This table is used for page layout" width="100%" border= ...
[13] <table summary="This table is used for page layout" width="100%" border= ...
[14] <table summary="This table is used for page layout" width="100%" border= ...
[15] <table summary="This table is used for page layout" width="100%" border= ...
[16] <table summary="This table is used for page layout" width="100%" border= ...
[17] <table summary="This table is used for page layout" width="100%" border= ...
[18] <table summary="This table is used for page layout" class="themetable" b ...
[19] <table summary="This table is used for page layout" class="themetable" b ...
[20] <table summary="This table is used for page layout" width="100%" border= ...
...

There are 26 total tables on this page! But it looks like the first 20 do not contain the table of interest. Here are the last ones.

page %>% 
  xml_find_all(".//table") %>%
  tail()
{xml_nodeset (6)}
[1] <table border="0" cellpadding="6" cellspacing="1" class="data"><tbody>\n< ...
[2] <table class="layoutRow" cellpadding="0" cellspacing="0" width="100%"><tr ...
[3] <table summary="This table is used for page layout" class="themetable" bo ...
[4] <table summary="This table is used for page layout" class="themetable" bo ...
[5] <table summary="This table is used for page layout" width="100%" border=" ...
[6] <table summary="This table is used for page layout" class="themetable" bo ...

And we see that the data table is the one with class="data", that differentiates it from the rest. We can use this to pull out just the table with the attribute class = "data" using xml search techniques. The syntax is a bit odd as it is based upon xml search syntax but you can get the notion.

page %>% 
  xml_find_all(".//table") %>% 
  xml_find_all("//table[contains(@class, 'data')]")
{xml_nodeset (1)}
[1] <table border="0" cellpadding="6" cellspacing="1" class="data"><tbody>\n< ...

Now we can turn that table into a data.frame and format the columns appropriately2

page %>% 
  xml_find_all(".//table") %>% 
  xml_find_all("//table[contains(@class, 'data')]") %>% 
  html_table( fill = TRUE ) %>% 
  .[[1]] %>%
  mutate( Name = factor(Name), State = factor(State) ) -> raw_data

summary( raw_data)
      FIPS               Name          State     
 Min.   : 1001   Washington:  32   TX     : 254  
 1st Qu.:19046   Jefferson :  26   GA     : 159  
 Median :30044   Franklin  :  25   VA     : 136  
 Mean   :31573   Jackson   :  24   KY     : 120  
 3rd Qu.:46136   Lincoln   :  24   MO     : 115  
 Max.   :78030   Madison   :  20   KS     : 105  
                 (Other)   :3081   (Other):2343  

PERFECT!! Now, to pull the parts that are relevant to us, the ones from Virginia.

raw_data %>% 
  filter( State == "VA" ) %>%
  select(Name, FIPS) %>%
  droplevels()

And there you go.


  1. Federal Information Processing Standards (FIPS), now known as Federal Information Processing Series, are numeric codes assigned by the National Institute of Standards and Technology (NIST). Typically, FIPS codes deal with US states and counties. US states are identified by a 2-digit number, while US counties are identified by a 3-digit number. For example, a FIPS code of 51159, represents Virginia (51-) and Richomnd City -159.↩︎

  2. The .[[1]] part is how we grab just the first row from the list that is returned.↩︎

LS0tCnRpdGxlOiAiU2NyYXBpbmcgV2Vic2l0ZXMiCm91dHB1dDogCiAgaHRtbF9ub3RlYm9vawotLS0KClRoZXJlIGFyZSB0aW1lcyB3aGVuIHdlIG5lZWQgdG8gc2NyYXBlIHNvbWUgZGF0YSBmcm9tIGEgd2Vic2l0ZSB0byB1c2UgaW4gb3VyIGFuYWx5c2VzLiAgSW4gYSBwZXJmZWN0IHdvcmxkLCB0aGUgcHJvdmlkZXIgb2YgdGhlIGRhdGEgd291bGQgcHJvdmlkZSBhIGNzdiBvciBqc29uIGRvd25sb2FkIGJ1dCBsZXQncyBmYWNlIGl0Li4uIHdlIGRvIG5vdCBsaXZlIGluIGEgcGVyZmVjdCB3b3JsZCBhbmQgb2Z0ZW4gZGF0YSB0aGF0IGlzIHBvc3RlZCB0byB0aGUgaW50ZXJuZXQgaXMgZG9uZSBieSBwZW9wbGUgd2hvIHJlYWxseSBkbyBub3QgY2FyZSBhYm91dCBzdWJzZXFlbnQgdXNlIGJ5IG90aGVycyAoZWxzZSB0aGV5IHdvdWxkIGhhdmUgbWFkZSBpdCBlYXN5IHRvIHVzZSByYXRoZXIgdGhhbiBqdXN0IHNob3dpbmcgaXQpLgoKSGVyZSBpcyBhIHF1aWNrIHR1dG9yaWFsIG9uIG9uZSBtZXRob2QgSSB1c2UgdG8gZG8gdGhpcy4KCgojIyBOZWNlc3NhcnkgUmVzb3VyY2VzCgpGb3IgdGhpcyBleGFtcGxlLCBJJ20gZ29pbmcgdG8gdXNlIHRoZSBgcnZlc3RgIGFuZCBgZHBseXJgIGxpYnJhcmllcyB0byBzY3JhcGUgYSBVU0RBIHBhZ2UgZm9yIGNvdW50eSBGSVBTIGNvZGVzW14xXSBmb3IgYWxsIHRoZSBjb3VudGllcyBpbiBWaXJnaW5pYS4gICAKCmBgYHtyfQppZiggISgiZHBseXIiICVpbiUgaW5zdGFsbGVkLnBhY2thZ2VzKCkpICkgewogIGluc3RhbGwucGFja2FnZXMoInRpZHl2ZXJzZSIpCn0KaWYoICEoInJ2ZXN0IiAlaW4lIGluc3RhbGxlZC5wYWNrYWdlcygpICkgICkgewogIGluc3RhbGwucGFja2FnZXMoInJ2ZXN0IikKfQoKbGlicmFyeSggcnZlc3QgKQpsaWJyYXJ5KCB0aWR5dmVyc2UgKSAKYGBgCgoKVG8gc3RhcnQsIGdyYWIgdGhlIFVSTCBvZiB0aGUgcGFnZSB5b3UgYXJlIGludGVuZGluZyB0byBzY3JhcGUuICBUaGlzIG9uZSBpcyBhIHByZXR0eSBzdHJhaWdodCBmb3J3YXJkIHN0cnVjdHVyZS4KCiFbVVNEQSBXZWJzaXRlXShodHRwczovL2xpdmUuc3RhdGljZmxpY2tyLmNvbS82NTUzNS81MDE3MzM1NTQ2Nl83NTMwNmJiNDU2X2NfZC5qcGcpCgpgYGB7cn0KdXJsIDwtICJodHRwczovL3d3dy5ucmNzLnVzZGEuZ292L3dwcy9wb3J0YWwvbnJjcy9kZXRhaWwvP2NpZD1ucmNzMTQzXzAxMzY5NyIgCnBhZ2UgPC0gcmVhZF9odG1sKHVybCkKcGFnZQpgYGAKCk5vdyB3ZSBjYW4gdXNlIHNvbWUgdGlkeSBtZXRob2RzIHRvIGdldCB0byB0aGUgY29tcG9uZW50cy4gIFdlIHdpbGwgZ28gdGhyb3VnaCB0aGUgbm9kZXMgYW5kIGdyYWIgdGhlICJib2R5IiBjb21wb25lbnQgdGhlbiBzaG93IHRoZSBjaGlsZHJlbiBpbiB0aGUgYm9keS4KCmBgYHtyfQpwYWdlICU+JSAKICBodG1sX25vZGUoImJvZHkiKSAlPiUKICBodG1sX2NoaWxkcmVuKCkKYGBgCgpUaGUgcGFnZSBoYXMgYnJva2VuIHVwIGludG8gY29tcG9lbm50cywgb2Ygd2hpY2ggd2UgY2FuIHNlYXJjaC4gIFRvIGZpZ3VyZSBvdXQgd2hhdCBwYXJ0cyB3ZSBhcmUgZ29pbmcgdG8gZ3JhYiBkYXRhIGZyb20sIHdlIG5lZWQgdG8gbG9vayBhdCB0aGUgcmF3IGNvZGUgaW4gdGhlIGRvY3VtZW50LiAgWW91ciBicm93c2VyIGNhbiBkbyB0aGlzIGFuZCB5b3UnbGwgaGF2ZSB0byBtYW51YWxseSBsb29rIHRocm91Z2ggdGhlIGNvbXBvbmVudHMuICBIZXJlIGlzIHdoYXQgdGhpcyBwYWdlIGxvb2tzIGxpa2UuCgohW0hUTUwgQ29udGVudF0oaHR0cHM6Ly9saXZlLnN0YXRpY2ZsaWNrci5jb20vNjU1MzUvNTAxNzI4OTc4NThfZGJlYzk0YzIwY19jX2QuanBnKQoKSGVyZSB5b3Ugbm90aWNlIHRoYXQgdGhlIHRhYmxlIHRoYXQgY29udGFpbnMgdGhlIGRhdGEgd2UgYXJlIGludGVyZXN0ZWQgaW4gbG9va2luZyBhdCBoYXMgYSBjbGFzcyBlcXVhbCB0byBgZGF0YWAuICBXZSBjYW4gZ3JhYiBhbGwgdGhlIGB0YWJsZWAgY29tcG9uZW50cyBmcm9tIHRoZSBwYWdlIGFuZCBsb29rIHRvIHNlZSB3aGljaCBvbmUgd2UgYXJlIGludGVyZXN0ZWQgaW4uCgpgYGB7cn0KcGFnZSAlPiUgCiAgeG1sX2ZpbmRfYWxsKCIuLy90YWJsZSIpIApgYGAKClRoZXJlIGFyZSAyNiB0b3RhbCB0YWJsZXMgb24gdGhpcyBwYWdlISAgQnV0IGl0IGxvb2tzIGxpa2UgdGhlIGZpcnN0IDIwIGRvIG5vdCBjb250YWluIHRoZSB0YWJsZSBvZiBpbnRlcmVzdC4gSGVyZSBhcmUgdGhlIGxhc3Qgb25lcy4KCmBgYHtyfQpwYWdlICU+JSAKICB4bWxfZmluZF9hbGwoIi4vL3RhYmxlIikgJT4lCiAgdGFpbCgpCmBgYAoKQW5kIHdlIHNlZSB0aGF0IHRoZSBkYXRhIHRhYmxlIGlzIHRoZSBvbmUgd2l0aCBgY2xhc3M9ImRhdGEiYCwgdGhhdCBkaWZmZXJlbnRpYXRlcyBpdCBmcm9tIHRoZSByZXN0LiAgV2UgY2FuIHVzZSB0aGlzIHRvIHB1bGwgb3V0IGp1c3QgdGhlIHRhYmxlIHdpdGggdGhlIGF0dHJpYnV0ZSBgY2xhc3MgPSAiZGF0YSJgIHVzaW5nIHhtbCBzZWFyY2ggdGVjaG5pcXVlcy4gIFRoZSBzeW50YXggaXMgYSBiaXQgb2RkIGFzIGl0IGlzIGJhc2VkIHVwb24geG1sIHNlYXJjaCBzeW50YXggYnV0IHlvdSBjYW4gZ2V0IHRoZSBub3Rpb24uCgpgYGB7cn0KcGFnZSAlPiUgCiAgeG1sX2ZpbmRfYWxsKCIuLy90YWJsZSIpICU+JSAKICB4bWxfZmluZF9hbGwoIi8vdGFibGVbY29udGFpbnMoQGNsYXNzLCAnZGF0YScpXSIpCmBgYAoKTm93IHdlIGNhbiB0dXJuIHRoYXQgdGFibGUgaW50byBhIGBkYXRhLmZyYW1lYCBhbmQgZm9ybWF0IHRoZSBjb2x1bW5zIGFwcHJvcHJpYXRlbHlbXjJdCgoKYGBge3J9CnBhZ2UgJT4lIAogIHhtbF9maW5kX2FsbCgiLi8vdGFibGUiKSAlPiUgCiAgeG1sX2ZpbmRfYWxsKCIvL3RhYmxlW2NvbnRhaW5zKEBjbGFzcywgJ2RhdGEnKV0iKSAlPiUgCiAgaHRtbF90YWJsZSggZmlsbCA9IFRSVUUgKSAlPiUgCiAgLltbMV1dICU+JQogIG11dGF0ZSggTmFtZSA9IGZhY3RvcihOYW1lKSwgU3RhdGUgPSBmYWN0b3IoU3RhdGUpICkgLT4gcmF3X2RhdGEKCnN1bW1hcnkoIHJhd19kYXRhKQpgYGAKCioqUEVSRkVDVCEhKiogIE5vdywgdG8gcHVsbCB0aGUgcGFydHMgdGhhdCBhcmUgcmVsZXZhbnQgdG8gdXMsIHRoZSBvbmVzIGZyb20gVmlyZ2luaWEuCgpgYGB7cn0KcmF3X2RhdGEgJT4lIAogIGZpbHRlciggU3RhdGUgPT0gIlZBIiApICU+JQogIHNlbGVjdChOYW1lLCBGSVBTKSAlPiUKICBkcm9wbGV2ZWxzKCkKYGBgCgpBbmQgdGhlcmUgeW91IGdvLgoKCgoKClteMV06IEZlZGVyYWwgSW5mb3JtYXRpb24gUHJvY2Vzc2luZyBTdGFuZGFyZHMgKEZJUFMpLCBub3cga25vd24gYXMgRmVkZXJhbCBJbmZvcm1hdGlvbiBQcm9jZXNzaW5nIFNlcmllcywgYXJlIG51bWVyaWMgY29kZXMgYXNzaWduZWQgYnkgdGhlIE5hdGlvbmFsIEluc3RpdHV0ZSBvZiBTdGFuZGFyZHMgYW5kIFRlY2hub2xvZ3kgKE5JU1QpLiBUeXBpY2FsbHksIEZJUFMgY29kZXMgZGVhbCB3aXRoIFVTIHN0YXRlcyBhbmQgY291bnRpZXMuIFVTIHN0YXRlcyBhcmUgaWRlbnRpZmllZCBieSBhIDItZGlnaXQgbnVtYmVyLCB3aGlsZSBVUyBjb3VudGllcyBhcmUgaWRlbnRpZmllZCBieSBhIDMtZGlnaXQgbnVtYmVyLiBGb3IgZXhhbXBsZSwgYSBGSVBTIGNvZGUgb2YgNTExNTksIHJlcHJlc2VudHMgVmlyZ2luaWEgKDUxLSkgYW5kIFJpY2hvbW5kIENpdHkgLTE1OS4KClteMl06IFRoZSBgLltbMV1dYCBwYXJ0IGlzIGhvdyB3ZSBncmFiIGp1c3QgdGhlIGZpcnN0IHJvdyBmcm9tIHRoZSBsaXN0IHRoYXQgaXMgcmV0dXJuZWQuIA==