
本文介绍为何传统 html 解析无法获取网页中由 javascript 动态渲染的 pdf 链接,并提供基于 f# 的浏览器自动化解决方案(canopy + chromedriver),实现可靠、可等待、可交互的 pdf 链接提取。
在爬取类似 https://www.kodis.cz/lines/region?tab=232-293 这类现代前端网站时,你可能会发现:无论使用 FSharp.Data.HtmlProvider、HtmlAgilityPack 还是 HttpClient 获取原始 HTML,都无法提取到 PDF 文件链接(如 https://kodis-files.s3.eu-central-1.amazonaws.com/...pdf)。根本原因在于:这些 <a href="...pdf"> 标签并非存在于服务端返回的初始 HTML 中,而是由 React 框架在浏览器中运行 JavaScript 后动态插入 DOM 的。
这意味着:
- 服务端响应中 <div id="__next"> 内容为空或仅含占位符;
- 所有 PDF 链接均由客户端 JS 异步加载(可能通过 API 请求后渲染);
- 纯静态 HTML 解析器(包括 HtmlDocument.Load())只能看到“骨架”,看不到“血肉”。
✅ 正确解法:使用真实浏览器环境执行 JS,再提取渲染后的 DOM。
推荐方案:F# + Canopy 浏览器自动化
Canopy 是一个轻量、函数式风格的 F# 浏览器自动化库(底层基于 Selenium WebDriver),专为简洁、可读性强的 UI 测试与爬虫场景设计。
Windows 命令行编码与兼容规范。涵盖 GBK/UTF-8 编码、PowerShell/pwsh 互操作、Python/Node.js、Git 配置及代码生成规则。适用于 Windows 10/11 + MSYS2/Git Bash 环境下的所有命令行操作。
立即学习“Java免费学习笔记(深入)”;
✅ 快速上手步骤
-
安装依赖(在 .fsx 脚本顶部):
#r "nuget:canopy" #r "nuget:Selenium.WebDriver" #r "nuget:Selenium.WebDriver.ChromeDriver"
-
配置 ChromeDriver 路径(需提前下载匹配版本的 chromedriver.exe):
open canopy open canopy.classic
canopy.configuration.chromeDir <- @"C:\path\to\your\chromedriver\" // Windows 示例 // Linux/macOS: "/usr/local/bin/chromedriver"
3. **启动浏览器并导航**: ```fsharp start chrome url "https://www.kodis.cz/lines/region?tab=232-293"
-
智能等待 + 提取 PDF 链接(关键!避免因加载延迟漏抓):
// 定义就绪条件:至少出现 11 个 PDF 卡片(根据实际页面结构调整) let pdfCardsLoaded () = (elements ".Card_wrapper__ZQ5Fp").Length >= 11
compareTimeout <- 10.0 // 允许最长等待 10 秒 waitFor pdfCardsLoaded
// 提取所有含 PDF 的 href 属性 elements "a" |> List.filter (fun el -> let href = el.GetAttribute("href") System.String.IsNullOrEmpty(href) |> not && href.EndsWith(".pdf", System.StringComparison.OrdinalIgnoreCase)) |> List.iter (fun el -> let href = el.GetAttribute("href") printfn "PDF link: %s" href)
> ? 提示:若页面支持“加载更多”(如 “Další” 按钮),可模拟点击分页:
> ```fsharp
> click (elementWithText "button" "Další") // 或用更精确的选择器,如 "[data-testid='load-more']"
> waitFor (fun () -> (elements ".Card_wrapper__ZQ5Fp").Length >= 22)
> ```
#### ⚠️ 注意事项
- **务必设置 `waitFor`**:React 页面渲染有延迟,直接 `elements "a"` 可能返回空结果;
- **使用 `GetAttribute("href")` 而非 `InnerText()`**:PDF 链接常位于空 `<a>` 标签内(无文本内容),仅靠 `href` 属性存在;
- **PDF 判断建议用 `.EndsWith(".pdf", OrdinalIgnoreCase)`**:避免误判查询参数(如 `?file=report.pdf&t=123`);
- **生产环境请添加异常处理与资源清理**:
```fsharp
try
// ... 主逻辑
finally
quit() // 关闭浏览器? 替代方案对比
| 方案 | 是否执行 JS | 是否需浏览器 | 易用性 | 适用场景 |
|---|---|---|---|---|
| HtmlAgilityPack / FSharp.Data | ❌ | ❌ | ⭐⭐⭐⭐ | 静态 HTML(服务端渲染 SSR) |
| Puppeteer Sharp (.NET) | ✅ | ✅ | ⭐⭐⭐ | 需精细控制(如拦截请求、截图) |
| Canopy | ✅ | ✅ | ⭐⭐⭐⭐⭐ | F# 项目首选:语法简洁、等待机制成熟、社区活跃 |
| 直接调用后端 API(如你已发现的 /api/schedules) | N/A | ❌ | ⭐⭐⭐⭐⭐ | 最优解(若接口稳定且无鉴权)——绕过渲染层,高效可靠 |
✅ 最佳实践建议:优先逆向分析网络请求(Chrome DevTools → Network → XHR/Fetch)。若发现 PDF 列表由 GET /api/documents?tab=232-293 返回 JSON,则直接调用该 API(配合 HttpClient + JWT/cookie 处理)比浏览器自动化更稳定、更快、更易维护。
总之,面对 JS 渲染的 PDF 链接,放弃“静态解析幻想”,拥抱“真实浏览器执行”——Canopy 让这一过程在 F# 中既强大又优雅。


















