본문 바로가기
Back-end/JAVA

[크롤링] Selenium을 이용한 JAVA 크롤러 (2) - Jsoup과 비교 (With. Twitter)

by 허도치 2020. 2. 28.

2020/02/25 - [Back-end/JAVA] - [크롤링] Jsoup을 이용한 JAVA 크롤러 (1) - HTML 파싱

2020/02/25 - [Back-end/JAVA] - [크롤링] Jsoup을 이용한 JAVA 크롤러 (2) - 파일 다운로드

2020/02/27 - [Back-end/JAVA] - [크롤링] Selenium을 이용한 JAVA 크롤러 (1) - HTML 파싱

 

 

0. 서론

 지난 포스트에서 Selenium을 이용하여 간단하게 웹페이지를 크롤링해보았다. 정적 웹페이지를 크롤링 했기 때문에 결과만 보면 Jsoup과 다를게 없다. 오히려 Selenium의 수집속도가 더 느려서 왜 사용하나 싶을 수 있다. 그래서, 이번에는 동적 웹페이지를 크롤링하여 Jsoup과 어떤 차이가 있는지 비교해보려고 한다.
 

 

 

 

1. 프로젝트 준비
1-1. 수집 대상 선정

1) 수집 대상은 'Twitter'
   : 대표적으로 Facebook, Instagram, Twitter 등이 있는데, Facebook과 Instagram은 계정이 있어야하므로 Twitter를 대상으로 선정하였음.

2) 수집 내용은 '코로나'
   : 최근 가장 큰 이슈인 '코로나'를 Twitter에서 hashtag로 검색한 결과를 수집.

 

1-2. Jsoup

1) Jsoup을 이용한 크롤러는 아래의 포스트를 참고
   : [크롤링] Jsoup을 이용한 JAVA 크롤러 (1)

 

1-3. Selenium

1) Selenium을 이용한 크롤러는 아래의 포스트를 참고
   : [크롤링] Selenium을 이용한 JAVA 크롤러 (1) 

 

 

 

2. HTML 파일로 저장하는 함수
2-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// App.java
 
package selenium;
 
import java.io.File;
import java.io.FileOutputStream;
 
public class App {
 
    // 중략
 
    public static void saveHtml(String filename, byte[] html) {
        File savedir = new File("C:/work/java/crawler/html");
        if!savedir.exists() ) {
            savedir.mkdirs();
        }
 
        File file = new File(savedir, filename);
        try {
            FileOutputStream out = new FileOutputStream(file);
            out.write( html );
            out.close();            
        } catch (Exception e) {
            e.printStackTrace();
        }        
    }    
 
    // 중략
    
}
cs

1) Request를 던졌을 때, Response 받은 페이지를 파일로 저장

2) [ 13~16 ln ] - 저장 폴더 설정
   : 폴더가 없으면 자동으로 생성.

3) [ 18~25 ln ] - 파일 작성
   : String 타입을 Bytes로 형변환하여 파일 작성.

 

 

 

2. Jsoup을 이용한 Twitter 크롤링 함수
2-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// App.java
 
package selenium;
 
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
 
public class App {
    
    // 중략
 
    public static void runJsoup(String URL) throws Exception { 
        // 1. Connection 객체 생성
        Connection conn = Jsoup.connect(URL);
        conn.header("Content-Type""application/html;charset=UTF-8");
        conn.timeout(30*1000);
 
        // 2. HTML 파싱
        Document html = conn.get();
        
        // 3. HTML 저장.
        saveHtml("twitter-jsoup.html", html.toString() );
 
        try {
            // 4. 트윗 목록 조회
            Element parent = html.selectFirst("section[aria-labelledby*=\"accessible-list\"]");
            if(parent == null ) { throw null; }
        
            // 5. 트윗 콘텐츠 조회
            Elements contents = parent.select("div.css-1dbjc4n.r-my5ep6.r-qklmqi.r-1adg3ll");
            System.out.println"조회된 콘텐츠 수 : "+contents.size() );
            
            // 6. 트윗 내용 파싱.
            if( contents.size() > 0 ) {
                // 7. 트윗 상세 내용 탐색
                for(Element content : contents ) {
                    try {
                        String username = content.selectFirst("span > span.css-901oao.css-16my406.r-1qd0xha.r-ad9z0x.r-bcqeeo.r-qvutc0").text();
                        String id = content.selectFirst("span.css-901oao.css-16my406.r-1qd0xha.r-ad9z0x.r-bcqeeo.r-qvutc0").text();
                        String text = content.selectFirst("div.css-901oao.r-hkyrab.r-1qd0xha.r-a023e6.r-16dba41.r-ad9z0x.r-bcqeeo.r-bnwqim.r-qvutc0").text();
 
                        System.out.println"========================" );
                        System.out.println( username+" "+id );
                        System.out.println( text );
                        System.out.println"========================" );
                    } catch ( Exception e ) {
                        // pass
                    }
                }
            }
        } catch ( Exception e ) {
            System.out.println("목록을 찾을 수 없습니다.");
        } finally {
            // 3. HTML 저장.
            saveHtml("twitter-jsoup-loaded.html", html.toString() );
        }
    }
    
    // 중략
    
}
cs

