Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

リンクの抽出

Web ページの HTML からすべてのリンクを抽出する

reqwest-badge select-badge cat-net-badge

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 ページのリンク切れを確認する

reqwest-badge select-badge url-badge cat-net-badge

get_base_url を呼び出してベース URL を取得します。ドキュメントに base タグがある場合は、 base タグから href attr を取得します。元の URL の Position::BeforePath がデフォルトとして機能します。

ドキュメント内のリンクを反復処理し、各リンクを url::ParseOptionsUrl::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-badge regex-badge cat-net-badge

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(())
}