1. Chrome 개발자도구 Network 탭

크롬에서 F12키나 Ctrl+Shift+I 혹은 메뉴의 '도구 더 보기'에서 열 수 있는 개발자도구는 아주 강력하다. HTML dom 탐색은 물론 javascript나 css 소스를 탐색하고 수정할 수 있을뿐 아니라 수정해 곧바로 적용해 볼 수 있다. 이번 포스트에서 주로 사용하는 Network탭에서는 실시간으로 Request / Response 정보를 확인 수 있다.



각 요청이 시작되기 전/후를 스크린샷으로 남기는 기능과 원하는 요청만 표시하는 필터같은 유용한 기능도 있다. 다양한 기능을 한번씩 사용해 보면 많은 도움이 된다.




2. Jsoup로 네이버 검색어 자동완성 목록 얻어오기

네이버 검색창에 단어를 입력했을 때 나오는 검색어 자동완성 목록을 Jsoup로 얻어보자.


이거.



먼저 크롬의 개발자 도구를 열어 두고 네이버 검색창에 단어를 입력해 보자. 키를 누를 때마다 Request/Response가 감지된다.





검색창에 입력할 때마다 특정 URL로 get Request가 있다는 사실을 알 수 있다. Response 탭을 이용해 Response를 확인해 보자.




뭔가 이상하다. 이건 무슨 코드일까?




다시 Request의 헤더를 보자. _callback 파라미터와 Response의 첫 부분이 같다는 사실을 알 수 있다. 


 


또한 window라는 Javascript Object와 __jindo_callback... 형식이 함수와 유사하다는 점을 통해


 클라이언트에서 _callback 파라미터로 함수의 이름을 전달하고, 

 서버에서 _callback 파라미터로 전달된 함수의 파라미터로 '결과' Json을 적어 반환해 

 클라이언트에서 결과 '문자열'을 실행하거나 정의하는 형식이라고 추측할 수 있다. 


그림으로 표현하면 아래와 같다.



그야말로 막연한 추측이다. 하지만 _callback 파라미터를 조정해 볼 필요는 있다. 한번 시도해 보자.



Chrome 확장프로그램 Advanced REST client로 테스트한 결과.

_callback 파라미터를 공백으로 요청하자 Json 형태로 결과를 얻을 수 있었다.



개인적인 경험상, 개발의 편의성 때문인지 HTML 코드를 그대로 반환하는 사이트가 가장 많고, 그 다음으로 위와 같이 Json과 다른 형식의 코드가 섞인 경우가 많았다. Json 포맷으로 Response가 오는 정직한 경우는 별로 없으니 얻어낸 문자열을 다시 가공하거나 위의 경우처럼 파라미터를 조정해 볼 필요가 있다.



아무튼, 위에서 알아낸 URL과 Request 헤더들을 이용해 Jsoup로 네이버의 검색어 자동완성 목록을 얻어내 보자.


String q = "스칼라"; // 검색어

Document doc = Jsoup.connect("https://ac.search.naver.com/nx/ac")
.header("origin", "http://www.naver.com")
.header("referer", "https://www.naver.com/")
.header("accept-encoding", "gzip, deflate, sdch, br")
.data("con", "1")
.data("_callback", "") // _callback 파라미터를 비우면 JSON이 리턴된다!
.data("rev", "4")
.data("r_enc", "UTF-8")
.data("q", q) // 임의로 몇개의 파라미터와 헤더를 생략했다.
.data("st", "100") // 각 파라미터가 무엇을 뜻하는지를 확인해 적절하게 사용하는 것도 좋지만
.data("q_enc", "UTF-8") // 비정상적인 요청으로 감지해 아이디나 아이피가 밴 될 우려도 있으므로
.data("r_format", "json") // 특별한 이유가 없다면 모두 포함하는 것이 좋다.
.data("t_koreng", "1")
.data("ans", "2")
.data("run", "2")
.ignoreContentType(true) // HTML Document가 아니므로 Response의 컨텐트 타입을 무시한다.
.get();

