JAVA

리액티브 스프링 크롤링과 플라스크 시각화하기

리액티브 스프링 크롤링과 플라스크 시각화하기

프로젝트 생성

1.Spring Starter Project를 생성한다.
reactive-spring reactive-spring

선택한 라이브러리

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());
    
  }
  
}

 

플라스크 시각화

programing
스프링부트 파이썬으로 배치프로그램 시각화하기스트링부트를 이용해 웹 크롤링을 하여 파이썬으로 배치프로그램 시각화하는 과정을 상세하게 설명합니다....
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>

 

최신글