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