Извлеките родственные элементы с помощью скребкового ящика

#web-scraping #rust #rust-tokio #reqwest

Вопрос:

Изучая ржавчину, я пытаюсь создать простой веб-скребок. Моя цель-наскрести https://news.ycombinator.com/ и получите заголовок, гиперссылку, голоса и имя пользователя. Я использую для этого внешние библиотеки reqwest и scraper и написал программу, которая удаляет HTML-ссылку с этого сайта.

Груз.томл

 [package]
name = "stackoverflow_scraper"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
scraper = "0.12.0"
reqwest = "0.11.2"
tokio = { version = "1", features = ["full"] }
futures = "0.3.13"
 

src/main.rs

 use scraper::{Html, Selector};
use reqwest;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "https://news.ycombinator.com/";
    let html = reqwest::get(url).await?.text().await?;
    let fragment = Html::parse_fragment(html.as_str());
    let selector = Selector::parse("a.storylink").unwrap();

    for element in fragment.select(amp;selector) {
        println!("{:?}",element.value().attr("href").unwrap());
        // todo println!("Title");
        // todo println!("Votes");
        // todo println!("User");
    }

    Ok(())
}
 

Как мне получить соответствующее название, голоса и имя пользователя?

Ответ №1:

Элементы на первой странице хранятся в table классе with .itemlist .

Поскольку каждый элемент состоит из трех последовательных <tr> элементов , вам придется перебирать их частями по три. Я решил сначала собрать все узлы.

Первая строка содержит:

  • Название
  • Домен

Вторая строка содержит:

  • Очки
  • Автор
  • Пост-возраст

Третий ряд-это распорка, которую следует игнорировать.

Примечание:

  • Сообщения, созданные в течение последнего часа, по-видимому, не отображают никаких точек, поэтому с этим необходимо обращаться соответствующим образом.
  • Рекламные объявления не содержат имени пользователя.
  • Последние две строки таблицы tr.morespace и tr содержащие a.morelink их следует игнорировать. Вот почему я решил сначала .collect() использовать узлы, а затем использовать .chunks_exact() .
 use reqwest;
use scraper::{Html, Selector};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "https://news.ycombinator.com/";
    let html = reqwest::get(url).await?.text().await?;
    let fragment = Html::parse_fragment(html.as_str());

    let selector_items = Selector::parse(".itemlist tr").unwrap();

    let selector_title = Selector::parse("a.storylink").unwrap();
    let selector_score = Selector::parse("span.score").unwrap();
    let selector_user = Selector::parse("a.hnuser").unwrap();

    let nodes = fragment.select(amp;selector_items).collect::<Vec<_>>();

    let list = nodes
        .chunks_exact(3)
        .map(|rows| {
            let title_elem = rows[0].select(amp;selector_title).next().unwrap();
            let title_text = title_elem.text().nth(0).unwrap();
            let title_href = title_elem.value().attr("href").unwrap();

            let score_text = rows[1]
                .select(amp;selector_score)
                .next()
                .and_then(|n| n.text().nth(0))
                .unwrap_or("0 points");

            let user_text = rows[1]
                .select(amp;selector_user)
                .next()
                .and_then(|n| n.text().nth(0))
                .unwrap_or("Unknown user");

            [title_text, title_href, score_text, user_text]
        })
        .collect::<Vec<_>>();

    println!("links: {:#?}", list);

    Ok(())
}
 

Это должно привести вас к следующему списку:

 [
    [
        "Docker for Mac M1 RC",
        "https://docs.docker.com/docker-for-mac/apple-m1/",
        "327 points",
        "mikkelam",
    ],
    [
        "A Mind Is Born – A 256 byte demo for the Commodore 64 (2017)",
        "https://linusakesson.net/scene/a-mind-is-born/",
        "226 points",
        "matthewsinclair",
    ],
    [
        "Show HN: Video Game in a Font",
        "https://www.coderelay.io/fontemon.html",
        "416 points",
        "ghub-mmulet",
    ],
    ...
]
 

Кроме того, существует доступный API, который можно использовать:

Ответ №2:

Это скорее вопрос селекторов, и он зависит от html-кода очищаемого сайта. В этом случае легко получить титул, но сложнее получить очки и пользователя. Поскольку используемый вами селектор выбирает ссылку, содержащую как href, так и заголовок, вы можете получить заголовок с помощью метода .text()

 let title = element.text().collect::<Vec<_>>();
 

где элемент такой же, как и для href

Однако, чтобы получить другие значения, было бы проще изменить первый селектор и получить данные из него. Начиная с заголовка и ссылки новостной статьи на news.ycombinator.com находится в элементе с классом .athing, а голоса и пользователь находятся в следующем элементе, у которого нет класса (что затрудняет выбор), возможно, лучше всего выбрать "table.itemlist tr.athing" и повторить эти результаты. Из каждого найденного элемента вы можете затем "a.storylink" выбрать элемент и отдельно получить следующий элемент tr и выбрать точки и пользовательские элементы

 let select_item = Selector::parse("table.itemlist tr.athing").unwrap();
let select_link = Selector::parse("a.storylink").unwrap();
let select_score = Selector::parse("span.score").unwrap();

for element in fragment.select(amp;select_item) {
    // Get the link element that contains the href and title
    let link_el = element.select(amp;select_link).next().unwrap();
    println!("{:?}", link_el.value().attr("href").unwrap());

    // Get the next tr element that follows the first, with score and user
    let details_el = ElementRef::wrap(element.next_sibling().unwrap()).unwrap();
    // Get the score element from within the second row element
    let score = details_el.select(amp;select_score).next().unwrap();
    println!("{:?}", score.text().collect::<Vec<_>>());
}
 

Это показывает только получение href и оценку. Я оставлю это вам, чтобы вы получили пользователя от details_el