List<String> result = new ArrayList<>();

// org.json 라이브러리를 사용해 결과를 파싱한다.
JSONObject jsonObject = new JSONObject(doc.text());

JSONArray items = (JSONArray) ((JSONArray) jsonObject.get("items")).get(0);
for(int i=0; i<items.length(); i++) {
String item = (String) (((JSONArray) items.get(i)).get(0));
result.add(item);
}

// 얻어낸 추천 검색어 목록.
// 테스트 프로젝트의 자바 버전이 낮아 for문을 사용했다.
for(String item : result) {
System.out.println(item);
}
/*
스칼라티움 강남
스칼라티움
구글스칼라
스칼라
강남 스칼라티움
첼로 스칼라티 105
스칼라티움 상암
수원 스칼라티움
상암 스칼라티움
첼로 스칼라티
구리 스칼라티움
스칼라 동시성 프로그래밍
스칼라 월드 북스 3
스칼라 월드 북스 4
스칼라 월드 북스 5
*/


원하는 정보를 얻어냈다.(웨딩홀 이름이 가장 위라니 조금 슬프다)




이 예제에는 없었지만 XMLHttpRequest 객체를 사용하는 Request에는 'X-Requested-With' 헤더 값으로 'XMLHttpRequest'가 전송되기도 한다. 다시한번 말하지만 사이트마다 다르고 비정상적인 요청으로 간주될 수 있으니 브라우저에서 직접 헤더를 확인해 보고 Jsoup의 헤더에 똑같이 작성하는 것이 좋다.

블로그 이미지

연미남편 nineDeveloper

안녕하세요 현직 개발자 입니다 ~ 빠르게 변화하는 세상에 뒤쳐지지 않도록 우리모두 열심히 공부합시다 ~! 개발공부는 넘나 재미있는 것~!

이 포스트에서는 로그인이 필요한 사이트와 Request Header를 검사하는 사이트를 파싱하는 과정을 적어둔다.



0. 웹 사이트 로그인

먼저 웹 사이트에 로그인에 대해 다시 생각해 볼 필요가 있다. 최근 웹 사이트에서 사용되는 로그인 방법은 크게 두 가지로 볼 수 있다. 첫 번째는 세션을 이용한 방법이고, 두 번째는 Restful API에 주로 사용되는 토큰 인증이다. 발급 받은 토큰을 이용하는 방법은 이전 포스트에서 원하는 값을 Jsoup의 Document를 파싱해 얻어낸 것처럼, 간단하게 얻어낼 수 있다. 물론 토큰이 HTML요소가 아니라 Script 요소로 있는 경우도 많지만 정규식이나 replace, split 같은 메소드를 이용하면 별 어려움이 없다.


다시 첫 번째 세션 로그인으로 돌아가면, 세션은 결국 쿠키라는 사실을 기억해야 한다. 상태를 유지하지 않는 HTTP 프로토콜의 특성 상 사이트에 로그인하는데 성공하면 서버는 클라이언트에게 세션ID를 발급해주고 

ID/PW는 Request에, 세션ID는 쿠키에 담겨 있다

 

클라이언트는 서버로부터 받은 세션ID를 다음 Request부터 쿠키에 포함해 전송하게 된다. 서버는 클라이언트가 전송한 쿠키에서 얻어낸 세션ID를 이용해 이 유저가 '로그인 한' 유저인지 여부를 확인할 수 있게 된다.


Response에도 당연히 쿠키가 포함되어 있다.

뭐 결국 간단히 말하자면 세션으로 로그인을 체크하는 사이트라면 로그인하고 얻은 쿠키를 다음 Request부터 계속 사용하면 된다는 말이다.


 

0. 사이트의 CSRF Token, Request Header

대부분의 유명한(사용자가 많은) 사이트에서는 비정상적인 접근을 막기 위해 여러가지 방법을 사용한다. 그 중 신경써야 할 것은 CSRF 토큰과 Request Header이다. 

 

