在Lettura中已经基本实现了RSS阅读器的能力,日常的基本使用已经没有太大问题。 但是有很多RSS的源返回的主体内容只会有一些很简单的摘要信息,如果想查看更多内容,只能点击外链前往浏览器阅读。当遇到一些“标题党”文章时,前往浏览器阅读又会带来一些来自于这个额外操作的挫败感。所以一直想在Lettura中加上直接阅读全文的能力,让使用体验能够更加舒服一些。
在一些桌面阅读器中,也支持访问Web页面的能力。部分原生桌面程序比如Reeder、Readkit等通过WebView访问页面,有一些软件则通过工具提取页面的内容,再自定义展示。
可能有同学会想到,在Web中可以通过iframe标签代替WebView实现页面的加载。但是实际上iframe有诸多的限制,站点可以通过设置响应头X-Frame-Options
或者Content-Security-Policy
禁止自己的站点被iframe嵌入。
"X-Frame-Options": deny|saneorigin|allow-from [url]
"Content-Security-Policy": "frame-ancestors 'self'"
也可以通过JavaScrpit判断当前的页面的顶级窗口window.top
和自身窗口window.self
是否相等,如果不相等,则是因为嵌入了iframe。
if (window.top != window.self) {
window.top.location = window.self.location; // 替换顶级窗口的地址
}
iframe并不能完成解决当前的问题。在Tauri生态中,也有关于相关的讨论,截止到本文发布时间,还没有实现相关特性。
页面内容提取在不同编程语言中都有相对较为热门的工具,比如Python中的mercy-reader,Node.js中的 @postlight/mercury-parser。万变不离其宗,其核心的思路都可以简化为以下几个步骤:
- 发起HTTP请求访问url
- 解析返回的HTML内容,构建返回的数据格式
既然如此,用Rust来实现内容的提取吧。参考自 https://www.scrapingbee.com/blog/web-scraping-rust/ 的文章,使用 reqwest 发起网络请求,使用 scraper 解析页面内容,抓取页面案例也依然使用IMDB。
创建项目,添加依赖
为了更好的演示,创建一个全新的项目。
cargo new web_scraper
在Cargo.toml中添加依赖。
[dependencies]
reqwest = {version = "0.11", features = ["blocking"]}
scraper = "0.12.0"
获取网页的HTML
reqwest是Rust生态中使用相当广泛的HTTP请求库,它提供的功能非常全面,基本上能满足大部分浏览器的能力。reqwest::Client 默认是异步的,少量请求时,使用同步的reqwest::blocking会更加方便一些。
fn main() {
let response = reqwest::blocking::get(
"https://www.imdb.com/search/title/?groups=top_100&sort=user_rating,desc&count=100",
)
.unwrap()
.text()
.unwrap();
}
从 HTML 中提取信息
Web 抓取中最难的部分通常是从 HTML 文档中获取需要的特定信息。在Python中有pyquery,Node.js中有cherrio,JavaScript中有jQuery,Rust 中常用的工具是 scraper 库。它将 HTML 文档解析为树状结构来工作,可以使用 CSS 选择器来查询元素。在核心用法上这几个工具基本上大同小异。
首先将返回的数据解析成为scraper定义的数据结构。
let document = scraper::Html::parse_document(&response);
然后通过选择器找到自己想要的内容。比如,在这个例子中,我要找到IMDB中Top100的电影标题。首先通过浏览器的审查元素找到页面的结构,
<h3 class="lister-item-header">
<span class="lister-item-index unbold text-primary">1.</span>
<a href="/title/tt0111161/?ref_=adv_li_tt">The Shawshank Redemption</a>
<span class="lister-item-year text-muted unbold">(1994)</span>
</h3>
构造出能够命中元素的CSS选择器,获取到元素内容
let title_selector = scraper::Selector::parse("h3.lister-item-header>a").unwrap();
let titles = document.select(&title_selector).map(|x| x.inner_html());
titles
是一个包含了100个元素的迭代器,可以遍历输出
titles.zip(1..101).for_each(|(item, number)| println!("{}. {}", number, item));
完整的代码如下:
fn main() {
let response = reqwest::blocking::get(
"https://www.imdb.com/search/title/?groups=top_100&sort=user_rating,desc&count=100",
)
.unwrap()
.text()
.unwrap();
let document = scraper::Html::parse_document(&response);
let title_selector = scraper::Selector::parse("h3.lister-item-header>a").unwrap();
let titles = document.select(&title_selector).map(|x| x.inner_html());
titles
.zip(1..101)
.for_each(|(item, number)| println!("{}. {}", number, item));
}
接下来执行cargo run
,如果编译通过,你应该可以看到以下的输出:
1. The Shawshank Redemption
2. The Godfather
3. The Dark Knight
4. The Lord of the Rings: The Return of the King
5. Schindler's List
6. The Godfather: Part II
7. 12 Angry Men
8. Pulp Fiction
9. Inception
10. The Lord of the Rings: The Two Towers
...