
로스트아크 관련 웹사이트를 제작하고자 하였고, 메인기능으로 직업별 보석 최저가 중 최고가를 알려주는 기능을 만들 것이다
지난 개발까지는 초기세팅 및 기능구현의 틀을 잡았다
보석을 그저 경매장에 검색해서 알려주는 것으로는 해당 보석이 안쓰이는 보석일 수도 있기때문에, 보석이 비싸게 팔리려면 해당 보석이 주로 사용하는 보석이야한다. 상위 랭커들의 보석 채용이 대부분의 유저에게 보편적으로 사용되기 때문에, 전적검색이 가능한 로아 관련 사이트들을 크롤링하여 상위 랭커들의 닉네임을 뽑아오고자 한다. 로스트아크 공식 전적검색은 레벨 순위를 제공하고 있지않다.
- 크롤링하여 직업각인별 상위 20명의 닉네임을 가져온다
- 해당 닉네임을 로아 open API를 이용하여 사용 보석을 카운팅한다
- 해당 정보를 문서화하여 가지고 있고, 프론트 요청에 전달한다
- 정보 갱신은 주에 1회 진행한다
간단하게 말해서, 웹 페이지의 내용을 수집하여 추출해내는 것을 말합니다. 이는 스크래핑과 동일하지만, 크롤링은 동적으로 웹페이지를 돌아다니며 수집하는 것을 말합니다.
합법적으로 크롤링하기 위해서는 robots.txt 문서를 확인하여야한다.
robots.txt는 검색 엔진 크롤러에게 웹 사이트의 크롤링 가능 범위를 알려주는 파일로, 크롤링을 원하는 사이트의 메인주소뒤에 붙여주면 확인이 가능하다.
아래같은 경우는 딱히 제한을 걸어두지 않은 것으로, 내가 크롤링할 목적 사이트인 kloa.gg 사이트이다.

나는 크롤링 라이브러리 중에도 Selenium을 사용할 것이다
Selenium을 사용하는 이유는 움짤처럼 직업각인별 상위랭커를 순위를 검색하기 위해서는 동적인 페이지의 클릭이벤트를 활용해야하기 때문이다.