CSRF 토큰은 로그인 시도 전에 한 가지 단계를 더 거치면 된다. 로그인을 처리하는 URL에 바로 요청하는 것이 아니라 '로그인 페이지' 에 접근해서 토큰을 얻어낸 후로그인 처리 URL에 토큰을 포함해 요청해야 한다. 티스토리를 예로 들어보자.

 

파란색으로 표시된 '눈에 보이는' 직접 입력하는 fieldset 외에도 

특수한 키가 적혀있는 파라미터 두 개가 있다.

 

스크린샷에서 볼 수 있는 두 가지 파라미터 "ofp"와 "nfp"처럼 로그인 페이지에 접속해야 얻을 수 있는 값이 있다. 때문에 먼저 '로그인 페이지'에 접근해 저런 값들을 얻어낸 후, '로그인 처리 URL'에 보내는 데이터에 그 값들을 포함하면 된다.

 

Request Header는 HTTP 표준에 맞게 전송하는 것이 원칙이다. 정상적인 브라우저라면 따로 신경 쓸 필요가 없지만 Jsoup를 통한 접속에서는 신경써야 한다. 몇몇 사이트에서는 Request Header를 철저하게 검사해 접근을 막거나 아이디를 밴하기도 한다. 

Header값들을 얻는 가장 쉬운 방법은 브라우저로 직접 로그인 해 보고 헤더 값들을 모두 복사해 그대로 사용하는 것이다.


 

티스토리에 로그인할때 전송된 Request header

(Chrome 확장 프로그램 HTTP Headers를 이용함)

 

다른 값들은 그냥 넣는다고 해도 User-Agent만큼은 조금 신경 쓸 필요가 있다. 사용자의 브라우저를 확인하는 값이기 때문이다. 이 값을 모바일 브라우저로 변환한다면 모바일 페이지를 따로 사용하는 사이트에서는 모바일 페이지로 리다이렉트된다. 만약 얻어내고자 하는 값이 모바일 화면의 값이라면 적절한 User-Agent를 하나 구해 사용하자.(기종명 User Agent로 검색하면 다 나온다)




그럼 이제 티스토리에 로그인하고 블로그 관리 페이지에서 내 블로그 목록을 얻어내는 코드를 만들어 보자.


select의 option값


1. 티스토리 로그인 페이지에 접속해 토큰 얻어내기

위에서 본 것처럼 티스토리에서는 로그인에 두 가지 토큰을 발급받아 전송한다. 각각이 무슨 의미인지는 모르겠지만 일단 가져와 보자.

// 로그인 페이지 접속
Connection.Response loginPageResponse = Jsoup.connect("https://tistory.com/auth/login/")
.timeout(3000)
.header("Origin", "http://tistory.com/")
.header("Referer", "https://www.tistory.com/auth/login")
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept-Encoding", "gzip, deflate, br")
.header("Accept-Language", "ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4")
.method(Connection.Method.GET)
.execute();

// 로그인 페이지에서 얻은 쿠키
Map<String, String> loginTryCookie = loginPageResponse.cookies();

// 로그인 페이지에서 로그인에 함께 전송하는 토큰 얻어내기
Document loginPageDocument = loginPageResponse.parse();

String ofp = loginPageDocument.select("input.ofp").val();
String nfp = loginPageDocument.select("input.nfp").val();

첫 번째 포스트와 마찬가지지만 필요한 헤더를 작성하고 get() 이나 post() 메소드가 아니라 execute() 메소드를 이용해 Document보다 상위 객체인 Response 객체를 얻어왔다. Response 객체의 cookies() 메소드를 이용해 쿠키를 얻어내고, parse() 메소드로 Document를 얻어낸 후 Document에서 두 가지 토큰을 가져왔다. 

티스토리는 로그인 페이지에 접근하기만 해도 뭔지 모를 쿠키들을 전송해 주기 때문에 로그인 페이지에서부터 쿠키를 가져왔다.



