Week 7 Assignment: Working with HTML and JSON

Author

Supriya P.

Published

October 18, 2026

Introduction

For this assignment, I chose three books on learning R and statistics: R for Data Science, OpenIntro Statistics, and Hands-On Programming with R. Two of the three have multiple authors, and two authors (Mine Çetinkaya-Rundel and Garrett Grolemund) each appear on more than one book. For each book, I recorded the title, authors, edition, publication year, and publisher.

I wrote both data files by hand using Claude AI as a guide and saved them in my GitHub repository:

  • books.html with the data in an HTML table
  • books.json with the same data as a JSON array of book objects

The goal is to load each file into its own data frame and see whether the two come out the same.

Loading the HTML File

Code
library(tidyverse)
library(rvest)
library(jsonlite)
library(knitr)

html_url <- "https://raw.githubusercontent.com/0pree/symmetrical-robot/refs/heads/main/books.html"

books_html <- read_html(html_url) %>%
  html_element("table") %>%
  html_table()

kable(books_html)
title authors edition year publisher
R for Data Science Hadley Wickham, Mine Çetinkaya-Rundel, Garrett Grolemund 2 2023 O’Reilly Media
OpenIntro Statistics David Diez, Mine Çetinkaya-Rundel, Christopher Barr 4 2019 OpenIntro
Hands-On Programming with R Garrett Grolemund 1 2014 O’Reilly Media
Code
str(books_html)
tibble [3 × 5] (S3: tbl_df/tbl/data.frame)
 $ title    : chr [1:3] "R for Data Science" "OpenIntro Statistics" "Hands-On Programming with R"
 $ authors  : chr [1:3] "Hadley Wickham, Mine Çetinkaya-Rundel, Garrett Grolemund" "David Diez, Mine Çetinkaya-Rundel, Christopher Barr" "Garrett Grolemund"
 $ edition  : int [1:3] 2 4 1
 $ year     : int [1:3] 2023 2019 2014
 $ publisher: chr [1:3] "O'Reilly Media" "OpenIntro" "O'Reilly Media"

read_html() reads the page, html_element("table") finds the table, and html_table() turns it into a data frame, using the header row as the column names. Even though everything in an HTML file is text, html_table() guessed that the edition and year columns are numbers and converted them to integers. Because a table cell can only hold one value, the authors for each book are stored together in a single text string separated by commas.

Loading the JSON File

Code
json_url <- "https://raw.githubusercontent.com/0pree/symmetrical-robot/refs/heads/main/books.json"

books_json <- fromJSON(json_url)

books_json
                        title
1          R for Data Science
2        OpenIntro Statistics
3 Hands-On Programming with R
                                                   authors edition year
1 Hadley Wickham, Mine Çetinkaya-Rundel, Garrett Grolemund       2 2023
2      David Diez, Mine Çetinkaya-Rundel, Christopher Barr       4 2019
3                                        Garrett Grolemund       1 2014
       publisher
1 O'Reilly Media
2      OpenIntro
3 O'Reilly Media
Code
str(books_json)
'data.frame':   3 obs. of  5 variables:
 $ title    : chr  "R for Data Science" "OpenIntro Statistics" "Hands-On Programming with R"
 $ authors  :List of 3
  ..$ : chr  "Hadley Wickham" "Mine Çetinkaya-Rundel" "Garrett Grolemund"
  ..$ : chr  "David Diez" "Mine Çetinkaya-Rundel" "Christopher Barr"
  ..$ : chr "Garrett Grolemund"
 $ edition  : int  2 4 1
 $ year     : int  2023 2019 2014
 $ publisher: chr  "O'Reilly Media" "OpenIntro" "O'Reilly Media"

fromJSON() converts the array of book objects into a data frame with one row per book. In the JSON file I wrote the edition and year as numbers without quotes, so they load as integers here too. The main difference is the authors column. In the JSON file each book’s authors are stored as an array, so R loads that column as a list, where each row holds a vector of one to three names.

Comparing the Two Data Frames

Code
identical(books_html, books_json)
[1] FALSE
Code
all.equal(books_html, books_json)
[1] "Attributes: < Component \"class\": Lengths (3, 1) differ (string compare on first 1) >"
[2] "Attributes: < Component \"class\": 1 string mismatch >"                                
[3] "Component \"authors\": Modes: character, list"                                         
[4] "Component \"authors\": target is character, current is list"                           

The two data frames are not identical. all.equal() shows two reasons:

  • Class: html_table() returns a tibble, while fromJSON() returns a base R data frame.
  • Authors: the HTML version stores authors as one text string per book, while the JSON version stores them as a list.

The titles, editions, years, and publishers all match so values are the same. The differences come from the way each format stores the data.

Making the Two Match

To check that the content really is the same, I converted the JSON version to a tibble and combined each book’s authors into one comma separated string, the same way they appear in the HTML table.

Code
books_json_flat <- books_json %>%
  as_tibble() %>%
  mutate(authors = map_chr(authors, ~ paste(.x, collapse = ", ")))

identical(books_html, books_json_flat)
[1] TRUE

After these two changes, the data frames are identical.

One Row per Author

Joining the authors into one string makes the two versions match but it isn’t the most useful format for analysis. The JSON structure keeps each author as a separate value which makes it easy to reshape the data so each row is one book and one author.

Code
books_by_author <- books_json %>%
  as_tibble() %>%
  unnest_longer(authors) %>%
  rename(author = authors)

kable(books_by_author)
title author edition year publisher
R for Data Science Hadley Wickham 2 2023 O’Reilly Media
R for Data Science Mine Çetinkaya-Rundel 2 2023 O’Reilly Media
R for Data Science Garrett Grolemund 2 2023 O’Reilly Media
OpenIntro Statistics David Diez 4 2019 OpenIntro
OpenIntro Statistics Mine Çetinkaya-Rundel 4 2019 OpenIntro
OpenIntro Statistics Christopher Barr 4 2019 OpenIntro
Hands-On Programming with R Garrett Grolemund 1 2014 O’Reilly Media
Code
books_by_author %>%
  count(author, name = "books") %>%
  arrange(desc(books)) %>%
  kable()
author books
Garrett Grolemund 2
Mine Çetinkaya-Rundel 2
Christopher Barr 1
David Diez 1
Hadley Wickham 1

This shows that Çetinkaya-Rundel and Grolemund each wrote two of the three books. Getting the same result from the HTML version would mean splitting the authors text on the commas first.

Conclusions

Both files held the same information but they did not load into identical data frames. The values matched but the structure differed in two ways: the HTML table loaded as a tibble with authors stored as text and the JSON file loaded as a base data frame with authors stored as a list. Once I matched the class and the authors format, the two were identical.

Writing the files by hand also demonstrated a difference between the formats. HTML tables allow a field with several values to be squeezed into one cell. On the other hand, JSON can nest a list inside a record, which keeps multiple authors separate and makes the data easier to reshape later.

AI Citation

Anthropic. (2026). Sonnet 5.5 [Large language model]. https://claude.ai. Accessed October 2026.