リンクの抽出
Web ページの HTML からすべてのリンクを抽出する
reqwest::get を使用して HTTP GET リクエストを実行し、次に
Document::from_read を使用してレスポンスを HTML ドキュメントとして解析します。
Name の条件に “a” を指定した find は、すべてのリンクを取得します。
Selection に対して filter_map を呼び出すと、URL を取得できます
“href” attr(属性)を持つリンクから。
// select には rand v.0.8 が必要です
// cargo-deps: tokio="1", reqwest="0.11", select="0.6", thiserror="1"
mod links {
use thiserror::Error;
use select::document::Document;
use select::predicate::Name;
#[derive(Error, Debug)]
pub enum LinkError {
#[error("Reqwest エラー: {0}")]
ReqError(#[from] reqwest::Error),
#[error("IO エラー: {0}")]
IoError(#[from] std::io::Error),
}
pub async fn get_links(page: &str) -> Result<Vec<Box<str>>, LinkError> {
let res = reqwest::get(page)
.await?
.text()
.await?;
let links = Document::from(res.as_str())
.find(Name("a"))
.filter_map(|node| node.attr("href"))
.into_iter()
.map(|link| Box::<str>::from(link.to_string()))
.collect();
Ok(links)
}
}
#[tokio::main]
async fn main() -> Result<(), links::LinkError> {
let page_links = links::get_links("https://www.rust-lang.org/en-US/").await?;
for link in page_links {
println!("{}", link);
}
Ok(())
}
Web ページのリンク切れを確認する
get_base_url を呼び出してベース URL を取得します。ドキュメントに base タグがある場合は、
base タグから href attr を取得します。元の
URL の Position::BeforePath がデフォルトとして機能します。
ドキュメント内のリンクを反復処理し、各リンクを url::ParseOptions と Url::parse でパースする tokio::task::spawn タスクを作成します。
そのタスクは reqwest でリンクにリクエストを送信し、
StatusCode を検証します。 その後、プログラムを終了する前に
各タスクの完了を await します。
// cargo-deps: tokio="1", reqwest="0.11", select="0.6", thiserror="1", url="2", anyhow="1"
mod broken {
use thiserror::Error;
use reqwest::StatusCode;
use select::document::Document;
use select::predicate::Name;
use std::collections::HashSet;
use url::{Position, Url};
#[derive(Error, Debug)]
pub enum BrokenError {
#[error("Reqwest error: {0}")]
ReqError(#[from] reqwest::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("URL parse error: {0}")]
UrlParseError(#[from] url::ParseError),
#[error("Join error: {0}")]
JoinError(#[from] tokio::task::JoinError),
}
pub struct CategorizedUrls {
pub ok: Vec<String>,
pub broken: Vec<String>,
}
enum Link {
GoodLink(Url),
BadLink(Url),
}
async fn get_base_url(url: &Url, doc: &Document) -> Result<Url, BrokenError> {
let base_tag_href = doc.find(Name("base")).filter_map(|n| n.attr("href")).nth(0);
let base_url =
base_tag_href.map_or_else(|| Url::parse(&url[..Position::BeforePath]), Url::parse)?;
Ok(base_url)
}
async fn check_link(url: &Url) -> Result<bool, BrokenError> {
let res = reqwest::get(url.as_ref()).await?;
Ok(res.status() != StatusCode::NOT_FOUND)
}
pub async fn check(site: &str) -> Result<CategorizedUrls, BrokenError> {
let url = Url::parse(site)?;
let res = reqwest::get(url.as_ref()).await?.text().await?;
let document = Document::from(res.as_str());
let base_url = get_base_url(&url, &document).await?;
let base_parser = Url::options().base_url(Some(&base_url));
let links: HashSet<Url> = document
.find(Name("a"))
.filter_map(|n| n.attr("href"))
.filter_map(|link| base_parser.parse(link).ok())
.collect();
let mut tasks = vec![];
let mut ok = vec![];
let mut broken = vec![];
for link in links {
tasks.push(tokio::spawn(async move {
if check_link(&link).await.unwrap_or(false) {
Link::GoodLink(link)
} else {
Link::BadLink(link)
}
}));
}
for task in tasks {
match task.await? {
Link::GoodLink(link) => ok.push(link.to_string()),
Link::BadLink(link) => broken.push(link.to_string()),
}
}
Ok(CategorizedUrls { ok, broken })
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let categorized = broken::check("https://www.rust-lang.org/en-US/").await?;
println!("OK: {:?}", categorized.ok);
println!("Broken: {:?}", categorized.broken);
Ok(())
}
MediaWikiマークアップから一意なリンクをすべて抽出する
reqwest::get を使って MediaWiki ページのソースを取得し、その後
Regex::captures_iter を使って内部リンクと外部リンクのすべての出現箇所を
検索します。Cow を使用すると、String の過剰な割り当てを避けられます。
MediaWiki のリンク構文はこちらで説明されています。 呼び出し元の 関数は文書全体を保持し、リンクは元の文書を参照するスライス として返されます。
// cargo-deps: tokio="1", reqwest="0.11", regex="1", anyhow="1"
mod wiki {
use regex::Regex;
use std::borrow::Cow;
use std::collections::HashSet;
use std::sync::LazyLock;
pub fn extract_links(content: &str) -> HashSet<Cow<str>> {
static WIKI_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(
r"(?x)
\[\[(?P<internal>[^\[\]|]*)[^\[\]]*\]\] # 内部リンク
|
(url=|URL\||\[)(?P<external>http.*?)[ \|}] # 外部リンク
"
)
.unwrap()
);
let links: HashSet<_> = WIKI_REGEX
.captures_iter(content)
.map(|c| match (c.name("internal"), c.name("external")) {
(Some(val), None) => Cow::from(val.as_str()),
(None, Some(val)) => Cow::from(val.as_str()),
_ => unreachable!(),
})
.collect::<HashSet<_>>();
links
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let content = reqwest::get(
"https://en.wikipedia.org/w/index.php?title=Rust_(programming_language)&action=raw",
)
.await?
.text()
.await?;
println!("{:#?}", wiki::extract_links(content.as_str()));
Ok(())
}