[moaloa] 미니프로젝트 개발일지 #1

msw-Hub·2024년 11월 2일
0

moaloa

목록 보기
2/7
post-thumbnail

> 지난 일지

로스트아크 관련 웹사이트를 제작하고자 하였고, 메인기능으로 직업별 보석 최저가 중 최고가를 알려주는 기능을 만들 것이다
지난 개발까지는 초기세팅 및 기능구현의 틀을 잡았다

> 보석 검색 기능 구현 예상순서

보석을 그저 경매장에 검색해서 알려주는 것으로는 해당 보석이 안쓰이는 보석일 수도 있기때문에, 보석이 비싸게 팔리려면 해당 보석이 주로 사용하는 보석이야한다. 상위 랭커들의 보석 채용이 대부분의 유저에게 보편적으로 사용되기 때문에, 전적검색이 가능한 로아 관련 사이트들을 크롤링하여 상위 랭커들의 닉네임을 뽑아오고자 한다. 로스트아크 공식 전적검색은 레벨 순위를 제공하고 있지않다.

  1. 크롤링하여 직업각인별 상위 20명의 닉네임을 가져온다
  2. 해당 닉네임을 로아 open API를 이용하여 사용 보석을 카운팅한다
  3. 해당 정보를 문서화하여 가지고 있고, 프론트 요청에 전달한다
  4. 정보 갱신은 주에 1회 진행한다

> 크롤링이란❓

간단하게 말해서, 웹 페이지의 내용을 수집하여 추출해내는 것을 말합니다. 이는 스크래핑과 동일하지만, 크롤링은 동적으로 웹페이지를 돌아다니며 수집하는 것을 말합니다.

합법적으로 크롤링하기 위해서는 robots.txt 문서를 확인하여야한다.
robots.txt는 검색 엔진 크롤러에게 웹 사이트의 크롤링 가능 범위를 알려주는 파일로, 크롤링을 원하는 사이트의 메인주소뒤에 붙여주면 확인이 가능하다.
아래같은 경우는 딱히 제한을 걸어두지 않은 것으로, 내가 크롤링할 목적 사이트인 kloa.gg 사이트이다.
robots

나는 크롤링 라이브러리 중에도 Selenium을 사용할 것이다
Selenium을 사용하는 이유는 움짤처럼 직업각인별 상위랭커를 순위를 검색하기 위해서는 동적인 페이지의 클릭이벤트를 활용해야하기 때문이다.
클릭이벤트
Selenium을 통한 크롤링은 아래 사이트를 참조하였다.
[스프링에서 크롤링 데이터 수집하기](https://1545154.tistory.com/97)


> 크롤링 시도

1. 크롬드라이버 다운로드

우선 본인이 사용하는 크롬의 버전을 체크해야한다.
[ 크롬열고 > 더보기 > 설정 > 크롬정보 ]
체크했다면, 아래 링크를 타고 들어가 크롬드라이버를 설치한다.

https://googlechromelabs.github.io/chrome-for-testing/

현 시점 기준으로는 대부분 115버전 이상이기 때문에, 통합 드라이버를 설치하게된다. 114버전 이하라면 각 버전에 맞는 드라이버를 찾아 설치해야한다.
설치했다면 경로상에 한글이 들어가지 않도록 위치해주며, 위치를 기억해둔다.

2. build.gradle 작성

본인 프로젝트에 dependencies 맨 아래 selenium만 추가 후 빌드시켜주자.
내 기준, 최신버전이였다.

	// 크롤링을 위한 Selinium 의존성
    
	implementation 'org.seleniumhq.selenium:selenium-java:4.25.0'

3. Controller 작성

@GetMapping("/test")
    public String test() {
        crawlingService.crawlAndClick();
        return "success";
    }

컨트롤러는 간단하게만 작성해주자.

4. Service 작성

	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());
        }
	}

해당 서비스 로직을 설명하자면 다음과 같다.

---사전에 할일---

  1. 데이터베이스 연결한다
  2. 내가 누르고자 하는 버튼의 id 값을 찾아낸다. (id 찾는데 너무 오래걸렸다. 버튼을 xpath 카피하여 찾았지만, 실제와는 달랐다. 이는 id 출력 매서드를 짜서 확인하는게 좋다)
  3. 클로아에서는 [클래스]버튼을 누르게 되면, 리스트 박스가 나오게 된다. 이때의 직업버튼들의 id 속성들을 매핑시켜둔다.
  4. [클래스]버튼을 누른후, 직업각인을 고르는 버튼을 누르게 되면 마찬가지로 리스트박스가 나오게되며, 각각의 id속성들을 추가로 매핑시켜준다.
    직업각인의 경우 r13 과 r17버튼이다. (동적페이지다 보니 누를때마다 각 버튼의 id값이 바뀌어 id 값들을 찾는데 고생을 했다. 직업각인의 경우 위에 버튼은 r13 아래 버튼은 r14지만, 위의 직업 각인을 누른 후 다시 고를려면 아래버튼의 id속성값이 r17로 바뀌게된다. id 속성값이 바뀌기때문에 직업각인을 2번 누른 후에는 페이지를 새로고침시킨다.)

---로직 순서---

직업선택버튼 누르기 > 리스트박스중 직업 누르기 > 직업각인선택버튼 누르기 > 리스트박스중 직업각인 누르기 > 상위 20명 이름 가져오기 > 데이터베이스에 저장하기 > 페이지 초기화 > 앞선과정반복

⭐id값은 미리 찾아둬야한다
⭐map을 사용했기에, 순서는 입력순서와 다르게 동작하여, 배열로 순서를 명시했다 // 추가적으로 맵핑을 사용한 이유가 db에 넣을때, 조건문을 사용하게 되면 else-if문에 연속이라 코드가 너무 지저분해서 그렇다
⭐로딩시간을 넣어줘야한다 ( 페이지가 로딩되기전에 값을 가져오려하면 잘못된 값을 가져온다 )


> 다음 개발 순서

로아 open api를 활용하여 데이터베이스에 저장된 유저 닉네임으로 보석사용을 조사한다.

profile
천천히 시작하는 개발자

0개의 댓글