[인실리코젠] WebCrawling - 2

2024. 1. 22. 16:37·인실리코젠
목차
  1. 스프링 부트 어플리케이션 시작 클래스
  2. 컨트롤러
  3. DTO
  4. Ajax 통신이란?
  5. Ajax 장점
  6. Ajax 기본 형태
  7. 서비스
더보기

본 글은 WebCrawling- 1 에 이어서 작성하는 글입니다. 문제 풀이 과정을 기록하는 글이기에 완성된 코드가 없을 수 있고 부족한 부분이 있을 수 있다는 점을 미리 알립니다.

Spring Boot + MVC 패턴 활용하여 크롤링한 정보 화면에 띄우기

해당 글에서 작성한 내용을 바탕으로 Spring Boot와 MVC 패턴을 활용하여 프로젝트를 이어서 진행하였다.

https://minwook6457.tistory.com/22

 

[인실리코젠] Spring Boot + MVC 이론 정리

MVC (Model - View - Controller) 사용자 인터페이스, 데이터 및 논리 제어를 구현하는데 널리 사용되는 소프트웨어 디자인 패턴 모델 : 데이터와 비즈니스 로직 관리 뷰 : 레이아웃과 화면을 처리 컨트롤

minwook6457.tistory.com

 

기본적인 세팅에 대한 내용은 언급하지 않겠다. 세세한 내용을 적어가며 브리핑을 하기엔 많인 내용이 기입되어야 하고 기록하면 좋겠지만, 충분히 기억하고 찾아볼 수 있는 내용들이기 때문이다.

 

 

스프링 부트 어플리케이션 시작 클래스

package com.insilicogen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CrawlingApplication {

	public static void main(String[] args) {
		SpringApplication.run(CrawlingApplication.class, args);
	}
}

 

  • @SpringBootApplication 어노테이션
    • 스프링 부트 어플리에키션을 나타내는 메타 어노테이션
    • 어플리케이션을 설정하고 실행하는데 여러 가지 기능을 자동으로 활성화 
      • @EnableAutoConfiguration: 스프링 부트가 클래스패스 상의 설정을 기반으로 빈을 자동으로 구성
      • @ComponentScan: 현재 패키지와 하위 패키지에서 @Component, @Service, @Repository 등의 어노테이션이 붙은 클래스들을 찾아 빈으로 등록
      • @Configuration: 해당 클래스를 애플리케이션의 구성 클래스로 지정
      • 기본적으로 8080 포트에 적용

 

컨트롤러

package com.insilicogen.crawl.controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import com.insilicogen.crawl.dto.InfoDto;
import com.insilicogen.crawl.service.NewsService;

@Controller
public class NewsController {

	@Autowired
	private NewsService newsService;

	@GetMapping("/news")
	public String news() {
		return "news"; // news.jsp로 포워딩
	}

	/*
	@GetMapping("/crawling")
	public String crawlingNews(Model model) {
		List<InfoDto> newsList = newsService.crawlAndSaveNews();
		model.addAttribute("newsList", newsList);
		model.addAttribute("imagePath", NewsService.destinationFolder);
		return "news";
	}
	*/

	@GetMapping("/selectNewsList")
	@ResponseBody
	public Map<String, Object> selectNewsList(Model model, 
	        @RequestParam(defaultValue = "1") int page,
	        @RequestParam(defaultValue = "20") int pageSize) {

	    List<InfoDto> newsList = newsService.getPage(page, pageSize);
	    int totalItems = newsService.getTotalNewsCount();

	    Map<String, Object> response = new HashMap<>();
	    response.put("newsList", newsList);
	    response.put("totalItems", totalItems);
	    
	    return response;
	}


