스프링부트 파이썬으로 배치프로그램 시각화하기
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>
<결과화면>
