스프링부트 파이썬으로 배치프로그램 시각화하기
0.프로젝트 생성
프로젝트명 : newsapp
버전 : 11
jar / java
라이브러리 선택
lombook
Spring Boot DevTool
Spring Web
Spring Data MongoDB
1.스프링부트와 몽고DB 연결
spring: data: mongodb: host: localhost port: 27017 database: greendb
2.크롤링 (뉴스 데이터)
테스트 파일 util.NaverCrawTest.java에서 네이버 뉴스 데이터 크롤링 테스트를 한다.
package com.cos.newsapp.util; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; public class NaverCrawTest { int aid = 1; @Test public void test() { String aidStr = String.format("%010d", aid); String url = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=102&oid=022&aid=" + aidStr; RestTemplate rt = new RestTemplate(); // 안드로이드 : Retrofit2(내부 쓰레드) String html = rt.getForObject(url, String.class); // String.class 응답받은 타입 Document doc = Jsoup.parse(html); Element companyElement = doc.selectFirst(".press_logo img"); String companyAttr = companyElement.attr("title"); System.out.println(companyAttr); Element titleElement = doc.selectFirst("#articleTitle"); String title = titleElement.text(); System.out.println(title); Element createAtElement = doc.selectFirst(".t11"); String createAt = createAtElement.text(); System.out.println(createAt); } }
위 예제에서는 url에서 html 문서를 받아오기 때문에 파싱할 때 getForObject를 사용하기 적절합니다. 하지만 받아오는 데이터가 json 데이터라면 exchange 사용면 json 데이터를 바로 파싱해서 받아올 수 있습니다. 깃허브 저장소 NaverCrawTest.java에 주석으로 자세한 설명이 있으니 필요하다면 확인해 보세요.
3.몽고 저장 – 배치 프로그램(1분마다)
1.NaverNewsApplication에서 @EnableScheduling 어노테이션을 추가한다.
package com.cos.navernews; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; @EnableScheduling @SpringBootApplication public class NaverNewsApplication { public static void main(String[] args) { SpringApplication.run(NaverNewsApplication.class, args); } }
2.batch.NaverNewsCrawBatch.java 파일을 만들어 배치를 만들어서 미리 테스트 했두었던 네이버 뉴스 크롤링 코드를 넣어준다.
package com.cos.newsapp.batch; import java.util.ArrayList; import java.util.List; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import com.cos.newsapp.domain.NaverNews; import com.cos.newsapp.domain.NaverNewsRepository; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Component public class NaverNewsCrawBatch { private int aid = 1; private final NaverNewsRepository naverNewsRepository; @Scheduled(fixedDelay = 1000*60*1) public void newsCraw() { List<NaverNews> newsList = new ArrayList<>(); for (int i = 0; i < 5; i++) { String aidStr = String.format("%010d", aid); String url = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=102&oid=022&aid=" + aidStr; RestTemplate rt = new RestTemplate(); String html = rt.getForObject(url, String.class); Document doc = Jsoup.parse(html); Element companyElement = doc.selectFirst(".press_logo img"); String company = companyElement.attr("title"); Element titleElement = doc.selectFirst("#articleTitle"); String title = titleElement.text(); Element createAtElement = doc.selectFirst(".t11"); String createAt = createAtElement.text(); NaverNews nn = NaverNews.builder() .company(company) .title(title) .createAt(createAt) .build(); newsList.add(nn); aid++; } // end of for naverNewsRepository.saveAll(newsList); } // end of newsCraw() }
package com.cos.newsapp.domain; import org.springframework.data.mongodb.core.mapping.Document; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @Builder @AllArgsConstructor @Data @Document(collection = "naver_news") public class NaverNews { private String _id; private String company; private String title; private String createAt; }
package com.cos.newsapp.domain; import org.springframework.data.mongodb.repository.MongoRepository; public interface NaverNewsRepository extends MongoRepository<NaverNews, String>{ }
코드 풀이
19 @Component
jvm이 이 파일을 컴포넌트 스캔할 수 있게 만든다.
18 @RequiredArgsConstructor
, 24 private final NaverNewsRepository naverNewsRepository;
의존성 주입. final을 찾아 생성자를 만들어준다.
26 @Scheduled(fixedDelay = 1000*60*1)
1분마다 실행되도록 만든다.
29 List<NaverNews> newsList = new ArrayList<>();
벌크 컬렉터, for문이 돌 때마다 저장하지 않고 한꺼번에 저장한다. 클래스 타입으로 데이터 세 가지 담을 수 있게 NaverNews 모델을 만들어준다.
31 for (int i = 0; i < 5; i++) {
크롤링 코드를 for문으로 감싸 코드를 반복하게 만들어준다.
35 RestTemplate rt = new RestTemplate();
RestTemplate 객체를 생성. HTTP 서버와 통신할 수 있게 만들어준다.
37 String html = rt.getForObject(url, String.class);
String 타입으로 html을 응답받는다.
39 Document doc = Jsoup.parse(html);
Jsoup를 사용해서 html을 파싱하여 Document에 담아준다.
41 Element companyElement = doc.selectFirst(".press_logo img");
사용할 요소를 찾아 Element에 담아준다.
42 String company = companyElement.attr("title");
요소의 title 속성을 찾아 company에 담아준다.
45 String title = titleElement.text();
요소의 text를 title에 담는다.
50 NaverNews nn = NaverNews.builder()
.company(company)
.title(title)
.createAt(createAt)
.build();
.company(company)
.title(title)
.createAt(createAt)
.build();
생성자를 사용해서 데이터를 넣어주고 Builder를 사용해서 필요한 값만 가지고 온다. 모델에서 @AllArgsConstructor, @Data 어노테이션을 추가해야한다.
56 newsList.add(nn);
만들어둔 리스트 안에 받아온 데이터를 차례대로 쌓아준다.
58 aid++;
for 문이 끝나기 전에 뉴스 번호를 더해서 다음 뉴스로 넘어갈 수 있게 한다.
62 naverNewsRepository.saveAll(newsList);
3.서버를 재실행하고 몽고디비에 저장되었는지 확인한다.
use greendb;
db.naver_news.count();
4.통신 오류를 잡기 위해서 try ~ catch 처리를 한다.
package com.cos.newsapp.batch; import java.util.ArrayList; import java.util.List; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import com.cos.newsapp.domain.NaverNews; import com.cos.newsapp.domain.NaverNewsRepository; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Component public class NaverNewsCrawBatch { private int aid = 1; private final NaverNewsRepository naverNewsRepository; @Scheduled(fixedDelay = 1000*60*1) public void newsCraw() { List<NaverNews> newsList = new ArrayList<>(); for (int i = 0; i < 5; i++) { String aidStr = String.format("%010d", aid); String url = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=102&oid=022&aid=" + aidStr; RestTemplate rt = new RestTemplate(); try { String html = rt.getForObject(url, String.class); Document doc = Jsoup.parse(html); Element companyElement = doc.selectFirst(".press_logo img"); String company = companyElement.attr("title"); Element titleElement = doc.selectFirst("#articleTitle"); String title = titleElement.text(); Element createAtElement = doc.selectFirst(".t11"); String createAt = createAtElement.text(); NaverNews nn = NaverNews.builder() .company(company) .title(title) .createAt(createAt) .build(); newsList.add(nn); } catch (Exception e) { System.out.println("통신 오류!!"); } // end of try~ catch aid++; } // end of for naverNewsRepository.saveAll(newsList); } // end of newsCraw() }
4.API 컨트롤러 구축
1.컨트롤러 파일 NaverNewsController.java와 데이터를 제대로 받았는지 확인하기 위한 공통 DTO CMRespDto.java를 만든다.
package com.cos.newsapp.web; import java.util.List; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import com.cos.newsapp.domain.NaverNews; import com.cos.newsapp.domain.NaverNewsRepository; import com.cos.newsapp.web.dto.CMRespDto; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @RestController // 데이터 리턴 public class NaverNewsController { private final NaverNewsRepository naverNewsRepository; @GetMapping("/naverNews") public CMRespDto<?> findAll(){ System.out.println("실행됨??"); List<NaverNews> naverNewsList = naverNewsRepository.findAll(); return new CMRespDto<>(1, "성공", naverNewsList); // 값을 넣을 때(리턴 시) 타입이 정해진다.(동적 리턴 가능) } }
package com.cos.newsapp.web.dto; import lombok.AllArgsConstructor; import lombok.Data; @AllArgsConstructor @Data public class CMRespDto <T> { private int code; private String msg; private T data; }
제네릭(Generic )
제네릭은 자바에서 타입을 정의하지 않고 동적으로 타입을 사용하기 위해 사용된다.
<T>
로 표현할 수 있으며<?>
안에 물음표를 넣는 것으로 묵시적 타입을 넣어 타입 추론을 할 수도 있다.위 코드에서 CMRespDto<List<NaverNews>> 을 CMRespDto<?>로 적으면 코드가 간결해져서 좋으며 동적인 리턴을 가능하게 하기 때문에 코드의 활용도가 높아진다.
5.Flask 서버 만들어서 API 호출해서 시각화
1.VSCode 에디터를 열어 navernewsapp 폴더를 만들어 Flask 파일 구조를 만들어준다.
static : css, js 파일을 넣어주는 폴더
templates : html 파일을 넣어주는 폴더
2.Flask와 Requests 라이브러리를 설치한다.
pip install flask
pip install requests
3.router.py 파일에서 Flask 서버를 만들고 서버 테스트를 한다.
from flask import Flask, render_template import requests app = Flask(__name__) @app.route("/naverNews") 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)
코드 풀이
render_template
파일을 리턴하기 위해 사용하는 라이브러리
requests
json 데이터를 요청하기 위한 라이브러리
response = requests.get("http://localhost:8080/naverNews")
cmRespDto = response.json();
스프링으로 만든 서버에서 데이터를 제이슨 타입으로 받는다.
if cmRespDto["code"] == 1:
return render_template("index.html",naverNewsList=cmRespDto["data"])
else:
return "데이터를 가져올 수 없습니다."
데이터 요청이 실패 했을 때를 대비한 예외처리
router 파일 실행하기
명령어 실행 : python router.py
맥북 키보드 스크린 실행 :
<실행중>
4.데이터 바인딩하기 위한 index.html 파일을 만들어준다.
<!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>