	@GetMapping("/initCrawling")
	@ResponseBody
	public ModelAndView initCrawling(Model model) {
		try {			
			List<InfoDto> newsList = newsService.crawlAndSaveNews();
			model.addAttribute("newsList", newsList);
			model.addAttribute("imagePath", NewsService.destinationFolder);
			return new ModelAndView("news", model.asMap());
		} catch (Exception e) {
			e.printStackTrace();
			return new ModelAndView("errorPage", "error", "Error initiating crawling.");
		}
	}
}
  • @Controller : 컨트롤러를 나타내는 어노테이션
  • @Autowired : 필드, 생성자, Setter 메서드 등에 사용되어 해당 타입의 빈을 자동으로 주입
    • NewsService 타입의 빈 주입
  • @GetMapping("/news") : /news url로 get 요청 처리

 

  • news 
    • news.jsp로 포워딩
  • selectNewsList
    • /selectNewsList 엔드포인트에 대한 get 요청 처리
    • page 와 pageSize 를 parameter로 받으며 특정 범위의 뉴스 목록을 가져와 응답
    • Map<String,Object> json 응답
    • 파라미터 기본값으로 page : 1, pageSize 20을 요청
  • initCrawling
    • /initCrawling 엔드포인트에 대한 get 요청 처리
    • 뉴스를 크롤링하여 데이터를 저장한 후 news.jsp로 포워딩하여 뉴스 목록을 화면에 표시
    • ModelAndView (뷰 이름과 모델 데이터) 응답

ModelAndView 메소드

import org.springframework.web.servlet.ModelAndView;

public ModelAndView(String viewName, @Nullable Map<String, ?> model) {
		this.view = viewName;
		if (model != null) {
			getModelMap().addAllAttributes(model);
		}
	}

DTO

크롤링한 데이터를 담을 DTO (수정)

package com.insilicogen.crawl.dto;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonIgnore;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Getter // Getter 메서드 자동 주입
@Setter
@Table(name="info_dto")
@AllArgsConstructor 
@Entity // 디비와 연결해주겠다. 라는 의미
// 외부 시스템과 데이터 통신을 할 경우 DTO로 정의
// DB에서 가져오는 Data 는 VO로 정의 후 사용
public class InfoDto {
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
	
    private String title;
    private String content;
    private String publisher;
    private String upload;
    
    // 1:1 관계 설정
    @JsonIgnore
    @OneToOne(mappedBy = "infoDto", cascade = CascadeType.ALL, orphanRemoval = true)
    private ImageDto imageDto;	
    

    public InfoDto() {
        // Default Constructor
    }

    public InfoDto(String title, String content, String publisher, String upload) {
    	this.title = title;
        this.content = content;
        this.publisher = publisher;
        this.upload = upload;
    }
}

 

이미지 정보를 담을 DTO

package com.insilicogen.crawl.dto;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;

import com.fasterxml.jackson.annotation.JsonIgnore;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
public class ImageDto {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String imageUrl;

    @JsonIgnore
    @OneToOne
    @JoinColumn(name = "info_id", unique = true) // info_id를 외래키로 가짐
    private InfoDto infoDto;

    public ImageDto() {
        // Default Constructor
    }

    public ImageDto(String imageUrl, InfoDto infoDto) {
        this.imageUrl = imageUrl;
        this.infoDto = infoDto;
    }
 
}

 

각 DTO를 살펴보면 @JsonIgnore 라는 어노테이션을 추가하였다. 해당 어노테이션은 Jackson 직렬화를 무시하겠다라는 의미로 해석할 수 있다. 해당 어노테이션을 추가한 이유는 Front 단에서 ajax 통신을 이용하여 데이터를 불러올 때 InfoDto 와 ImageDto가 1:1 관계로 매핑 되어 있어 무한 루프에 빠지게되어 오류를 발생하는 문제가 발생하여 이를 해결하기 위해 추가하였다.


Ajax 통신이란?

자바스크립트의 라이브러리 중 하나로 비동기적으로 서버와 웹 브라우저 간에 데이터를 교환하는 기술이고 페이지 전체를 리로드하지 않고도 웹 페이지의 일부를 업데이트한다.

 

Ajax 장점

  • 페이지의 갱신없이 서버와 비동기 통신을 가능하게 해줌
  • 화면이 새로 로딩되는 것이 아니기에 속도면에서 성능 향상
  • 별도의 플러그인이 필요하지 않음
  • HTTP 전송 중에도 클라이언트가 웹 어플리케이션과 상호작용

동기 vs 비동기

Ajax를 이해하려면 동기와 비동기 방식의 차이를 알아야 한다.

 

   <동기식>                                                                                  <비동기식>

 

동기식은 요청 후 응답을 받아야 다음 동작이 이루어지고 비동기식에서는 요청을 보낸 후 응답과는 상관없이 동작한다.

