ABOUT ME

-

Total
-
  • Rust: scraper를 이용한 네이버 날씨 파싱
    컴퓨터/Rust 2021. 7. 28. 23:17
    728x90
    반응형

    Rust

     

    Rust Programming Language

    A language empowering everyone to build reliable and efficient software.

    www.rust-lang.org

     

    1. 크롤링

    우선 데이터를 가져올 사이트는 다음과 같다. ?cpName=ACCUWEATHER을 넘겨서 아큐웨더 제공자 사용함

    (아주대 지역 날씨)

     

    네이버 날씨

    국내외 날씨와 미세먼지에 대한 종합정보 제공

    weather.naver.com

    weather.naver.com

     

    Rust언어에서 Python의 beautifulsoup4처럼 html 요소를 쉽게 선택할 수 있는 라이브러리는

    scraper가 가장 괜찮은 것 같다.

     

    Cargo.toml

    scraper = "0.12.0"

     

    2. Rust언어 소스

    맨 위에 보이는 현재 온도 = <strong class="current">"12"</strong>

    현재 날씨 = <span class="weather">흐림</span>

    기타 정보 = <em class="level_text"></em> 4개가 있으므로 FindAll (select)

    ([미세먼지, 초미세먼지, 자외선, 일몰 시간])

     

    (기상청은 자정에서 정오 전이 오전, 정오에서 자정 전이 오후이고,

    해외 제공사는 일출부터 일몰 전이 낮, 일몰부터 일출 전이 밤입니다.)

     

    주간예보에 있는 오늘 낮, 오늘 밤 부분에 강수 확률과 최고/최저 온도가 있다.

    최고, 최저 온도 = <span class="data"></span> 2개가 있으므로 FindAll (select)

    강수 확률 = <span class="rainfall"></strong> 2개가 있으므로 FindAll (select)

     

    크롤링

    Weather란 struct에 정보를 다 string으로 저장할 것이다.

    use reqwest::header::USER_AGENT;
    use scraper::{Html, Selector};
    
    #[derive(Debug, Default)]
    pub struct Weather {
        pub max_temp: String,
        pub min_temp: String,
        pub current_temp: String,
        pub current_status: String,
        pub wind_chill: String, // 체감온도
        pub rain_day: String,
        pub rain_night: String,
        pub fine_dust: String,
        pub ultra_dust: String,
        pub uv: String,
        pub icon: String,
    }
    
    const NAVER_WEATHER: &'static str = "https://weather.naver.com/today/02117530?cpName=ACCUWEATHER"; // 아주대 지역 날씨
    
    pub fn weather_parse() -> Result<Weather, reqwest::Error> {
        // Blocking NON-ASYNC
        let client = reqwest::blocking::Client::builder()
            .danger_accept_invalid_certs(true)
            .build()?;
    
        let res = client.get(NAVER_WEATHER).header(USER_AGENT, "User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36").send()?;
        let body = res.text()?;
    
    
        // HTML Parse
        let mut weather: Weather = Default::default();
    
        let document = Html::parse_document(&body);
    
        // scraper
        let current_temp = Selector::parse("strong.current").unwrap();
        let current_stat = Selector::parse("span.weather").unwrap();
        let temps = Selector::parse("span.data").unwrap();
        let rains = Selector::parse("span.rainfall").unwrap();
        let stats = Selector::parse("em.level_text").unwrap();
        let icon = Selector::parse("div.today_weather > i").unwrap();
        let wind_chill = Selector::parse("div.weather_area > dl > dd:nth-child(6)").unwrap();
    
        let current_temp_element = document.select(&current_temp).next().unwrap();
        let current_stat_element = document.select(&current_stat).next().unwrap();
        let mut temps_element = document.select(&temps);
        let mut stats_element = document.select(&stats);
        let mut rains_element = document.select(&rains);
        let icon_element = document.select(&icon).next().unwrap();
        let wind_chill_element = document.select(&wind_chill).next().unwrap();
    
        let current_temp = current_temp_element.text().collect::<Vec<_>>()[1]
            .trim()
            .to_string()
            + "도"; // "28도"
    
        let current_stat = current_stat_element.text().collect::<Vec<_>>()[0]
            .trim()
            .to_string(); // "구름조금"
    
        let day_temp = temps_element.next().unwrap().text().collect::<Vec<_>>()[1]
            .trim()
            .to_string()
            + "도"; // "최고 온도"
        let night_temp = temps_element.next().unwrap().text().collect::<Vec<_>>()[1]
            .trim()
            .to_string()
            + "도"; // "최저 온도"
    
        let fine_dust = stats_element.next().unwrap().text().collect::<Vec<_>>()[0]
            .trim()
            .to_string(); // "보통"
        let ultra_dust = stats_element.next().unwrap().text().collect::<Vec<_>>()[0]
            .trim()
            .to_string(); // "나쁨"
        let uv = stats_element.next().unwrap().text().collect::<Vec<_>>()[0]
            .trim()
            .to_string(); // "높음"
    
        let day_rain = rains_element.next().unwrap().text().collect::<Vec<_>>()[1]
            .trim()
            .to_string(); // "55%"
        let night_rain = rains_element.next().unwrap().text().collect::<Vec<_>>()[1]
            .trim()
            .to_string(); // "8%"
    
        let wind_chill = wind_chill_element.text().collect::<Vec<_>>()[0].trim(); // "33"
        let wind_chill = wind_chill.replace("°", "") + "도";
    
        let mut icon = icon_element.value().attr("data-ico").unwrap().to_string();
        let icon_classes = icon_element.value().attr("class").unwrap();
    
        // struct Weather init
        weather.current_temp = current_temp;
        weather.wind_chill = wind_chill;
        weather.current_status = current_stat;
        weather.max_temp = day_temp;
        weather.min_temp = night_temp;
        weather.rain_day = day_rain;
        weather.rain_night = night_rain;
        weather.fine_dust = fine_dust;
        weather.ultra_dust = ultra_dust;
        weather.uv = uv;
    
        if icon_classes.contains("night") {
            icon = icon + "_night";
        }
    
        weather.icon = format!(
            "https://raw.githubusercontent.com/Alfex4936/KakaoChatBot-Golang/main/imgs/{}.png?raw=true",
            icon
        );
    
        Ok(weather)
    }
    
    #[cfg(test)]
    mod test {
        use super::*;
    
        #[test]
        fn weather_test() {
            let weather = weather_parse().unwrap();
            println!("{:#?}", weather);
        }
    }
    /* 결과
    Weather {
        max_temp: "33도",
        min_temp: "26도",
        current_temp: "27도",
        current_status: "구름많음",
        wind_chill: "33도",
        rain_day: "55%",
        rain_night: "8%",
        fine_dust: "보통",
        ultra_dust: "보통",
        uv: "높음",
        icon: "https://raw.githubusercontent.com/Alfex4936/KakaoChatBot-Golang/main/imgs/ico_animation_wt5_night.png?raw=true",
    }
    */
    

     

    카카오톡 챗봇 예제

    #[post("/weather")]
    pub async fn ask_weather(_: web::Json) -> impl Responder {
        let weather = weather_parse().await.unwrap();
        let description = format!("현재 날씨는 {}, {} (체감 {})\n최저기온 {}, 최고기온은 {}\n\n낮, 밤 강수 확률은 {}, {}\n미세먼지 농도는 {}\n자외선 지수는 {}",
            weather.current_status, weather.current_temp, weather.wind_chill,
            weather.min_temp, weather.max_temp,
            weather.rain_day, weather.rain_night,
            weather.fine_dust, weather.uv);
    
        let mut result = Template::new();
    
        let basic_card = BasicCard::new()
            .set_title("[수원 영통구 기준]".to_string())
            .set_desc(description)
            .set_thumbnail(weather.icon)
            .add_button(Button::Link(
                LinkButton::new("자세히".to_string()).set_link(NAVER_WEATHER.to_string()),
            ));
    
        result.add_output(basic_card.build());
    
        HttpResponse::Ok()
            .content_type("application/json")
            .body(serde_json::to_string(&result).unwrap())
    }
    

    풀소스

     

    Parse Naver weather · Alfex4936/Rust-Server@1e74f55

    Weather { max_temp: "33도", min_temp: "26도", current_temp: "27도", current_status: "구름많음", wind_chill: "33도", rain_day: "...

    github.com

     

    728x90

    댓글