1) Jsoup을 이용하여 Twitter에서 '코로나' 콘텐츠를 수집하는 함수

2) [ 25 ln ] - 현재 페이지를 HTML 파일로 저장

3) [ 29~53 ln ] - Twitter에서 글 목록을 파싱하는 로직
   : Twitter는 동적 웹페이지이기 때문에 실제로는 동작하지 않음.

4) [ 58 ln ] - 파싱을 완료한 후 현재 페이지를 HTML 파일로 다시 저장
   : 시작과 끝의 페이지 변화를 확인하기 위함.

 

 

 

3. Selenium을 이용한 Twitter 크롤링 함수
3-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// App.java
 
package selenium;
 
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Properties;
 
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
 
public class App {
    
    // 중략
 
    public static void runSelenium(String URL) throws Exception {
        // 1. WebDriver 경로 설정
        Path path = Paths.get(System.getProperty("user.dir"), "src/main/resources/chromedriver.exe");
        System.setProperty("webdriver.chrome.driver", path.toString());
        
        // 2. WebDriver 옵션 설정
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--start-maximized");          // 최대크기로
        options.addArguments("--headless");                 // Browser를 띄우지 않음
        options.addArguments("--disable-gpu");              // GPU를 사용하지 않음, Linux에서 headless를 사용하는 경우 필요함.
        options.addArguments("--no-sandbox");               // Sandbox 프로세스를 사용하지 않음, Linux에서 headless를 사용하는 경우 필요함.
        
        // 3. WebDriver 객체 생성
        ChromeDriver driver = new ChromeDriver( options );
        
        // 4. 웹페이지 요청
        driver.get(URL);
        
        // 5. HTML 저장.
        saveHtml("twitter-selenium.html", driver.getPageSource() );
        
        try {
            // 6. 트윗 목록 Block 조회, 로드될 때까지 최대 30초간 대기
            WebDriverWait wait = new WebDriverWait(driver, 30);
            WebElement parent = wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("section[aria-labelledby*=\"accessible-list\"]")));
            
            // 7. 트윗 콘텐츠 조회
            List<WebElement> contents = parent.findElements(By.cssSelector("div.css-1dbjc4n.r-my5ep6.r-qklmqi.r-1adg3ll"));
            System.out.println"조회된 콘텐츠 수 : "+contents.size() );
            
            if( contents.size() > 0 ) {
                // 8. 트윗 상세 내용 탐색
                for(WebElement content : contents ) {
                    try {
                        String username = content.findElement(By.cssSelector("span > span.css-901oao.css-16my406.r-1qd0xha.r-ad9z0x.r-bcqeeo.r-qvutc0")).getText();
                        String id = content.findElement(By.cssSelector("span.css-901oao.css-16my406.r-1qd0xha.r-ad9z0x.r-bcqeeo.r-qvutc0")).getText();
                        String text = content.findElement(By.cssSelector("div.css-901oao.r-hkyrab.r-1qd0xha.r-a023e6.r-16dba41.r-ad9z0x.r-bcqeeo.r-bnwqim.r-qvutc0")).getText();
                        
                        System.out.println"========================" );
                        System.out.println( username+" "+id );
                        System.out.println( text );
                        System.out.println"========================" );
                    } catch ( NoSuchElementException e ) {
                        // pass
                    }
                }
            }
            
        } catch ( TimeoutException e ) {
            System.out.println("목록을 찾을 수 없습니다.");
        } finally {            
            // 9. HTML 저장.
            saveHtml("twitter-selenium-loaded.html", driver.getPageSource() );
        }
                       
        // WebDriver 종료
        driver.quit();
    }
    
    // 중략
}
cs

