JAVA

스프링부트 파이썬으로 배치프로그램 시각화하기

스프링부트 파이썬으로 배치프로그램 시각화하기

image

 

0.프로젝트 생성

spring

프로젝트명 : newsapp

버전 : 11

jar / java

spring

라이브러리 선택

lombook
Spring Boot DevTool
Spring Web
Spring Data MongoDB

 

1.스프링부트와 몽고DB 연결

spring:
  data:
    mongodb:
      host: localhost
      port: 27017
      database: greendb
서버가 작동하는지 실행해 보세요.

2.크롤링 (뉴스 데이터)

programing
스트링부트 웹 크롤링 하는 방법[jsoup]Jsoup는 자바 언어로 HTML을 쉽게 파싱할 수 있게 도와주는 라이브러리이다....
테스트 파일 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분마다)

programing
배치(Batch) 프로그램으로 일괄 처리하는 방법배치란 일괄처리라는 의미로 클라이언트와의 상호작용 없이 일련의 작업들을 작업 단위로 묶어 정기적으로 반복 수행하거나 정해진 규칙에 따라 일괄 처리하는 것을 말한다....
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();

생성자를 사용해서 데이터를 넣어주고 Builder를 사용해서 필요한 값만 가지고 온다. 모델에서 @AllArgsConstructor, @Data 어노테이션을 추가해야한다.

56 newsList.add(nn);

만들어둔 리스트 안에 받아온 데이터를 차례대로 쌓아준다.

58 aid++;

for 문이 끝나기 전에 뉴스 번호를 더해서 다음 뉴스로 넘어갈 수 있게 한다.

62 naverNewsRepository.saveAll(newsList);
3.서버를 재실행하고 몽고디비에 저장되었는지 확인한다.
use greendb;
db.naver_news.count();
MongoDB
MongoDB 기본 명령어몽고디비 접속하기부터 기본 명령어 show, use, save, find, update, remove 를 소개합니다....
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
python
파이썬 웹 프레임워크 Flaskhtml에서 파이썬을 사용하기 위해서 Flask라는 웹 프레임워크를 설치해야한다. 터미널에서 pip install flask 명령어로 Flask를 설치할 수 있다. Flask는 Jinja2라는 템플릿 엔진을 기반으로 하고 있다. 다음의 간단한 예제로 Flask의 동작 원리를 파악해 보자....
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
맥북 키보드 스크린 실행 :
keyboard
<실행중>
keyboard
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>

 

<결과화면>
browser
최신글