Java) selenium 을 이용한 크롤링

박우영·2023년 6월 4일
0

자바/코틀린/스프링

목록 보기
24/37

setting


  1. 크롬 드라이버 설치

이곳에서크롬 드라이버를 설치 를 한다. 내 크롬버전은 114 이므로 114 로 설치

local 환경 에서는 m1 을사용하기 때문에 mac, 같이작업하는사람들을 위해 window, 배포환경에서의 linux를 설치했다. 그 후에 루트 디렉토리에서 drivers 디렉토리를 만들고 관리

  1. 디펜던시 추가

build.gradle 에 디펜던시 추가

implementation group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '4.9.1'

프로젝트 적용


그전에..

내가 적용하고자 하는 사이트는 원티드 사이트였다. 크롤링 하기전엔 먼저 사이트가 어떻게 동작되고 내가 원하는 정보가 어떤건지 정리해보자.

  1. 내가 원하는 정보
    회사명, 채용공고명, 마감일시, 직무, 경력 이렇게 5개의 정보를 얻고싶다.

  2. 사이트 특징
    먼저 채용탭으로 들어가보자

    1) 정보를 어떻게 제공하나?
    들어와서 크롬검사를 누르면 회사명, 공고명, 경력, 직무는 얻을 수 있지만 마감일시는 얻을 수 없었다. 하지만 클릭해서 들어가보면

    다음과 같이 마감일을 알 수 있는데 하나하나 채용정보를 클릭해서 확인해야한다.
    2) 페이징은 어떻게 되어있나?

    위 사진처럼 li 태그형태로 되어있고 ::after 로 스크롤을 내려서 정보가 갱신되는것을 알 수있었다.

또한 마감일을 얻기 위해선 접속 후에도 스크롤을 내려야 확인가능하다.

이 두가지 특징을 바탕으로 최초에 스크롤을 내려서 채용공고의 수 를 얻어내고 for 문을 통하여 그 숫자만큼 반복하는 형태로 접근 하고자 한다.

코드 작성

