Backend

자바, Spring Boot로 크롤링하기 - Selenium 이용 (동적페이지), 속도 개선 방법

연_우리 2022. 3. 8. 00:23
반응형

목차

     

     

    이전글

     

    자바, Spring Boot로 크롤링하기 - Jsoup 이용 (정적페이지)

    목차 Jsoup이란? jsoup은 Dom메서드와 CSS Selector를 사용하여 HTML의 데이터를 추출할 수 있는 Java 라이브러리이다. 크롤링해보기 크롤링할 URL 준비, Dom Selector 찾기 나는 단어장 앱을 만들려고한다. 단

    lotuus.tistory.com

     

     

    Selenium이란?

    셀레니움은 사실 웹사이트 테스트 도구이다.

    사람이 손으로 일일이 브라우저를 켜서 웹사이트를 확인하고, 수정하고, 테스트하고... 하는게 번거로워서 코드를 작성하여 웹브라우저를 동작시켜 테스트하자! 하고 나온 기술인데, 웹사이트가 동적으로 변하는 상황을 해결하기 위해 셀레니움을 크롤링에서 활용하게 되었다.

     

     

     

    크롤링 해보기

    SpringBoot build.gradle에 의존성 추가

    https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java 참고

    implementation 'org.seleniumhq.selenium:selenium-java:4.1.2'

     

     

    셀레니움 드라이버 다운로드

    컴퓨터에 설치된 크롬의 버전을 확인한다.

     

     

     

    https://chromedriver.chromium.org/downloads

    위 링크에서 해당되는 버전에 맞게 다운로드한다. (윈도우 64비트도 32비트로 다운받는다)

     

     

     

     

    크롤링 코드

    import org.openqa.selenium.By;
    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.WebElement;
    import org.openqa.selenium.chrome.ChromeDriver;
    import org.springframework.stereotype.Component;
    
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;
    
    @Component
    public class CrawlingExample {
        private WebDriver driver;
    
        private static final String url = "https://yourei.jp/腕を磨く";
        public void process() {
            System.setProperty("webdriver.chrome.driver", "C:\\Users\\Desktop\\chromedriver.exe");
            //크롬 드라이버 셋팅 (드라이버 설치한 경로 입력)
    
            driver = new ChromeDriver();
            //브라우저 선택
    
            try {
                getDataList();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            driver.close();	//탭 닫기
            driver.quit();	//브라우저 닫기
        }
    
    
        /**
         * data가져오기
         */
        private List<String> getDataList() throws InterruptedException {
            List<String> list = new ArrayList<>();
    
            driver.get(url);    //브라우저에서 url로 이동한다.
            Thread.sleep(1000); //브라우저 로딩될때까지 잠시 기다린다.
    
            //WebElement sentence = driver.findElement(By.cssSelector("#sentence-example-list .sentence-list li"));
            //System.out.println(sentence.getText());
            //この先腕を磨いていけば、いつかはこの男に勝てる日がくるのだろうか。 ...
            //ベニー松山『風よ。龍に届いているか(下)』
            // findElement (끝에 s없음) 는 해당되는 선택자의 첫번째 요소만 가져온다
    
            List<WebElement> elements = driver.findElements(By.cssSelector("#sentence-example-list .sentence-list li"));
            for (WebElement element : elements) {
                System.out.println("----------------------------");
                System.out.println(element);	//⭐
            }
            
            return list;
        }
    
    }

    메서드 참고

    https://www.selenium.dev/documentation/webdriver/elements/finders/#find-elements-from-element

     

    findElement, findElements 안에 By.id, By.class, By.tagName, By.cssSelector... 을 작성하여 요소를 가져올 수 있다.

     

     

     

     

     

    ⭐부분을 실행하면 아래와 같이 출력된다

    ----------------------------
    この先腕を磨いていけば、いつかはこの男に勝てる日がくるのだろうか。 ...
    ベニー松山『風よ。龍に届いているか(下)』
    ----------------------------
    最高の武芸者たるべきことを目標とし、己の腕を磨き続けることである。 ...
    伏見健二『サイレンの哀歌が聞こえる』
    ----------------------------
    
    ....
    
    ----------------------------
    いくら戦いの腕
    うで
    を磨
    みが
    いたって、それだけじゃ英雄になれねえ。 ...
    山本弘『サーラの冒険 5 幸せをつかみたい !』
    ----------------------------

     

    맨 마지막에 해당되는 부분을 확인해보면 위의 훈독도 같이 출력된 것을 볼 수 있다.

     

     

     

     

     

    훈독인지 확인하기 위해 코드를 수정해보겠다.

    /**
         * data가져오기
         */
        private List<String> getDataList() throws InterruptedException {
            List<String> list = new ArrayList<>();
    
            driver.get(url);    //브라우저에서 url로 이동한다.
            Thread.sleep(1000); //브라우저 로딩될때까지 잠시 기다린다.
    
            List<WebElement> elements = driver.findElements(By.cssSelector("#sentence-example-list .sentence-list li"));
            for (WebElement element : elements) {
                System.out.println("----------------------------");
                System.out.println(element.getText());
    
                List<WebElement> rubys = element.findElements(By.tagName("ruby"));
                for (WebElement ruby : rubys) {
                    System.out.println("ruby : " + ruby.getText());
    
                    List<WebElement> rts = ruby.findElements(By.tagName("rt"));
                    for (WebElement rt : rts) {
                        System.out.println("rt : " + rt.getText());
                    }
                }
    
            }
    
            return list;
        }
    ----------------------------
    いくら戦いの腕
    うで
    を磨
    みが
    いたって、それだけじゃ英雄になれねえ。 ...
    山本弘『サーラの冒険 5 幸せをつかみたい !』
    ruby : 腕
    うで
    rt : うで
    ruby : 磨
    みが
    rt : みが
    ----------------------------

    이렇게 element안에서도 findElements를 통해 더 자세하게 요소를 찾아낼 수 있다.

     

     

     

     

    Selenium 실행시간과 속도

    현재 전체 li (18개)를 가져오는데 걸린 시간은 빨라야 8초이다. 느릴땐 20초대가 나온다..

    이를 개선하기 위해 2가지를 설정해보자

     

     

    드라이버 옵션

    애플리케이션을 실행해보면 아래 문구가 표시된 브라우저가 실행되었다가 종료되는데,

    사람이 실행하는 것처럼 브라우저를 실행하고, url을 이동하고, 이미지도 로딩하고... 해야하니 당연히 느릴 수 밖에 없다.

     

     

     

    크롤링할때 굳이 브라우저가 켜져야할 이유가 없다.

    또한 광고로 뜨는 이미지들도 다운받을 이유 없다.

    public void process() {
        System.setProperty("webdriver.chrome.driver", "C:\\Users\\Desktop\\chromedriver.exe");
        //크롬 드라이버 셋팅
    
        //driver = new ChromeDriver();
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--disable-popup-blocking");       //팝업안띄움
        options.addArguments("headless");                       //브라우저 안띄움
        options.addArguments("--disable-gpu");			//gpu 비활성화
        options.addArguments("--blink-settings=imagesEnabled=false"); //이미지 다운 안받음
        driver = new ChromeDriver(options);
    
        try {
            getDataList();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        driver.close();
        driver.quit();
    }

    8초 -> 7초로 1초 감소했다.

    또한 최소 8초 ~ 최대20초의 간격이 많이 좁혀졌다.

     

     

     

     

    페이지 로딩을 기다리는 방법

    셀레니움은 동적 페이지를 가져오는만큼, 페이지가 모두 로딩될때까지 기다려야 파싱 결과를 정상적으로 얻어올 수 있다.

    페이지 로딩을 기다리는데에는 3가지 방법이 있다.

     

     

    1. Thread.sleep(10000) : 무조건 10초를 기다린다.

    Thread.sleep(10000);
    장점 가장 간편하게 사용할 수 있다.
    단점 페이지가 3초 이내에 로딩완료되면 불필요한 2초를 기다려야하며,
    페이지가 3초 이후에 로딩완료되면 이후 InterruptedException이 발생하게된다.
    상황에 따라 다른 로딩시간을 임의로 지정하는 것은 안좋은 선택이다.

     

     

    2. Implicitly wait : 웹페이지 전체가 로딩될때까지 최대 N초 기다린다.

    driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

    driver.get()하여 브라우저가 실행되고 최대 10초를 기다린다.

    10초 이전에 웹페이지 전체가 로딩이 완료되었다면 바로 다음 명령어를 실행하게된다.

    10초가 넘어가면 다음 명령어를 실행하게된다. 이때 웹페이지 로딩이 10초가 지났는데도 완료되지 않았다면 에러가 발생한다.

    장점 웹페이지 로딩이 완료되면 바로 다음 명령어를 실행하면서 실행 시간을 단축할 수 있다.
    단점 10초 이전에 웹페이지 로딩이 완료되었지만 동적작업(자바스크립트)이 완료되지 않았다면
    정확하지 않은 파싱결과를 얻게된다. (ex. 블로그 방문 시 블로그 내용이 먼저 보이고 몇초 후에 광고가 나타나는 경우)

     

     

    3. Explicitly wait : 웹페이지 일부분이 나타날때까지 최대 N초 기다린다.

    private List<String> getDataList() throws InterruptedException {
        List<String> list = new ArrayList<>();
    
        WebDriverWait webDriverWait = new WebDriverWait(driver, 10);	//⭐⭐⭐
        //드라이버가 실행된 뒤 최대 10초 기다리겠다.
    
        driver.get(url);    //브라우저에서 url로 이동한다.
        //Thread.sleep(1000); //브라우저 로딩될때까지 잠시 기다린다.	
    
        webDriverWait.until(	
                ExpectedConditions.presenceOfElementLocated(By.cssSelector("#sentence-example-list .sentence-list li"))
                //cssSelector로 선택한 부분이 존재할때까지 기다려라
        );	//⭐⭐⭐
    
        List<WebElement> elements = driver.findElements(By.cssSelector("#sentence-example-list .sentence-list li"));
        for (WebElement element : elements) {
            System.out.println("----------------------------");
            System.out.println(element.getText());
            
            ......
        }
        return list;
    }

    driver.get()하여 브라우저가 실행되고 최대 10초를 기다린다.

    10초 이전에 내가 지정한 부분의 로딩이 완료되었다면 바로 다음 명령어를 실행하게된다.

    10초가 넘어가면 다음 명령어를 실행하게된다. 이때 웹페이지 로딩이 10초가 지났는데도 완료되지 않았다면 에러가 발생한다. (implicitly와 동일)

    장점 일부분에 대한 ExpectedConditions의 로딩 조건이 true가 될때까지 기다렸다가
    로딩이 완료되면 바로 다음 명령어를 실행하면서 실행 시간을 단축할 수 있다.

    정확하지 않은 파싱결과를 얻을 가능성이 낮아진다.

     

     

    ExpectedConditions

    (By locator는 id, class, css 등등의 dom요소를 선택하는 부분이다.)

    visibilityOf(WebElement element) WebElement가 화면에 출력될때까지 기다린다.
    visibilityOfElementLocated(By locator) locator가 화면에 출력될때까지 기다린다.
    presenseOfElementLocated(By locator) locator가 존재할때까지 기다린다.
    (화면에 출력되는지는 체크하지 않는다.)

    이외 메서드는 아래 문서 참고

    https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/support/ui/ExpectedConditions.html

     

     

    프로젝트에 Explicitly wait - ExpectedConditions.presenseOfElementLocated를 적용하였다.

    7초 -> 6초로 1초 감소했다.

     

     

     

     

     

    결론

    셀레니움은 findElements로 값을 가져오는 것 뿐만 아니라 

    요소.click(), 요소.send_keys(~), 팝업창 이동, 스크롤 내리기... 등등의 다양한 작업이 가능하다.

    다양한 작업은 https://gorokke.tistory.com/8​ 를 참고하자 (정리 잘되어있다)

     

    셀레니움 사용시 드라이버 옵션과 Explicitly wait은 필수로 사용해주자!!!!

    반응형
    • 네이버 블러그 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 구글 플러스 공유하기
    • 카카오톡 공유하기