主要是之前 Python Obsidian(Projects)导出豆瓣书单为了反爬,直接用自动化操作浏览器,但是这样不知道浏览器何时加载好,之前就不管如何等待 10 秒,本来就比爬虫慢,这样就更慢了,于是想到通过 Flutter 用内置的 WebView 加载,加载后再去获取 html。同时用来练手,实践适合桌面端的一些 API 和库。
第一步,获取到 html
Flutter 在桌面平台的 WebView 库不怎么多,许多也不怎么好用,最后选了一个 desktop_webview_window 算是可以用,它不是在 Widget 中加载,而是打开了一个新的窗口。所以我把它尺寸设成 0 了,不要让用户看到。
代码语言:dart复制webview ??= await WebviewWindow.create(
configuration: const CreateConfiguration(
windowHeight: 0,
windowWidth: 0,
),
);
webview!.launch(url); // 加载 url
如何获取 html 呢?问 AI,说是执行一句 'document.documentElement.outerHTML'
,但现在问题是要知道何时已经加载好了,可以执行这句 js 了,网上查了点资料,说这个 webview!.isNavigating
可以用来判断,如果正在加载着还没结束,肯定还是正在 navigating 状态的。类型是 ValueListenable<bool>
,由于我想把这个逻辑放在一个 package 里,不涉及 Widget,所以也就没放到 ValueListenableBuilder 里等值变成 fasle 再去执行 js,就直接监听
listener = () async {
if (!webview!.isNavigating.value) { // fasle 表示加载完成
var html = await webview!.evaluateJavaScript('document.documentElement.outerHTML');
// 如果页面加载出错,返回 <html><head></head><body></body></html>
if (html == r'<html><head></head><body></body></html>') {
html = null;
}
callback(html); // Function(String?) callback 一个回调函数
}
};
webview!.isNavigating.addListener(listener!);
如果加载失败,传一个非法的路径,好像也没什么特别的现象,于是自己加一个超时。
代码语言:dart复制Duration timeout = const Duration(seconds: 10)
timer = Timer(timeout, () {
callback(null);
});
listener = () async {
if (!webview!.isNavigating.value) { // fasle 表示加载完成
// 如果超时那执行过了,就不用再回调了
if (timer?.isActive ?? false) {
timer?.cancel();
...
}
}
};
最后定义一个释放的方法供调用者在销毁页面时调用
代码语言:dart复制void dispose() {
if (listener != null) {
webview?.isNavigating.removeListener(listener!);
}
timer?.cancel();
}
解析 html
基本上就是仿着之前到 Python 代码写。比如豆列的解析
代码语言:dart复制var document = parse(html);
var name = document.querySelector('#content > h1 > span')?.text.trim();
var items = document.getElementsByClassName('doulist-item');
for (var item in items) {
var title = item.getElementsByClassName('title');
if (title.isNotEmpty) {
var tagA = title[0].getElementsByTagName('a');
if (tagA.isNotEmpty) {
var bookUrl = tagA[0].attributes['href']; // 拿到每本书的 url
break;
}
}
}
像 <span class="pl">出版年:</span> 2016-6<br/>
这种写法,之前在 Python 里通过 next_sibling
可以拿到,但是 Flutter 这 html 库没找到对应方法,有个 nextElementSibling 结果是 <html br>
,各种属性用遍了,没找到能把 2016-6 给解析出来的,然后我就自己取子串了。
for (var pl in document.getElementsByClassName('pl')) {
final text = pl.text.trim();
// outerHtml 就是 <span class="pl">出版年:</span>
final outerHtml = pl.outerHtml;
if (pl.text.contains('出版年')) {
int start = html.indexOf(outerHtml);
int end = html.indexOf('<', start outerHtml.length);
// 拿到 2016
publishYear = html.substring(start outerHtml.length, end).trim().substring(0,4);
}
}
项目地址