很多数据平台的网页只展示前几页或前几百条记录,想拿全量数据靠浏览器翻页几乎不现实。但如果打开浏览器开发者工具观察网络请求,会发现页面渲染的数据其实来自一个个XHR请求返回的JSON。只要找到这些接口的地址和参数规律,用R语言直接构造请求,就能绕开前端展示限制,拿到比页面上多得多的数据。下面这张示意图展示了从页面到接口的数据流向。

用开发者工具定位真实的XHR接口
打开目标网页后按下F12,切换到网络面板,再刷新页面并触发一次查询或翻页操作。网络面板会列出页面发出的所有请求,其中类型为xhr或fetch的就是我们关心的异步请求。这些请求通常返回application/json格式的数据,与页面本身的HTML文档请求有明显区别。点击某个请求可以在响应预览中直接查看JSON结构,如果能看到页面表格中展示的数值,说明找对了接口。
定位接口时要重点关注三个部分:请求的URL路径、请求方法(GET还是POST)以及载荷或查询字符串中的参数。URL路径一般包含接口的功能标识,参数则决定了返回哪部分数据。例如分页参数page和pageSize控制返回第几页、每页多少条,时间范围参数控制数据区间。把一个请求的完整URL和参数抄下来,在浏览器新标签页直接访问,如果能返回同样的JSON,说明这是一个无需复杂认证的公开接口,抓取难度最低。
有些网站的接口参数看起来杂乱无章,混着时间戳、随机数和一串看不懂的编码。这时候可以多次触发同一操作,对比几次请求的参数差异。变化的部分往往就是分页序号、时间戳或签名,固定不变的则是接口版本号、平台标识等常量。把变化规律找出来,参数构造的思路就清晰了。
用httr2与jsonlite构造请求并解析数据
R语言中处理HTTP请求首选httr2包,它提供了链式语法的请求构建方式,配合jsonlite包解析JSON非常方便。以下示例演示如何请求一个典型的分页数据接口:
library(httr2)
library(jsonlite)
# 构造请求:设置地址、请求头和查询参数
resp <- request("https://api.ipipp.com/v1/market/list") |>
req_headers(
"User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Referer" = "https://www.ipipp.com/data",
"Accept" = "application/json"
) |>
req_url_query(
page = 1,
pageSize = 500,
sortBy = "date",
order = "desc"
) |>
req_perform()
# 解析返回的JSON为R对象
data <- resp |> resp_body_string() |> fromJSON(simplifyVector = FALSE)
str(data, max.level = 2)这段代码里有几个关键点值得展开。首先是请求头:很多接口会校验User-Agent和Referer,直接用R默认的请求头可能被拒绝。把浏览器中看到的请求头原样复制过来,通常就能通过基本校验。其次是pageSize参数,前端页面可能固定每页20条,但接口本身往往支持更大的值。试着把pageSize调到500甚至1000,如果服务器正常返回,一次请求就能顶前端翻几十页,这正是绕过前端限制的核心技巧。
对于POST请求的接口,参数要放在请求体中传递,httr2使用req_body_json或req_body_form函数来设置。带签名的接口还会要求在请求头或参数中附带token,这个token可能来自首次访问页面时的某个请求,需要在R中先请求一次拿token,再带着token去请求数据接口:
# POST接口示例:先获取token,再携带token请求数据
token_resp <- request("https://api.ipipp.com/v1/auth/guest") |>
req_perform()
token <- token_resp |> resp_body_string() |> fromJSON()$token
data_resp <- request("https://api.ipipp.com/v1/market/detail") |>
req_headers("Authorization" = paste("Bearer", token)) |>
req_body_json(list(code = "000001", start = "2024-01-01", end = "2024-12-31")) |>
req_perform() |>
resp_body_string() |>
fromJSON()
# 转为数据框便于后续处理
df <- as.data.frame(do.call(rbind, data_resp$rows))解析JSON时建议先用str函数查看结构层次,找到真正装数据的字段,通常在rows、list、data等键下面。JSON嵌套结构转数据框时可能出现列表列,用unnest或循环展平即可。
分页循环、反爬应对与数据落地
单次请求只能拿到一页数据,批量抓取需要循环构造分页参数。循环之前最好先探测总记录数:很多接口返回结果中带有total或totalCount字段,用它除以每页条数就能算出总页数,避免盲目翻页触发空请求。下面是完整的批量抓取框架:
library(httr2)
library(jsonlite)
library(dplyr)
all_rows <- list()
page <- 1
repeat {
result <- request("https://api.ipipp.com/v1/market/list") |>
req_headers("User-Agent" = "Mozilla/5.0", "Referer" = "https://www.ipipp.com/data") |>
req_url_query(page = page, pageSize = 500) |>
req_perform() |>
resp_body_string() |>
fromJSON(simplifyVector = FALSE)
rows <- result$rows
if (length(rows) == 0) break # 没有更多数据时停止
all_rows[[page]] <- bind_rows(lapply(rows, as.data.frame))
message("已抓取第 ", page, " 页,累计 ", nrow(all_rows[[page]]), " 条")
Sys.sleep(runif(1, 1, 3)) # 随机延迟,降低被封风险
page <- page + 1
}
final_data <- bind_rows(all_rows)
write.csv(final_data, "market_data.csv", row.names = FALSE)反爬方面有几个实用经验。第一是频率控制,请求之间加入随机延迟,比固定间隔更不容易被识别为机器行为。第二是异常处理,网络抖动或服务器限流会让某次请求失败,用httr2的req_retry或tryCatch包裹请求,失败后重试几次而不是直接中断整个流程。第三是参数签名问题,如果发现请求参数中有一个随内容变化的加密字段,这类接口的逆向成本较高,可以考虑用R的reticulate包调用现成的Python签名库,或者退而求其次减少抓取量。
还有一个容易被忽略的细节:会话保持。部分接口依赖Cookie识别会话,httr2通过req_cookie_preserve把响应中的Cookie保留并带到后续请求中,模拟浏览器行为。抓取大量数据后建议把原始JSON先存档到本地,解析和清洗放在后续步骤进行,这样即使解析逻辑有误也不必重新请求接口。
合规边界与抓取思路总结
直接请求接口本质上是访问网站已经对外开放的数据通道,但合规性仍需谨慎对待。抓取前先查看网站的robots协议和服务条款,确认目标数据是否允许批量获取;只抓取公开的、非个人隐私的数据;控制请求频率,避免对服务器造成压力。商业数据平台的付费内容不要试图绕过授权,那已经超出技术讨论的范畴。
整体思路可以总结为四步:先用开发者工具找到XHR接口,再分析参数的构造规律,然后用httr2构造请求验证可行性,最后用循环和解析逻辑完成批量落地。相比传统的HTML解析方式,接口抓取拿到的是干净的结构化数据,没有解析页面结构的麻烦,数据字段也更稳定。掌握这套方法后,无论是金融行情、公开统计数据还是舆情信息,只要页面上有的数据,基本都能通过分析接口高效获取,让R语言真正成为数据获取到分析的一体化工具。