웹 스크래핑에 Rust 사용하기

Feb 13 2023
웹 스크래핑은 웹사이트에서 정보를 추출하는 기술입니다. 웹 사이트에서 대량의 데이터를 가져오고, 시간과 노력을 절약하고, 정보를 다양한 목적으로 사용할 수 있도록 하는 자동화된 프로세스입니다.

웹 스크래핑은 웹사이트에서 정보를 추출하는 기술입니다. 웹 사이트에서 대량의 데이터를 가져오고, 시간과 노력을 절약하고, 정보를 다양한 목적으로 사용할 수 있도록 하는 자동화된 프로세스입니다.

이 게시물에서는 Rust를 사용하여 웹사이트에서 인용문을 추출하는 방법을 보여드리겠습니다 .

웹 스크래핑에 Rust를 사용할 때의 이점

  1. 성능 : Rust는 정적으로 유형이 지정된 컴파일된 언어로 고성능을 제공하므로 웹 스크래핑에 이상적인 선택입니다.
  2. 동시성 : Rust는 내장된 동시성 지원을 제공하여 여러 요청을 동시에 처리하기 쉽게 하여 웹 스크래핑의 효율성을 향상시킵니다.
  3. 메모리 안전 : Rust는 강력한 메모리 안전 보장을 제공하여 널 포인터 예외 및 버퍼 오버플로와 같은 일반적인 메모리 관련 버그를 방지하여 웹 스크래핑을 더 안전하게 만듭니다.
  4. 배우기 쉬움 : Rust는 다른 프로그래밍 언어에 비해 상대적으로 간단한 구문을 가지고 있어 개발자가 배우기 쉽습니다.
  1. 가파른 학습 곡선 : Rust의 구문은 상대적으로 단순하지만 특히 정적으로 유형이 지정되고 컴파일된 언어를 처음 사용하는 개발자에게는 언어를 배우기가 어려울 수 있습니다.
  2. 제한된 커뮤니티 : Rust에는 성장하는 커뮤니티가 있지만 여전히 다른 프로그래밍 언어만큼 널리 사용되지는 않습니다. 즉, 리소스와 지원을 찾는 것이 어려울 수 있습니다.
  3. 내장 웹 스크래핑 라이브러리 없음 : Rust에는 웹 스크래핑을 위한 내장 라이브러리가 없습니다. 즉, 개발자는 덜 편리할 수 있는 타사 라이브러리를 사용해야 합니다.

이제 를 설치하고 Rust의 버전을 확인 Cargo하고 세 개의 웹 스크래핑 라이브러리인 , reqwest및 scraper명령 select줄 인터페이스를 설치합니다.

녹 설치

Rust를 설치하려면 Rustup 도구를 사용할 수 있습니다. 이는 Rust를 설치하는 데 권장되는 방법입니다. 터미널에서 다음 명령을 실행할 수 있습니다.

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

화물 설치

Cargo는 Rust의 패키지 관리자이며 Rust를 설치하면 자동으로 설치됩니다. 다음 명령을 실행하여 Cargo가 설치되어 있는지 확인할 수 있습니다.

cargo --version

터미널에서 다음 명령을 실행합니다.

cargo new toscrape_quotes

reqwestRust 웹 스크래핑 라이브러리 , scraper및 을 설치하려면 파일 select에 다음 종속성을 추가할 수 있습니다 Cargo.toml.

# Do not forget to update the dependencies section of your Cargo.toml file 

[dependencies]
csv = "1.1"
reqwest = "0.10.8"
scraper = "0.13.0"
select = "0.6.0"

cd toscrape_quotes && cargo install --path .

웹 스크래핑을 해봅시다. 할까요?

Toscrape.com은 소프트웨어 엔지니어링 분야에서 그러한 노력을 기울이고자 하는 사람들을 위한 웹 스크래핑 샌드박스입니다.

기업 플랫폼이나 조직의 기업 변호사로부터 원치 않는 관심을 끌지 않고 놀이터로 사용할 수 있습니다. 그래, 당신은 그 사람들을보고 싶지 않아! 일부는 에어 조던 1 시카고 와 짝을 이루는 수트를 입습니다 .

녹 웹 스크래핑 코드

extern crate csv;
extern crate reqwest;
extern crate scraper;

use std::error::Error;
use std::fs::File;
use std::time::Duration;
use std::thread::sleep;

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

const BASE_URL: &str = "http://quotes.toscrape.com";
const MAX_PAGE: u32 = 5;
const MAX_RETRIES: u32 = 3;
const RETRY_SLEEP_SECONDS: u64 = 2;

struct Quote {
    text: String,
    author: String,
    tags: Vec<String>,
}