2. 로그인하고 로그인 세션ID 얻어내기

먼저 로그인을 처리하는 URL, 즉 form의 action과 method, 전송할 값들을 알아내야 한다.


form의 method와 action(로그인 처리 URL)


전송해야 하는 파라미터는 "redirectUrl", "loginId", "loginPw", "rememberLoginId"와 

토큰 "ofp", "nfp" 총 여섯 개다.


티스토리는 아주 정직하게 태그에 표시되어 쉽게 알 수 있지만 어떤 사이트는 자바스크립트로 어지럽게 작성되어 있다. 그런 경우는(특히 js가 압축된 경우!) 크롬 개발자도구의 Network 탭을 이용하면 편하다.


위에서 확인한 파라미터를 이용해 Jsoup Connection의 데이터로 추가하고 post로 요청하면 '로그인 된' 세션ID를 얻어낼 수 있다.

// Window, Chrome의 User Agent.
String userAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36";

// 전송할 폼 데이터
Map<String, String> data = new HashMap<>();
data.put("loginId", "아이디");
data.put("password", "비밀번호");
data.put("rememberLoginId", "1");
data.put("redirectUrl", "http://tistory.com/");
data.put("ofp", ofp); // 로그인 페이지에서 얻은 토큰들
data.put("nfp", nfp);

// 로그인(POST)
Connection.Response response = Jsoup.connect("https://www.tistory.com/auth/login")
.userAgent(userAgent)
.timeout(3000)
.header("Origin", "http://tistory.com/")
.header("Referer", "https://www.tistory.com/auth/login")
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept-Encoding", "gzip, deflate, br")
.header("Accept-Language", "ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4")
.cookies(loginTryCookie)
.data(data)
.method(Connection.Method.POST)
.execute();

// 로그인 성공 후 얻은 쿠키.
// 쿠키 중 TSESSION 이라는 값을 확인할 수 있다.
Map<String, String> loginCookie = response.cookies();

이제 로그인에 성공했다. 얻어낸 이 '로그인 된' 쿠키를 계속 사용하면 된다. 세션ID의 키는 서버사이드 설정에 따라 언어의 기본 값(PHP는 PHPSESSID, JSP는 JSESSIONID 등)이거나 따로 지정한 이름이다. 딱히 중요한 내용은 아니지만 서버사이드 언어를 유추하는 방법 중 하나가 된다.



3. 티스토리 블로그 관리 페이지에서 내 블로그 목록 얻어내기

위에서 얻은 쿠키를 사용한다는 점 외에는 이전 포스트와 차이가 없다. 접속하고 값을 얻어내면 된다. 

// 티스토리 관리자 페이지
Document adminPageDocument = Jsoup.connect("http://partnerjun.tistory.com/admin")
.userAgent(userAgent)
.header("Referer", "http://www.tistory.com/")
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept-Encoding", "gzip, deflate, sdch")
.header("Accept-Language", "ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4")
.cookies(loginCookie) // 위에서 얻은 '로그인 된' 쿠키
.get();

// select 내의 option 태그 요소들
Elements blogOptions = adminPageDocument.select("select.opt_blog > option");

// 블로그 이름과 url 얻어내기
for(Element option : blogOptions) {
String blogName = option.text();
String blogUrl = option.attr("abs:value");

System.out.println(blogName); // 간단한 블로그
System.out.println(blogUrl); // http://partnerjun.tistory.com/admin/center/
}


최근 많은 사이트에서는 보안 목적으로 로그인 후에 추가적인 과정을 요구하기도 한다. 대표적인 것은 새로운 기기에서의 로그인 시 이메일 체크나 capcha다. 이메일 체크야 직접 한번 해주면 되지만 capcha는 아직 만만한 문제가 아니다. 기계학습을 이용해 capcha를 해결하는 방법이 나왔다는 이야기를 들었는데 얼마 후면 라이브러리로 제공될지도 모르겠다. 기술의 발전을 기뻐해야 하는 건지, 개발하는 측에 있는 사람으로써 두려움에 떨어야 하는지는 모를 일이다.