크롬 드라이버 설정

   public void crawlWebsite(int jobCode, int career) throws InterruptedException, IOException, WebDriverException {
        String os = System.getProperty("os.name").toLowerCase();

        String company; //회사명
        String subject; // 제목
        String url; // url
        String sector = null; // 직무 분야
        int sectorCode = 0; // 직무 코드
        LocalDateTime localDateTime = LocalDateTime.now();
        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        String createDate = localDateTime.format(dateFormatter);
        String deadLine;
        // 상시채용일 경우


        if (os.contains("win")) {
            System.setProperty("webdriver.chrome.driver", "drivers/chromedriver_win.exe");
        } else if (os.contains("mac")) {
            Process process = Runtime.getRuntime().exec("xattr -d com.apple.quarantine drivers/chromedriver_mac");
            process.waitFor();
            System.setProperty("webdriver.chrome.driver", "drivers/chromedriver_mac");
        } else if (os.contains("linux")) {
            System.setProperty("webdriver.chrome.driver", "drivers/chromedriver_linux");
        }
        ChromeOptions chromeOptions = new ChromeOptions();
        chromeOptions.addArguments("--headless=new");
        chromeOptions.addArguments("--no-sandbox");
        chromeOptions.addArguments("--disable-dev-shm-usage");
        chromeOptions.addArguments("--disable-gpu");
        chromeOptions.setCapability("ignoreProtectedModeSettings", true);

        WebDriver driver = new ChromeDriver(chromeOptions);

접속한 사용자 os 에 맞게 mac,window,linux 드라이버를 설치했으니 각각 환경에 맞게 드라이버를 실행하게 만들어줬고 내가 원하는 정보들에 대해 명시해줬다.

또한 크롬 옵션을 만들어 크롤링을할때 좀더 개선되도록 함.

채용페이지

       // url = base1 + jobCode + base2 + year + wontedEnd
        String wontedURL = WONTED_BASE1 + jobCode + WONTED_BASE2 + career + WONTED_END;

        driver.get(wontedURL); // 크롤링하고자 하는 웹페이지 주소로 변경해주세요.

        JavascriptExecutor js = (JavascriptExecutor) driver;
        while (true) {
            //현재 높이 저장
            Long lastHeight = (Long) js.executeScript("return document.body.scrollHeight");

            // 스크롤
            js.executeScript("window.scrollTo(0, document.body.scrollHeight)");

            // 새로운 내용이 로드될 때까지 대기
//            Thread.sleep(2000);

            // 새로운 높이를 얻음
            Long newHeight = (Long) js.executeScript("return document.body.scrollHeight");

            // 새로운 높이가 이전 높이와 같으면 스크롤이 더는 내려가지 않은 것으로 판단
            if (newHeight.equals(lastHeight)) {
                break;
            }
        }
        int elementSize = driver.findElements(By.className("Card_className__u5rsb")).size();
        log.debug(elementSize + "");

        By byTag = By.tagName("a");
        WebElement aTag = null;

내가 원하는 경력, 직무 코드를 입력해서 정보를 가져올 수있도록 url을 만들고
javascript를 활용하여 스크롤을 최대한 내려서 for 문이 반복할 횟수를 정의했다.

detail


        for (int i = 0; i < elementSize; i++) {
            System.out.println("for 문시작 " + elementSize + "i : " +  i);
            WebElement webElement;
            try {
                webElement = driver.findElements(By.className("Card_className__u5rsb")).get(i);
            } catch (IndexOutOfBoundsException e) {
                break;
            }
            WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
            JavascriptExecutor executor = (JavascriptExecutor) driver;

            while (true) { 
                //executeScript: 해당 페이지에 JavaScript 명령을 보내는 거
                try {
                    Thread.sleep(500); //리소스 초과 방지
                    wait.until(ExpectedConditions.visibilityOfElementLocated(byTag));
                    executor.executeScript("arguments[0].scrollIntoView(true);", webElement.findElement(byTag)); // 스크롤 이동
                    aTag = webElement.findElement(byTag);
                    executor.executeScript("arguments[0].click();", aTag); //클릭
                    url = aTag.getAttribute("href");
                } catch (StaleElementReferenceException | InterruptedException | NoSuchElementException e) {
                    System.out.println("a태그 찾는곳 에러");
                    log.debug(e.getMessage());
                }
                if (aTag != null) {
                    break;
                }
            }

//            executor.executeScript("arguments[0].click();", aTag); //클릭

            try {
                url = aTag.getAttribute("href");
            } catch (StaleElementReferenceException e) {
                continue;
            }
            long stTime = new Date().getTime();

            System.out.println("url : "  + url);

            WebElement eleSubject = null;
            WebElement eleCompany = null;
            WebElement deadLineEle = null;
            while (new Date().getTime() < stTime + 30000) { //30초 동안 무한스크롤 지속
                //executeScript: 해당 페이지에 JavaScript 명령을 보내는 거
                try {
                    Thread.sleep(500); //리소스 초과 방지
                    ((JavascriptExecutor) driver).executeScript("window.scrollTo(0, document.body.scrollHeight)");
                    wait.until(ExpectedConditions.visibilityOfElementLocated(By.className("JobHeader_className__HttDA")));
                    wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[@id=\"__next\"]/div[3]/div[1]/div[1]/div[1]/div[2]/section[2]/div[1]/span[2]")
                    ));

                } catch (StaleElementReferenceException | InterruptedException | TimeoutException e) {
                    System.out.println("클릭하고 들어온곳 찾는곳 에러");
                    log.debug(e.getMessage());
                    continue;
                }
                try {
                    eleSubject = driver.findElement(By.xpath("//*[@id=\"__next\"]/div[3]/div[1]/div[1]/div[1]/section[2]/h2"));
                    eleCompany = driver.findElement(By.xpath("//*[@id=\"__next\"]/div[3]/div[1]/div[1]/div[1]/section[2]/div[1]/h6/a"));
                    deadLineEle = driver.findElement(By.xpath("//*[@id=\"__next\"]/div[3]/div[1]/div[1]/div[1]/div[2]/section[2]/div[1]/span[2]"));
                } catch (NoSuchElementException e) {
                    System.out.println("제목, 회사, 마감일 못뽑음");
                }
                if (eleSubject != null && eleCompany != null && deadLineEle != null) {
                    subject = eleSubject.getText();
                    deadLine = deadLineEle.getText();
                    company = eleCompany.getAttribute("data-company-name");
                    log.debug("company : " + company);
                    log.debug("subject : " + subject);
                    log.debug("마감일 : " + deadLineEle.getText());


                    if (jobCode == 669) {
                        sectorCode = 92;
                        sector = "프론트";
                    } else if (jobCode == 872) {
                        sectorCode = 84;
                        sector = "백엔드";
                    } else if (jobCode == 873) {
                        sectorCode = 2232;
                        sector = "풀스택";
                    }
                    System.out.println("back 까지 진행");
                    try {
                        driver.get(wontedURL);
                        Thread.sleep(1000);
                        JobResponseDto jobResponseDto = new JobResponseDto(company, subject, url, sector, sectorCode, createDate, deadLine, career);
                        WontedStatistic.setJobDtos(jobResponseDto);
                    } catch (InterruptedException e) {
                        log.error(e.getMessage());
                    }
                    System.out.println("back 이후 진행");
                    break;
                }
            }
        }
        System.out.println("스텝 종료");
    }

