코딩항해기
[JSP] 서버 시작할 때 크롤링한 샘플데이터 추가하기 본문
Listener와 selenium4 동적 크롤링을 이용해 서버가 시작할 때 상품 DB를 확인하고 비어있다면 크롤링한 샘플 데이터를 추가할 수 있도록 하는 기능을 추가하려고 한다.
크롤링
크롤링 기초는 이전 글을 참고 바란다.
해당 방식은 스크롤을 한 번에 진행하는 방식이었는데, 해당 방식으로 똑같이 진행할 경우 이미지가 제대로 로딩되지 않고 지나가 이미지 크롤링이 제대로 진행되지 않았다.
이러한 문제를 보완하기 위해 스크롤을 픽셀 단위로 할 수 있도록 수정했으며, 대기를 걸어 이미지가 전부 로딩된 이후에 스크롤이 다시 내려갈 수 있도록 로직을 보완했다. 종료 방식은 적당한 양의 데이터가 크롤링되면 스크롤을 종료하도록 했다.
package model.common;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import io.github.bonigarcia.wdm.WebDriverManager;
import model.jiyoon.ProductDTO;
public class Crawling {
//네이버 쇼핑 - 붕어빵 악세서리 검색
private static final String URL = "https://search.shopping.naver.com/search/all?query=%EB%B6%95%EC%96%B4%EB%B9%B5%20%EC%95%85%EC%84%B8%EC%84%9C%EB%A6%AC"; //크롤링할 주소
private static final String CSS_ELEMENTS_PRICE = "div > div.product_info_area__xxCTi > div.product_price_area__eTg7I > strong > span.price > span > em"; //가격 Css Selector
private static final String CSS_ELEMENTS_IMAGE = "div > div.product_img_area__cUrko > div > a > img"; //이미지, 상품명 Css Selector
private static final int SCROLL_AMOUNT = 100; // 한번에 스크롤할 픽셀 양
public static ArrayList<ProductDTO> findProductInfo() {
ArrayList<ProductDTO> datas = new ArrayList<>(); //반환할 데이터
List<WebElement> priceElements = null; //가격 크롤링 담을 리스트
List<WebElement> imageElements = null; //이미지 크롤링 담을 리스트 (이미지, 상품명)
int index = 0; //가격 index
//WebDriverManager를 통한 크롬 드라이버 관리
WebDriverManager.chromedriver().setup();
WebDriver driver = new ChromeDriver();
//스크롤을 위한 캐스팅
JavascriptExecutor js = (JavascriptExecutor) driver;
//웹페이지 로드
driver.get(URL);
try {
//스크롤하며 요소를 단계적으로 요소를 찾기 위한 반목문
while (true) {
priceElements = driver.findElements(By.cssSelector(CSS_ELEMENTS_PRICE)); //가격
imageElements = driver.findElements(By.cssSelector(CSS_ELEMENTS_IMAGE)); //이미지 (+상품명)
js.executeScript("window.scrollBy(0, " + SCROLL_AMOUNT + ");"); //스크롤
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5)); //암시적 대기
if (imageElements.size() >= 35) {
//요소를 35개 이상 찾으면 종료
System.out.println("log: Crawling Elements find 35 up");
break;
}
}//while문 종료
//크롤링한 정보 담기
for (WebElement image : imageElements) {
//이미지 태그 요소를 기준으로 상품 객체에 정보를 넣고 datas 리스트에 추가
ProductDTO data = new ProductDTO();
data.setProductName(image.getAttribute("alt")); //이미지 설명 (상품명과 동일함)
data.setProductProfileWay(image.getAttribute("src")); //이미지 주소
data.setProductPrice(Integer.parseInt(priceElements.get(index++).getText().replaceAll("[^\\d]", ""))); //가격
datas.add(data);//추가
}//for문 종료
} catch (NoSuchElementException e) {
System.err.println("log: Crawling Exception : "+e.getMessage());
datas.clear();
} catch (NumberFormatException e) {
System.err.println("log: Crawling Exception : "+e.getMessage());
datas.clear();
} catch (ArrayIndexOutOfBoundsException e) {
System.err.println("log: Crawling Exception : "+e.getMessage());
datas.clear();
} catch (Exception e) {
System.err.println("log: Crawling Exception : "+e.getMessage());
datas.clear();
} finally {
//페이지 닫기
driver.quit();
}
return datas;
}
}
확인하기
//해당 main을 추가해서 제대로 크롤링 되고 있는지 확인할 수 있다.
public static void main(String[] args) {
ArrayList<ProductDTO> datas = findProductInfo();
for(ProductDTO data : datas) {
System.out.println(data);
}
}
리스너 Listener
리스너도 마찬가지로 이전에 정리한 내용을 참고 바란다.
리스너에서는 먼저 DB가 비어있는지를 확인해야한다. DB가 비어있는지를 확인하지 않으면 서버가 재시작, 시작될 때마다 크롤링이 다시 이뤄지기 때문에 서버 켜지는데 시간이 오래 걸리게 된다.
만약 DB가 비어있다면 크롤링한 데이터를 받아올 것이다. 이때 크롤링이 실행되게 된다.
//크롤링한 데이터 받아오기
ArrayList<ProductDTO> datas = Crawling.findProductInfo();
크롤링한 데이터를 받아왔으면 이제 상품 DB에 넣어야하는데, 상품 DB에 insert할 때 카테고리 번호가 필수적으로 들어가야하므로, 먼저 현재 카테고리 번호가 어떤 것들이 있는지 불러와줄 예정이다. 해당 객체는 상품 DB에 데이터가 있으면 생성할 필요가 없으므로 if문 안에서 new 연산자를 사용해 생성했다.
//현재 있는 상품 카테고리 리스트 받아오기
ProductCateDTO productCateDTO = new ProductCateDTO();
ProductCateDAO productCateDAO = new ProductCateDAO();
ArrayList<ProductCateDTO> productCateList = productCateDAO.selectAll(productCateDTO);
받아온 상품 카테고리의 PK값을 랜덤으로 넣어줄 예정이므로 index번호와 Random 객체를 사용했다.
//크롤링한 데이터를 순회하며 카테고리 번호를 랜덤값으로 넣음
data.setProductCateNum(productCateList.get(rand.nextInt(productCateList.size())).getProductCateNum());
그럼 이제 insert할 준비가 완료 됐다.
package controller.common;
import java.util.ArrayList;
import java.util.Random;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
import model.common.Crawling;
import model.jiyoon.ProductCateDAO;
import model.jiyoon.ProductCateDTO;
import model.jiyoon.ProductDAO;
import model.jiyoon.ProductDTO;
@WebListener
public class SampleListener implements ServletContextListener {
private ProductDAO productDAO;
private ProductDTO productDTO;
//생성자
public SampleListener() {
this.productDAO = new ProductDAO();
this.productDTO = new ProductDTO();
productDTO.setEndNum(10);//페이지네이션 용 값
}
//서버 시작시
public void contextInitialized(ServletContextEvent sce) {
if(productDAO.selectAll(productDTO).isEmpty()) {
System.out.println("log: Listener Product isEmpty");
//만약 상품 DB가 비어있을 경우
//크롤링한 데이터 받아오기
ArrayList<ProductDTO> datas = Crawling.findProductInfo();
//현재 있는 상품 카테고리 리스트 받아오기
ProductCateDTO productCateDTO = new ProductCateDTO();
ProductCateDAO productCateDAO = new ProductCateDAO();
ArrayList<ProductCateDTO> productCateList = productCateDAO.selectAll(productCateDTO);
//랜덤값을 위한 랜덤 객체 선언
Random rand = new Random();
for(ProductDTO data : datas) {
//크롤링한 데이터를 순회하며 카테고리 번호를 랜덤값으로 넣어
data.setProductCateNum(productCateList.get(rand.nextInt(productCateList.size())).getProductCateNum());
data.setCondition("CRAWLING_ONLY");
//DB에 상품 추가
productDAO.insert(data);
}
}
System.out.println("log: Listener End");
}
//서버 종료시
public void contextDestroyed(ServletContextEvent sce) {
}
}
ProductDAO insert
public boolean insert(ProductDTO productDTO) {
System.out.println("log: Product insert start");
Connection conn = JDBCUtil.connect();
PreparedStatement pstmt = null;
try {
if(productDTO.getCondition().equals("CRAWLING_ONLY")) {
System.out.println("log: Product insert condition : CRAWLING_ONLY");
//오직 크롤링 insert용 (Controller에서 사용하지 않음 / Crawling insert (Listener)는 model에서 담당)
pstmt = conn.prepareStatement(INSERT_CRAWLING);
pstmt.setString(1, productDTO.getProductName()); //상품 이름
pstmt.setInt(2, productDTO.getProductPrice()); //상품 가격
pstmt.setString(3, productDTO.getProductProfileWay()); //썸네일
pstmt.setInt(4, productDTO.getProductCateNum()); //상품 카테고리 번호
//넘어온 값 확인 로그
System.out.println("log: parameter getProductName : "+productDTO.getProductName());
System.out.println("log: parameter getProductPrice : "+productDTO.getProductPrice());
System.out.println("log: parameter getProductProfileWay : "+productDTO.getProductProfileWay());
System.out.println("log: parameter getProductCateNum : "+productDTO.getProductCateNum());
}
else {
//새 상품작성
pstmt = conn.prepareStatement(INSERT);
pstmt.setString(1, productDTO.getProductName()); //상품 이름
pstmt.setInt(2, productDTO.getProductPrice()); //상품 가격
pstmt.setString(3, productDTO.getProductProfileWay()); //썸네일
pstmt.setInt(4, productDTO.getBoardNum()); //상품설명게시글번호
pstmt.setInt(5, productDTO.getProductCateNum()); //상품 카테고리 번호
//넘어온 값 확인 로그
System.out.println("log: parameter getProductName : "+productDTO.getProductName());
System.out.println("log: parameter getProductPrice : "+productDTO.getProductPrice());
System.out.println("log: parameter getProductProfileWay : "+productDTO.getProductProfileWay());
System.out.println("log: parameter getBoardNum : "+productDTO.getBoardNum());
System.out.println("log: parameter getProductCateNum : "+productDTO.getProductCateNum());
}
if(pstmt.executeUpdate() <= 0) {
//쿼리는 정상적으로 실행됐으나 실패
System.err.println("log: Product insert execute fail");
return false;
}
} catch (SQLException e) {
System.err.println("log: Product insert SQLException fail");
e.printStackTrace();
return false;
} catch (Exception e) {
System.err.println("log: Product insert Exception fail");
e.printStackTrace();
return false;
} finally {
//연결해제
if(!JDBCUtil.disconnect(conn, pstmt)) {
//연결해제 실패
System.err.println("log: Product insert disconnect fail");
return false;
}
System.out.println("log: Product insert end");
}
System.out.println("log: Product insert true");
return true;
}
완료 (DB확인)
이제 서버를 키면 리스너가 실행되면서 DB를 확인하고 DB가 비어있다면 크롤링을 실행해 상품 DB를 크롤링한 데이터로 넣게 된다. 상품 테이블을 확인해보면 정상적으로 데이터가 들어온 것을 확인할 수 있다.
NoClassDefFoundError: .../selenium/WebDriver 에러가 발생할 경우 해당 글을 참고 바란다.
'JSP' 카테고리의 다른 글
[JSP] JDBC - MyBatis 기초 (1) | 2024.09.27 |
---|---|
[JSP] JDBC - 컨디션 하드코딩 방지, 컨디션 값 변동이 예상될 때 (1) | 2024.09.15 |
[JSP] JDBC - 필터 검색 (Model 파트/HashMap 사용) (0) | 2024.09.08 |
[JSP] Servlet - 필터 Filter (0) | 2024.09.05 |
[JSP] alert창 띄우고 페이지 전환하기 (0) | 2024.09.04 |