아무튼, Jsoup로 로그인하고 '로그인 한' 사용자만 접근 가능한 페이지의 값을 얻어내 보았다. 다음 포스트에서는 XMLHttpRequest 객체를 이용한 Ajax 요청을 Jsoup로 해 보려고 한다(사실 특별한 내용은 없지만 크롬 개발자도구의 Network탭 그림 때문에 분리한다).

블로그 이미지

연미남편 nineDeveloper

안녕하세요 현직 개발자 입니다 ~ 빠르게 변화하는 세상에 뒤쳐지지 않도록 우리모두 열심히 공부합시다 ~! 개발공부는 넘나 재미있는 것~!

Jsoup는 아주 강력하고 재미있는 라이브러리다. 단순한 HTML 문서 파싱을 넘어 웹 사이트에 대한 Request, Response를 모두 처리할 수 있다. 덕분에 일부 특별한 경우(플래시, 애플릿, ActiveX같은 비표준이나 WebSocket)가 아니라면 브라우저로 사이트를 이용하는 상황을 그대로 재현해낼 수 있다. 다시 말해, 대부분의 사이트의 원하는 정보만 뽑아내는 '뷰어'를 만들 수 있다는 것이다. 몇 가지 간단한 예제를 통해 사이트에서 원하는 정보만 뽑아내는 과정을 적어보려 한다.



0. Gradle 디펜전시 추가

compile group: 'org.jsoup', name: 'jsoup', version: '1.10.2'

Maven Repository를 통해 간단하게 디펜전시를 추가 할 수 있다.


Jsoup는 크게 static 메소드를 체이닝해서 URL(혹은 로컬HTML)에 연결하고 결과를 얻어오는 org.jsoup.Jsoup 패키지와 얻어온 결과의 구조를 위한 객체들이 포함된 org.jsoup.nodes 패키지, 연결 방법과 Response, Request등을 가지고 있는 org.jsoup.Connection 패키지로 이루어져 있다. 



Jsoup의 주요 요소는 크게 다섯 가지로 볼 수 있다.


 클래스명

설명 

Document 

 Jsoup 얻어온 결과 HTML 전체 문서

Element

 Document의 HTML 요소

Elements

 Element가 모인 자료형. for나 while 등 반복문 사용이 가능하다.

Connection

 Jsoup의 connect 혹은 설정 메소드들을 이용해 만들어지는 객체, 연결을 하기 위한 정보를 담고 있다.

Response

 Jsoup가 URL에 접속해 얻어온 결과. Document와 다르게 status 코드, status 메시지나 charset같은 헤더 메시지와 쿠키등을 가지고 있다.




Jsoup로 하는 작업은 크게 보았을 때 Connection 객체를 통해 URL에 접속하고(혹은 로컬 파일/문자열), Response 객체에서 세션ID같은 쿠키와 HTML Document를 얻어낸 후, Document의 Element들을 파싱하는 과정으로 나누어진다고 볼 수 있다.




1. URL 접속해 결과 얻어오기

URL에 접속해 Document를 얻어내기는 아주 쉽다.

// 간략화된 GET, POST
Document google1 = Jsoup.connect("http://www.google.com").get();
Document google2 = Jsoup.connect("http://www.google.com").post();

// Response로부터 Document 얻어오기
Connection.Response response = Jsoup.connect("http://www.google.com")
.method(Connection.Method.GET)
.execute();
Document google3 = response.parse();

http://www.google.com에 접속하는 방법들



얻어낸 Document는 두가지 방법으로 출력할 수 있다. .html() 메소드와 .text() 메소드 두 가지다.

Connection.Response response = Jsoup.connect("http://www.google.com")
.method(Connection.Method.GET)
.execute();
Document document = response.parse();

String html = document.html();
String text = document.text();

html과 text는 JQuery의 메소드와 유사하다. 문서의 html 그 자체를 가져올지, html 태그 사이의 문자열만을 가져올지를 택하는 것이다.