이러한 비동기식을 사용하기 때문에 HTTP 전송 중에도 클라이언트가 웹 어플리케이션과 상호작용을 할 수 있는 것이다.

 

Ajax 기본 형태

$.ajax({
	type : "get",
        url : "/testUrl",
        data : "",
        async : true,
        dataType : "json",
        timeout : 10000,   
        success : function(res) {
        	console.log("success : " + res )
        }
})
  • type : HTTP 타입 (GET,POST,PUT,DELETE)
  • url : 호출 url
  • async : 디폴드 => true (false시에 동기 통신으로 변경되며 서버에 응답이 올 때 까지 기다림)
  • data : url 호출 시 보낼 파라미터 데이터
  • dataType : 서버에 반환되는 데이터 형식
  • timeout : 제한시간 설정

Ajax 통신을 위해 news.jsp 코드를 아래와 같이 작성하였다.

 

function loadNews(page, pageSize) {
        $.ajax({
            type: "GET",
            url: "/selectNewsList",
            data: {
                page: page,
                pageSize: pageSize
            },
            dataType: "json",
            success: function (response) {
                newsList = response.newsList;
                displayNews();
                console.log(newsList);
            },
            error: function () {
                alert("Error loading News");
            }
        })
    }

 

page 와 pageSize를 /selectNewsList 파라미터로 받아와 json 형태로 응답한다. 응답된 결과는 아래와 같다.

 

 

데이터베이스에서 데이터를 가져오는 Repository 코드는 아래와 같다.

package com.insilicogen.crawl.repository;


import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import com.insilicogen.crawl.dto.InfoDto;

@Repository
public interface NewsRepository extends JpaRepository<InfoDto, Long> {

    @Query(value = "SELECT * FROM INFO_DTO info_dto LIMIT ?1, ?2", nativeQuery = true)
    List<InfoDto> findNewsList(int offset , int pageSize);

}

필자는 JPA를 사용하였다. InfoDto를 엔터티로 설정하고 JpaRepository<InfoDto, Long>을 통해 CRUD 작업이 가능하다. 하지만 직접적으로 offset 과 limit의 설정이 필요하기 때문에 @Query 어노테이션을 사용하여 sql 쿼리를 작성하였다.

SELECT * FROM INFO_DTO info_dto LIMIT ?1, ?2

offset 부터 pageSize 까지의 범위의 데이터를 가져온다. 중요하게 볼 부분은 LIMIT ?1, ?2 부분이다.

?1 은 시작 인덱스이고 ?2는 가져올 행의 수 이다.

  • ?1 : 시작인덱스로 몇 번째 행부터 결과를 가져올 지 지정한다.
  • ?2 : 가져올 행의 수로 시작 인덱스부터 몇 개의 행을 가져올 지 지정한다.

만약 LIMIT 0,10 이라면 첫 번째 행부터 시작하여 10개의 행을 가져온다. 이 쿼리를 통해 앞단에서의 페이지네이션 알고리즘을 작성하지 않고도 INFO_DTO 테이블에서 특정 범위의 행을 선택하여 데이터를 가져올 수 있다.

 

