리액티브 스프링 크롤링과 플라스크 시각화하기
프로젝트 생성
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>