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를 사용하면 실제 화면에 그려진 데이터는 수집할 수 없는 것이다.
결론적으로, Jsoup은 HTTP Request를 사용하는 라이브러리이기 때문에 React를 사용하는 Twitter의 콘텐츠를 수집할 수 없는 것이며, WebDriver를 이용하는 Selenium은 수집할 수 있는 것이다.
5-2. Jsoup으로도 동적 웹페이즈를 크롤링 할 수 있다?
한가지 편법이 있다. 브라우저에서 서버에 데이터를 요청하려면 URL이 필요한데, 이 URL을 찾아서 데이터를 직접 수집하는 방법이다. 단, 이 방법은 제약사항이 많다. 해당 서버에서 인증키를 요구하는 경우, Parameter로 전송해야하는 필수데이터를 모르는 경우 등 데이터를 수집할 수 없는 경우가 많다.
마치며
- Jsoup과 Selenium을 비교하기 적절한 예제를 만들어보려다가 소스가 많이 복잡해진것 같다. 그래도, 두 함수의 로직을 비슷하게 만들어놨기 각각 어떻게 사용되는지 쉽게 비교가 될 것이라고 생각된다.
'Back-end > JAVA' 카테고리의 다른 글
[JAVA] 파일 분할 - 용량 단위, 아래에서 위로 ↑ (0) | 2020.10.22 |
---|---|
[JAVA] 파일 분할 - 용량 단위, 위에서 아래로 ↓ (0) | 2020.10.22 |
[크롤링] Selenium을 이용한 JAVA 크롤러 (1) - HTML 파싱 (0) | 2020.02.27 |
[크롤링] Jsoup을 이용한 JAVA 크롤러 (2) - 파일 다운로드 (0) | 2020.02.25 |
[크롤링] Jsoup을 이용한 JAVA 크롤러 (1) - HTML 파싱 (1) | 2020.02.25 |
댓글