Selenium을 통한 크롤링은 아래 사이트를 참조하였다.
[스프링에서 크롤링 데이터 수집하기](https://1545154.tistory.com/97)
우선 본인이 사용하는 크롬의 버전을 체크해야한다.
[ 크롬열고 > 더보기 > 설정 > 크롬정보 ]
체크했다면, 아래 링크를 타고 들어가 크롬드라이버를 설치한다.
https://googlechromelabs.github.io/chrome-for-testing/
현 시점 기준으로는 대부분 115버전 이상이기 때문에, 통합 드라이버를 설치하게된다. 114버전 이하라면 각 버전에 맞는 드라이버를 찾아 설치해야한다.
설치했다면 경로상에 한글이 들어가지 않도록 위치해주며, 위치를 기억해둔다.
본인 프로젝트에 dependencies 맨 아래 selenium만 추가 후 빌드시켜주자.
내 기준, 최신버전이였다.
// 크롤링을 위한 Selinium 의존성
implementation 'org.seleniumhq.selenium:selenium-java:4.25.0'
@GetMapping("/test")
public String test() {
crawlingService.crawlAndClick();
return "success";
}
컨트롤러는 간단하게만 작성해주자.
private Map<String, String> createJobMap() {
Map<String, String> jobMap = new HashMap<>();
jobMap.put("headlessui-listbox-option-:r8:", "디스트로이어");
jobMap.put("headlessui-listbox-option-:r9:", "워로드");
...
return jobMap;
}
private Map<String, Map<String, String>> createEngraveMap() {
Map<String, Map<String, String>> engraveMap = new HashMap<>();
Map<String, String> destroyerEngraves = new HashMap<>();
destroyerEngraves.put("headlessui-listbox-option-:r13:", "분노의망치");
destroyerEngraves.put("headlessui-listbox-option-:r17:", "중력수련");
engraveMap.put("headlessui-listbox-option-:r8:", destroyerEngraves);
...
return engraveMap;
}
public void crawlAndClick() {
System.setProperty("webdriver.chrome.driver", 앞에서 말했던 파일경로 ); // 여기에 실제 경로를 입력하세요.
// WebDriver 인스턴스 생성
WebDriver driver = new ChromeDriver();
try {
String url = "https://kloa.gg/characters";
driver.get(url);
// 웹드라이버 인스턴스 생성(최대 10초 대기)
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
// 직업 순서를 명시적으로 정의
String[] orderedJobIds = {
"headlessui-listbox-option-:r8:", ...
};
// 각인 옵션 ID 배열 정의
String[] engraveOptionIds = {
"headlessui-listbox-option-:r13:", "headlessui-listbox-option-:r17:",
};
// 직업과 각인 매핑 생성
Map<String, String> jobMap = createJobMap();
Map<String, Map<String, String>> engraveMap = createEngraveMap();
// 직업에 대해 반복
for (String jobId : orderedJobIds) {
String jobName = jobMap.get(jobId);
if (jobName == null) {
log.error("No job name found for ID: {}", jobId);
continue;
}
log.info("Processing job: {} ({})", jobName, jobId);
// 직업 버튼 클릭
clickJobButton(jobId,driver,wait);
// 현재 직업에 맞는 각인 Map 가져오기
Map<String, String> currentEngraveMap = engraveMap.get(jobId);
// 직업 각인 옵션에 대해 반복 (2가지)
for (String engraveId : engraveOptionIds) {
String engraveName = currentEngraveMap.get(engraveId);
// 각인 버튼 클릭
clickEngraveButton(engraveId,driver,wait);
Thread.sleep(3000); // 클릭 후 2초 대기 (화면 로딩)
// 상위 20명의 닉네임 db에 저장
for (int i = 1; i <= 20; i++) {
// 닉네임이 위치한 XPath
String xpath = "//*[@id='content-container']/div/div/ul/li[" + i + "]/div/a";
// 닉네임 텍스트를 가져옴
String name = wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath(xpath))).getText();
// DB에 저장
CrawlingEntity entity = new CrawlingEntity();
entity.setCharacterClassName(jobName);
entity.setEngraveName(engraveName);
entity.setUserNickName(name);
crawlingRepository.save(entity);
}
}
driver.navigate().refresh(); // 페이지 새로고침
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 브라우저 닫기
driver.quit();
}
}
public void clickJobButton(String jobId,WebDriver driver,WebDriverWait wait) {
try {
// [전체] 직업 버튼 클릭 대기 (찾는중)
WebElement firstButton = wait.until(ExpectedConditions.elementToBeClickable(By.id("headlessui-listbox-button-:r3:")));
firstButton.click(); // [전체] 직업 버튼 클릭
// 버튼이 확장될 때까지 대기
wait.until(ExpectedConditions.attributeToBe(By.id("headlessui-listbox-button-:r3:"), "aria-expanded", "true"));
// [특정 직업] 버튼 클릭 대기 - ID로 찾기 + 보일때까지 대기
WebElement jobOption = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id(jobId)));
((JavascriptExecutor) driver).executeScript("arguments[0].click();", jobOption); // javascript로 클릭
} catch (ClawlingClickException e) {
System.out.println("Timeout while trying to click the job button: " + e.getMessage());
}
}
public void clickEngraveButton(String engraveId,WebDriver driver,WebDriverWait wait){
try {
// [전체] 각인 버튼 클릭 대기 (찾는중)
WebElement engraveButton = wait.until(ExpectedConditions.elementToBeClickable(By.id("headlessui-listbox-button-:r5:")));
engraveButton.click(); // [전체] 각인 버튼 클릭
wait.until(ExpectedConditions.attributeToBe(By.id("headlessui-listbox-button-:r5:"), "aria-expanded", "true")); // 버튼이 확장될 때까지 대기
// [특정 각인] 버튼 클릭 대기 (찾는중)
WebElement engraveOption = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id(engraveId)));
((JavascriptExecutor) driver).executeScript("arguments[0].click();", engraveOption); // javascript로 클릭
} catch (ClawlingClickException e) {
System.out.println("Timeout while trying to click the engrave button: " + e.getMessage());
}
}
해당 서비스 로직을 설명하자면 다음과 같다.
직업선택버튼 누르기 > 리스트박스중 직업 누르기 > 직업각인선택버튼 누르기 > 리스트박스중 직업각인 누르기 > 상위 20명 이름 가져오기 > 데이터베이스에 저장하기 > 페이지 초기화 > 앞선과정반복
⭐id값은 미리 찾아둬야한다
⭐map을 사용했기에, 순서는 입력순서와 다르게 동작하여, 배열로 순서를 명시했다 // 추가적으로 맵핑을 사용한 이유가 db에 넣을때, 조건문을 사용하게 되면 else-if문에 연속이라 코드가 너무 지저분해서 그렇다
⭐로딩시간을 넣어줘야한다 ( 페이지가 로딩되기전에 값을 가져오려하면 잘못된 값을 가져온다 )
로아 open api를 활용하여 데이터베이스에 저장된 유저 닉네임으로 보석사용을 조사한다.