Web scraping

One way to get data these days is via web scraping. That is, you write a computer program that automatically traverses a specific set of web pages that you know contain some type of data that you want. Your program needs to download these pages, parse them, and output a file with the data in some palatable format. This is quite common for sports data because so many news sites present scores and other statistics in a tabular format.

The format of our data

Let’s use this technique to gather the data from our personal data exercise. Recall that you entered the data in a somewhat specific format. For my daughter Audrey’s entry, it looked like so:

| Gender | Age |  Height  |
| --- | ---- | -------- |
| f |  8  | 4' 2'' |

You can read a bit more about typing tables in this post. Utilmately, though, this is not what our webscraper will actually see because the forum software reformats it to look like so:

<table>
  <thead>
    <tr>
      <th>Gender</th> <th>Age</th> <th>Height</th>
    </tr>
  </thead>
  <tbody>
    <tr>
     <td>f</td> <td>8</td> <td>4’2’’</td>
    </tr>
  </tbody>
</table>

This kind of code is called HTML and is exactly what your web browser needs to see to know how to format your input into a table. It also just so happens that there is an R function that can parse this kind of info directly to a Data Frame.

R functions for reading over the web

R has plenty of functions to read in formatted data. We’ll often use read.csv to read CSV files right off of my webspace. Functions to deal with other types of files are contained in libraries, so let’s load the libraries that we’ll need. Note that these libraries are not all part of the standard R installation so, if you want to try this yourself you might need to use the install.packages command. It’s not at all hard and you can read more about it here.

library(httr)      # To read data over the web
library(xml2)      # Required by the next library
library(rvest)     # To parse HTML
library(knitr)     # To format tables nicely

Using this new functionality, we can directly read and display part of my webpage:

input = content(GET('https://www.marksmath.org'), 'text', encoding='UTF-8')
cat(substring(input, 266, 770))

In fact, you could do this with any webpage. We’ll do something like this with our personal data question page.

Scraping our personal data page

The process of scraping our page of interest is takes us bit beyond the scope of this class. Nonetheless, here is the code. We’re going to define an empty Data Frame with columns named Gender, Age, and Height. Then, we’re going to read in all the posts on that page, step through them to extract out the data stored in the tables that you entered, and use that build up our classroom Data Frame.

topic_url = 'https://statdiscourse.marksmath.org/t/some-personal-data/15'
class_df = data.frame(Gender=character(), Age = integer(), Height = character())
json_in = content(GET(paste(topic_url, '.json', sep="")), as="parsed")
page = 0
while(page < ceiling(json_in$highest_post_number/20)) {
  page = page+1
  page_url = paste(topic_url, '.json?page=', toString(page), sep="")
  page_json = content(GET(page_url), as="parsed")
  posts = page_json$post_stream$posts
  if(page == 1 ) {
    start = 2
  } else {
    start = 1
  }
  for(i in start:length(posts)) {
    df = html_table(html_node(read_html(posts[[i]]$cooked), 'table'))
    class_df = rbind(class_df, df)
  }
}
kable(head(class_df))

OK, prett cool - but, let’s convert those heights to actual numbers.

ftin_to_feet = function(ftin_str) {
  spl = strsplit(ftin_str, split="’")[[1]]
  ft = as.numeric(spl[1])
  inch = as.numeric(spl[2])
  if(is.numeric(ft) & !is.na(ft) & is.numeric(inch) & !is.na(inch)) {
    return(ft + inch/12)
  }
  else {
    return(0)
  }
}
class_df$Height = as.numeric(lapply(class_df$Height, ftin_to_feet))
kable(head(class_df))

Awesome!

Finally, the CSV file that we actually read to easily work with this data is created like so:

write.csv(class_df, 'class_data_Fall2017.csv', row.names = F)

It’s worth mentioning that our class outline is generated using R. For example, a histogram for height can be generated as follows:

hist(subset(class_df, Age>10)$Height, col = 'gray', main='',
  xlab = 'Height'
)

