先说结论这个项目标题里提到的“西瓜小视频”我在这里把它当成一个纯演示用的虚构站点数据也都是模拟数据不特指任何真实平台。整篇文章分享的是用 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 17Maven 管理依赖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 groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version4.20.0/version /dependency dependency groupIdorg.jsoup/groupId artifactIdjsoup/artifactId version1.17.2/version /dependency dependency groupIdio.github.bonigarcia/groupId artifactIdwebdrivermanager/artifactId version5.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”的错。更省心的方式是用 WebDriverManagerWebDriverManager.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 classvideo-card>Document doc Jsoup.parse(pageSource); Elements cards doc.select(div.video-card); ListVideoItem 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(); ListVideoItem 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(ListVideoItem 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 也一样。