fn main() -> Result<(), Box<dyn Error>> {
    let client = Client::new().timeout(Duration::from_secs(30)).build()?;
    let quotes = get_quotes(&client)?;
    write_quotes_to_csv(&quotes)?;
    Ok(())
}

fn get_quotes(client: &Client) -> Result<Vec<Quote>, Box<dyn Error>> {
    let mut quotes = Vec::new();
    for page in 1..=MAX_PAGE {
        let html = retry_get(client, &format!("{}/page/{}/", BASE_URL, page))?.text()?;
        let fragment = Html::parse_document(&html);
        let quote_selector = Selector::parse("span.text").unwrap();
        let author_selector = Selector::parse("span > small.author").unwrap();
        let tag_selector = Selector::parse("div.tags > a.tag").unwrap();

        for quote in fragment.select(&quote_selector) {
            let quote_text = quote.text().collect::<Vec<_>>()[0].trim().to_owned();
            let author = quote
                .parent()
                .unwrap()
                .select(&author_selector)
                .next()
                .unwrap()
                .text()
                .collect::<Vec<_>>()[0]
                .trim()
                .to_owned();

            let tags = quote
                .parent()
                .unwrap()
                .select(&tag_selector)
                .map(|tag| tag.text().collect::<Vec<_>>()[0].trim().to_owned())
                .collect::<Vec<_>>();

            quotes.push(Quote {
                text: quote_text,
                author,
                tags,
            });
        }
    }
    Ok(quotes)
}

fn write_quotes_to_csv(quotes: &[Quote]) -> Result<(), Box<dyn Error>> {
    let mut file = File::create("toscrape_quotes.csv")?;
    let mut writer = csv::Writer::from_writer(&mut file);

    for quote in quotes {
        writer.write_record(&[quote.text, quote.author, quote.tags.join(", ")])?;
    }
    writer.flush()?;
    Ok(())
}

fn retry_get(client: &Client, url: &str) -> Result<reqwest::Response, Box<dyn Error>> {
    let mut retries = 0;
    loop {
        match client.get(url).send() {
            Ok(res) => {
                if res.status().is_success() {
                    return Ok(res);
                } else {
                    retries += 1;
                    if retries >= MAX_RETRIES {
                        return Err(format!("GET request to {} failed with status code {:?}", url, res.status()).into());
                    }
                }
            }
            Err(err) => {
                retries += 1;
                if retries >= MAX_RETRIES {
                    return Err(format!("GET request to {} failed with error: {}", url, err).into());
                }
            }
        }
        sleep(Duration::from_secs(RETRY_SLEEP_SECONDS));
    }
}

기능 main_

이 main함수는 Rust 코드의 진입점이며 다음 단계를 수행합니다.

  1. 를 사용하여 새 HTTP 클라이언트를 만듭니다 reqwest::Client.
  2. 함수 를 호출 get_quotes하고 클라이언트를 전달하여 Quote구조체 벡터를 반환합니다.
  3. 함수 를 호출 하고 따옴표를 .csv 파일에 쓰는 구조체 write_quotes_to_csv벡터를 함수에 전달합니다 .Quote
  4. write_quotes_to_csv모든 것이 성공한 경우 함수 의 결과를 반환 Ok(())하고 문제가 발생한 경우 오류를 반환합니다.

fn main() -> Result<(), Box<dyn Error>> {
    let client = Client::new().timeout(Duration::from_secs(30)).build()?;
    let quotes = get_quotes(&client)?;
    write_quotes_to_csv(&quotes)?;
    Ok(())
}

이 get_quotes함수는 에 대한 참조를 인수로 사용 하고 웹사이트에서 스크랩한 구조체 reqwest::Client벡터를 반환합니다 .Quotehttp://quotes.toscrape.com". 다음은 우리가 수행하는 작업에 대한 분석입니다.

  1. 구조체 의 빈 벡터를 초기화합니다 Quote.
  2. 스크랩할 페이지를 1부터 까지 반복합니다 MAX_PAGE.
  3. 각 페이지에 대해 함수를 사용하여 페이지의 URL에 GET 요청을 만들고 retry_get페이지의 HTML을 검색합니다.
  4. HTML을 scraper::Html조각으로 구문 분석합니다.
  5. CSS 선택기를 사용하여 HTML에서 인용문 text, author및 을 선택합니다.tags
  6. 각 인용문에 대한 새 Quote구조체를 만들고 인용문 벡터에 추가합니다.
  7. 따옴표로 구성된 벡터를 반환합니다.