더보기
{
    "newsList": [
        {
            "id": 1,
            "title": "SK하이닉스, 작년 4분기 흑자전환 성공…영업익 3460억원",
            "content": "SK하이닉스가 지난해 4분기 3천460억 원의 영업이익을 기록하며 흑자 전환에 성공했다. 이로써 회사는 2022년 4분기부터 이 …",
            "publisher": "지디넷코리아",
            "upload": "2분전"
        },
        {
            "id": 2,
            "title": "'운명의 날' 제4통신사 선정 위한 주파수 경매 시작",
            "content": "과학기술정보통신부는 25일 서울 송파구 아이티벤처타워에서 5G 28㎓ 대역 주파수 경매를 시작한다. 28㎓ 대역 주파수 할당을 …",
            "publisher": "노컷뉴스",
            "upload": "3분전"
        },
        {
            "id": 3,
            "title": "SK C&C, 하이브리드 클라우드 현대홈쇼핑 차세대 시스템 구축",
            "content": "SK C&C는 2022년 5월 착수한 현대홈쇼핑의 차세대 시스템 구축을 완료했다고 25일 밝혔다. SK C&C는 20개월에 걸쳐 …",
            "publisher": "연합뉴스",
            "upload": "3분전"
        },
        {
            "id": 4,
            "title": "“뼈에 금갔다면 기브스 말고 이것 붙이세요” KAIST, ‘뼈 반창고’ 개발",
            "content": "국내 연구진이 금이 간 뼈의 재생을 돕는 신소재 개발에 성공했다. 카이스트(KAIST)는 신소재공학과 홍승범 교수 연구팀이 전남 …",
            "publisher": "헤럴드경제",
            "upload": "6분전"
        },
        {
            "id": 5,
            "title": "'사랑의 호르몬' 옥시토신 역할…패배 후 자기방어도 관여",
            "content": "'사랑의 호르몬'으로 불리는 옥시토신(Oxytocin)이 패배에 관한 기억에도 관여해 공격자를 피하도록 한다는 연구 결과가 나왔 …",
            "publisher": "머니투데이",
            "upload": "6분전"
        },
        {
            "id": 6,
            "title": "SK하이닉스 전년 4분기 흑자전환…영업이익 3460억원",
            "content": "반도체 회복세에 SK하이닉스가 흑자전환에 성공했다. SK하이닉스는 지난해 4분기 매출 32조 7657억원, 영업이익 3460억원 …",
            "publisher": "디지털데일리",
            "upload": "9분전"
        },
        {
            "id": 7,
            "title": "DGIST 연구팀, 극한의 환경에도 작동하는 차세대 반도체 메모리 개발",
            "content": "DGIST(총장 이건우) 는 전기전자컴퓨터공학과 권혁준 교수팀(제1저자 장봉호 박사과정생)이 저온에서도 고품질의 산화막 제작과 …",
            "publisher": "한국경제",
            "upload": "10분전"
        },
        {
            "id": 8,
            "title": "SK하이닉스 작년 4분기 영업이익 흑자전환...1년만에 적자 탈출",
            "content": "메모리 반도체 업황 반등이 본격화된 가운데 SK하이닉스가 지난해 4분기 3460억 원의 영업이익을 기록하며 흑자 전환했다. 20 …",
            "publisher": "조선일보",
            "upload": "11분전"
        },
        {
            "id": 9,
            "title": "[미장브리핑] S&P500·나스닥 5거래일 연속 상승 마감",
            "content": "◇ 24일 (현지시간) 미국 증시 ▲다우존스산업평균(다우)지수 전 거래일 대비 0.26% 하락한 37806.39. ▲스탠다드앤푸 …",
            "publisher": "지디넷코리아",
            "upload": "12분전"
        },
        {
            "id": 10,
            "title": "[속보] 작년 4분기 0.6% 성장…반도체 수출 회복 등 영향",
            "content": "한국은행은 지난해 4분기 실질 국내총생산, GDP 성장률이 0.6%라고 밝혔습니다. 재작년 4분기 -0.3%였던 실질 GDP는 …",
            "publisher": "연합뉴스TV",
            "upload": "15분전"
        },
        {
            "id": 11,
            "title": "[1보] SK하이닉스, 지난해 영업손실 7조7303억...매출 26% 감소",
            "content": "SK하이닉스는 지난해 연결기준 영업손실 7조7천303억 원으로 전년(68조941억 원) 대비 적자전환했다고 25일 공시했다. 매 …",
            "publisher": "지디넷코리아",
            "upload": "27분전"
        },
        {
            "id": 12,
            "title": "페북 모회사 메타 1.43% 상승, 시총 1조 달러 돌파",
            "content": "페북의 모회사 메타가 시총 1조 달러를 돌파했다. 24일(현지시간) 뉴욕증시에서 메타는 전거래일보다 1.43% 상승한 390.7 …",
            "publisher": "뉴스1",
            "upload": "31분전"
        },
        {
            "id": 13,
            "title": "[단독]삼성전자 '2024년 하반기 언팩' 올림픽 열리는 파리에서 개최",
            "content": "파리올림픽 전 개최 위해 전년보다 2주 앞당겨… AI 장착 갤Z플립6·폴드6 공개 예정 최근 미국 새너제이에서 세계 최초 인공지 …",
            "publisher": "머니S",
            "upload": "31분전"
        },
        {
            "id": 14,
            "title": "국정원 보안 실태 평가서 원안위·방통위·서울시 등 7곳 '미흡'",
            "content": "지난해 하반기 국가정보원 정보보안 관리 실태 평가에서 '우수' 판정을 받은 중앙행정기관·광역지방자치단체는 기획재정부와 해양수산부 …",
            "publisher": "서울경제",
            "upload": "32분전"
        },
        {
            "id": 15,
            "title": "AI로 중심 이동한 메타, 2021년 이후 처음으로 시총 '1조 클럽' 재입성",
            "content": "사업 중심을 메타버스로 급선회했다가 뚜렷한 성과를 내지 못하고 휘청거렸던 메타가 시가총액 1조 클럽에 재입성했다. 24일(현지 …",
            "publisher": "조선일보",
            "upload": "35분전"
        },
        {
            "id": 16,
            "title": "엔비디아 2.5%-AMD 6% 급등, 필라델피아반도체지수 1.54%↑",
            "content": "생성형 인공지능(AI) 최대 수혜주 엔비디아가 2.5%, 제2의 엔비디아로 불리는 AMD가 6% 정도 급등함에 따라 반도체 모임 …",
            "publisher": "뉴스1",
            "upload": "41분전"
        },
        {
            "id": 17,
            "title": "5G 28㎓ 경매 시작… 최종 낙찰자는 누구",
            "content": "5세대 이동통신(5G) 28기가헤르츠(㎓) 주파수 할당 신규 사업자를 선정하기 위한 경매가 25일 진행되는 가운데 최종 낙찰자는 …",
            "publisher": "머니S",
            "upload": "43분전"
        },
        {
            "id": 18,
            "title": "과기정통부, AI 산업 지원책 올해 1분기 안에 마련키로",
            "content": "과학기술정보통신부가 올해 1분기 내에 온디바이스AI를 포함한 AI 종합 지원 대책을 마련한다. 24일 박윤규 과기정통부 2차관은 …",
            "publisher": "아시아경제",
            "upload": "44분전"
        },
        {
            "id": 19,
            "title": "MS 장중 시총 '3조 달러' 클럽 진입… 목표주가 줄상향",
            "content": "마이크로소프트(MS) 시가총액이 장중 3조 달러(약 4000조 원)을 돌파했다. 잠시 동안이지만 애플에 이어 세계에서 두번째로 …",
            "publisher": "서울경제",
            "upload": "50분전"
        },
        {
            "id": 20,
            "title": "이상일 경상의대 교수, 42대 대한면역학회장 취임",
            "content": "대한면역학회는 이상일 경상의대 류마티스내과 교수가 제42대 대한면역학회 회장으로 취임했다고 25일 밝혔다. 이 교수는 “대한면역 …",
            "publisher": "아시아경제",
            "upload": "52분전"
        }
    ],
    "totalItems": 20000
}