LS0tCnRpdGxlOiAiU2NyYXBpbmcgb3VyIGNsYXNzcm9vbSBkYXRhIgpvdXRwdXQ6CiAgaHRtbF9kb2N1bWVudDogZGVmYXVsdAogIGh0bWxfbm90ZWJvb2s6IGRlZmF1bHQKLS0tCgojIyBXZWIgc2NyYXBpbmcKCk9uZSB3YXkgdG8gZ2V0IGRhdGEgdGhlc2UgZGF5cyBpcyB2aWEgW3dlYiBzY3JhcGluZ10oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvV2ViX3NjcmFwaW5nKS4gVGhhdCBpcywgeW91IHdyaXRlIGEgY29tcHV0ZXIgcHJvZ3JhbSB0aGF0IGF1dG9tYXRpY2FsbHkgdHJhdmVyc2VzIGEgc3BlY2lmaWMgc2V0IG9mIHdlYiBwYWdlcyB0aGF0IHlvdSBrbm93IGNvbnRhaW4gc29tZSB0eXBlIG9mIGRhdGEgdGhhdCB5b3Ugd2FudC4gWW91ciBwcm9ncmFtIG5lZWRzIHRvIGRvd25sb2FkIHRoZXNlIHBhZ2VzLCBwYXJzZSB0aGVtLCBhbmQgb3V0cHV0IGEgZmlsZSB3aXRoIHRoZSBkYXRhIGluIHNvbWUgcGFsYXRhYmxlIGZvcm1hdC4gVGhpcyBpcyBxdWl0ZSBjb21tb24gZm9yIHNwb3J0cyBkYXRhIGJlY2F1c2Ugc28gbWFueSBuZXdzIHNpdGVzIHByZXNlbnQgc2NvcmVzIGFuZCBvdGhlciBzdGF0aXN0aWNzIGluIGEgdGFidWxhciBmb3JtYXQuCgojIyMgVGhlIGZvcm1hdCBvZiBvdXIgZGF0YQoKTGV0J3MgdXNlIHRoaXMgdGVjaG5pcXVlIHRvIGdhdGhlciB0aGUgZGF0YSBmcm9tIG91ciBbcGVyc29uYWwgZGF0YSBleGVyY2lzZV0oaHR0cHM6Ly9zdGF0ZGlzY291cnNlLm1hcmtzbWF0aC5vcmcvdC9zb21lLXBlcnNvbmFsLWRhdGEvMTUpLiBSZWNhbGwgdGhhdCB5b3UgZW50ZXJlZCB0aGUgZGF0YSBpbiBhIHNvbWV3aGF0IHNwZWNpZmljIGZvcm1hdC4gRm9yIG15IGRhdWdodGVyIEF1ZHJleSdzIGVudHJ5LCBpdCBsb29rZWQgbGlrZSBzbzoKCiAgICB8IEdlbmRlciB8IEFnZSB8ICBIZWlnaHQgIHwKICAgIHwgLS0tIHwgLS0tLSB8IC0tLS0tLS0tIHwKICAgIHwgZiB8ICA4ICB8IDQnIDInJyB8CgpZb3UgY2FuIHJlYWQgYSBiaXQgbW9yZSBhYm91dCB0eXBpbmcgdGFibGVzIGluIFt0aGlzIHBvc3RdKGh0dHBzOi8vc3RhdGRpc2NvdXJzZS5tYXJrc21hdGgub3JnL3QvaG93LWRvLWktZm9ybWF0LW15LXBvc3RzLWFuZC1lbnRlci10YWJsZXMtY29kZS1hbmQtZ3Jvb3Z5LXR5cGVzZXQtbWF0aGVtYXRpY3MvMTkvMikuIFV0aWxtYXRlbHksIHRob3VnaCwgdGhpcyBpcyBub3Qgd2hhdCBvdXIgd2Vic2NyYXBlciB3aWxsIGFjdHVhbGx5IHNlZSBiZWNhdXNlIHRoZSBmb3J1bSBzb2Z0d2FyZSByZWZvcm1hdHMgaXQgdG8gbG9vayBsaWtlIHNvOgoKICAgIDx0YWJsZT4KICAgICAgPHRoZWFkPgogICAgICAgIDx0cj4KICAgICAgICAgIDx0aD5HZW5kZXI8L3RoPiA8dGg+QWdlPC90aD4gPHRoPkhlaWdodDwvdGg+CiAgICAgICAgPC90cj4KICAgICAgPC90aGVhZD4KICAgICAgPHRib2R5PgogICAgICAgIDx0cj4KICAgICAgICAgPHRkPmY8L3RkPiA8dGQ+ODwvdGQ+IDx0ZD404oCZMuKAmeKAmTwvdGQ+CiAgICAgICAgPC90cj4KICAgICAgPC90Ym9keT4KICAgIDwvdGFibGU+CgpUaGlzIGtpbmQgb2YgY29kZSBpcyBjYWxsZWQgSFRNTCBhbmQgaXMgZXhhY3RseSB3aGF0IHlvdXIgd2ViIGJyb3dzZXIgbmVlZHMgdG8gc2VlIHRvIGtub3cgaG93IHRvIGZvcm1hdCB5b3VyIGlucHV0IGludG8gYSB0YWJsZS4gSXQgYWxzbyBqdXN0IHNvIGhhcHBlbnMgdGhhdCB0aGVyZSBpcyBhbiBSIGZ1bmN0aW9uIHRoYXQgY2FuIHBhcnNlIHRoaXMga2luZCBvZiBpbmZvIGRpcmVjdGx5IHRvIGEgRGF0YSBGcmFtZS4KCiMjIyBSIGZ1bmN0aW9ucyBmb3IgcmVhZGluZyBvdmVyIHRoZSB3ZWIKClIgaGFzIHBsZW50eSBvZiBmdW5jdGlvbnMgdG8gcmVhZCBpbiBmb3JtYXR0ZWQgZGF0YS4gV2UnbGwgb2Z0ZW4gdXNlIGByZWFkLmNzdmAgdG8gcmVhZCBDU1YgZmlsZXMgcmlnaHQgb2ZmIG9mIG15IHdlYnNwYWNlLiBGdW5jdGlvbnMgdG8gZGVhbCB3aXRoIG90aGVyIHR5cGVzIG9mIGZpbGVzIGFyZSBjb250YWluZWQgaW4gbGlicmFyaWVzLCBzbyBsZXQncyBsb2FkIHRoZSBsaWJyYXJpZXMgdGhhdCB3ZSdsbCBuZWVkLiBOb3RlIHRoYXQgdGhlc2UgbGlicmFyaWVzIGFyZSBub3QgYWxsIHBhcnQgb2YgdGhlIHN0YW5kYXJkIFIgaW5zdGFsbGF0aW9uIHNvLCBpZiB5b3Ugd2FudCB0byB0cnkgdGhpcyB5b3Vyc2VsZiB5b3UgbWlnaHQgbmVlZCB0byB1c2UgdGhlIGBpbnN0YWxsLnBhY2thZ2VzYCBjb21tYW5kLiBJdCdzIG5vdCBhdCBhbGwgaGFyZCBhbmQgeW91IGNhbiBbcmVhZCBtb3JlIGFib3V0IGl0IGhlcmVdKGh0dHBzOi8vd3d3LnItYmxvZ2dlcnMuY29tL2luc3RhbGxpbmctci1wYWNrYWdlcy8pLgpgYGB7cn0KbGlicmFyeShodHRyKSAgICAgICMgVG8gcmVhZCBkYXRhIG92ZXIgdGhlIHdlYgpsaWJyYXJ5KHhtbDIpICAgICAgIyBSZXF1aXJlZCBieSB0aGUgbmV4dCBsaWJyYXJ5CmxpYnJhcnkocnZlc3QpICAgICAjIFRvIHBhcnNlIEhUTUwKbGlicmFyeShrbml0cikgICAgICMgVG8gZm9ybWF0IHRhYmxlcyBuaWNlbHkKYGBgCgpVc2luZyB0aGlzIG5ldyBmdW5jdGlvbmFsaXR5LCB3ZSBjYW4gZGlyZWN0bHkgcmVhZCBhbmQgZGlzcGxheSBwYXJ0IG9mIG15IHdlYnBhZ2U6CgpgYGB7cn0KaW5wdXQgPSBjb250ZW50KEdFVCgnaHR0cHM6Ly93d3cubWFya3NtYXRoLm9yZycpLCAndGV4dCcsIGVuY29kaW5nPSdVVEYtOCcpCmNhdChzdWJzdHJpbmcoaW5wdXQsIDI2NiwgNzcwKSkKYGBgCgpJbiBmYWN0LCB5b3UgY291bGQgZG8gdGhpcyB3aXRoIGFueSB3ZWJwYWdlLiBXZSdsbCBkbyBzb21ldGhpbmcgbGlrZSB0aGlzIHdpdGggW291ciBwZXJzb25hbCBkYXRhIHF1ZXN0aW9uIHBhZ2VdKGh0dHBzOi8vc3RhdGRpc2NvdXJzZS5tYXJrc21hdGgub3JnL3Qvc29tZS1wZXJzb25hbC1kYXRhLzE1KS4KCiMjIyBTY3JhcGluZyBvdXIgcGVyc29uYWwgZGF0YSBwYWdlCgpUaGUgcHJvY2VzcyBvZiBzY3JhcGluZyBvdXIgcGFnZSBvZiBpbnRlcmVzdCBpcyB0YWtlcyB1cyBiaXQgYmV5b25kIHRoZSBzY29wZSBvZiB0aGlzIGNsYXNzLiBOb25ldGhlbGVzcywgaGVyZSBpcyB0aGUgY29kZS4gV2UncmUgZ29pbmcgdG8gZGVmaW5lIGFuIGVtcHR5IERhdGEgRnJhbWUgd2l0aCBjb2x1bW5zIG5hbWVkIGBHZW5kZXJgLCBgQWdlYCwgYW5kIGBIZWlnaHRgLiBUaGVuLCB3ZSdyZSBnb2luZyB0byByZWFkIGluIGFsbCB0aGUgcG9zdHMgb24gdGhhdCBwYWdlLCBzdGVwIHRocm91Z2ggdGhlbSB0byBleHRyYWN0IG91dCB0aGUgZGF0YSBzdG9yZWQgaW4gdGhlIHRhYmxlcyB0aGF0IHlvdSBlbnRlcmVkLCBhbmQgdXNlIHRoYXQgYnVpbGQgdXAgb3VyIGNsYXNzcm9vbSBEYXRhIEZyYW1lLgoKYGBge3J9CnRvcGljX3VybCA9ICdodHRwczovL3N0YXRkaXNjb3Vyc2UubWFya3NtYXRoLm9yZy90L3NvbWUtcGVyc29uYWwtZGF0YS8xNScKY2xhc3NfZGYgPSBkYXRhLmZyYW1lKEdlbmRlcj1jaGFyYWN0ZXIoKSwgQWdlID0gaW50ZWdlcigpLCBIZWlnaHQgPSBjaGFyYWN0ZXIoKSkKanNvbl9pbiA9IGNvbnRlbnQoR0VUKHBhc3RlKHRvcGljX3VybCwgJy5qc29uJywgc2VwPSIiKSksIGFzPSJwYXJzZWQiKQpwYWdlID0gMAp3aGlsZShwYWdlIDwgY2VpbGluZyhqc29uX2luJGhpZ2hlc3RfcG9zdF9udW1iZXIvMjApKSB7CiAgcGFnZSA9IHBhZ2UrMQogIHBhZ2VfdXJsID0gcGFzdGUodG9waWNfdXJsLCAnLmpzb24/cGFnZT0nLCB0b1N0cmluZyhwYWdlKSwgc2VwPSIiKQogIHBhZ2VfanNvbiA9IGNvbnRlbnQoR0VUKHBhZ2VfdXJsKSwgYXM9InBhcnNlZCIpCiAgcG9zdHMgPSBwYWdlX2pzb24kcG9zdF9zdHJlYW0kcG9zdHMKICBpZihwYWdlID09IDEgKSB7CiAgICBzdGFydCA9IDIKICB9IGVsc2UgewogICAgc3RhcnQgPSAxCiAgfQogIGZvcihpIGluIHN0YXJ0Omxlbmd0aChwb3N0cykpIHsKICAgIGRmID0gaHRtbF90YWJsZShodG1sX25vZGUocmVhZF9odG1sKHBvc3RzW1tpXV0kY29va2VkKSwgJ3RhYmxlJykpCiAgICBjbGFzc19kZiA9IHJiaW5kKGNsYXNzX2RmLCBkZikKICB9Cn0Ka2FibGUoaGVhZChjbGFzc19kZikpCmBgYAoKT0ssIHByZXR0IGNvb2wgLSBidXQsIGxldCdzIGNvbnZlcnQgdGhvc2UgaGVpZ2h0cyB0byBhY3R1YWwgbnVtYmVycy4KCmBgYHtyfQpmdGluX3RvX2ZlZXQgPSBmdW5jdGlvbihmdGluX3N0cikgewogIHNwbCA9IHN0cnNwbGl0KGZ0aW5fc3RyLCBzcGxpdD0i4oCZIilbWzFdXQogIGZ0ID0gYXMubnVtZXJpYyhzcGxbMV0pCiAgaW5jaCA9IGFzLm51bWVyaWMoc3BsWzJdKQogIGlmKGlzLm51bWVyaWMoZnQpICYgIWlzLm5hKGZ0KSAmIGlzLm51bWVyaWMoaW5jaCkgJiAhaXMubmEoaW5jaCkpIHsKICAgIHJldHVybihmdCArIGluY2gvMTIpCiAgfQogIGVsc2UgewogICAgcmV0dXJuKDApCiAgfQp9CmNsYXNzX2RmJEhlaWdodCA9IGFzLm51bWVyaWMobGFwcGx5KGNsYXNzX2RmJEhlaWdodCwgZnRpbl90b19mZWV0KSkKa2FibGUoaGVhZChjbGFzc19kZikpCmBgYAoKQXdlc29tZSEgCgpGaW5hbGx5LCB0aGUgQ1NWIGZpbGUgdGhhdCB3ZSBhY3R1YWxseSByZWFkIHRvIGVhc2lseSB3b3JrIHdpdGggdGhpcyBkYXRhIGlzIGNyZWF0ZWQgbGlrZSBzbzoKCmBgYHtyfQp3cml0ZS5jc3YoY2xhc3NfZGYsICdjbGFzc19kYXRhX0ZhbGwyMDE3LmNzdicsIHJvdy5uYW1lcyA9IEYpCmBgYAoKSXQncyB3b3J0aCBtZW50aW9uaW5nIHRoYXQgb3VyIGNsYXNzIG91dGxpbmUgaXMgZ2VuZXJhdGVkIHVzaW5nIFIuIEZvciBleGFtcGxlLCBhIGhpc3RvZ3JhbSBmb3IgaGVpZ2h0IGNhbiBiZSBnZW5lcmF0ZWQgYXMgZm9sbG93czoKCmBgYHtyfQpoaXN0KHN1YnNldChjbGFzc19kZiwgQWdlPjEwKSRIZWlnaHQsIGNvbCA9ICdncmF5JywgbWFpbj0nJywKICB4bGFiID0gJ0hlaWdodCcKKQpgYGAKCi0tLQ==