Я запускаю Map_dfr на наборе веб-страниц, чтобы извлечь различные элементы, которые будут объединены в фрейм данных. Я привел простой пример, чтобы показать проблему, просматривая всего две страницы. На первой странице из двух есть таблица «защиты», но ее нет на второй странице цикла. (Для обоих есть таблица нарушений – к вашему сведению)
Я искал, что сделать, чтобы цикл не прерывался для любых страниц, которые не включают этот элемент (или другие элементы, но я просто использовал этот для примера). Появилась функция «Возможно», поэтому я включил ее в свою попытку ниже. Кажется, проблема в том, что код создает «na» для таблицы защиты, которой не существует в итерации № 2, но когда приходит время добавить «защиту» в виде столбца внизу, это выдает ошибку, указанную внизу. моего кода. Я думаю, это как-то связано с тем, что столбец является вложенной таблицей, но я не уверен.
Моя цель — запустить этот цикл, сохранив вложенную таблицу защиты в виде столбца и просто сделав любую страницу, которая не содержит эту таблицу, «NA» или пустой.
Предпочтительно решение DPLYR, но открыто все, что работает.
library(tidyverse)
library(rvest)
library(RSelenium)
library(netstat)
rs_driver_object <- rsDriver(browser = "firefox",
verbose = F,
chromever = NULL,
port = free_port())
remDr <- rs_driver_object$client
games <- c("https://www.pro-football-reference.com/boxscores/197301140mia.htm",
"https://www.pro-football-reference.com/boxscores/196010230was.htm")
remDr <- rs_driver_object$client
test_df <-
map_dfr(games,
function(game_pull){
Sys.sleep(3)
remDr$navigate(game_pull)
x <- remDr$getPageSource() %>% unlist()
page <- read_html(x)
szn <-
page |>
html_elements(xpath = "//*[@class='hoversmooth']") |>
html_text2() |>
parse_number()
offense <- map_dfr(page,
possibly(~
page |>
html_elements(xpath = "//*[@id='all_player_offense']") |>
html_table() |>
as.data.frame() |>
janitor::row_to_names(row_number = 1) |>
janitor::clean_names() |>
nest(),
otherwise = "na"
))
defense <- map_dfr(page,
possibly(~
page |>
html_elements(xpath = "//*[@id='all_player_defense']") |>
html_table() |>
as.data.frame() |>
janitor::row_to_names(row_number = 1) |>
janitor::clean_names() |>
nest(),
otherwise = "na"
))
df <- page |>
html_elements(xpath = "//table[@class='linescore nohover stats_table no_freeze']") |>
html_table() |>
as.data.frame() |>
setNames(c("trash", "team", 'q1', "q2", "q3", "q4", "final")) |>
mutate(offense = offense,
defense = defense)
df
})
Error in `map()`:
ℹ In index: 2.
Caused by error in `mutate()`:
ℹ In argument: `defense = defense`.
Caused by error:
! `defense` must be size 2 or 1, not 0.
К сожалению, данные на этом сайте организованы довольно непоследовательно. Похоже, ваша основная проблема заключается в том, что вы пытаетесь заполнить NA при возникновении ошибки, но rvest не всегда выдает ошибку, когда данные не найдены, а иногда вместо этого возвращает NULL. Если бы я попытался очистить сайт, я мог бы использовать что-то вроде приведенного ниже, где есть реальная (неанонимная) функция, которая определена и затем может быть применена к каждому URL-адресу.
Вам также действительно не нужен RSelenium для очистки этого сайта, поскольку данные загружаются вместе со страницей, вместо того, чтобы делать что-то необычное с Javascript за кулисами, поэтому я удалил это, чтобы изолировать саму проблему.
library(rvest)
library(dplyr)
getGameData <- function(url){
page <- read_html(url)
szn <- page |>
html_elements(xpath = "//*[@class='hoversmooth']/li[2]") |>
html_text2() |>
readr::parse_number()
game_data <- page %>%
html_elements(xpath = "//table[@class='linescore nohover stats_table no_freeze']") %>%
html_table() %>%
as.data.frame() %>%
setNames(c("trash", "team", 'q1', "q2", "q3", "q4", "final")) %>%
select(team:final)
offense_elem <- html_element(page, xpath = "//*[@id='all_player_offense']")
if (is(offense_elem, "xml_node")){
offense_data <- html_table(offense_elem, header = FALSE) %>%
filter(X3! = "Passing") %>%
filter(X1! = "Player") %>%
setNames(c("Player", "Tm", "Cmp", "Att", "Yds", "TD", "Int", "Sk", "Yds",
"Lng", "Rate", "Att", "Yds", "TD", "Lng", "Rec", "Yds", "TD",
"Lng")) %>%
split(.$Tm)
} else {
offense_data <- list(NA, NA)
}
game_data$offense <- offense_data
defense_elem <- html_element(page, xpath = "//*[@id='all_player_defense']")
if (is(defense_elem, "xml_node")){
defense_table <- html_table(defense_elem, header = FALSE)
if (nrow(defense_table)>0){
defense_data <- defense_table %>%
filter(X3! = "Passing") %>%
filter(X1! = "Player") %>%
setNames(c("Player", "Tm", "Cmp", "Att", "Yds", "TD", "Int", "Sk", "Yds",
"Lng", "Rate", "Att", "Yds", "TD", "Lng", "Rec", "Yds", "TD",
"Lng")) %>%
split(.$Tm)
} else {
defense_data <- list(NA, NA)
}
} else {
defense_data <- list(NA, NA)
}
game_data$defense <- defense_data
return(game_data)
}
games <- c("https://www.pro-football-reference.com/boxscores/197301140mia.htm",
"https://www.pro-football-reference.com/boxscores/196010230was.htm")
all_game_data <- lapply(games, getGameData) %>%
bind_rows()
С помощью полной функции мы можем определить лучшую логику обработки ошибок, когда узел не существует (class="xml_missing" вместо "xml_node"), а также когда узел существует, но возвращает пустую таблицу.
К сожалению, похоже, что на обеих страницах, которыми вы поделились, отсутствовал узел all_player_defense
, поэтому мне пришлось немного догадаться об ожидаемом формате.
@dubakay Надеюсь, я попробую это когда-нибудь сегодня, но прежде я просто хочу поблагодарить вас за приложенные усилия. Я думал, что оператор if else может быть решением! Заранее спасибо.
Итак, я думаю, что Reselenium необходим, потому что таблица all_player_defense является идентификатором, но если вы проверите html, вы увидите все это в форме комментариев (раздражает), поэтому я использовал проверку страницы для этого. Тем не менее, я собираюсь адаптировать это к своему коду rselen и посмотреть, что получится.
Насколько я понимаю, прокомментированный код был просто заполнителем для фактических значений, которые были бы там, если бы это было так, но я не провел полную проверку, необходимую, чтобы выяснить это, так что дайте мне знать, если это неправильно!
Привет @dubukay, поэтому даже для страницы, на которой есть таблица статистики защиты, html_element не идентифицирует узел xml. Когда я запускаю вашу функцию на обеих итерациях, она работает, но дает NA для статистики защиты даже для первой страницы, на которой явно есть эта таблица. Вы можете проверить HTML-код в браузере и увидеть, что он имеет идентификатор «player_defense». Но read_html(page, xpath = "//*[@id='player_defense']" ничего не возвращает.
Вы запускаете то же самое с player_offense, и он его подхватывает. Я не понимаю, почему он не идентифицирует этот узел для таблицы защиты, но мне кажется, что это как-то связано с тем фактом, что прямо на html-странице это выглядит так, будто таблица защиты существует в виде комментария (оскорбление не ).
purrr::possibly()
отлично подходит для обработки ошибок в таких случаях, и его значение по умолчанию может быть более совместимым с желаемым выходным значением;comment()
;map_dfr()
был заменен в purrr
1.0;left_join()
было бы немного более надежным для объединения кадров, но, видимо, построить надежную справочную таблицу несколько сложнее; отредактировано для использования bind_cols()
вместо этого.Функции разбора:
library(dplyr, warn.conflicts = FALSE)
library(rvest, warn.conflicts = FALSE)
library(readr, warn.conflicts = FALSE)
library(janitor, warn.conflicts = FALSE)
library(stringr)
library(purrr)
# hide type_convert() spec message
options(readr.num_columns = 0)
# parse box score tables, return nested tibble,
# list column name is set by `key`
# tm offense
# <chr> <list<tibble[,18]>>
# 1 PIT [7 × 18]
# 2 WAS [8 × 18]
parse_bxs <- function(table_elem, key = "data"){
html_table(table_elem, header = FALSE) %>%
# build column names from 2-row header, magrittr pipe to acess lhs in nested calls
set_names(str_c(abbreviate(.[1,], minlength = 3), .[2,], sep = "_")) |>
clean_names() |>
filter(!player %in% c("", "Player")) |>
type_convert(guess_integer = TRUE) |>
# group_by /nest_by will sort grouping varibles, which can break row alignment
# when binding columns; but factor order is maintained, so lets turn tm to factor,
# levels in order of apparence
mutate(tm = factor(tm, levels=unique(tm))) |>
nest_by(tm, .key = key)
}
# collect tables from `url_`, bind columns to a single frame
collect_tables <- function(url_){
html <- read_html(url_)
#> tm offense
#> <chr> <list<tibble[,18]>>
#> 1 PIT [7 × 18]
#> 2 WAS [8 × 18]
tbl_off <- html |>
html_element(xpath = "//div[@id='all_player_offense']/div/table") |>
parse_bxs("offense")
# if not found:
#> # A tibble: 0 × 2
#> # ℹ 2 variables: tm <chr>, defence <list>
tbl_def <- html |>
html_element(xpath = "//div[@id='all_player_defense']/comment()") |>
# extract table element from html comments
html_text() |> minimal_html() |> html_element("table") |>
possibly(parse_bxs,
# let's default to 2x2 NA tibble with correct column names
otherwise = tibble(tm = c(NA, NA), defence = c(NA, NA))
)("defence")
#> team q1 q2 q3 q4 final
#> <chr> <int> <int> <int> <int> <int>
#> 1 Pittsburgh Steelers 7 3 7 10 27
#> 2 Washington Redskins 7 3 7 10 27
tbl_lscore <- html |>
html_element(xpath = "//table[@class='linescore nohover stats_table no_freeze']") |>
html_table() |>
set_names(c("trash", "team", 'q1', "q2", "q3", "q4", "final")) |>
select(-1)
# bind collected tables, keep tm_ columns for now so it would bepossible to verify that
# alignment is correct
bind_cols(tbl_lscore,
rename(tbl_off, tm_off = tm),
rename(tbl_def, tm_def = tm)
)
}
Основной цикл:
games <- c("https://www.pro-football-reference.com/boxscores/197301140mia.htm",
"https://www.pro-football-reference.com/boxscores/196010230was.htm",
"https://www.pro-football-reference.com/boxscores/198509150was.htm",
"https://www.pro-football-reference.com/boxscores/202309100atl.htm")
# limit requests to 1/sec
slow_collect <- slowly(collect_tables, rate = rate_delay(1))
games %>%
set_names(basename(.)) |>
map(slow_collect) |>
list_rbind(names_to = "src")
#> # A tibble: 8 × 11
#> src team q1 q2 q3 q4 final tm_off offense tm_def defence
#> <chr> <chr> <int> <int> <int> <int> <int> <fct> <list<ti> <fct> <list<ti>
#> 1 1973011… Miam… 7 7 0 0 14 MIA [8 × 21] MIA [11 × 16]
#> 2 1973011… Wash… 0 0 0 7 7 WAS [7 × 21] WAS [11 × 16]
#> 3 1960102… Pitt… 7 3 7 10 27 PIT [7 × 21] <NA>
#> 4 1960102… Wash… 7 3 7 10 27 WAS [8 × 21] <NA>
#> 5 1985091… Hous… 0 10 3 0 13 HOU [9 × 21] HOU [5 × 16]
#> 6 1985091… Wash… 13 3 0 0 16 WAS [10 × 21] WAS [3 × 16]
#> 7 2023091… Caro… 0 7 3 0 10 CAR [9 × 21] CAR [18 × 16]
#> 8 2023091… Atla… 0 7 3 14 24 ATL [6 × 21] ATL [21 × 16]
Первая версия с left_join()
оказалась не такой надежной - https://stackoverflow.com/revisions/78424112/1
Столько новых функций в этом я еще не использовал. Не могу дождаться, чтобы покопаться в этом и узнать больше! Надеюсь, моя справочная функция Rstudio обновлена, лол. Большое спасибо @margusl, возможно, скоро у меня возникнут вопросы!
Я постепенно начинаю понимать кое-что из того, что вы сделали, но сейчас я замечаю, что таблицы нападения и защиты не отображаются для конкретной команды в другом игровом журнале, который не был включен в мой пример: 'pro-football-reference.com/boxscores/198509150was.htm'. Я пробовал другие игры «Хьюстон Ойлерз», и произошло то же самое. Это потянет другую команду. Почему это может быть так? @маргусл
Я это понял (еще не исправил, но разобрался). Когда команды меняют свои названия по прошествии нескольких лет, созданный столбец «tm» становится другим, что разрушает объединение «tm» в конце.
@JeffHenderson, хороший улов! Я отредактировал свой ответ, чтобы вместо этого использовать dplyr::bind_cols()
, он также сохраняет названия команд для таблиц блоков, поэтому можно было бы проверить, правильно ли выровнены строки, хотя это не должно быть проблемой. Не стесняйтесь оставлять комментарии, если некоторые фрагменты кажутся вам несколько загадочными.
Подойдя ближе, капрал @margusl, я заметил некоторые вещи. Одну я исправил. При этом некоторые игры переходят в овертайм, поэтому в таблице tbl_lscore есть дополнительный столбец. Я исправил это с помощью оператора if else, который работал отлично. Но я думаю (не уверен на 100%), что функцияnest_by() расставляет строки по алфавиту на основе названия команды. Во всех трех примерах первая строка уже составлена по алфавиту, поэтому проблемы нет, но если вы добавите следующий URL-адрес в наш набор игр, вы должны увидеть проблему: «pro-football-reference.com/boxscores /202309100atl.htm".
Например, Десмонд Риддер — первое имя во вложенном столбце нападения с Каролиной в качестве команды. На самом деле он играет за «Атланту», а Брайс Янг (имя в обиде за «Атланту») на самом деле играет за Каролину.
Решил это! Возможно, не так элегантно, но это работает. В функцию parse_bxs добавлено - mutate(tm_num = ifelse(tm == lag(tm), 0, 1), tm_num = ifelse(is.na(tm_num), 0, tm_num), tm_num = cumsum(tm_num)) |> # порядок изменения mutate(tm_num = ifelse(tm_num == 1, 0, 1)) |> mutate(tm = forcats::fct_reorder(tm, -tm_num)) |>nest_by(tm, .key = "data")
Кстати, могу ли я послать вам совет за то, что вы так помогли? Кроме того, если вы готовы к обучению, я очень хочу стать лучше в веб-скрапинге в R и чувствую, что многому научусь, если вы открыты к этому на любом уровне. Еще раз спасибо в любом случае!
Ох... вы правы, группирующие переменные сортируются. Некоторое обсуждение этого можно найти здесь для group_by
+ summarise
, но это относится и к nest_by
. Я внес еще одно изменение, теперь порядок должен быть исправлен на mutate(tm = factor(tm, levels=unique(tm)))
в parse_bxs()
, поскольку порядок на уровне факторов сохраняется. Пустой tbl_def
(как в pro-football-reference.com/boxscores/196010230was.htm ) должен быть закрыт possibly(..., otherwise = tibble(tm = c(NA, NA), defence = c(NA, NA))
, поскольку он возвращает тиббл 2x2.
Рад, что оказался прав в своих мыслях! Это обнадеживает. Дополнительный вопрос: если бы я хотел добавить идентификатор игрока, встроенный в html имени каждого игрока, и добавить строку в каждую вложенную таблицу, как бы я это сделал? Вот код, который извлекает их для таблицы защиты: html |> html_elements(xpath = '//*[@id = "all_player_defense"]/comment()') |> html_text() |> минимальный_html() |> html_element( "таблица") |> html_elements(xpath = ".//a") |> html_attr('href').
Обычно вам нужно создать собственный анализатор таблиц для сбора и обработки атрибутов HTML, но в этом конкретном случае вам может обойтись только фрейм, построенный из <a>
элементов, которые будут содержать пары name - href, которые позже можно будет объединить с другими фреймами. Хотя без тестирования ничего не скажешь. И я боюсь, что это также начинает выходить за рамки этой темы, так что, возможно, пришло время задать дополнительный вопрос.
Спасибо. Мне удалось разобраться в этом, возможно, неэффективно, но эффективно, даже не создавая никаких функций синтаксического анализа. Очень ценю вашу помощь - еще раз, если вы готовы заняться репетиторством, я ищу кого-то вроде вас, кто мог бы помочь мне стать лучше в парсинге в R. Дайте мне знать! Одна вещь, которую я не понимаю из вашего кода, это то, почему мы заканчиваем возможную функцию словом "("защита")" или как-то еще, как мы хотим ее назвать.
possibly(
)` принимает функцию в качестве входных данных, изменяет ее и возвращает другие функции, поэтому ("defence")
в конце предназначен для вызова измененных функций. При использовании в конвейерах это может немного запутать, но tbl_def <- ... |> html_element(...) |> possibly(parse_bxs, otherwise = tibble(...))("defence")
можно переписать так: table_element <- html_element(...); parse_bxs_p <- possibly(parse_bxs, otherwise = tibble(...)); tbl_def <- parse_bxs_p(table_element, "defence")
.
Что касается репетиторства, боюсь, сейчас мне это не подойдет, но я могу порекомендовать бывшее учебное сообщество R4DS, теперь переименованное в DSLC, у них есть хороший и довольно активный канал Slack, ссылка доступна через dslc.io . Я также считаю r4ds.hadley.nz/webscraping обязательным к прочтению наряду с остальной частью R4DS). А компания HW недавно провела лекцию по парсингу, в нее должны быть включены некоторые темы, не затронутые в R4DS. Слайды и материалы доступны по адресу github.com/hadley/web-scraping
Ты лучший @margusl, я посмотрю все, что ты мне только что прислал. Я отмечу вас в будущих вопросах, если у вас будет возможность ответить. Еще раз спасибо!
Постарайтесь изолировать проблему. Я не думаю, что это самый маленький воспроизводимый пример.