Flutter 实现导出豆瓣书单功能

2024-09-02 16:28:08 浏览数 (2)

主要是之前 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,就直接监听

代码语言:dart复制
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 给解析出来的,然后我就自己取子串了。

代码语言:dart复制
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);
      }
}

项目地址

0 人点赞