用R抓取亚马逊商品数据,最大的障碍不是解析页面,而是请求还没到解析那一步就被反爬系统拦了下来。亚马逊的风控层会同时检查TLS握手特征、HTTP请求头顺序、Cookie会话链以及浏览器环境参数,缺一项就可能返回503或验证码页。很多人试图用简单的httr::GET直接拉取商品页,失败几次后得出R不适合爬亚马逊的结论,这其实低估了请求层配置的重要性。

价格监控系统的本质是稳定、低频率地获取商品信息并记录变化。稳定意味着不能今天能抓、明天就被封;低频率意味着不能用爬虫的思路去高频扫全站。把请求层做扎实,再配合合理的会话管理和定时策略,R语言完全可以胜任个人级别的亚马逊价格监控任务。
一、先把反爬触发点拆开看
很多人把亚马逊反爬简单理解为IP封禁,但其实IP只是最后一环。真正容易触发风控的是这四类信号:请求头完整度、TLS握手指纹、Cookie会话一致性、JavaScript执行证据。比如R语言默认的httr包发送的User-Agent会带有libcurl字样,服务端一眼就能判断这不是真实浏览器;默认的TLS指纹也与Chrome或Safari不同,某些接口会直接拒绝。
还有一个容易被忽视的细节是请求头顺序。HTTP协议本身没有规定头顺序,但亚马逊边缘服务会记录顺序并用于风控评分。如果你用httr的add_headers逐个添加头,R底层的libcurl会按传入顺序发送,这和浏览器固定顺序有差异。所以不要随意打乱头的排列,尽量按真实浏览器抓包顺序添加。下面的代码示例已经按Chrome常见顺序排好,可以直接复用。
library(httr) library(rvest) headers <- c( "User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept" = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language" = "zh-CN,zh;q=0.9,en;q=0.8", "Accept-Encoding" = "gzip, deflate, br", "Connection" = "keep-alive" ) resp <- GET( url = "https://www.amazon.com/dp/B0DXXXXXXX", add_headers(.headers = headers) )
设置好请求头只是第一步。上面代码中的Accept-Encoding不要手动设置为gzip, deflate, br后直接解压,因为httr会自动处理响应压缩。如果强制指定br而本地没有解析器,可能收到一堆乱码。建议保留Accept-Encoding,让httr的curl底层协商。另一个点是URL里的商品编码要使用真实存在的ASIN,随意写B0XXXX可能请求不到商品,需要替换成你实际监控的ASIN。
二、用会话保持绕过基础风控
访问商品页之前先访问首页,看起来多了一次请求,但对风控评分影响很大。真实用户从首页搜索进入商品页,会携带首页种下的ubid-main、session-id和aws-target-data等Cookie。直接冷启动请求商品页,Cookie链不完整,很容易被判定为脚本。R语言的httr包提供handle对象,可以在多个请求之间自动保存并回传Cookie,不需要手动从浏览器粘贴。
使用handle时要注意域名一致性。不要先handle www.amazon.com,又用完整URL请求 www.amazon.com/dp/ASIN,最好始终用handle加path的方式。这样curl的Cookie存储区才能稳定积累会话。如果发现返回页包含验证码文本,说明当前会话已经被标记,建议停止脚本,间隔数小时再试,而不是无限重试。
session <- handle("https://www.amazon.com")
home_resp <- GET(handle = session, path = "/", add_headers(.headers = headers))
prod_resp <- GET(handle = session, path = "dp/B0DXXXXXXX", add_headers(.headers = headers))
status_code(prod_resp)
cookies(session)
构建好会话后再抓商品页,可以明显降低验证码出现概率。但是遇到价格区和评论区的异步接口仍然可能拿不到最终数据。此时状态码是200,页面里却看不到价格,说明服务端用JavaScript二次渲染,需要动态浏览器方案。
三、动态渲染与验证码边界
当静态HTML里没有价格信息时,说明前端在浏览器端调用API或读取JSON状态。R语言有两种处理方式:一是用RSelenium驱动真实Chrome执行JS,二是直接抓取页面内嵌的金额字符串。前者更通用但慢,后者快但依赖页面结构。RSelenium本质上是把R当作Selenium客户端,通过WebDriver协议控制浏览器,因此能执行亚马逊的JavaScript挑战。
使用RSelenium前必须确认本机安装了Java和对应版本的ChromeDriver,否则rsDriver会因为驱动不匹配而失败。代码中的chromever需要填写你电脑上Chrome的实际版本号前几位,不是随便写。执行JS渲染后,XPath中的类名仍可能变化,如果抓取失败,先检查页面是否跳到了验证码或登录页。
library(RSelenium)
rD <- rsDriver(browser = "chrome", port = 4568L, chromever = "124.0.6367.155")
remDr <- rD$client
remDr$navigate("https://www.amazon.com/dp/B0DXXXXXXX")
Sys.sleep(3)
price <- remDr$findElement(
using = "xpath",
value = "//span[contains(@class,'a-price')]//span[contains(@class,'a-offscreen')]"
)$getElementText()[[1]]
title <- remDr$getTitle()[[1]]
验证码出现后,不建议在R脚本里做OCR识别或自动打码。对个人价格监控来说,遇到验证码最合理的处理是延迟并降低抓取频率。可以设置一个冷却时间变量,每次触发验证码就把下次运行时间推后。监控系统如果没有这个熔断机制,几天后IP可能被更长时间封锁。
四、价格监控系统落地
完成抓取后,剩下的工作是把数据沉淀下来并形成可用的监控任务。最简单的存储是CSV文件,适合只有一两个商品、偶尔跑一次的脚本;如果商品多、需要按小时记录,建议用SQLite。R语言的RSQLite包可以很方便地建表和插入记录,不需要额外部署数据库服务。
library(DBI)
library(RSQLite)
price_text <- "US$1,299.00"
price_number <- as.numeric(gsub("[^0-9.]", "", price_text))
if (is.na(price_number)) {
price_number <- NA_real_
}
con <- dbConnect(SQLite(), "price_monitor.sqlite")
dbWriteTable(con, "price_log", data.frame(
asin = "B0DXXXXXXX",
title = title,
price = price_number,
time = Sys.time()
), append = TRUE)
dbDisconnect(con)
亚马逊价格字符串往往混杂美元符号和逗号,用gsub去掉所有非数字和小数点再转成numeric,是最稳妥的清洗方式。需要注意德国站或英国站的本地货币符号和千位分隔符不同,同一个正则可能不适用。存储时不要只存价格,必须同时保存ASIN、标题、抓取时间和原始字符串,方便回查和排除异常值。
定时任务在Windows下可以用任务计划程序调用Rscript,Linux或macOS可以用cron。R语言里cronR包可以创建Linux cron任务,代码块如下。监控频率建议至少间隔30分钟,最好控制在每小时1到2次,频繁请求会迅速提高风控分数。
library(cronR)
cmd <- cron_rscript("amazon_price_monitor.R")
cron_add(command = cmd, frequency = "hourly", id = "amazon_price_monitor")
| 模块 | 推荐R包 | 作用 |
|---|---|---|
| 请求层 | httr | 伪装请求头与Cookie会话 |
| 解析层 | rvest | 提取标题、价格、评分 |
| 动态渲染 | RSelenium | 执行JavaScript获取异步内容 |
| 存储层 | RSQLite | 保存历史价格和时间 |
| 调度层 | cronR | 定时运行抓取脚本 |
整套系统跑通后,可以把价格变化画成趋势图,或者设置阈值提醒。需要注意的是,亚马逊商品页面结构随时可能调整,选择器失效是常态而不是意外。监控脚本要在一个地方集中维护选择器,并定期人工检查页面,不要指望一次写完永远不用改。