리액티브 스프링 크롤링과 플라스크 시각화하기
프로젝트 생성
1.Spring Starter Project를 생성한다.
선택한 라이브러리
Spring Boot Dev Tool
Lombok
Spring Data Reactive MongoDB
Spring Reactive Web
데이터베이스 연결
2-1.application.yml 파일로 변경 후 몽고디비를 연결한다.
spring: data: mongodb: host: localhost port: 27017 database: greendb
2-2.패키지 생성
com.cos.navercrawapp.batch
com.cos.navercrawapp.domain
com.cos.navercrawapp.web
모델과 레파지토리
3.NaverNews.java 모델과 NaverNewsRepository.java(interface) 레파지토리를 만든다.
package com.cos.navercrawapp.domain; import java.util.Date; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @Builder @AllArgsConstructor @Data @Document(collection = "naver_realtime") public class NaverNews { @Id private String _id; private String company; private String title; private Date createdAt; // MongoDB에 Timestamp 타입이 없다. }
package com.cos.navercrawapp.domain; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.mongodb.repository.Tailable; import reactor.core.publisher.Flux; public interface NaverNewsRepository extends ReactiveMongoRepository<NaverNews, String> { //db.runCommand({convertToCapped:'naver_realtime', size:8192}); -> 사이즈 조절 @Tailable @Query("{}") Flux<NaverNews> mFindAll(); }
배치(Batch)
4.NaverCrawBatch.java 파일에 테스트한 배치 코드를 가지고 온다.
testController
package com.cos.navercrawapp.batch; import java.sql.Timestamp; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.junit.jupiter.api.Test; import com.cos.navercrawapp.domain.NaverNews; public class NaverCrawBatchTest { // 8Byte long aid = 277493; //aid 처리 -> 1.file에 기록, 2.DB에 저장 @Test public void 뉴스수집_테스트() { System.out.println("배치프로그램 시작========================"); List<NaverNews> naverNewsList = new ArrayList<>(); int errorCount = 0; int susseseCount = 0; int crawCount =0; while (true) { String aidStr = String.format("%010d", aid); System.out.println("aidStr : " + aidStr); String url = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=103&oid=437&aid=" + aidStr; try { Document doc = Jsoup.connect(url).get(); // System.out.println(doc); // company, title, createAt String title = doc.selectFirst("#articleTitle").text(); String company = doc.selectFirst(".press_logo img").attr("alt"); String createAt = doc.selectFirst(".t11").text(); // System.out.println("title : "+title); // System.out.println("company : "+company); // System.out.println("createAt : "+createAt); // 기사 날짜 // 오늘 날짜 LocalDate today = LocalDate.now(); // 2021-10-12 // System.out.println("today"+today); // 어제 날짜 LocalDate yesterday = today.minusDays(1); // System.out.println("yesterday"+yesterday); // 기사 날짜 파싱 createAt = createAt.substring(0, 10); // yyyy-MM-dd createAt = createAt.replace(".", "-"); // yyyy.MM.dd // System.out.println("createAt"+createAt); // System.out.println(LocalDateTime.now()); if(today.toString().equals(createAt)) { // DB에 aid insert System.out.println("createAt:"+createAt); break; } // 어제 날짜(yesterday)와 기사 날짜(createAt)를 비교 if (yesterday.toString().equals(createAt)) { // List 컬렉션에 모았다가 DB에 save 하기 System.out.println("어제 기사 입니다. 크롤링 잘 됨 "); naverNewsList.add(NaverNews.builder() .title(title) .company(company). createdAt(Timestamp.valueOf(LocalDateTime.now().minusDays(1))) //어제 날짜 Timestamp 타입으로 넣기 .build() ); crawCount++; } susseseCount++; } catch (Exception e) { System.out.println("해당 주소에 페이지를 찾을 수 없습니다 : " + e.getMessage()); errorCount++; } aid++; }// end of while System.out.println("배치프로그램 종료========================"); System.out.println("성공횟수 : " + susseseCount); System.out.println("실패횟수 : " + errorCount); System.out.println("크롤링 성공 횟수 : " +crawCount); System.out.println("마지막 aid 값" + aid); } @Test public void 로컬_문자열날짜_테스트() { LocalDate today = LocalDate.now(); String createdAt = "2021-10-12"; System.out.println(today); System.out.println(createdAt); if(today.toString().equals(createdAt)) { System.out.println("같은 날입니다."); } } }
package com.cos.navercrawapp.batch; import java.sql.Timestamp; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import com.cos.navercrawapp.domain.NaverNews; import com.cos.navercrawapp.domain.NaverNewsRepository; import lombok.RequiredArgsConstructor; import reactor.core.publisher.Flux; // 동기적 배치 프로그램 (약속, 어음을 받을 수 없다.) @RequiredArgsConstructor @Component public class NaverCrawBatch { private long aid = 277493; private final NaverNewsRepository naverNewsRepository; //@Scheduled(cron = "0 0 1 * * *", zone = "Asia/Seoul") @Scheduled(cron = "0 39 12 * * *", zone = "Asia/Seoul") public void 뉴스크롤링() { System.out.println("배치프로그램 시작========================"); List<NaverNews> naverNewsList = new ArrayList<>(); int errorCount = 0; int susseseCount = 0; int crawCount =0; while (true) { String aidStr = String.format("%010d", aid); System.out.println("aidStr : " + aidStr); String url = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=103&oid=437&aid=" + aidStr; try { Document doc = Jsoup.connect(url).get(); // company, title, createAt String title = doc.selectFirst("#articleTitle").text(); String company = doc.selectFirst(".press_logo img").attr("alt"); String createAt = doc.selectFirst(".t11").text(); // 오늘 날짜 LocalDate today = LocalDate.now(); // 2021-10-12 // 어제 날짜 LocalDate yesterday = today.minusDays(1); // 기사 날짜 파싱 createAt = createAt.substring(0, 10); // yyyy-MM-dd createAt = createAt.replace(".", "-"); // yyyy.MM.dd if(today.toString().equals(createAt)) { System.out.println("createAt:"+createAt); break; } // 어제 날짜(yesterday)와 기사 날짜(createAt)를 비교 if (yesterday.toString().equals(createAt)) { // List 컬렉션에 모았다가 DB에 save 하기 System.out.println("어제 기사 입니다. 크롤링 잘 됨 "); naverNewsList.add(NaverNews.builder() .title(title) .company(company) .createdAt(Timestamp.valueOf(LocalDateTime.now().minusDays(1).plusHours(9))) .build() ); crawCount++; } susseseCount++; } catch (Exception e) { System.out.println("해당 주소에 페이지를 찾을 수 없습니다 : " + e.getMessage()); errorCount++; } aid++; }// end of while System.out.println("배치프로그램 종료========================"); System.out.println("성공횟수 : " + susseseCount); System.out.println("실패횟수 : " + errorCount); System.out.println("크롤링 성공 횟수 : " +crawCount); System.out.println("마지막 aid 값" + aid); System.out.println("컬렉션에 담은 크기 : " + naverNewsList.size()); //naverNewsRepository.saveAll(naverNewsList); Flux.fromIterable(naverNewsList) .flatMap(naverNewsRepository::save) .subscribe(); } }
비동기로 save 요청하기
Flux.fromIterable(naverNewsList)
.flatMap(naverNewsRepository::save)
.subscribe();
현재 서버와 데이터베이스가 비동기적로 움직이도록 만들었다. 하지만 batch는 Thread 기반으로 돌아가고 있기 때문에 동기적으로 움직이고 있기 때문에 데이터베이스에 데이터가 저장되지 않는다 이 문제를 해결할 수 있는 것이 위 코드이다.
컨트롤러 생성
testController
package com.cos.navercrawapp; import java.time.Duration; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; @RestController public class TestController { @GetMapping("/flux") public Flux<Integer> flux(){ return Flux.just(1,2,3,4).delayElements(Duration.ofSeconds(1)).log(); } @GetMapping(value = "/flux/steam", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<Integer> fluxStream(){ return Flux.just(1,2,3,4).delayElements(Duration.ofSeconds(1)).log(); } }
5.NaverNewsController.java 컨트롤러를 만들어준다.
package com.cos.navercrawapp.web; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import com.cos.navercrawapp.domain.NaverNews; import com.cos.navercrawapp.domain.NaverNewsRepository; import lombok.RequiredArgsConstructor; import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; // 비동기 서버 @RequiredArgsConstructor @RestController public class NaverNewsController { private final NaverNewsRepository naverNewsRepository; @GetMapping(value = "/naverNews", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<NaverNews> home(){ // 새로운 쓰레드가 만들어져서 응답을 지고 있음. return naverNewsRepository.mFindAll() .subscribeOn(Schedulers.boundedElastic()); } }
플라스크 시각화
스프링부트 파이썬으로 배치프로그램 시각화하기스트링부트를 이용해 웹 크롤링을 하여 파이썬으로 배치프로그램 시각화하는 과정을 상세하게 설명합니다....
6.파이썬 플라스크로 크롤링한 데이터를 시각화한다.
from flask import Flask, render_template import requests app = Flask(__name__) @app.route("/") def NaverNews(): response = requests.get("http://localhost:8080/naverNews") cmRespDto = response.json(); if cmRespDto["code"] == 1: return render_template("index.html",naverNewsList=cmRespDto["data"]) else: return "데이터를 가져올 수 없습니다." if __name__ == "__main__": app.run(debug=True)
<!DOCTYPE html> <html lang="en"> <head> <title>네이버 뉴스 리스트</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> <style> header,footer{ width: 100%; height:100px; background-color: #17CE5F; text-align:center; line-height:100px; color: #fff; font-size: xx-large; } .m_box { display: grid; grid-template-columns: 1fr 1fr 1fr; grid-gap: 10px; } .m_tm_20 { margin-top: 20px; } .card:hover{ background-color: rgba(0,0,0,0.3); color:#fff; cursor:pointer; } </style> </head> <body> <header style="margin-bottom: 100px;">네이버 신문 기사</header> <div class="container m_box m_tm_20"> {% for naverNews in naverNewsList %} <!-- 신문 카드 시작 --> <div class="card"> <div class="card-body"> <h4 class="card-title">{{naverNews.title}}</h4> <p class="card-text">{{naverNews.createdAt}}</p> <p class="card-text" style="text-align:right;">{{naverNews.company}}</p> </div> </div> <!-- 신문 카드 끝 --> {% endfor %} </div> <footer style="margin-top: 100px;"></footer> <script> function myPolling(){ location.reload(); } setInterval(myPolling, 1000*60); </script> </body> </html>