Как выполнить парсинг с помощью Map_dfr, не пропуская его, когда он ломается из-за того, что не находит элемент определенной страницы

Я запускаю 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.

Постарайтесь изолировать проблему. Я не думаю, что это самый маленький воспроизводимый пример.

Friede 02.05.2024 19:49
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
132
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

К сожалению, данные на этом сайте организованы довольно непоследовательно. Похоже, ваша основная проблема заключается в том, что вы пытаетесь заполнить 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 может быть решением! Заранее спасибо.

Jeff Henderson 02.05.2024 20:44

Итак, я думаю, что Reselenium необходим, потому что таблица all_player_defense является идентификатором, но если вы проверите html, вы увидите все это в форме комментариев (раздражает), поэтому я использовал проверку страницы для этого. Тем не менее, я собираюсь адаптировать это к своему коду rselen и посмотреть, что получится.

Jeff Henderson 02.05.2024 21:23

Насколько я понимаю, прокомментированный код был просто заполнителем для фактических значений, которые были бы там, если бы это было так, но я не провел полную проверку, необходимую, чтобы выяснить это, так что дайте мне знать, если это неправильно!

Dubukay 02.05.2024 22:08

Привет @dubukay, поэтому даже для страницы, на которой есть таблица статистики защиты, html_element не идентифицирует узел xml. Когда я запускаю вашу функцию на обеих итерациях, она работает, но дает NA для статистики защиты даже для первой страницы, на которой явно есть эта таблица. Вы можете проверить HTML-код в браузере и увидеть, что он имеет идентификатор «player_defense». Но read_html(page, xpath = "//*[@id='player_defense']" ничего не возвращает.

Jeff Henderson 03.05.2024 19:01

Вы запускаете то же самое с player_offense, и он его подхватывает. Я не понимаю, почему он не идентифицирует этот узел для таблицы защиты, но мне кажется, что это как-то связано с тем фактом, что прямо на html-странице это выглядит так, будто таблица защиты существует в виде комментария (оскорбление не ).

Jeff Henderson 03.05.2024 19:01
Ответ принят как подходящий
  • purrr::possibly() отлично подходит для обработки ошибок в таких случаях, и его значение по умолчанию может быть более совместимым с желаемым выходным значением;
  • мы можем извлекать HTML-комментарии с помощью XPath 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, возможно, скоро у меня возникнут вопросы!

Jeff Henderson 04.05.2024 16:53

Я постепенно начинаю понимать кое-что из того, что вы сделали, но сейчас я замечаю, что таблицы нападения и защиты не отображаются для конкретной команды в другом игровом журнале, который не был включен в мой пример: 'pro-football-reference.com/boxscores/198509150was.htm'. Я пробовал другие игры «Хьюстон Ойлерз», и произошло то же самое. Это потянет другую команду. Почему это может быть так? @маргусл

Jeff Henderson 05.05.2024 00:41

Я это понял (еще не исправил, но разобрался). Когда команды меняют свои названия по прошествии нескольких лет, созданный столбец «tm» становится другим, что разрушает объединение «tm» в конце.

Jeff Henderson 05.05.2024 07:24

@JeffHenderson, хороший улов! Я отредактировал свой ответ, чтобы вместо этого использовать dplyr::bind_cols(), он также сохраняет названия команд для таблиц блоков, поэтому можно было бы проверить, правильно ли выровнены строки, хотя это не должно быть проблемой. Не стесняйтесь оставлять комментарии, если некоторые фрагменты кажутся вам несколько загадочными.

margusl 05.05.2024 08:53

Подойдя ближе, капрал @margusl, я заметил некоторые вещи. Одну я исправил. При этом некоторые игры переходят в овертайм, поэтому в таблице tbl_lscore есть дополнительный столбец. Я исправил это с помощью оператора if else, который работал отлично. Но я думаю (не уверен на 100%), что функцияnest_by() расставляет строки по алфавиту на основе названия команды. Во всех трех примерах первая строка уже составлена ​​по алфавиту, поэтому проблемы нет, но если вы добавите следующий URL-адрес в наш набор игр, вы должны увидеть проблему: «pro-football-reference.com/boxscores /202309100atl.htm".

Jeff Henderson 05.05.2024 21:07

Например, Десмонд Риддер — первое имя во вложенном столбце нападения с Каролиной в качестве команды. На самом деле он играет за «Атланту», а Брайс Янг (имя в обиде за «Атланту») на самом деле играет за Каролину.

Jeff Henderson 05.05.2024 21:07

Решил это! Возможно, не так элегантно, но это работает. В функцию 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")

Jeff Henderson 06.05.2024 18:22

Кстати, могу ли я послать вам совет за то, что вы так помогли? Кроме того, если вы готовы к обучению, я очень хочу стать лучше в веб-скрапинге в R и чувствую, что многому научусь, если вы открыты к этому на любом уровне. Еще раз спасибо в любом случае!

Jeff Henderson 06.05.2024 18:24

Ох... вы правы, группирующие переменные сортируются. Некоторое обсуждение этого можно найти здесь для 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.

margusl 06.05.2024 21:27

Рад, что оказался прав в своих мыслях! Это обнадеживает. Дополнительный вопрос: если бы я хотел добавить идентификатор игрока, встроенный в html имени каждого игрока, и добавить строку в каждую вложенную таблицу, как бы я это сделал? Вот код, который извлекает их для таблицы защиты: html |> html_elements(xpath = '//*[@id = "all_player_defense"]/comment()') |> html_text() |> минимальный_html() |> html_element( "таблица") |> html_elements(xpath = ".//a") |> html_attr('href').

Jeff Henderson 07.05.2024 23:54

Обычно вам нужно создать собственный анализатор таблиц для сбора и обработки атрибутов HTML, но в этом конкретном случае вам может обойтись только фрейм, построенный из <a> элементов, которые будут содержать пары name - href, которые позже можно будет объединить с другими фреймами. Хотя без тестирования ничего не скажешь. И я боюсь, что это также начинает выходить за рамки этой темы, так что, возможно, пришло время задать дополнительный вопрос.

margusl 08.05.2024 13:36

Спасибо. Мне удалось разобраться в этом, возможно, неэффективно, но эффективно, даже не создавая никаких функций синтаксического анализа. Очень ценю вашу помощь - еще раз, если вы готовы заняться репетиторством, я ищу кого-то вроде вас, кто мог бы помочь мне стать лучше в парсинге в R. Дайте мне знать! Одна вещь, которую я не понимаю из вашего кода, это то, почему мы заканчиваем возможную функцию словом "("защита")" или как-то еще, как мы хотим ее назвать.

Jeff Henderson 11.05.2024 15:24
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") .
margusl 14.05.2024 00:09

Что касается репетиторства, боюсь, сейчас мне это не подойдет, но я могу порекомендовать бывшее учебное сообщество R4DS, теперь переименованное в DSLC, у них есть хороший и довольно активный канал Slack, ссылка доступна через dslc.io . Я также считаю r4ds.hadley.nz/webscraping обязательным к прочтению наряду с остальной частью R4DS). А компания HW недавно провела лекцию по парсингу, в нее должны быть включены некоторые темы, не затронутые в R4DS. Слайды и материалы доступны по адресу github.com/hadley/web-scraping

margusl 14.05.2024 00:09

Ты лучший @margusl, я посмотрю все, что ты мне только что прислал. Я отмечу вас в будущих вопросах, если у вас будет возможность ответить. Еще раз спасибо!

Jeff Henderson 15.05.2024 16:30

Другие вопросы по теме