document.html()의 결과

<!doctype html>

<html itemscope itemtype="http://schema.org/WebPage" lang="ko">

 <head>

  <meta content="/images/branding/googleg/1x/googleg_standard_color_128dp.png" itemprop="image">

  <link href="/images/branding/product/ico/googleg_lodp.ico" rel="shortcut icon">

  <meta content="origin" id="mref" name="referrer">

  <title>Google</title> 

  <script>(function(){window.google={kEI:'P7QKWeJtioTzBfTmg6AB',kEXPI:'1352553,3700294,3700347,4029815,4031109,4032677,4036527,4038214,4038394,4039268,4041776,4043492,4045096,4045293,4045841,4046904,4047140,4047454,4048347,4048980,4050750,4051887,4056126,4056682,4058016,4061666,4061980,4062724,4064468,4064796,4065786,4069829,4071757,4071842,4072270,4072364,4072774,4076095,4076999,4078430,4078588,4078763,4080760,4081038,4081165,4082131,4082230,4083046,4090550,4090553,4090806,4091353,4092934,4093313,4093498,4093524,4094251,4094544,4094837,4095910,4095999,4096323,4097150,4097922,4097929,4098096,4098458,4098721,4098728,4098752,4100169,4100174,4100376,4100679,4100714,4100828,4101376,4101429,4101750,4102028,4102032,4102107,4102238,4103215,4103254,4103475,4103845,4103849,4103999,4104202,4104204,4104527,4105085,4105099,4105178,4105317,4105470,4105788,4106085,4106209,4106949,4107094,4107221,4107395,4107422,4107525,4107555,4107628,4107634,4107807,4107895,4107900,4107957,4107966,4107968,4108012,4108016,4108027,4108033,4108417,4108479,4108537,4108539,4108553,4108687,4108885,4109075,4109293,4109316,4109489,4109498,4110094,8300508,8503585,8508113,8508229,8508931,8509037,8509373,8509826,10200083,10200096,19001874,19002112,19002127,41027342',authuser:0,j:{en:1,bv:24,pm:'p',u:'c9c918f0',qbp:0},kscs:'c9c918f0_24'};google.kHL='ko';})();(function(){google.lc=[];google.li=0;google.getEI=function(a){for(var b;a&&(!a.getAttribute||!(b=a.getAttribute("eid")));)a=a.parentNode;return b||google.kEI};google.getLEI=function(a){for(var b=null;a&&(!a.getAttribute||!(b=a.getAttribute("leid")));)a=a.parentNode;return b};google.https=function(){return"https:"==window.location.protocol};google.ml=function(){return null};google.wl=function(a,b){try{google.ml(Error(a),!1,b)}catch(c){}};google.time=function(){return(new Date).getTime()};google.log=function(a,b,c,d,g){a=google.logUrl(a,b,c,d,g);if(""!=a){b=new Image;var e=google.lc,f=google.li;e[f]=b;b.onerror=b.onload=b.onabort=function(){delete e[f]};window.google&&window.google.vel&&window.google.vel.lu&&window.google.vel.lu(a);b.src=a;google.li=f+1}};google.logUrl=function(a,b,c,d,g){var e="",f=google.ls||"";c||-1!=b.search("&ei=")||(e="&ei="+google.getEI(d),-1==b.search("&lei=")&&(d=google.getLEI(d))&&(e+="&lei="+d));a=c||"/"+(g||"gen_204")+"?atyp=i&ct="+a+"&cad="+b+e+f+"&zx="+google.time();/^http:/i.test(a)&&google.https()&&(google.ml(Error("a"),!1,{src:a,glmm:1}),a="");return a};google.y={};google.x=function(a,b){google.y[a.id]=[a,b];return!1};google.lq=[];google.load=function(a,b,c){google.lq.push([[a],b,c])};google.loadAll=function(a,b){google.lq.push([a,b])};}).call(this);

