资讯动态

R语言rvest包网页爬虫实战:从HTML解析到数据采集

发布时间:2026/10/9 6:52:08 来源:尧图企业网站定制
做数据分析的人迟早会遇到一个问题需要的数据不在 Excel 里不在数据库里只存在于某个网站上。我刚入行那会儿还在用 RCurl 把整页源码拉下来再用正则表达式去匹配标题和价格页面一改版代码就跟着报废。后来换成 rvest才真正体会到什么叫“用解析网页的方式写爬虫”在数据爬虫这件事上省下了一大半时间。rvest 是 R 语言生态里最成熟的网页爬虫包由 Hadley Wickham 团队维护底层依赖 xml2 解析器。它的核心逻辑非常清晰读取 HTML 文档、定位目标节点、提取内容、再进入数据清洗流程。对于数据分析师、研究生、以及所有不想额外学一门 Python 但又有网页数据采集需求的人来说rvest 几乎是 R 生态下的最优解。这篇内容我不打算讲虚的直接从工具选型、环境搭建、核心函数、翻页实战、电商数据场景、以及高频报错这几个维度把我实际用过的东西完完整整写出来。你可以照着敲一遍也可以当成 rvest 速查手册收藏。1. 为什么 R 语言用户做网页采集会首选 rvest1.1 从 RCurl 到 rvest我换工具的真实原因在 rvest 出现之前R 用户抓网页通常只有两条路一条是 RCurl 加正则表达式另一条是 XML 包里的 XPath。RCurl 能做的只是把网页源码拉下来真正困难的部分——从几十 KB 的 HTML 里准确找到书名、价格、链接——全靠自己写正则。写过正则的人都知道那是什么体验标签嵌套、属性顺序变化、空白字符干扰任何一个意外都能让匹配结果变成空。更痛苦的是页面结构一改版你的正则就得推倒重来。有一次我抓一个书评网站对方只是把div classbook-info换成了article classbook我整整一个下午都在调试那串像天书一样的匹配规则。后来接触到 rvest发现它解决的就是这个核心痛点不让你在字符串层面“肉搏”而是把网页当成一棵结构化的 DOM 树你用 CSS 选择器直接定位节点再用函数把文本、属性、表格批量提取出来。页面结构调整了多数情况下你只需要改一行选择器而不是重写一段正则。1.2 rvest 在 R 生态里的准确位置说句实话rvest 不是万能的。它擅长的是“静态 HTML 页面”的数据抽取——也就是数据在服务端渲染好、直接包含在返回的 HTML 里。你可以通过它抓取文章列表、新闻标题、政府公开数据、图书信息、天气数据等大多数常规网页内容。但如果你面对的是需要登录后才能查看的后台数据、点击“加载更多”才出现的动态内容或者像拼多多这类完全靠 JavaScript 动态渲染的电商页面rvest 单靠自己就力不从心了。很多人一开始没有搞清楚这个边界拿着 rvest 去抓动态网页折腾半天得到一堆空对象以为是包有问题其实是选错了工具。搞清楚“这东西能做什么、不能做什么”是入门 rvest 之前必须先过的一道坎。1.3 和其他技术栈的横向对比我经常被问到一个问题“我都用 Python 写爬虫了为什么还要学 rvest”这个问题的前提就不对。如果你已经在 Python 生态里如鱼得水确实没必要换。但对已经用 R 做数据分析的人来说为了抓一个小数据集就切换到 Python再学一遍 requests、BeautifulSoup、Scrapy这个学习成本并不划算。各方案的比较可以从下面这张表看清楚方案语言学习成本动态页面支持适合场景rvestR低弱需配合浏览器自动化解决R 环境内的快速数据获取、数据清洗流程requests BeautifulSoupPython中弱同样需要配合 SeleniumPython 技术栈的通用入门爬虫ScrapyPython高中配 Selenium 后较强大规模、分布式爬虫项目RSelenium / chromoteR中强模拟真实浏览器渲染需要登录、点击、滚动加载的动态场景我的建议是如果你本职工作是数据分析、统计建模日常用的是 R那 rvest 完全够用如果你要做的是千万级页面的采集工程那应该直接学 Scrapy而不是在 R 里凑合。工具服务于场景别为了用某个工具而折腾自己。2. 搭建抓取环境会话、编码与请求头的第一道坎2.1 安装与依赖rvest 的安装非常简单一条命令搞定install.packages(rvest)它会自动带上几个关键依赖xml2 负责把 HTML 解析成结构化的 XML 文档selectr 负责把 CSS 选择器转换成 XPath 表达式httr 提供底层 HTTP 请求能力。理解这几个依赖很重要因为它们决定了 rvest 的行为方式。比如 selectr 存在你才能用h3 a这种 CSS 写法去定位节点而不是自己手写繁琐的 XPath。加载的时候养成好习惯把后面会用到的包一起加载上library(rvest) library(dplyr) library(readr) library(data.table)readr用来解析数字和文本格式data.table的rbindlist在合并多页抓取结果时极其好用。2.2 两种读页面方式read_html 与 html_sessionrvest 读页面有两种主要方式很多人一开始没分清楚结果在处理登录场景时浪费了很多时间。第一种是read_html()适合访问无状态的公开页面。它做的事情本质上是一次性 GET 请求把返回的 HTML 解析成文档对象然后你就可以在这个对象上做节点定位了page - read_html(https://books.toscrape.com/)第二种是html_session()它创建一个带 Cookie 的持久会话。如果你要模拟登录、保持登录状态、或者进行表单操作必须用这个方式。我当时抓一个需要登录的论坛数据用read_html()每次请求都拿不到完整内容换成html_session()之后登录状态一直维持着后面翻页就顺利多了。session - html_session(https://example.com/login) # 配合表单操作 form - session %% html_form() form - form %% html_form_set(username my_name, password my_password) session - session %% html_submit(form)这里有一个细节值得注意html_session()返回的本身就是一个会话对象你可以像管道操作一样把它传给下一步。它的 cookie store 由 httr 在底层帮你维护你不需要手动处理 Set-Cookie 头。这是 rvest 比“手动构造 httr 请求”更方便的地方。2.3 User-Agent、超时与重试很多网站的第一道反爬就是检查请求头里的 User-Agent。如果你直接用 R 的默认 UA 去请求服务器一看就知道是脚本在访问可能直接返回 403 或者验证页面。我的习惯是每创建一个新会话立刻把 UA 设置成常见浏览器的版本session - html_session(https://books.toscrape.com/) session - session %% session_user_agent(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36)超时设置同样重要。默认情况下如果服务器响应慢R 可能会一直挂在那里既浪费时间又影响后续请求。用httr的时间配置可以控制请求时长page - read_html(url, timeout 15)对于稍微正式一点的采集任务重试机制是必须的。网络抖动、服务器临时过载都会导致单次请求失败。简单的重试逻辑可以直接用purrr包的insistently()也可以自己写个循环fetch_with_retry - function(url, max_attempts 3) { for (i in 1:max_attempts) { res - tryCatch(read_html(url, timeout 30), error function(e) e) if (!inherits(res, error)) return(res) Sys.sleep(2 * i) } stop(请求失败, url) }这个函数我在实际项目里一直在用遇到瞬时错误会自动让步重试基本能做到“挂掉一次不耽误大局”。2.4 中文站点的编码乱码问题rvest 解析 HTML 时会自动读取页面头部meta标签里声明的字符集所以绝大多数 UTF-8 编码的页面都不会有问题。真正容易翻车的是那些老旧的 GB2312、GBK 编码站点页面可能没写 charset或者声明了错误的编码抓回来全是乱码。遇到这种情况先用guess_encoding()猜测一下实际编码bin - readBin(url, raw, n 10000) guess_encoding(bin)拿到猜测结果后再用iconv()或者 xml2 的编码参数做转换。我的经验是对中文站点先打印前 500 个字符检查别埋头往下写等抓完大量数据才发现全乱码返工成本很高。这也是我给所有做中文数据采集的人的第一条建议——开工前先验证编码。3. 核心三件套的配合定位节点、提取属性、读取文本3.1 CSS 选择器语法和调试方法rvest 的节点定位基于 CSS 选择器。如果你有过前端基础这部分直接能上手如果没有记住这几个最常用的写法就够了目标CSS 写法示例标签选择器h1、p、a所有 h1 标签类选择器.price_colorclass 里含 price_color 的元素ID 选择器#mainid 为 main 的元素属性选择器[href]带 href 属性的元素后代选择器h3 ah3 标签内部的 a 标签直接子元素ul liul 的直接子 li新手最容易栽在“选择器写对了但返回空”这个坑。我的调试方法是把页面保存到本地用浏览器打开按 F12 在开发者工具里用 CtrlF 搜索 CSS 选择器能搜到元素再回来写代码。另一个辅助技巧是使用 SelectorGadget 这类浏览器插件点几下就能生成目标元素的选择器虽然它生成的选择器有时比较冗余但作为初筛非常好用。3.2 html_elements、html_attr 与 html_text 的配合逻辑新版 rvest 中html_nodes()已更名为html_elements()旧名字仍然可用但建议直接适应新写法。这个函数的作用是给定一个节点集合返回匹配选择器的所有节点。对应的html_element()只返回第一个匹配项适合只取一个元素的场景。拿到节点之后下一步就是“提取”。常用的三个函数分工明确html_text()提取节点内部的文本内容比如书名、价格文字html_attr()提取节点的属性值比如href、src、classhtml_table()直接把table标签转成 data.frame抓表格型数据时效率极高举一个最经典的例子抓取书本网站的单页数据page - read_html(https://books.toscrape.com/) titles - page %% html_elements(h3 a) %% html_text() prices - page %% html_elements(.price_color) %% html_text() links - page %% html_elements(h3 a) %% html_attr(href)这段代码背后是三层结构html_elements()负责定位html_text()或html_attr()负责提取管道运算符%%把前一步的结果传给后一步。这就是 rvest 和 tidyverse 风格完美融合的体现——抓取数据本身就可以看作一次数据清洗流程的前置环节。3.3 管道思维下的“一条龙抓取”rvest 的优势之一是你可以把所有步骤串成一条链读起来像自然语言。比如抓取书名、价格、评分然后直接整理成 data.framelibrary(rvest) library(dplyr) page - read_html(https://books.toscrape.com/) books - tibble( title page %% html_elements(h3 a) %% html_text(), price page %% html_elements(.price_color) %% html_text(), rating_class page %% html_elements(p.star-rating) %% html_attr(class) )这种写法最大的好处是每一行代码都对应一个清晰的逻辑单元后续有任何一步出错你能立刻定位是选择器的问题、还是提取函数用错了。我在刚开始学习时喜欢把中间结果打印出来逐个看而不是一股脑跑完再从头排查。建议你也保留这个习惯。4. 完整实战把整个图书榜单抓下来4.1 先分析 URL 规律别急着写代码我见过太多人拿到任务第一反应就是打开 RStudio 写代码结果写到一半才发现 URL 结构没搞清楚。正确的顺序是先打开浏览器手动翻几页观察 URL 的变化规律。这次我用的示例站点是 Books to Scrape它是一个专门为爬虫练习设计的网站。它的目录页 URL 规律非常标准https://books.toscrape.com/catalogue/page-1.html https://books.toscrape.com/catalogue/page-2.html ... https://books.toscrape.com/catalogue/page-50.html总共 50 页每页 20 本书正好 1000 本。分析完 URL 规律之后再确认列表页里书名、价格、评分对应的 DOM 结构。这个站点结构稳定很适合做 rvest 的完整练习。4.2 单页抓取的完整代码先写单页逻辑确认没问题了再扩展成循环。这是我一直坚持的开发节奏——小步快跑别一次到位。单页抓取代码library(rvest) library(dplyr) library(readr) base_url - https://books.toscrape.com/catalogue/page-1.html page - read_html(base_url) books_page - tibble( title page %% html_elements(h3 a) %% html_text(), price_text page %% html_elements(.price_color) %% html_text(), link page %% html_elements(h3 a) %% html_attr(href), rating_class page %% html_elements(p.star-rating) %% html_attr(class) ) price_num - parse_number(books_page$price_text) books_page$price - price_num这里特别提一下parse_number()它来自 readr 包专门用来处理“£51.77”这类带货币符号的文本它会自动把数字部分提取成数值型。如果你想自己用正则处理也行但parse_number()更省事而且对“1,234.56”这种带千分位的文本也能正确处理。评分字段star-rating Three里包含了评分信息提取出来之后需要映射成数值。我写一个简单的映射字典rating_map - c(One 1, Two 2, Three 3, Four 4, Five 5) rating_key - stringr::str_extract(books_page$rating_class, One|Two|Three|Four|Five) books_page$rating - rating_map[rating_key]4.3 翻页循环与随机延时单页逻辑跑通后扩展成循环就很简单了。核心思路是先把每一页的结果存进一个列表最后用rbindlist()一次性合并。注意循环里一定要加延时——这不是做做样子而是对目标服务器的基本尊重也是减少被封概率的有效手段。library(data.table) library(rvest) library(readr) all_books - list() for (i in 1:50) { url - sprintf(https://books.toscrape.com/catalogue/page-%d.html, i) page - read_html(url, timeout 15) df - data.frame( title page %% html_elements(h3 a) %% html_text(), price page %% html_elements(.price_color) %% html_text() %% parse_number(), link page %% html_elements(h3 a) %% html_attr(href), rating_class page %% html_elements(p.star-rating) %% html_attr(class), stringsAsFactors FALSE ) all_books[[i]] - df Sys.sleep(runif(1, 0.5, 1.5)) } books - rbindlist(all_books, fill TRUE)有几个细节值得解释。sprintf()负责生成带页码的 URL比手写字符串拼接更安全。Sys.sleep(runif(1, 0.5, 1.5))的含义是每次请求后随机休眠 0.5 到 1.5 秒固定间隔很容易被服务器识别为机器行为随机化之后更像人工访问。fill TRUE保证即使某页字段缺失也不会中断合并。4.4 数据清洗、去重与导出抓下来的数据还不能直接用需要经过一轮清洗。首先是处理重复值——翻页抓取时偶尔会出现页面内容重复或者你的循环重复执行所以去重是必须的library(dplyr) books_clean - books %% distinct(title, .keep_all TRUE)然后是链接补齐。抓下来的link字段是相对路径类似../../../../catalogue/a-light-in-the-attic_1000.html直接使用会失效。用xml2::url_absolute()可以基于基础 URL 拼出完整地址library(xml2) full_links - url_absolute(books_clean$link, base_url) books_clean$full_link - full_links最后导出成 CSV方便后续分析write.csv(books_clean, books_toscrape.csv, row.names FALSE)如果想保留更多格式信息可以用writexl::write_xlsx()导出 Excel。到这里一个完整的“列表页翻页抓取 数据合并 清洗导出”流程就跑通了。整个流程的核心技巧就是“先单页、再循环、合并清洗”这套模式可以迁移到绝大多数静态网站。5. 商品数据的现实问题动态渲染、签名接口与合规边界5.1 电商商品数据抓取的典型形态最近半年问“怎么抓拼多多商品数据”的朋友特别多。先别急我们来拆解一下典型的电商商品数据结构。大多数电商平台的数据分两层列表页和详情页。列表页展示商品标题、价格、销量、店铺名详情页包含更深入的 SKU 信息、评价、库存等。如果目标网站是服务端渲染的rvest 抓取效率会非常高列表页一个循环就能把核心字段全拿下来。以老牌的静态电商页面为例列表页结构通常是ul包裹的li集合每个li内部有几段固定的 DOM 结构。你要做的就是像第 4 章那样定位商品节点、提取文本、翻页循环、合并清洗。这套流程对服务端渲染的电商网站完全适用。5.2 为什么拼多多这类平台 rvest 很难直接抓但拼多多这类平台完全是另一回事。你通过普通请求拿到的 HTML 只是一个空壳商品数据全部靠 JavaScript 动态渲染填充。如果直接read_html()得到的页面节点里根本没有价格和销量选择器自然什么都选不中。更麻烦的是它的数据接口带有签名参数请求签名算法复杂且经常变化想通过模拟接口拿到数据需要做大量的逆向工作这已经不是 rvest 的能力边界也超出了常规爬虫的技术范畴。每次有人满怀期待地问我“用 rvest 怎么抓拼多多”我都得先把这个现实讲清楚不是 rvest 不行而是这类平台压根不适合用静态解析工具去碰。与其在反爬体系上硬刚不如换个思路。5.3 面对动态页面的合规路径与替代方案遇到动态渲染页面R 生态里不是没有出路但要分清主次。第一种思路查找页面背后是否暴露了相对规整的 JSON 接口。很多网站的数据其实是通过接口返回的拿到 JSON 之后用jsonlite::fromJSON()解析比解析 HTML 还干净。不过要注意接口是否开放、能否绕过签名限制这取决于站点的技术策略不要投入太多时间在逆向这种灰色操作上。第二种思路用浏览器自动化方案。R 里可以用chromote包启动一个无头 Chrome让页面在真实浏览器里加载完成等 JavaScript 执行完毕之后再把最终的 HTML 取回交给 rvest 解析。思路大致是启动浏览器、访问页面、等待数秒、获取外层 HTML、回到 rvest 处理。这种方案的缺点是慢、吃内存且比较容易被检测适合小规模、低频的数据采集。第三种思路也是最建议的路径优先寻找官方开放数据渠道。电商平台大多有开放平台或数据分析工具哪怕只能拿到聚合数据也比冒着合规风险硬爬划算。我个人做商品价格研究时第一选择永远是公开数据集、官方接口和第三方正规数据服务只有这些渠道确实覆盖不了需求时才会考虑亲自采集并且严格控制频率、控制用量。这里必须把话说得重一点。robots.txt协议要遵守平台服务条款要尊重数据用于个人学习研究和批量下载用于商业竞争性质完全不同。涉及个人信息的数据还受到个人信息保护法、数据安全法等法律法规的约束。做数据采集的人一定要有底线思维目的正当、手段合规、频率克制、规模适度。别为了一次分析任务把自己搭进去这不值得。6. 抓取路上的高频报错与反爬实战经验6.1 HTTP 状态码暗藏的语义爬虫过程中服务器不会陪你演戏它只会用状态码告诉你结果。我整理了一个快速对照表你可以直接收藏状态码含义常见原因应对策略200正常-继续抓取301 / 302重定向页面更换地址检查最终 URL或让会话自动跟随403禁止访问UA 被识别、IP 受限换 UA、降频、暂停一段时间404页面不存在URL 规律分析有误重新检查 URL 结构429请求太多触发了限流立即停止加长延时503服务暂不可用服务器过载或主动反爬等待后重试降低频率403 是新手最常碰到的墙。很多时候不是因为你的 IP 被封而是 UA 太“裸”了一眼就被识别。我见过一个案例全部代码只差一行session_user_agent()加上之后立刻恢复正常抓取。所以当遇到 403 时先检查请求头再考虑是不是频率问题。6.2 选择器返回空最让人抓狂的“假成功”html_elements()返回空对象比直接报错更让人头疼——代码没报错但数据就是空的。我总结下来主要有四种原因页面结构变了、选择器写错了、目标内容是 JS 动态加载的、以及页面给了你一个反爬的假页面。排查路径很固定。先把 HTML 保存下来page - read_html(url) writeLines(as.character(page), page_backup.html)然后用浏览器打开这个备份文件按 F12 在 Elements 面板里检查结构重新确认选择器。如果本地备份文件里根本没有你要找的内容那就是动态加载问题需要换浏览器自动化方案如果本地文件有内容说明是选择器或者 URL 参数的问题。这套排查流程我用了很多年效率很高。6.3 超时、连接失败与 SSL 问题抓取过程中另外一类常见报错来自网络层面。Error in open.connection表示服务器没有响应Operation timed out说明请求超时SSL 证书错误则可能因为目标站点的证书链不完整。这些问题的共性解法是调大timeout参数默认时间往往不够用加入重试机制避免一次网络的偶发故障就中断整个采集任务优先使用 https 协议访问目标站点减少被中间环节干扰的可能有一个细节容易被忽略如果你的采集任务是自动运行的比如放在服务器上定时执行一定要加上完整的日志记录。每次抓取成功后写一行日志记录时间、URL、状态和数据行数。等某一天数据异常时这份日志能帮你快速定位问题发生在哪个环节。6.4 多页数据合并中的几个小坑翻页抓取最让人头疼的问题往往不在抓取阶段而在合并阶段。最常见的报错是rbindlist遇到NULL元素。当某一页因为网络原因返回空数据时列表里就会混入空元素合并时直接报错。解决办法是合并前过滤掉空的页结果all_books - Filter(function(x) !is.null(x) nrow(x) 0, all_books) books - rbindlist(all_books, fill TRUE)另一个坑是字段类型不一致。比如第一页价格是数值型后面的页面由于数据缺失变成了字符型合并时自动转换失败。解决方法是循环里统一字段类型或者在合并后用type.convert()做一次整体校正books - rbindlist(all_books, fill TRUE) books - type.convert(books, as.is TRUE)最后一个常见问题是字符编码混用。我抓过一些站点前 30 页是 UTF-8后 20 页突然变成 GBK合并后部分中文标题变乱码。这种情况下循环里最好显式统一编码解析阶段就做转换别等到合并之后再补救。最后分享一个我保持了很多年的习惯任何抓下来的原始网页我都会先存一份本地备份再解析。别嫌这一步占磁盘当你需要回头排查选择器问题、或者想验证某个字段是本来就缺还是没抓到的时候这份备份能救你的命。爬虫写到后面你会发现真正的核心竞争力不是爬得有多快而是出了问题能多快定位、多稳恢复。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