fn get_quotes(client: &Client) -> Result<Vec<Quote>, Box<dyn Error>> {
    let mut quotes = Vec::new();
    for page in 1..=MAX_PAGE {
        let html = retry_get(client, &format!("{}/page/{}/", BASE_URL, page))?.text()?;
        let fragment = Html::parse_document(&html);
        let quote_selector = Selector::parse("span.text").unwrap();
        let author_selector = Selector::parse("span > small.author").unwrap();
        let tag_selector = Selector::parse("div.tags > a.tag").unwrap();

        for quote in fragment.select(&quote_selector) {
            let quote_text = quote.text().collect::<Vec<_>>()[0].trim().to_owned();
            let author = quote
                .parent()
                .unwrap()
                .select(&author_selector)
                .next()
                .unwrap()
                .text()
                .collect::<Vec<_>>()[0]
                .trim()
                .to_owned();

            let tags = quote
                .parent()
                .unwrap()
                .select(&tag_selector)
                .map(|tag| tag.text().collect::<Vec<_>>()[0].trim().to_owned())
                .collect::<Vec<_>>();

            quotes.push(Quote {
                text: quote_text,
                author,
                tags,
            });
        }
    }
    Ok(quotes)
}

이 함수는 구조체를 슬라이스하고 따옴표를 파일에 쓰기 write_quotes_to_csv위해 참조를 사용합니다 . 우리가 하는 일에 대한 분석은 다음과 같습니다.Quote.csv

  1. .csv이름으로 새 파일을 만듭니다 toscrape_quotes.csv.
  2. csv::Writer상자를 사용하여 새로 만드십시오 csv.
  3. 따옴표 조각의 각 따옴표에 대해 .csv 파일에 새 레코드를 쓰고 따옴표 text, author및 tags쉼표로 구분합니다.
  4. 기록기를 플러시하여 모든 데이터가 파일에 기록되도록 합니다.
  5. 모든 것이 성공한 경우 쓰기 작업의 결과를 반환하거나 Ok(())문제가 발생한 경우 오류를 반환합니다.

fn write_quotes_to_csv(quotes: &[Quote]) -> Result<(), Box<dyn Error>> {
    let mut file = File::create("toscrape_quotes.csv")?;
    let mut writer = csv::Writer::from_writer(&mut file);

    for quote in quotes {
        writer.write_record(&[quote.text, quote.author, quote.tags.join(", ")])?;
    }
    writer.flush()?;
    Ok(())
}

이 retry_get함수는 문자열 URL에 대한 참조 reqwest::Client와 참조를 취하고 클라이언트를 사용하여 URL에 HTTP GET 요청을 합니다. 요청이 실패하면 함수는 각 재시도 사이의 MAX_RETRIES휴면 시간(초)으로 요청을 최대 횟수 재시도합니다 . RETRY_SLEEP_SECONDS우리가 하는 일에 대한 분석은 다음과 같습니다.

  1. 재시도 횟수 카운터를 초기화합니다.
  2. 클라이언트를 사용하여 URL에 GET 요청을 만들고 응답을 검색합니다.
  3. 응답이 성공하면(즉, 응답의 상태 코드가 2xx 범위에 있음) 응답을 반환합니다.
  4. 응답이 실패하면 재시도 카운터를 증가시킵니다.
  5. 재시도 카운터가 미만이면 몇 초 MAX_RETRIES동안 기다렸다가 RETRY_SLEEP_SECONDSGET 요청을 재시도하십시오.
  6. 재시도 카운터가 같으면 MAX_RETRIESGET 요청이 실패했음을 나타내는 오류를 반환합니다.

fn retry_get(client: &Client, url: &str) -> Result<reqwest::Response, Box<dyn Error>> {
    let mut retries = 0;
    loop {
        match client.get(url).send() {
            Ok(res) => {
                if res.status().is_success() {
                    return Ok(res);
                } else {
                    retries += 1;
                    if retries >= MAX_RETRIES {
                        return Err(format!("GET request to {} failed with status code {:?}", url, res.status()).into());
                    }
                }
            }
            Err(err) => {
                retries += 1;
                if retries >= MAX_RETRIES {
                    return Err(format!("GET request to {} failed with error: {}", url, err).into());
                }
            }
        }
        sleep(Duration::from_secs(RETRY_SLEEP_SECONDS));
    }
}

그러나 Rust는 Python과 같은 고급 웹 스크래핑 프로그래밍 언어에 비해 상대적으로 새로운 언어입니다. 따라서 더 작은 커뮤니티와 사용 가능한 라이브러리가 더 적을 수 있습니다. Rust의 구문은 또한 초보자, 특히 동적 의미론이 있는 Python과 같은 고급 프로그래밍 언어에 익숙한 사람들에게는 더 어려울 수 있습니다.

똑똑하고 웹 스크랩을 즐기며 기업 변호사를 피하십시오. 하지만 변호사는 있을 것입니다! 해시태그, 뭐? #청구서