google.j.b=(!!location.hash&&!!location.hash.match('[#&]((q|fp)=|tbs=rimg|tbs=simg|tbs=sbi)'))

||(google.j.qbp==1);(function(){google.hs={h:true,pa:true,q:false};})();(function(){goo


(이하 생략)



document.text()의 결과

Google 스크린 리더 사용자는 여기를 클릭하여 Google 순간 검색을 설정 해제하시기 바랍니다. Gmail 이미지 로그인 Google 순간 검색을 사용할 수 없습니다. 검색어를 입력한 후 Enter를 누르세요. 자세히 알아보기 Google연결 속도 문제로 순간 검색이 중지되었습니다. 검색하려면 Enter를 누르세요. 검색하려면 Enter를 누르세요. 부적절한 예상 검색어 신고 × 한국 'Ok Google'이라고 말하면 음성 검색이 시작됩니다. 손가락 하나 움직이지 않고 검색해 보세요. 'Ok Google' 다음에 말한 내용을 Chrome에서 검색합니다. 자세히 알아보기아니요'Ok Google' 사용 개인정보처리방침 약관 설정 검색 설정 고급검색 기록 검색 도움말 의견 보내기 Google.com 사용 광고 비즈니스 Google 정보 내 계정 검색 지도 YouTube Play 뉴스 Gmail 드라이브 캘린더 Google+ 번역 사진더보기 문서 도서 Blogger 주소록 행아웃 KeepGoogle 제품 모두 보기



이 두가지 메소드는 Document뿐 아니라 Element에도 구현되어 있다. 



2. 얻어온 결과에서 특정 값 뽑아내기

특정 값, 그러니까 특정한 html 요소를 얻으려면 select("css query") 메소드를 사용하면 된다. 

구글 메인 페이지 검색 버튼의 value를 얻어 보자.


검색 버튼의 name은 btnK다.


Connection.Response response = Jsoup.connect("http://www.google.com")
.method(Connection.Method.GET)
.execute();
Document googleDocument = response.parse();
Element btnK = googleDocument.select("input[name=btnK]").first();
String btnKValue = btnK.attr("value");

System.out.println(btnKValue); // Google 검색

select의 결과는 Elements다. 그 중 첫번째 Element를 first() 메소드로 선택했다.





※ 목표가 있는 예제

불법만화로 유명한 그 사이트(머루)의 뷰어를 만든다고 상상해보자.

얻어내야 할 값은 크게 두 가지다.


1. 만화의 목록

2. 만화의 이미지 파일


이 값들을 얻어내기 위해서

1) 만화 목록을 얻어낸다. 

2) 글 내용에서 실제 만화 이미지가 있는 링크를 얻어낸다.

3) 이미지가 있는 링크에 접속한 후 이미지를 뽑아낸다. 

이렇게 세 가지 과정으로 진행해 보자.



1) 만화 목록 얻어내기

앞서 살펴본 Jsoup의 Conenction 메소드를 이용해 '업데이트' 페이지에 접속해 Doucment를 얻어낸다.

Document rawData = Jsoup.connect(URL)
.timeout(5000)
.get();

이 불법적이고 무서운 사이트는 Request Header를 검사하지 않는다. 그래서 위 코드처럼 아무런 추가적인 정보 없이 간단하게 결과를 얻어 올 수 있다. 하지만 Request를 철저하게 검사하는 사이트에는 더 많은 정보가 필요하다. 그런 사이트는 다음 글에 적을 예정이다.



아무튼, 이제 얻어낸 Document에서 정보를 뽑아낼 차례다. 구글 크롬의 개발자 도구를 이용해 업데이트 페이지를 확인해 보자.


'업데이트' 페이지 HTML


게시판은 table 태그를 사용하고, 각 행은 tr 태그에 매칭되며 공지사항은 tr_notice 클래스를 가지고 있다는 사실을 알 수 있다. 




tr 태그의 내부


tr 태그에 포함된 요소들을 살펴보자. 

