지난 포스팅에서는 Spring 프레임워크를 이용해 MySQL과 연결하고 사용하려는 Dependency를 설정하였다.
이번 포스팅에서는 Selenium을 이용하여 동적 웹 페이지에서 데이터를 크롤링하여 데이터를 DB에 저장하고, 이를 웹 페이지에 표시하는 방법에 대해 포스팅 하겠다.
이 프로젝트는 크게 다섯 부분으로 구성된다.
CrawlingController라는 컨트롤러 클래스를 정의한 것이다.
이 클래스는 웹 요청을 처리하고, 그에 따른 비즈니스 로직을 호출하며, 그 결과를 보여줄 뷰를 선택하는 역할을 수행한다.
@Slf4j
@Controller
public class CrawlingController {
@Autowired
private CrawlingService service;
@GetMapping("/crawl")
public String crawl(Model model) {
service.crawlAndSaveGames(); // DB저장
List<Games> games = service.getGamesFromDb(); // DB SELECT
if (games == null || games.isEmpty()) {
log.error("games list is null or empty");
return "error";
}
model.addAttribute("games", games);
return "crawl";
}
}
CrawlingController 클래스를 통해 /crawl 경로로 GET 요청이 오면 crawl 메소드를 호출하도록 한다.crawlAndSaveGames 메소드를 호출하여 웹사이트에서 게임 정보를 크롤링하고 그 결과를 DB에 저장한다.getGamesFromDb 메소드를 호출하여 DB에서 게임 정보를 가져온다.@Slf4
@Service
public class CrawlingService {
@Autowired
private CrawlingDao dao;
CrawlingService 클래스는 웹 크롤링과 관련된 비즈니스 로직을 수행하는 서비스 클래스이다.@Slf4j 어노테이션은 로그 메시지를 출력하는데 사용되며, @Service 어노테이션이 붙어 있어 스프링에서 서비스 클래스로 인식한다. CrawlingDao는 데이터베이스와의 통신을 담당하는 DAO 클래스로, @Autowired 어노테이션을 통해 자동 주입된다.public void crawlAndSaveGames() {
System.setProperty("webdriver.chrome.driver", "chromedriver-win64/chromedriver.exe");
ChromeOptions options = new ChromeOptions();
options.addArguments("--remote-allow-origins=*");
options.addArguments("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537");
crawlAndSaveGames 메소드는 웹 크롤링을 수행하고 그 결과를 데이터베이스에 저장하는 기능을 수행한다. System.setProperty는 크롬 드라이버의 위치를 설정하며, ChromeOptions는 크롬 드라이버의 동작을 설정한다.addArguments 메소드를 사용하여 크롬 드라이버의 원격 클라이언트 연결 허용 원본과 사용자 에이전트를 설정한다.--remote-allow-origins=*: user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537: ※ selenium은 사용하는 브라우저의 Driver를 다운받아야 한다. ※
(https://chromedriver.chromium.org/downloads)
WebDriver driver = new ChromeDriver(options);
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
String baseUrl = "https://store.steampowered.com/category/action_fps/?flavor=contenthub_all";
driver.get(baseUrl);
wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".salepreviewwidgets_SaleItemBrowserRow_y9MSd")));
List<WebElement> gameElements = driver.findElements(By.cssSelector(".salepreviewwidgets_SaleItemBrowserRow_y9MSd"));
for (WebElement gameElement : gameElements) {
WebDriver 객체를 생성하여 크롬 브라우저를 제어하고, WebDriverWait 객체를 생성하여 웹 요청의 응답을 기다리는 최대 시간을 설정한다.driver.get 메소드를 사용하여 웹 페이지에 접속하고, wait.until 메소드로 웹 페이지의 특정 요소가 보일 때까지 기다린다.driver.findElements 메소드는 웹 페이지의 특정 요소들을 찾아 리스트로 반환한다.String title = gameElement.findElement(By.cssSelector(".salepreviewwidgets_StoreSaleWidgetTitle_3jI46.StoreSaleWidgetTitle")).getText();
findElement 메소드는 웹 요소 중에서 CSS 선택자에 해당하는 첫 번째 요소를 찾아 반환하며, getText 메소드는 해당 웹 요소의 텍스트를 가져온다.Games games = new Games();
games.setTitle(title);
games.setReleases(releases);
games.setCart(cart);
if (dao.getGameByTitle(title) == null) {
dao.setGameList(games);
dao.setPriceList(price);
dao.setReviewList(review);
}
dao.getGameByTitle 메소드를 호출하여 동일한 제목의 게임이 데이터베이스에 있는지 확인하고, 없는 경우 게임 정보를 데이터베이스에 저장한다.해당 부분은 크게 세 단계로 이루어진다.
프로젝트 디렉토리 설정, 이미지 다운로드 및 저장, 그리고 DB에 이미지 정보 저장.
File projectDir = new File(System.getProperty("user.dir"));
File dir = new File(projectDir, "/src/main/webapp/resources/images");
try {
URL imageUrl = new URL(img);
BufferedImage image = ImageIO.read(imageUrl);
String safeTitle = title;
if (title.contains(":")) {
safeTitle = title.replaceAll(":", "-");
}
BufferedImage 객체를 생성한다.:)이 포함되어 있다면 이를 대쉬(-)로 변환한다. 이는 파일 이름에 콜론이 포함되어 있으면 문제를 일으킬 수 있기 때문이다. Images images = new Images();
images.setOriginName(safeTitle + ".jpg");
images.setTitle(title);
String imageName = DigestUtils.sha256Hex(title).substring(0, 20) + ".jpg";
images.setImageName(imageName);
Images 객체를 생성하고, 이 객체에 원본 이름과 제목을 설정한다. File outputfile = new File(dir, images.getImageName());
ImageIO.write(image, "jpg", outputfile);
System.out.println(safeTitle + " 이미지 저장 완료");
if (dao.getImageByName(imageName) == null) {
dao.setImage(images);
}
} catch (IOException e) {
e.printStackTrace();
}
Images 객체를 저장한다.List<WebElement> tagElements = gameElement.findElements(By.cssSelector(".salepreviewwidgets_StoreSaleWidgetTags_3OSJs a"));
List<String> tag = new ArrayList<>();
for (WebElement tagElement : tagElements) {
tag.add(tagElement.getText());
}
for (String singleTag : tag) {
if (!singleTag.isEmpty()) {
Tags tags = new Tags();
tags.setTag(singleTag);
Tags 객체를 생성하고, 이 객체에 태그를 설정한다. Tags existingTag = dao.getTagByTag(singleTag);
if (existingTag == null) {
dao.setTags(tags);
existingTag = dao.getTagByTag(singleTag);
}
if (existingTag != null) {
GameTags gameTag = new GameTags();
gameTag.setTitle(title);
gameTag.setTag(singleTag);
GameTags 객체를 생성하고, 이 객체에 게임의 제목과 태그를 설정다.
GameTags existingGameTag = dao.getGameTagByTitleAndTag(title, singleTag);
if (existingGameTag == null) {
dao.setGameTagList(gameTag);
}
}
}
}
public List<Game> getGamesFromDb() {
List<Games> games = dao.getAllGames();
for (Games game : games) {
List<String> tags = dao.getTagsByTitle(game.getTitle());
game.setTag(tags);
}
return games;
}
getGamesFromDb 메소드는 dao.getAllGames를 호출하여 데이터베이스에 저장된 모든 게임 데이터를 가져온다.private String getWebElementText(WebElement element, String cssSelector) {
List<WebElement> elements = element.findElements(By.cssSelector(cssSelector));
return elements.isEmpty() ? "" : elements.get(0).getText();
}
getWebElementText 메소드는 주어진 웹 요소에 대해 CSS 선택자에 해당하는 웹 요소들을 찾고, 그 중 첫 번째 웹 요소의 텍스트를 가져와 반환한다. @Slf4j
@Service
public class CrawlingService {
@Autowired
private CrawlingDao dao;
public void crawlAndSaveGames() {
// 크롬 드라이버 경로 설정
System.setProperty("webdriver.chrome.driver", "chromedriver-win64/chromedriver.exe");
ChromeOptions options = new ChromeOptions();
options.addArguments("--remote-allow-origins=*");
options.addArguments("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537");
// 크롬 드라이버 인스턴스 생성
WebDriver driver = new ChromeDriver(options);
// 웹 드라이버 대기 시간 설정
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
// 크롤링할 사이트 주소
String baseUrl = "https://store.steampowered.com/category/action_fps/?flavor=contenthub_all";
driver.get(baseUrl);
// 페이지 로딩 대기
wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".salepreviewwidgets_SaleItemBrowserRow_y9MSd")));
// 게임 정보를 담을 WebElement List List<WebElement> gameElements = driver.findElements(By.cssSelector(".salepreviewwidgets_SaleItemBrowserRow_y9MSd"));
for (WebElement gameElement : gameElements) {
// 각 게임 정보 추출
String title = gameElement.findElement(By.cssSelector(".salepreviewwidgets_StoreSaleWidgetTitle_3jI46.StoreSaleWidgetTitle")).getText();
String disc = getWebElementText(gameElement, ".salepreviewwidgets_StoreSaleDiscountBox_2fpFv");
String originPrice = getWebElementText(gameElement, ".salepreviewwidgets_StoreOriginalPrice_1EKGZ");
String discPrice = getWebElementText(gameElement, ".salepreviewwidgets_StoreSalePriceBox_Wh0L8");
String cart = gameElement.findElement(By.cssSelector(".addtocartbutton_Action_2ECxA.CartBtn")).getText();
String img = gameElement.findElement(By.cssSelector(".salepreviewwidgets_CapsuleImage_cODQh")).getAttribute("src");
String releases = gameElement.findElement(By.cssSelector(".salepreviewwidgets_WidgetReleaseDateAndPlatformCtn_2vdJg")).getText();
String oneLineReview = gameElement.findElement(By.cssSelector(".gamehover_ReviewScore_24NyY.ReviewScore.Focusable > div > div:nth-child(1)")).getText();
String reviewCount = gameElement.findElement(By.cssSelector(".gamehover_ReviewScore_24NyY.ReviewScore.Focusable > div > div:nth-child(2)")).getText();
// 게임 정보 객체 생성
Games games = new Games();
games.setTitle(title);
games.setReleases(releases);
games.setCart(cart);
// 가격 정보 객체 생성
Price price = new Price();
price.setTitle(title);
price.setDisc(disc);
price.setOriginPrice(originPrice);
price.setDiscPrice(discPrice);
// 리뷰 정보 객체 생성
Review review = new Review();
review.setTitle(title);
review.setOneLineReview(oneLineReview);
review.setReviewCount(reviewCount);
// 기존에 없는 게임 정보라면 DB에 저장
if (dao.getGameByTitle(title) == null) {
dao.setGameList(games);
dao.setPriceList(price);
dao.setReviewList(review);
}
// 프로젝트의 루트 디렉토리를 가져옵니다.
File projectDir = new File(System.getProperty("user.dir"));
// 이미지를 저장할 디렉토리를 설정합니다.
File dir = new File(projectDir, "/src/main/webapp/resources/images");
// 폴더가 없으면 생성
if (!dir.exists()) {
dir.mkdirs();
}
// 이미지 다운로드 및 저장
try {
URL imageUrl = new URL(img);
BufferedImage image = ImageIO.read(imageUrl);
String safeTitle = title;
if (title.contains(":")) {
safeTitle = title.replaceAll(":", "-");
}
// 이미지 객체 생성
Images images = new Images();
images.setOriginName(safeTitle + ".jpg");
images.setTitle(title);
// title을 SHA2 알고리즘으로 해싱한 후, 그 결과의 첫 20자리와 '.jpg'를 연결한 문자열을 생성
String imageName = DigestUtils.sha256Hex(title).substring(0, 20) + ".jpg";
// 생성한 문자열로 Images 객체의 IMAGE_NAME 필드 설정
images.setImageName(imageName);
File outputfile = new File(dir, images.getImageName()); // 파일 이름을 게임 제목으로 설정
ImageIO.write(image, "jpg", outputfile);
System.out.println(safeTitle + " 이미지 저장 완료");
// 기존에 없는 이미지라면 DB에 저장
if (dao.getImageByName(imageName) == null) {
dao.setImage(images);
}
} catch (IOException e) {
e.printStackTrace();
}
// 태그 정보 추출
List<WebElement> tagElements = gameElement.findElements(By.cssSelector(".salepreviewwidgets_StoreSaleWidgetTags_3OSJs a"));
// [FPS, PvP, eSports, Tactical, Multiplayer]
List<String> tag = new ArrayList<>();
for (WebElement tagElement : tagElements) {
tag.add(tagElement.getText());
}
for (String singleTag : tag) { // tag 배열의 각 요소(singleTag)에 대해 반복 한다.
if (!singleTag.isEmpty()) { // singleTag가 비어있지 않은 경우에만 아래의 처리를 실행.
Tags tags = new Tags(); // 새로운 Tags 객체를 생성.
tags.setTag(singleTag); // 생성한 Tags 객체에 singleTag를 설정.
// dao를 통해 singleTag와 일치하는 태그가 이미 데이터베이스에 존재하는지 확인.
Tags existingTag = dao.getTagByTag(singleTag);
if (existingTag == null) { // 해당 태그가 존재하지 않는 경우,
dao.setTags(tags); // 새로운 태그를 데이터베이스에 추가.
// 태그가 정상적으로 추가된 후, 다시 한번 해당 태그가 존재하는지 확인.
existingTag = dao.getTagByTag(singleTag);
}
if (existingTag != null) { // 해당 태그가 데이터베이스에 존재하는 경우,
GameTags gameTag = new GameTags(); // 새로운 GameTags 객체를 생성.
gameTag.setTitle(title); // 게임의 제목을 설정.
gameTag.setTag(singleTag); // 게임의 태그를 설정.
// dao를 통해 같은 게임 제목과 태그를 가진 데이터가 이미 데이터베이스에 존재하는지 확인.
GameTags existingGameTag = dao.getGameTagByTitleAndTag(title, singleTag);
if (existingGameTag == null) { // 같은 데이터가 존재하지 않는 경우,
dao.setGameTagList(gameTag); // 새로운 게임 태그를 데이터베이스에 추가.
}
}
}
}
}
// 크롬 드라이버 종료
driver.quit();
}
// DB SELECT 메서드
public List<Games> getGamesFromDb() {
List<Games> games = dao.getAllGames();
for (Games game : games) {
List<String> tags = dao.getTagsByTitle(game.getTitle());
game.setTag(tags);
}
return games;
}
// 첫 번째 웹 요소의 텍스트를 가져와 반환한다.
// 해당하는 웹 요소가 없는 경우 빈 문자열을 반환한다.
private String getWebElementText(WebElement element, String cssSelector) {
List<WebElement> elements = element.findElements(By.cssSelector(cssSelector));
return elements.isEmpty() ? "" : elements.get(0).getText();
}
}
public interface CrawlingDao {
int setGameList(Games game);
int setPriceList(Price price);
int setReviewList(Review review);
int setGameTagList(GameTags gameTags);
int setTags(Tags tags);
int setImage(Images images);
Tags getTagByTag(String singleTag);
Games getGameByTitle(String title);
Images getImageByName(String imageName);
List<String> getTagsByTitle(String title);
List<Games> getAllGames();
GameTags getGameTagByTitleAndTag(String title, String tag);
}
setGameList, setPriceList, setReviewList, setGameTagList, setTags, setImage 등의 메서드들은 각각 게임, 가격, 리뷰, 게임 태그, 태그, 이미지 정보를 데이터베이스에 저장한다.getTagByTag는 특정 태그에 해당하는 정보를 데이터베이스에서 조회하는 메서드이다.getGameByTitle 는 중복된 게임 정보를 검색하는 기능을 수행한다.getImageByName, 메서드와 게임 제목을 이용해 태그 목록을 데이터베이스에서 조회하는 getTagsByTitle 가 있다.getAllGames 메서드가 있다.getGameTagByTitleAndTag 메서드이다.@Repository
public class CrawlingDaoImpl implements CrawlingDao{
@Autowired
private SqlSessionTemplate sql;
// DB INSERT
@Override
public int setGameList(Games game) {
return sql.insert("gameMapper.setGameList", game);
}
@Override
public int setPriceList(Price price) {
return sql.insert("gameMapper.setPriceList", price);
}
@Override
public int setReviewList(Review review) {
return sql.insert("gameMapper.setReviewList", review);
}
@Override
public int setGameTagList(GameTags gameTags) {
return sql.insert("gameMapper.setGameTagList", gameTags);
}
@Override
public int setTags(Tags tags) {
return sql.insert("gameMapper.setTags", tags);
}
@Override
public int setImage(Images images) {
return sql.insert("gameMapper.setImage", images);
}
@Override
public Tags getTagByTag(String singleTag) {
return sql.selectOne("gameMapper.getTagByTag", singleTag);
}
@Override
public Games getGameByTitle(String title) {
return sql.selectOne("gameMapper.getGameByTitle", title);
}
@Override
public Images getImageByName(String imageName) {
return sql.selectOne("gameMapper.getImageByName", imageName);
}
@Override
public List<String> getTagsByTitle(String title) {
return sql.selectList("gameMapper.getTagsByTitle", title);
}
@Override
public List<Games> getAllGames() {
return sql.selectList("gameMapper.getAllGames");
}
@Override
public GameTags getGameTagByTitleAndTag(String title, String tag) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("title", title);
paramMap.put("tag", tag);
return sql.selectOne("gameMapper.getGameTagByTitleAndTag", paramMap);
}
}
CrawlingDaoImpl 클래스는 CrawlingDao 인터페이스의 메소드를 구현하고 있다. @Autowired 어노테이션을 이용해 SqlSessionTemplate 인스턴스를 주입받아 MyBatis를 통해 데이터베이스와의 통신을 수행할 수 있다.Lombok 라이브러리의 @Getter와 @Setter 어노테이션을 이용해 게터와 세터 메소드를 자동으로 생성해준다.
@Getter
@Setter
public class Games {
private String title; // 게임명
private String releases; // 출시일
private String cart; // 장바구니 추가
private String oneLineReview;
private String reviewCount;
private List<String> tag;
private String disc;
private String originPrice;
private String discPrice;
private String imageName;
}
}
@Getter
@Setter
public class GameTags {
private String title; // 게임명
private String tag; // 태그
}
@Getter
@Setter
public class Images {
private int imageId;
private String originName;
private String imageName;
private String title;
}
@Getter
@Setter
public class Price {
private String title; // 게임명
private String disc; // 할인율
private String originPrice; // 할인 전 가격
private String discPrice; // 할인 후 가격
}
@Getter
@Setter
public class Review {
private String title; // 게임명
private String oneLineReview; // 한 줄 평가
private String reviewCount; // 리뷰 수
}
@Getter
@Setter
public class Tags {
private String tag; // 태그
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="gameMapper">
<insert id="setGameList" parameterType="com.spring.web.dto.Games">
INSERT INTO GAMES(TITLE, RELEASES, CART)
VALUES (#{title}, #{releases}, #{cart})
</insert>
<insert id="setPriceList" parameterType="com.spring.web.dto.Price">
INSERT INTO PRICE(TITLE, DISC, ORIGIN_PRICE, DISC_PRICE)
VALUES (#{title}, #{disc}, #{originPrice}, #{discPrice})
</insert>
<insert id="setReviewList" parameterType="com.spring.web.dto.Review">
INSERT INTO REVIEW(TITLE, ONE_LINE_REVIEW, REVIEW_COUNT)
VALUES (#{title}, #{oneLineReview}, #{reviewCount})
</insert>
<insert id="setGameTagList" parameterType="com.spring.web.dto.GameTags">
INSERT INTO GAME_TAGS(TITLE, TAG)
VALUES (#{title}, #{tag})
</insert>
<insert id="setTags" parameterType="com.spring.web.dto.Tags">
INSERT INTO TAGS(TAG)
VALUES (#{tag})
</insert>
<insert id="setImage" parameterType="com.spring.web.dto.Images">
INSERT INTO IMAGES(TITLE, ORIGIN_NAME, IMAGE_NAME)
VALUES (#{title}, #{originName}, #{imageName})
</insert>
<select id="getTagByTag" parameterType="string" resultType="com.spring.web.dto.Tags">
SELECT * FROM TAGS
WHERE TAG = #{singleTag}
</select>
<select id="getGameByTitle" parameterType="string" resultType="com.spring.web.dto.Games">
SELECT * FROM GAMES
WHERE TITLE = #{title}
</select>
<select id="getImageByName" parameterType="string" resultType="com.spring.web.dto.Images">
SELECT * FROM IMAGES
WHERE IMAGE_NAME = #{imageName}
</select>
<select id="getAllGames" resultType="com.spring.web.dto.Games">
SELECT
GAMES.*,
PRICE.DISC, PRICE.ORIGIN_PRICE, PRICE.DISC_PRICE,
REVIEW.ONE_LINE_REVIEW, REVIEW.REVIEW_COUNT,
IMAGES.ORIGIN_NAME, IMAGES.IMAGE_NAME
FROM GAMES
INNER JOIN PRICE
ON GAMES.TITLE = PRICE.TITLE
INNER JOIN REVIEW
ON GAMES.TITLE = REVIEW.TITLE
INNER JOIN IMAGES
ON GAMES.TITLE = IMAGES.TITLE
</select>
<select id="getTagsByTitle" parameterType="string" resultType="string">
SELECT TAG FROM GAME_TAGS
WHERE TITLE = #{title}
</select>
<select id="getGameTagByTitleAndTag" parameterType="map" resultType="com.spring.web.dto.GameTags">
SELECT * FROM GAME_TAGS
WHERE TITLE = #{title} AND TAG = #{tag}
</select>
</mapper>
gameMapper라는 네임스페이스로 매핑 정보를 정의하고 있다. setGameList는 Games 객체를 받아 데이터베이스의 GAMES 테이블에 새로운 게임 정보를 삽입하는 쿼리문을 정의한다.setPriceList는 Price 객체를 받아 PRICE 테이블에 새로운 가격 정보를 삽입하는 쿼리문을 정의한다.setReviewList는 Review 객체를 받아 REVIEW 테이블에 새로운 리뷰 정보를 삽입하는 쿼리문을 정의한다.setGameTagList는 GameTags 객체를 받아 GAME_TAGS 테이블에 새로운 게임 태그 정보를 삽입하는 쿼리문을 정의한다.setTags는 Tags 객체를 받아 TAGS 테이블에 새로운 태그 정보를 삽입하는 쿼리문을 정의한다.setImage는 Images 객체를 받아 IMAGES 테이블에 새로운 이미지 정보를 삽입하는 쿼리문을 정의한다.getTagByTag는 태그를 파라미터로 받아 TAGS 테이블에서 해당하는 태그 정보를 검색하는 쿼리문을 정의한다.getGameByTitle은 게임 제목을 파라미터로 받아 GAMES 테이블에서 해당하는 게임 정보를 검색하는 쿼리문을 정의하고 있다. 이 메서드는 중복된 게임 정보를 검색하는 기능을 수행한다.getImageByName은 이미지 이름을 파라미터로 받아 IMAGES 테이블에서 해당하는 이미지 정보를 검색하는 쿼리문을 정의한다.getAllGames는 GAMES, PRICE, REVIEW, IMAGES 테이블에서 모든 게임 정보를 가져오는 쿼리문을 정의한다.getTagsByTitle은 게임 제목을 파라미터로 받아 GAME_TAGS 테이블에서 해당하는 게임의 태그 정보를 검색하는 쿼리문을 정의한다.getGameTagByTitleAndTag는 게임 제목과 태그를 파라미터로 받아 GAME_TAGS 테이블에서 해당하는 게임 태그 정보를 검색하는 쿼리문을 정의한다.<!DOCTYPE html>
<html>
<head>
<title>Games</title>
<link rel="stylesheet" href="../../resources/css/common.css">
</head>
<body>
<h1 style="color: white; text-align: center; padding: 20px 0;">Games</h1>
<div class="facetedbrowse_FacetedBrowseItems_NO-IP">
<c:forEach var="game" items="${games}">
<div class="game-item">
<img src="/resources/images/${game.imageName}" alt="Game Image">
<div class="game-item-info">
<h2>${game.title}</h2>
<div class="tags">
<c:forEach var="tag" items="${game.tag}">
<p>${tag}</p>
</c:forEach>
</div>
<p>${game.releases}</p>
<div class="reviews">
<p>${game.oneLineReview}</p>
<p>${game.reviewCount}</p>
</div>
</div>
<div class="price-info">
<div class="discount">
<p>${game.disc}</p>
</div>
<div class="prices">
<p class="origin-price">${game.originPrice}</p>
<p class="Price">${game.discPrice}</p>
</div>
<button>${game.cart}</button>
</div>
</div>
</c:forEach>
</div>
</body>
</html>
css파일은 별도로 경로 지정해 주었다.
localhost:8080/crawl 로 접속하면 스팀 사이트에 있는 게임정보들이 잘 들어가는 것을 볼 수 있다.

그리고 DB INSERT 한 데이터들이 local화면에 다시 출력되는 것 까지 확인 해 보았다.

이상으로, 웹 크롤링 및 데이터베이스 저장을 구현하는 방법에 대해 알아보았다.
이를 통해 웹 사이트의 정보를 자동으로 수집하고, 이를 데이터베이스에 저장하는 등의 작업을 할 수 있다.