1) Selenium을 이용하여 Twitter에서 '코로나' 콘텐츠를 수집하는 함수
   : 전체적인 흐름은 Jsoup을 이용한 크롤러와 같음

2) [ 42 ln ] - 현재 페이지를 HTML 파일로 저장

3) [ 46~47 ln ] - 입력한 Element를 찾을 때까지 최대 30초 동안 대기.
   : 30초 이내에 Element가 로드된다면, 결과값으로 해당 Element를 반환.
   : 30초를 초과할 경우 TimeoutException 발생

4) [ 53~69 ln ] - Twitter에서 글 목록을 파싱하는 로직

5) [ 75 ln ] - 파싱을 완료한 후 현재 페이지를 HTML 파일로 다시 저장

 

 

4. Main 함수
4-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// App.java
 
package selenium;
 
public class App {
 
    // 중략
    
    public static void main(String[] args) {
        try {
            // 
            String URL = "https://twitter.com/hashtag/코로나";
            
            runJsoup(URL);
            runSelenium(URL);
            
        } catch ( Exception e ) {
            e.printStackTrace();
        }
        
    }
    
    // 중략
    
}
cs

1) 앞서 작성한 크롤러들을 실행하는 Main함수

2) [ 12 ln ] - Twitter에서 '코로나'를 hashtag로 검색하는 URL

3) [ 14~15 ln ] - Jsoup과 Selenium 크롤러 실행

 

4-2. 실행

1) IDE를 이용하는 경우
   : Java Application 실행

 

4-3. 실행 결과

[ 실행 로그 ]
[ 저장된 파일 목록 ]

1) 로그 확인
   : runJsoup 함수는 '목록을 찾을 수 없습니다.'라는 메시지와 함께 바로 종료됨.
   : runSelenium 함수는 4개의 콘텐츠를 수집하여 상세 내용을 출력해줌.

2) 파일 확인
   : Jsoup은 두 파일이 모두 같은 내용으로 저장됨.
   : Selenium은 처음 저장한 파일은 Jsoup과 같으나, 종료된 후 생성한 파일은 콘텐츠의 내용이 담겨 있음.

 

 

 

5. 정리
5-1. Selenium은 되고, Jsoup은 안되는 이유

 요즘 웹사이트들은 빠른 반응성을 위하여 사용자가 콘텐츠를 서버에 요청했을 때, 서버는 데이터만 브라우저에 전송하고, 브라우저에서는 이 데이터를 가지고 화면을 랜더링하는 방식을 사용한다. 이러한 방식을 클라이언트 사이드 랜더링(CSR, Client-Side Rendering)이라고 하며, XHR(XMLHttpRequest), AJAX를 이용하여 사용되다가 React.js와 같은 Javscript 프레임워크들이 생겨나면서 그 영역이 점점 확대되어가고 있다.

 CSR은 브라우저에서 화면을 그려준다는 동적인 특성 때문에 서버에 데이터를 요청하는 HTTP Request를 사용하면 실제 화면에 그려진 데이터는 수집할 수 없는 것이다.

 결론적으로, JsoupHTTP Request를 사용하는 라이브러리이기 때문에 React를 사용하는 Twitter의 콘텐츠를 수집할 수 없는 것이며, WebDriver를 이용하는 Selenium은 수집할 수 있는 것이다.

 

5-2. Jsoup으로도 동적 웹페이즈를 크롤링 할 수 있다?

 한가지 편법이 있다. 브라우저에서 서버에 데이터를 요청하려면 URL이 필요한데, 이 URL을 찾아서 데이터를 직접 수집하는 방법이다. 단, 이 방법은 제약사항이 많다. 해당 서버에서 인증키를 요구하는 경우, Parameter로 전송해야하는 필수데이터를 모르는 경우 등 데이터를 수집할 수 없는 경우가 많다.

 

 

 

마치며

- Jsoup과 Selenium을 비교하기 적절한 예제를 만들어보려다가 소스가 많이 복잡해진것 같다. 그래도, 두 함수의 로직을 비슷하게 만들어놨기 각각 어떻게 사용되는지 쉽게 비교가 될 것이라고 생각된다.

댓글