如何在 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。
备注:内容仅供参考。