서비스

package com.insilicogen.crawl.service;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import org.jsoup.Jsoup;
import org.jsoup.helper.StringUtil;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.insilicogen.crawl.dto.ImageDto;
import com.insilicogen.crawl.dto.InfoDto;
import com.insilicogen.crawl.model.Info;
import com.insilicogen.crawl.repository.NewsRepository;

@Service
public class NewsService {

	public static String destinationFolder = "C:\\Users\\kih25\\OneDrive\\바탕 화면\\Test\\crawling\\image";
	private final String newsUrl = "https://news.naver.com/main/list.naver?mode=LS2D&sid2=230&sid1=105&mid=shm&";
	private final String newsTag = "#main_content > div.list_body.newsflash_body";

	@Autowired
	private NewsRepository newsRepository;

	@Autowired
	public NewsService(NewsRepository newsRepository) {
		this.newsRepository = newsRepository;
	}

	// 이미지 다운로드 함수
	public void downloadImage(String imageUrl, String destinationFolder, InfoDto i) {
		try {
			if (!imageUrl.equals("")) { // 수정된 부분
				URL url = new URL(imageUrl);
				try (InputStream in = url.openStream()) {
					String fileName = i.getImageDto().getId() + ".jpg";
					Path destinationPath = Paths.get(destinationFolder, fileName);
					Files.copy(in, destinationPath, StandardCopyOption.REPLACE_EXISTING);
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// 크롤링 함수 => 우선 데이터 정보를 객체에 담아두고 DTO에 저장
	private List<InfoDto> crawlingNewsInInfo(Elements elements) {
		List<InfoDto> newsList = new ArrayList<>();

		for (Element element : elements) {
			String title = element.select("dt:not(.photo) > a").text().trim();
			String content = element.select("dd > span.lede").text().trim();
			String publisher = element.select("dd > span.writing").text().trim();
			String upload1 = element.select("dd > span.date.is_new").text().trim(); // ~분전
			String upload2 = element.select("dd > span.date.is_outdated").text().trim(); // ~시간전
			String upload3 = element.select("dd > span.date.is_outdated").text().trim(); // ~6일전 까지는 is_outdated
			// 그 이후로는 date 값이 온다.
			String upload4 = element.select("dd > span.date").text().trim(); // 
			
			Element imageElement = element.selectFirst("dl > dt.photo img");
			
			

			if (upload1.contains("분")) {
				String imageUrl = (imageElement != null) ? imageElement.attr("src") : "";

				Info info = new Info(title, content, publisher, upload1, imageUrl);
				newsList.add(saveData(info));

			}

			else if (upload2.contains("시")) {
				String imageUrl = (imageElement != null) ? imageElement.attr("src") : "";

				Info info = new Info(title, content, publisher, upload2, imageUrl);
				newsList.add(saveData(info));
			} 
			
			else if (upload3.contains("일")) {
				String imageUrl = (imageElement != null) ? imageElement.attr("src") : "";

				Info info = new Info(title, content, publisher, upload3, imageUrl);
				newsList.add(saveData(info));
			}else {
				String imageUrl = (imageElement != null) ? imageElement.attr("src") : "";

				Info info = new Info(title, content, publisher, upload4, imageUrl);
				newsList.add(saveData(info));
			}
		}

		return newsList;
	}

	private InfoDto saveData(Info info) {
		InfoDto infoDto = new InfoDto(info.getTitle(), info.getContent(), info.getPublisher(), info.getUpload());
		ImageDto imageDto = new ImageDto(info.getUrl(), infoDto);
		infoDto.setImageDto(imageDto);

		return infoDto;
	}

	// 초기 크롤링 함수
	public List<InfoDto> crawlAndSaveNews() {
		System.out.println("크롤링 시작!!");
		List<InfoDto> newsList = new ArrayList<>();

		String page = "&page=1";

		try {
			for (int date = 0; date < 30; date++) {
				Date currentDate = new Date();
				currentDate = decrementDate(currentDate, date);
				SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");

				String formattedDate = simpleDateFormat.format(currentDate);
				String newUrl = newsUrl.concat("date=").concat(formattedDate).concat(page);
				System.out.println(newUrl);

				Document doc = Jsoup.connect(newUrl).get();
				Element body = doc.selectFirst(newsTag);

				// 헤드라인 기사와 일반기사 구문 하지 않음
				Elements headlineElements = body.select("ul.type06_headline > li");
				newsList.addAll(crawlingNewsInInfo(headlineElements));

				Elements normalElements = body.select("ul.type06 > li");
				newsList.addAll(crawlingNewsInInfo(normalElements));

				// 이미지 다운로드 및 뉴스 저장
				for (InfoDto info : newsList) {
					if (StringUtil.isBlank(info.getImageDto().getImageUrl())) {
						downloadImage(info.getImageDto().getImageUrl(), destinationFolder, info);
					}
				}

				System.out.println("크롤링 완료");

				

			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		newsRepository.saveAll(newsList);
		return newsList;
	}

	public List<InfoDto> getPage(int page, int pageSize) {
		int offset = (page - 1) * pageSize;
		return newsRepository.findNewsList(offset, pageSize);
	}

	public int getTotalNewsCount() {
		return (int) newsRepository.count();
	}

	/* 날짜가 주어지면 하루 씩 줄어드는 메소드 작성 */
	private static Date decrementDate(Date date, int minusDate) {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);

		// 현재 일자에서 1일을 뺍니다.
		calendar.add(Calendar.DATE, -minusDate);

		return calendar.getTime();
	}
}

서비스 부분은 크게 달라진 점은 없다.

 

새로 추가된 메소드와 수정된 메소드에 대해서 설명을 덧붙이도록 하겠다.

  • crawlingNewsInInfo()
    • 현재 날짜를 기준으로 하루 씩 감소하며 타겟 url에 해당되는 1 페이지의 기사를 크롤링한다.
    • 우선 Info 객체에 데이터를 저장하고 이를 InfoDto로 변환하여 newsList에 추가한다.
    • 코드를 수정하기 전 데이터가 하루 전 까지의 데이터 즉, 이틀 치만 데이터베이스에 저장이 되는 이슈가 있었다.
      • 이유를 파악해보니 태그 url의 upload를 나타내는 selector 부분이  4가지로 나뉘었다.
        • ~분전
        • ~시간전
        • ~일전
        • date
      • 이를 파악하기 전에는 upload1, upload2 로만 판단하여 데이터를 넣어 이슈가 발생하였고, 이를 해결하기 위해 위 코드 처럼 각 데이터에 맞는 태그를 맞춰주었다.
  • decrementDate()
    • 주어진 날짜를 기준으로 하루를 뺀 새로운 날짜를 반환한다.
    • crawlingNewsInInfo()에 호출되며 30번 반복 (한달)

첫 크롤링 후 데이터는 600개 (20개 * 30일)가 저장된다. postman을 통해 데이터가 잘 삽입되고 페이지 네이션이 이루어 지는지 확인해보자.

page : 10 , pageSize : 20 으로 parameter를 주었고 181번 부터 200번 까지의 데이터가 잘 응답 되었음.


이제 Front 단을 구축해보자. 

추가해야할 것은 테이블과 버튼 그리고 페이지네이션이다.

 

아래는 news.jsp 코드이다. 아직 미완성으로 ajax 통신이 이루어지는지 만 파악이 가능한 코드이다.

 

<!DOCTYPE html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<script src="resources/js/jquery/jquery-3.7.1.js"></script>
<script src="resources/js/bootstrap/bootstrap.bundle.js"></script>

<link href="resources/css/bootstrap/bootstrap.css" rel="stylesheet" />
<title>News Page</title>

<script>
    var newsList = [];
    var currentPage = 1;

    function initCrawling() {
        $.ajax({
            type: "GET",
            url: "/initCrawling",
            data: {},
            success: function (response) {
                console.log(response)
            },
            error: function () {
                alert("Error crawling.");
            }
        });
    }

    function loadNews(page, pageSize) {
        $.ajax({
            type: "GET",
            url: "/selectNewsList",
            data: {
                page: page,
                pageSize: pageSize
            },
            success: function (response) {
            	console.log(response);
                newsList = response.newsList;
                displayNews();
                console.log(newsList);
            },
            error: function () {
                alert("load News Error");
            }
        })
    }
    // 테이블 갱신 함수
    function displayNews() {
        // 기존에 표시된 내용 초기화
        $("#newsTableBody").empty();

        // newsList에 있는 데이터를 테이블에 추가
        for (var i = 0; i < newsList.length; i++) {
            var news = newsList[i];
            var imageUrl = news.imageDto ? news.imageDto.imageUrl : ""; // 이미지가 있는 경우만 URL 가져오기

            // 각 데이터를 테이블에 추가하는 로직
            var row = "<tr>" +
                "<td>" + news.title + "</td>" +
                "<td>" + news.content + "</td>" +
                "<td>" + news.publisher + "</td>" +
                "<td>" + news.upload + "</td>" +
                "<td><img src='" + imageUrl + "' alt='Image'></td>" +
                "</tr>";

            // 생성한 행을 테이블에 추가
            $("#newsTableBody").append(row);
        }
    }

    // 버튼 클릭 시 다음 페이지 호출
    $("button").on("click", function () {      
        loadNews(currentPage++, 20);        
    });

    // 초기 페이지 로드
    $(document).ready(function () {
        // 초기 페이지 로딩 시에도 displayNews 함수 호출
        loadNews(currentPage, 20);
    });

    //버튼 클릭 시 params를 ajax 통신으로 보내도록 수정
    $("button").on("click", function () {
        var url = "/initCrawling/button";
        var params = {
            page: currentPage,
            pageSize: 20
        };

        $.ajax({
            type: "GET",
            url: url,
            data: params,
            dataType: "json",
            success: function (response) {
                newsList = response.newsList;
                displayNews();
                console.log(newsList);
            },
            error: function () {
                alert("btn error");
            }
        });
    });
</script>

<div class="container pt-5">
    <button class="btn btn-primary" type="submit" style="float: right; position: relative;">Button</button>
    <h2 style="text-align: center;">News List</h2>
    <table class="table">
        <thead>
            <tr>
                <th>Title</th>
                <th>Content</th>
                <th>Publisher</th>
                <th>Upload Date</th>
                
                <th>Image</th>
            </tr>
        </thead>
        <tbody id="newsTableBody"></tbody>
    </table>
    <nav aria-label="Page navigation">
        <ul class="pagination" id="pagination">
            <li class="page-item"><a class="page-link" href="#">Previous</a></li>
            <li class="page-item"><a class="page-link" href="#">1</a></li>
            <li class="page-item"><a class="page-link" href="#">2</a></li>
            <li class="page-item"><a class="page-link" href="#">3</a></li>
            <li class="page-item"><a class="page-link" href="#">Next</a></li>
        </ul>
    </nav>
</div>

이것 저것 시도하다보니 굉장히 난잡하다.

 

현재 상황은 /initCrawling 으로 크롤링이 완료되면 200 응답을 보내고 loadNews() 로 넘어간다. 하지만, 에러가 발생하여 alert로 지정해둔 localhost:8080 load News Error 문구가 뜬다. 다음 포스팅 전까지 이를 해결하여 테이블과 페이지 네이션을 완료하겠다.

 

현재 화면은 아래와 같다.

 

추가적인 코드는 아래 깃 허브에서 확인할 수 있다.

https://github.com/MinWook6457/WebCrolling

 

GitHub - MinWook6457/WebCrolling: Insilicogen : WebCrawling using Jsoup with Spring Boot

Insilicogen : WebCrawling using Jsoup with Spring Boot - GitHub - MinWook6457/WebCrolling: Insilicogen : WebCrawling using Jsoup with Spring Boot

github.com

 

'인실리코젠' 카테고리의 다른 글

[인실리코젠] Spring CRUD 프로젝트 요구 사항 분석 및 데이터베이스 설계  (1) 2024.02.16
[인실리코젠] Web Crawling - Finish  (1) 2024.02.02
[인실리코젠] Spring : 용어 정리(기본)  (0) 2024.01.19
[인실리코젠] Spring Boot + MVC 이론 정리  (0) 2024.01.17
[인실리코젠] WebCrawling - 1  (0) 2024.01.16
  1. 스프링 부트 어플리케이션 시작 클래스
  2. 컨트롤러
  3. DTO
  4. Ajax 통신이란?
  5. Ajax 장점
  6. Ajax 기본 형태
  7. 서비스
'인실리코젠' 카테고리의 다른 글
  • [인실리코젠] Spring CRUD 프로젝트 요구 사항 분석 및 데이터베이스 설계
  • [인실리코젠] Web Crawling - Finish
  • [인실리코젠] Spring : 용어 정리(기본)
  • [인실리코젠] Spring Boot + MVC 이론 정리
min._.uuk_
min._.uuk_
하루 하나 기록하기
  • min._.uuk_
    기록하는 습관
    min._.uuk_
  • 전체
    오늘
    어제
    • 분류 전체보기 (33)
      • 알고리즘 (15)
      • 자바 스크립트 (2)
      • 인실리코젠 (15)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • github
  • 공지사항

  • 인기 글

  • 태그

    백준 #세그먼트 트리 #구간합 #c언어 #골드1 #탐색
    인실리코젠 #크롤링 #SpringBoot #MVC #jQuery
    Apache #POI #JAVA #maven
    #BOJ #백준 #10989번 #C언어 #카운팅정렬 #정렬
    백준 #BOJ #스택 #수열
    백준 #MST #최소비용신장트리 #크루스칼 #그리디 #알고리즘
    백준 #후위표기식 #스택 #중위표기식 #우선순위 #1918번
    백준 #동적계획법 #dp #백트래킹 #dfs
    백준 #1407번 #재귀함수
    JS #테트리스 #HTML #CSS #추억의 게임
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
min._.uuk_
[인실리코젠] WebCrawling - 2
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.