a 태그로 글 내용에 해당하는 url을 얻을 수 있고, a태그의 첫번째 div에서 제목을 얻을 수 있다. 마지막으로 small 태그를 통해 글이 작성된 날짜를 얻을 수 있다. 


이렇게 얻어낸 정보를 바탕으로 직접 코드를 작성하자.

Elements articles = rawData.select("tr:not(.tr_notice) a"); // 공지사항을 제외한 tr의 a 태그들을 얻어온다.

for(Element article : articles) {

String href = article.attr("abs:href"); // a태그 href의 절대주소를 얻어낸다.

// a 태그 안에 포함된 div들
Elements articleDiv = article.select("div");

String thumbUrl = ROOT_URL
+ articleDiv.first() // 첫 번째 div에서 썸네일 url을 얻어온다.
.attr("style")
.replace("background-image:url(", "")
.replace(")", "");

String title = articleDiv.get(1).ownText(); // 두 번째 div에서 제목을 얻어낸다.

String date = articleDiv.get(1).select("small").text()
.split("\\|")[0];

System.out.println(href); // http://ma../b/mangup/00000
System.out.println(thumbUrl); // http://ma../quickimage/...
System.out.println(title); // 제목
System.out.println(date); // 날짜
}

얻어내고자 한 요소들을 css 선택지로 얻어낸 후, split이나 replace등의 메소드를 이용해 정리한다.


이 '글 목록'에 해당하는 정보는 필요에 맞게 정의한 객체에 담아 보관하거나 유저에게 보여 줄 수 있다.




2) 글 내용에서 만화가 있는 링크 얻어내기

위에서 얻어낸 글 내용 url에 접속한 후, 실제 이미지가 있는 페이지에 접근할 차례다. 태그 분석을 위해 브라우저로 페이지에 직접 들어가 보자.


글 내용 HTML


글의 내용에 해당하는 div(#vContent)의 첫 번째 a 태그의 href 속성이 실제 만화 이미지가 포함된 URL이다.



Document rawData = Jsoup.connect(ARTICLE_URL)
.timeout(5000)
.get();

Elements contentATags = rawData.select("#vContent a"); // 공지사항을 제외한 tr의 a 태그들을 얻어온다.

String viewPageUrl = contentATags.first()
.attr("abs:href"); // 마찬가지로 절대주소 href를 얻어낸다

System.out.println(viewPageUrl); // http://wasabi.../archives/XXXXX...

아주 간단하게 이미지들이 포함된 주소를 얻어낼 수 있다. 




3) 만화 이미지가 있는 URL에 접속해 이미지 URL 얻어내기

마찬가지로 만화 이미지가 포함된 URL에 접속해 태그를 분석한다.


만화 이미지가 있는 페이지의 HTML


html 코드를 보면 이미지들이 가진 특정 클래스가 있다. 이 클래스를 가진 img 태그들을 얻어낸 후, data-src 속성을 뽑아내자.


  Document rawData = Jsoup.connect(VIEWER_URL)
.timeout(5000)
.get();

Elements imgs = rawData.select("img[class=lz-lazyload]"); // lz-lazyload 클래스를 가진 img들

List<String> imageUrls = new ArrayList<>();

for(Element img : imgs) {
imageUrls.add(img.attr("abs:data-src"));
}

System.out.println(imageUrls); // 이미지 URL들.
}


만화 내용이 되는 모든 이미지 URL을 뽑아넀다. 이 URL에 접속해 직접 파일로 다운로드 할 수도, 자기 나름의 뷰어에 출력 할 수도 있다. 또, 목록을 얻어 낼 때 필요한 data들을 포함함으로써 원하는 페이지나 검색까지 구현이 가능하다.

블로그 이미지

연미남편 nineDeveloper

안녕하세요 현직 개발자 입니다 ~ 빠르게 변화하는 세상에 뒤쳐지지 않도록 우리모두 열심히 공부합시다 ~! 개발공부는 넘나 재미있는 것~!