이번엔 약간 긴데 채용페이지에서 얻은 채용공고수 만큼 돌리는 for문이다.

변경

위와같이 코딩해서 크롤링을 진행해보니 네트워크 문제등으로

성능개선


🤔어떤것을 개선할 수있을까?

크롤링을 할때 원하는 정보를 얻는데 까지 4~50분 정도의 시간이 걸립니다.
이는 Spring batch의 동기방식으로 진행될때 속도인데 Job은 다음과 같이 구성되어있습니다.
잡을 나누지않은 이유는 동기방식으로 job을 처리하고 트래픽이 없는새벽시간대에 작업이 진행되도록 cron 표현식을 사용했기때문에 1~2시간정도 소요되는것은 괜찮다고 생각했습니다. 따라서 동기처리, 일련의 과정들을 나열하는것이 트랜잭션 처리 및 관리면에서 수월하다고 생각했고 적용했지만 성능상으로 어떤식으로 개선 할 수있을까 를 고민 해봤습니다.

Spring batch Job의구성

    @Bean
    public Job job1(JobRepository jobRepository) {
        return new JobBuilder("job1", jobRepository)
                .start(step1(jobRepository)) // 사람인
                .next(step2(jobRepository)) // 원티드 백엔드
                .next(step3(jobRepository)) // 원티드 프론트
                .next(step4(jobRepository)) // 원티드 풀스택
                .next(step5(jobRepository)) // 중복된 값 필터하고 db저장
                .next(step6(jobRepository)) // 기존 값들 초기화
                .next(step7(jobRepository)) // 데이터 삭제
                .build();
    }
  1. unique index 설정
    지금은 모든 처리를 was 단에서 모두 일임하고 있습니다. 이를 db에게도 일임하고 병렬처리 비동기처리로 변경하는 방법도 있습니다. 예를들어 현재 사용하고 있는 DB가 Mysql인데 insert into on duplicate key update 와 같이 중복된 데이터를 insert 하는것이아닌 update가 되도록 변경하는것입니다. 물론 채용공고 특성상 변경될일이 별로없지만 job을 여러개로 나누어 진행한다면 충분히 빠르게 진행 할수 있을것이라 생각합니다.
  • 이렇게 됐을때 추가 변경해야하는 사항은?
    현재 마감일이 정해지지않았을경우 크롤링한 날짜를 기준으로 1달뒤에 삭제되도록 설정했는데 update가 된다면 추가 컬럼을 생성해야 할것 이라 생각합니다.
  1. chunk processing
    현재는 1개의 스텝에 1개의 테스크렛으로 구성되어있습니다. 최초 업데이트할 경우 대략 3천개정도의 데이터가 삽입되는데 1개의 데이터당 쿼리를 1번씩 날리는 문젭니다. 이는 Spring batch 의 성능을 제대로 사용하지 못하고 있다고 생각합니다.
    따라서 chunk 단위를 100개 정도로 구성하여 쿼리를 최적화 시키는 방법입니다. 아직 Spring batch 의 숙련도가 낮아 테크스렛으로 진행하지만 청크프로세싱으로 리팩토링을 진행 할 것입니다.

에러처리


크롤링시에러모음 에 정리하였습니다.

0개의 댓글