先说结论:这个项目标题里提到的“西瓜小视频”,我在这里把它当成一个纯演示用的虚构站点,数据也都是模拟数据,不特指任何真实平台。整篇文章分享的是用 Java 配合 Jsoup 和 Selenium 抓取一个动态渲染的短视频列表页,把视频标题、封面、作者、播放量、播放页链接等项目信息完整抽出来,最终落成 CSV 文件。适合刚接触爬虫、想在 Java 生态里搞数据采集,或者对“静态解析器不能直接拿 JS 渲染页面”这个问题感到困惑的朋友参考。
我会把需求拆解、环境配置、核心代码、问题排查全部走一遍。特别是为什么要同时用 Selenium 和 Jsoup 两个工具、什么场景下该用谁、哪些坑值得提前记住,这些都会聊到。抓取类的项目越早把“边界”想清楚越好,所以最后我也会专门说明合规相关的东西。
1. 项目需求拆解与方案选型
1.1 先搞清楚要抓什么
拿到“抓取小视频”这个需求时,第一步一定不是写代码,而是拆数据字段。短视频站点的核心信息其实很清晰:标题、封面图、视频播放地址、作者名称、播放量/点赞数、发布时间、视频详情页链接。它们分布在两个层面,一个是列表页的卡片信息,另一个是点击进入后的详情页信息。大部分情况下,列表页已经能拿到 80% 的字段,真实视频播放地址往往藏在详情页里,而且很多平台会做签名、防盗链,直接抓下来也不一定能播放。
当时我的做法是先把目标站点在浏览器里打开,手动翻几页,把页面结构看清楚。发现这个站点的列表页是典型的“首屏静态,后续动态加载”模式,也就是说直接请求 HTML 源码只能拿到第一屏,往下滚动后新出现的视频卡片是通过 JavaScript 异步插入的。这个发现直接决定了技术选型:只用 Jsoup 是行不通的,必须有能驱动真实浏览器的工具,也就是 Selenium。
1.2 为什么是 Java + Jsoup + Selenium
先说为什么选 Java。我前面的项目里,数据采集模块需要和企业内部的数据处理服务集成,Java 的生态、日志体系、线程池管理都更顺。如果只是个人随便玩玩,用 Python 当然也行,但这个项目既然定了 Java 环境,那就按 Java 来做。
Selenium 在这里的定位是“获取渲染完成后的页面源码”,它通过 WebDriver 启动一个真实的浏览器,模拟人的操作:打开页面、滚动、等待网络请求返回。这个过程中的所有 DOM 都被浏览器重新构建,等我们拿到页面源码时,视频卡片已经在里面了。
那为什么还要再套一层 Jsoup?直接拿 WebDriver 的 findElements 不也能定位元素吗?能,但体验很差。Selenium 的定位方式写起来繁琐,特别是面对几十个卡片、每个卡片下面又有好几个子元素时,遍历代码会非常啰嗦。Jsoup 的强项是解析静态 HTML,它支持 CSS Selector,一行代码就能 select 出所有卡片节点,再逐个提取子字段,代码可读性和健壮性都明显更好。所以我把两者结合起来:Selenium 负责“让页面变完整”,Jsoup 负责“从完整页面里高效挖数据”。
1.3 最终抓取流程的整体设计
方案定下来后,流程顺理成章:
- 启动浏览器驱动,创建 WebDriver。
- 打开列表页,模拟滚动加载,直到页面末尾。
- 用 driver.getPageSource() 拿到渲染后的完整 HTML。
- 把 HTML 字符串交给 Jsoup 解析,按 CSS 选择器定位视频卡片集合。
- 遍历卡片,抽取标题、封面、作者、播放量、详情页链接。
- 统一写入 CSV 文件,并在控制台输出统计信息。
- 在 finally 块里执行 webDriver.quit(),释放浏览器进程资源。
这个流程看起来简单,但每一步都有细节值得展开,尤其是等待策略和滚动策略,下面会单独讲。
2. 开发环境准备与依赖配置
2.1 基础环境清单
开发如果能用比较新的 JDK 会省很多事。我当时用的是 JDK 17,Maven 管理依赖,IDE 用的是自己习惯的那套,这个不重要。重要的是确保本机 Chrome 浏览器版本和对应驱动版本能对上,或者干脆引入 WebDriverManager 自动管理驱动。
需要准备的软件清单大致是:JDK 17+、Maven 3.8+、Chrome 浏览器、一个不用特别新的 IDEA 或者 Eclipse。如果你电脑上已经装了 Chrome,那版本匹配问题交给 WebDriverManager 处理就好,不用手动去下载 chromedriver,这个工具会从配置好的仓库自动拉取匹配的驱动文件。
2.2 Maven 依赖如何配置
核心依赖只有两个:jsoup 和 selenium-java。jsoup 负责 HTML 解析,selenium-java 负责浏览器自动化。如果不想手动管理浏览器驱动,再额外加一个 webdrivermanager。
<dependencies> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>4.20.0</version> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.17.2</version> </dependency> <dependency> <groupId>io.github.bonigarcia</groupId> <artifactId>webdrivermanager</artifactId> <version>5.8.0</version> </dependency> </dependency>版本号我写的是当时实测可用的,你在实际使用时没必要死磕一致,只要是最近一年内发布的稳定版本都可以。selenium-java 4.x 和 jsoup 1.17.x 的配合非常成熟,网上能找到大量案例。
2.3 驱动配置的两种方式
如果你的网络访问不了 Maven 中央仓库的外部地址,可以先手动下载 chromiumdriver,再通过系统属性指定路径:
System.setProperty("webdriver.chrome.driver", "/path/to/chromedriver"); WebDriver driver = new ChromeDriver();但这种方式的痛点很明显:Chrome 只要一升级,驱动就可能失效,报一个“session not created”的错。更省心的方式是用 WebDriverManager:
WebDriverManager.chromedriver().setup(); WebDriver driver = new ChromeDriver();它会在启动前自动检查本机 Chrome 版本,下载匹配的 driver 二进制文件。我那次因为连续换两台电脑,手动下载了三次,换成自动管理后再也没纠结过版本问题。
注意:Selenium 4.x 使用 ChromeDriver 时,最好把浏览器和驱动都保持在较新版本,否则可能遇到“无法连接到已经退出的浏览器进程”之类的怪问题。
3. 核心实现:动态页面渲染与解析的关键点
3.1 Selenium 的等待策略:隐式等待和显式等待怎么选
动画页面最怕的就是“元素还没渲染出来,代码就冲上去找它”。Selenium 提供了隐式等待和显式等待两种方式。
隐式等待是指 driver 在查找元素时,如果元素不存在,会在一段时间内轮询等待。设置方法:
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));这段代码是“全局兜底”,只要 findElement 找不到元素,就等一会儿再找。但它的坑在于,对“需要判断多个条件”的场景不够灵活。
显式等待则是针对某个具体条件去等,更可靠:
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15)); wait.until(d -> ((JavascriptExecutor) d) .executeScript("return document.readyState").equals("complete"));核心项目的经验是:不要只依赖隐式等待。因为滚动加载新内容时,页面的 DOM 会不断变化,用显式等待去确认“新卡片数量是否增长”会更准确。比如记录当前卡片数,滚动后等待卡片数增长,再继续执行。
3.2 Jsoup 接过 pageSource 后的解析逻辑
Selenium 拿到完整页面后,立刻调 getPageSource() 获取当前 DOM 对应的 HTML,然后交给 Jsoup 解析。这一步性能不错,因为 Jsoup 是纯内存解析,不需要再发网络请求。
解析的核心是找到卡片的公共选择器。假设每张卡片的结构类似这样:
<div class="video-card">Document doc = Jsoup.parse(pageSource); Elements cards = doc.select("div.video-card"); List<VideoItem> items = new ArrayList<>(); for (Element card : cards) { VideoItem item = new VideoItem(); item.setVideoId(card.attr("data-vid")); item.setTitle(card.select("a.card-title").text()); item.setCover(card.select("img.card-cover").attr("src")); item.setAuthor(card.select("div.author").text()); item.setPlayCount(card.select("span.play-count").text()); items.add(item); }这里有几个点特别容易踩坑。第一,attr("src") 在某些页面可能返回相对路径,需要拼上域名。第二,播放量文本如果是“12.3万”,后续排序时要转成数值,否则 CSV 里按字符串排序会出错。第三,如果某个字段为空,不要直接 break,因为一个卡片出问题不代表整页失败。
3.3 封面图和视频地址的进一步处理
封面图地址通常会直接出现在 img 标签的 src 里,这个抓下来就能用。但视频真实播放地址一般不会出现在列表页 DOM 里,它往往在详情页的 JS 变量或请求接口返回的 JSON 中。如果项目只要求拿到视频的基本信息和播放页入口,那列表页已经够了。
如果硬要抓播放地址,需要额外进入详情页,再构造另一个请求去拿 JSON 接口,还得处理签名参数。这个复杂度会明显上升,而且容易触发风控。建议在一开始的计划阶段就明确:到列表页的卡片信息为止,还是真的要下载视频文件。大部分业务场景下,“拿到播放页链接 + 封面 + 标题”已经能支持后续做内容分析和推荐了。
4. 实操过程与关键代码走读
4.1 让浏览器像人一样滚动加载
列表页第一屏只有十个卡片,想拿更多数据就必须滚动。最简单的滚动方案是反复执行 window.scrollTo(0, document.body.scrollHeight),每滚一次就等待 1 到 2 秒,让新内容有时间渲染。限制滚动次数,比如最多 20 次,防止页面无限加载。
JavascriptExecutor js = (JavascriptExecutor) driver; int lastCardCount = 0; for (int i = 0; i < 20; i++) { js.executeScript("window.scrollTo(0, document.body.scrollHeight)"); Thread.sleep(1500); int currentCount = driver.findElements(By.cssSelector("div.video-card")).size(); if (currentCount == lastCardCount) { System.out.println("页面没有新内容加载,提前结束滚动"); break; } lastCardCount = currentCount; }这里用 Thread.sleep 其实是为了模拟真实操作节奏,而不是简单粗暴地无脑滚动。在滚动循环里判断卡片数量是否增长,可以避免盲目等待,效率更高。
4.2 组装完整的爬虫主流程
下面是一个完整的、可运行的 Java 示例类骨架。为了不绑定具体站点,所有选择器都写成变量,你替换成目标站点的配置即可。
public class VideoCrawler { private static final String LIST_URL = "https://example-video-demo.com/list"; private static final String OUTPUT_FILE = "videos.csv"; public static void main(String[] args) throws IOException { WebDriverManager.chromedriver().setup(); WebDriver driver = new ChromeDriver(); try { driver.manage().window().maximize(); driver.get(LIST_URL); scrollAndWait(driver); String pageSource = driver.getPageSource(); List<VideoItem> items = parsePage(pageSource); writeToCsv(items); System.out.println("共抓取到 " + items.size() + " 条视频信息"); } finally { if (driver != null) { driver.quit(); } } } }这个骨架里有几个动作是必须的:finally 里 quit、滚动后等待、输出文件目录校验。缺了哪一个,实战中都可能出问题。
4.3 数据落地:写 CSV 文件的细节
写 CSV 最常见的坑是编码和字段格式。Excel 打开 UTF-8 编码的 CSV 时会乱码,解决方案是生成 UTF-8 BOM 头,或者直接用 Excel 能识别的 GBK 编码。我当时用的是 UTF-8,加了一个 BOM 标识,保证 Excel 打开不乱码。
private static void writeToCsv(List<VideoItem> items) throws IOException { PrintWriter writer = new PrintWriter(new OutputStreamWriter( new FileOutputStream(OUTPUT_FILE), StandardCharsets.UTF_8)); writer.write("\uFEFF"); // BOM writer.println("videoId,title,author,playCount,cover,detailUrl"); for (VideoItem item : items) { writer.println(item.toCsvRow()); } writer.flush(); writer.close(); }字段里如果有逗号、换行符,必须处理。最省事的是统一给文本字段前后加英文双引号,再把文本内部的英文双引号替换成两个。否则文件打开后列会错位,而且排查起来特别痛苦。
4.4 一次完整的运行记录
我当时在示例站上测试,第一次只抓到 10 条,因为 Selenium 启动后页面加载还没完成,滚动就已经执行了。后来改成等 document.readyState 为 complete,并且滚动前先记录卡片数,运行才稳定下来。最终跑下来,一次抓了 240 条视频信息,输出 CSV 约 180KB,耗时 1 分钟左右。这个速度完全够用,因为中间故意插入了等待时间,避免给目标服务器造成压力。
5. 常见问题与排查技巧实录
5.1 页面元素定位不到或为空
这是 Selenium 新手最常碰到的问题,原因大多不在于选择器写错,而在于时机不对。页面刚打开时,JS 还没有把数据渲染进去,元素自然为空。解决办法是在滚动前增加显式等待,确认第一个卡片已经出现,再进入循环。
另外,某些站点会针对 WebDriver 移除真实内容,替换成反爬提示或空数据。遇到这种情况,可以先手动打开开发者工具,看看正常浏览器中 DOM 长什么样,再对比 Selenium 拿到的 pageSource。如果差异很大,优先检查请求头、User-Agent 和浏览器指纹。把 Chrome 的自动化控制标志隐藏掉可以缓解一部分问题。
5.2 Jsoup 解析到的内容与浏览器中看到的不一致
可能的原因有两个。一是 Selenium 拿到 pageSource 的时机太早,JS 还没全部渲染完;二是页面中存在 iframe,内容嵌在 iframe 里,需要先切入 iframe 再取页面源码。多数情况都是时机问题,建议在滚动完以后,再等 2 秒左右,让所有懒加载图片和异步数据都稳定下来。
5.3 滚动加载触发登录弹窗或风控
短视频类平台很常见的行为是:你连续滚动几十次后,突然弹出登录框,或者界面提示行为异常。遇到这种情况不要硬闯,最稳妥的做法是降低采集频率,增加随机等待时间。比如每次滚动后睡眠 2 到 4 秒之间随机取值,模拟人的浏览节奏,而不是固定 1 秒。
如果站点本身需要登录才能看到完整列表,更合理的方案是绕过自动化,先用 Selenium 登录一次,并把产生的 Cookie 存下来,后续请求带上 Cookie。但要注意,这种做法只适用于你有合法账号且平台允许数据下载的场景,不能用来绕开付费墙或用户授权。
5.4 浏览器进程没有被释放导致内存膨胀
跑几次爬虫后,如果系统里残留大量 chromedriver 进程,内存很容易被占满。原因通常是代码里没有调用 quit(),只调用了 close()。close 只关闭当前标签页,而进程还在后台。正确做法是在 finally 块里调用 driver.quit();如果程序崩溃了,还要在启动前先清理残留进程。Window 下可以手动结束,Linux/Mac 下用 pkill 也可以,但最好的方式还是把代码写严谨。
5.5 常用问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 找不到元素 NoSuchElementException | 页面未加载完成、选择器写错 | 增加显式等待,核对 CSS 选择器 |
| Chrome 启动失败 | 驱动版本与浏览器不匹配 | 使用 WebDriverManager 自动管理 |
| 中文乱码 | CSV 编码问题 | 输出 UTF-8 BOM 或改用 GBK |
| 页面滚动无新数据 | 触发了反爬、登录弹窗 | 降低频率,检查 Cookie 和 UA |
| 视频播放地址为空 | 播放地址在详情页接口里 | 明确抓取范围,不强行破解签名 |
6. 合规边界与个人体会
6.1 抓取前必须想清楚的边界问题
爬虫技术本身是中性的,但项目做不做、做到什么程度,边界一定要提前想清楚。公开的列表信息,在合理频率下抓取做数据分析,通常问题不大;但涉及用户个人信息、付费内容、商业机密,或者对目标站点造成明显性能压力,性质就完全不一样了。
我的建议是:第一,遵守目标站点公开的服务协议和 robots.txt 规则;第二,抓下来的数据不要公开传播,更不要用于商业推荐;第三,控制抓取频率和并发量,不要用多线程几十个并发去扫一个普通站点。以这个项目为例,单线程、每页等待 1.5 秒的方案,已经足够完成演示数据采集,不需要开线程池。
6.2 这个项目还可以怎么扩展
一次跑通的爬虫只是起点。做完整一点,可以增加数据去重和增量更新,把 videoId 记入数据库,下一次只抓新出现的卡片;也可以在解析阶段加入关键词过滤,只保留你关注的内容分类;还可以把 Jsoup 解析出来的数据清洗一遍,把“12.3万”之类的文本转成数值,再入库做排序。
我自己在实际操作中最深的一点体会是:技术选型不要太花哨。这个项目里,Selenium 解决“页面不完整”的问题,Jsoup 解决“解析简单”的问题,两个工具的定位非常清晰,配合起来几乎没有冗余代码。反爬当然是真实存在的挑战,但大部分情况下,做好等待、降频、伪装好 User-Agent,就能解决 80% 的问题,真正需要去对抗签名算法的场景少之又少,而且那种场景也大概率不该碰。爬虫项目最有价值的部分,不是把代码跑通的那一刻,而是从一开始就理解了页面结构、数据流向和边界限制。这套思路放之四海而皆准,Java 如此,换成 Python 也一样。