PAYATHON 2026

如何在 R 中用 rvest 或 selenider 点击分页并抓取所有表格?

数据小陈

可以用 rvest::read_html_live() 打开页面,再用 $click() 点击分页链接。抓取时不用预先知道总页数:从第一页开始,保存当前表格,点击当前页码右侧的链接,直到没有后续链接。成功保存的页面数就是本次遍历的总页数。

read_html_live() 使用 chromote 驱动 Chrome,支持读取实时页面和点击元素,可以在当前 rSelenium 无法使用时作为替代方案。运行环境需要安装 Chrome。rvest 官方说明

为什么提取 href 后仍然无法翻页?

read_html() 读取服务器返回的 HTML,不会执行页面上的 JavaScript;html_attr("href") 只提取属性值,不会触发点击。

先查看提取结果。如果链接以 javascript: 开头,例如包含 __doPostBack(...),就不能把它作为普通 HTTP 地址直接传给 read_html()。这类链接需要执行脚本,可能通过 postback 向服务器提交分页事件。ASP.NET 支持通过客户端脚本触发这种服务器回发。Microsoft 文档

另外,原描述中的 .pageLinks 与代码中的 .pagingLink 不一致,应以实际 DOM 为准。

怎样跨过“…”继续抓取?

可以根据分页栏中的当前位置翻页,无需预先收集所有数字链接。下面的实现依赖三个结构条件,使用前应在页面中确认:

  • 分页行是 tr.pagingLink,并位于结果表格内部。
  • 当前页码显示在某个 td 的直接子元素 span 中。
  • 该 td 的下一个兄弟 td 中,包含通往下一页的 a;在页码组末尾,该链接可以显示为“…”。

满足这些条件时,浏览器中的点击选择器可以写为:

tr.pagingLink td:has(> span) + td > a

该选择器定位当前页码右侧的链接。分页栏可能同时出现前后两个“…”,直接按文字选择容易回到上一组。

R 抓取示例

下面的示例读取包含分页行的结果表格,从表格快照中删除分页行后再解析数据。每次点击后,代码会等待页码递增;如果超时则报错,避免把旧页面重复保存为新页面。$click(css) 是 rvest 提供的实时页面交互方法。LiveHTML 文档

# 首次使用时安装:
# install.packages(c("rvest", "chromote", "xml2", "dplyr"))

library(rvest)

scrape_localities <- function(url, timeout = 30, max_pages = 1000) {
  live <- read_html_live(url)
  on.exit(live$session$close(), add = TRUE)

  # 调试时可以打开实时预览:
  # live$view()

  next_css <- "tr.pagingLink td:has(> span) + td > a"

  read_state <- function() {
    # 一次获取页面快照,再从同一快照读取页码与数据。
    root <- html_element(live, "html")
    pagers <- html_elements(root, "tr.pagingLink")
    if (length(pagers) == 0L) {
      stop("未找到分页行,请检查页面是否加载完成及选择器是否正确。")
    }
    pager <- pagers[[1L]]

    labels <- xml2::xml_find_all(pager, ".//td/span") |>
      html_text2()
    labels <- trimws(labels)
    labels <- labels[grepl("^[0-9]+$", labels)]
    if (length(labels) != 1L) {
      stop("无法唯一识别当前页码,请检查分页栏结构。")
    }

    next_links <- xml2::xml_find_all(
      pager,
      ".//td[span]/following-sibling::td[1]/a"
    )

    tbl <- xml2::xml_find_first(pager, "ancestor::table[1]")
    if (inherits(tbl, "xml_missing")) {
      stop("未找到包含分页行的结果表格。")
    }

    # 修改的是本地 HTML 快照,不影响浏览器中的分页链接。
    xml2::xml_remove(html_elements(tbl, "tr.pagingLink"))
    data <- html_table(tbl, convert = FALSE)
    if (!"Locality/Suburb" %in% names(data)) {
      stop("表格列名与预期不符,请检查是否选中了结果表格。")
    }

    list(
      page = as.integer(labels),
      has_next = length(next_links) > 0L,
      data = data
    )
  }

  state <- read_state()
  if (state$page != 1L) stop("请从第一页开始抓取。")
  tables <- list()

  repeat {
    tables[[length(tables) + 1L]] <- state$data
    if (!state$has_next) break
    if (length(tables) >= max_pages) {
      stop("达到 max_pages 上限,尚未确认抓取完成。")
    }

    expected_page <- state$page + 1L
    Sys.sleep(0.5)
    live$click(next_css)
    started <- proc.time()[["elapsed"]]

    repeat {
      Sys.sleep(0.25)
      candidate <- tryCatch(read_state(), error = function(e) NULL)
      if (!is.null(candidate) && candidate$page == expected_page) {
        state <- candidate
        break
      }
      if (proc.time()[["elapsed"]] - started > timeout) {
        stop(sprintf("等待第 %d 页超时,请检查页面。", expected_page))
      }
    }
  }

  list(
    total_pages = length(tables),
    tables = tables,
    data = dplyr::bind_rows(tables, .id = "source_page")
  )
}

result <- scrape_localities(
  "https://electorate.aec.gov.au/LocalitySearchResults.aspx?filter=0872&filterby=Postcode"
)

result$total_pages
result$data

这里的 total_pages 是遍历完成后统计出的页数。如果必须在抓取前得到总页数,需要另行确认页面是否提供总记录数、末页入口或其他可靠信息。当前显示的最大页码只是这一组分页的上限,不能直接视为总页数。

使用时要注意什么?

convert = FALSE 用于保留文本,避免邮编 0872 被自动转换成数值 872。html_table() 默认会尝试进行类型转换,也会展开合并单元格,因此解析前要移除分页行。html_table 文档

示例针对跨页重复出现的结果表格,不会把分页布局中的嵌套表格当作独立数据表。它依赖前面列出的 DOM 结构,尚未在你的 R 环境中运行验证。如果页码不递增、表格列名变化或选择器失效,应先通过 live$view() 检查页面,再调整定位方式;不要把超时当作已经到达末页。

对于这里的点击和表格提取需求,可以先使用 rvest 的实时页面功能。如果后续需要更复杂的交互,可以再考虑 selenider。

备注:内容仅供参考。