레이블이 크롤링인 게시물을 표시합니다. 모든 게시물 표시
레이블이 크롤링인 게시물을 표시합니다. 모든 게시물 표시

2018년 8월 13일 월요일

카카오스토리 글 모으기


카카오스토리에 올려진 글을 얻기 위한 크롤링 과정을 소개합니다.    카카오스토리 2018813일 기준이고 이후 카카오스토리가 개편되면 도움이 되지 못합니다.
www.kakaostory.com으로 접속하면  로그인 화면인  https://accounts.kakao.com/login/kakaostory로 넘어갑니다.   주로 이메일로 하는 아이디와 비밀번호가 핵심이고 로그인 버튼이 있습니다.


그러면 웹 브라우저 소스 보기기능을 이용하여 로그인 중심의 요소를 찾습니다.  아이디와 비밀번호는 <form> 태그를 통해서 서버로 넘어가는데 form 태그 안에 <input>으로 받은 여러 태그가 있어 헷갈리게 합니다. 



일단 아이디와 비밀번호외 다른 태그는 무시하고 아이디와 비밀번호만 사용하려고 합니다.  “id=loginEmail”로 아이디 입력으로 보이는 라인 997를 찾고 “type=password”로 비밀번호 입력으로 보이는 라인1013를 찾습니다.   아이디는 form 다음에 나오는 태그로 따지면  <form> <fieldset> <div> <input> 순입니다. 



라인 1038에는 로그인 버튼도 있습니다.



로그인에는 아래 selenium 프로그램을 이용합니다.  로그인하고  https://story.kakao.com/mudbull 로 이동하는 것까지 프로그램에 넣도록 합니다. 


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# -*- coding: utf-8 -*-
from selenium import webdriver

driver = webdriver.Firefox()   # geckodriver in the same directory with this script
driver.implicitly_wait(3)
driver.get('https://accounts.kakao.com/login/kakaostory')

driver.find_element_by_xpath("//form/fieldset/div/input[@name='email']").send_keys('yooXXXXX@gmail.com')
driver.find_element_by_xpath("//form/fieldset/div/input[@name='password']").send_keys('XXXXXXXXXXXX')

btn = driver.find_element_by_xpath("//button[text()='로그인']")
btn.click()

driver.get('https://story.kakao.com/mudbull')

라인4에서 firefox 웹 브라우저를 띄웁니다. 이 프로그램과 같은 디렉토리에   geckodriver.exe가 있어야  웹 브러우저가 뜹니다.  라인5에서 웹 브라우저가 준비될 때까지 3초를 기다리고 라인6에서 로그인 화면에 접속합니다.  라인 89는 각각 아이디와  비밀번호를 가리키는 element를 찾아와서 send_keys() 함수로 값을 부여하는 과정입니다. element를 찾을 때는 find_element_by_xpath() 함수를 사용하는데 인수를 지정해서 element를 찾습니다.   라인8에서 사용된 인수는  "//form/fieldset/div/input[@name='email']"  form -> fieldset -> div -> input 태그를 찾는데  input 태그는 attribute name‘email’이어야 함을 뜻합니다. form 앞에 “//” 두번은 form 태그 전에는 어떤 것이 와도 괜찮다는 의미입니다.   라인 11에는 로그인  button를 찾은 함수가 있는데  “//button[text()='로그인']” button 태그 중에 text로그인인 것을 찾습니다.  이와 비슷하게 라인 8에 나온  "//form/fieldset/div/input[@name='email']"도 더 줄여서 “//input[@name=’email’]”도 가능할 것 같습니다.   라인 12 button를 클릭하다는 의미이고 라인 14는 페이지를   'https://story.kakao.com/mudbull' 로 이동합니다.  

form 태그 안에 로그인 정보에 다른 여러 element가 있었는데 무시했었는데 로그인하는데에는 지장이 없습니다.

selenium 프로그램으로 자동 로그인까지는 성공해서 웹 브라우저에서는 글을 볼 수 있습니다.  아래 노란색 선 안의 글들을 모으려고 하는데 이 글들은  소스 보기에도 나오지 않고  SPA의 웹 크롤링 (https://blog.naver.com/yoojchul/221333353523)을 시도해도 글을 찾을 수가 없습니다.  카카오스토리는 backbone과 비슷한 자체 framework을 사용한다고 합니다.



그런데 웹 브라우저에서 F12를 눌러 나오는 디버깅 화면을 보니 “GET”으로 그림 뿐만 아니라 json 파일을 가져 오는 것을 알 수 있습니다.


오른쪽 하단 화면을 더 키우면 자세한 내용이 나오는데 특히 응답탭에서 찾고 있는 글이 여기에 있었습니다.



헤더탭을 보면 URL까지 나옵니다.  URL로 글들만 서버로부터 json 파일 형식으로 받아 오고 있었습니다.


그래서 아래와 같은 파이썬 프로그램으로 한 페이지안에 들어 있는 글들의 집합을 json으로 저장합니다.  json 형식을 바로 디코딩할 수도 있지만 이 페이지의 경우  json으로 디코딩해서 출력을 시도하면 hang이 되거나 Unicode exception이 발생해서 디코딩을 다음 기회로 미룹니다.  아마도 예상하지 못한 문자가 있어 이런 문제를 야기하는 것 같습니다.


 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
# -*- coding: utf-8 -*-
import  urllib.request
import json
import gzip

url = 'https://story.kakao.com/a/profiles/mudbull/activities?ag=false&since=&_=153396578026143'

hdr = {'Accept' : 'application/json'.encode('utf-8'),
       'Accept-Encoding' : 'gzip, deflate, br'.encode('utf-8'),
       'Accept-Language' : 'ko'.encode('utf-8'),
       'Connection' : 'keep-alive',
       'Cookie' : '_kadu=fr6-ASmuoqGhJFsG_1533960…6978; kuid=470960945171005441'.encode('utf-8'),
        'Host ' : 'story.kakao.com'.encode('utf-8'),
        'Referer' : 'https://story.kakao.com/mudbull'.encode('utf-8'),
       'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; W…) Gecko/20100101 Firefox/61.0'.encode('utf-8'),
       'X-Kakao-ApiLevel' : '45'.encode('utf-8'),
        'X-Kakao-DeviceInfo' : 'web:d;-;-'.encode('utf-8'),
        'X-Kakao-VC' : '4da14561d321f651ae6f'.encode('utf-8'),
        'X-Requested-With' : 'XMLHttpRequest'.encode('utf-8')}

req = urllib.request.Request(url, headers=hdr)
resp = urllib.request.urlopen(req)
print(resp.info())
filedata = gzip.decompress(resp.read())

with open("data.txt", 'wb') as f:
    f.write(filedata)

f.close()


글을 가져 올 때는 url 정보외에 header 정보도 필요합니다. 라인 8에서 19에 나온 header 정보는 디버깅 화면에서 확인된  요청 해더를 그대로 사용합니다.  zip으로 압축된 형식으로 서버에서 보내 주기 때문에 라인 24에서 gzip.decompress()로 푸는 함수가 필요합니다.

2018년 8월 5일 일요일

단일 페이지 어플리케이션의 웹 크롤링


단일 페이지 어플리케이션(SPA : Single Page Application)은 웹을 구성하는 정적인 리소스를 한번에 다운로드하고 새로운 페이지 구성에는 필요한 데이터만 서버로부터 받거나 클라이언트에서 만들어 사용합니다. ,  동적으로 웹 페이지를 구성합니다.  동적으로 웹 페이지가 만들어진다는 점에서 웹 크롤링에 어려움이 있습니다.  SPA 구성에는 주로 backbone, angular,  React와 같은 javascript framework를 이용하는데  backbonejs를 이용한 예제가  http://backbonejs.org/examples/todos/index.html 입니다.  



“book4”“book5”는 사용자의 키 입력과 javascript가 실행되면서 만들어진 요소입니다.  웹브라우저에서 제공하는 소스 보기기능을 이용해도  “book4” “book5”가 보이지 않아 당황스럽습니다.



http://backbonejs.org/examples/todos/index.html를 두가지 방법으로 웹 크롤링을 합니다. 하나는 selenium과 파이썬을 이용하는 방법이고  다른 하나는 casperjs를 이용한  방법입니다.
selenium를 사용하려면 웹 브라우저에 맞은 드라이버를 구해야 합니다.  여기서는 firefox를 사용할 것이라 https://github.com/mozilla/geckodriver/releases 에서 geckodrive 이름으로 된 드라이버를 구합니다. 압축을 푼 geckodriver.exe는 파이썬 실행 프로그램과 같은 디렉토리에 둡니다.  그리고 아래 파이썬 프로그램을 실행합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

driver = webdriver.Firefox()   # keckodriver in the same directory with this script
driver.implicitly_wait(3)
driver.get('http://backbonejs.org/examples/todos/index.html')


driver.find_element_by_id("new-todo").send_keys('book4')
driver.find_element_by_id("new-todo").send_keys(Keys.ENTER)

driver.find_element_by_id("new-todo").send_keys('book5')
driver.find_element_by_id("new-todo").send_keys(Keys.ENTER)

print(driver.page_source)

print("** body **")
print(driver.find_element_by_tag_name("body").text)

print("** li **")
elements = driver.find_elements_by_tag_name("li")
for e in elements:
    print(e.text)

라인 7까지는 웹 페이지 접속을 위한 기본 내용입니다. 라인10에서 14까지는 “book4”“book5”를 등록하는 과정인데  “ENTER”도 따로 보내야 함에 주의합니다.  라인 16 “driver.page_source”를 출력하면 웹 페이지의 html 소스를 볼 수 있는데  아래와 같이 “book4”“book5” 내용이 들어 있습니다. 이것은  웹 브라우저에서 볼 수 없었던 내용이고 이런 방식으로 동적으로 생성된 웹 페이지를 크롤링할 수 있습니다.   라인 19 에서는  <body> 태그에 담긴 텍스트, 라인 22이하에서는 <li> 태그에 담긴 텍스트를 정확하게 가져 옵니다.



selenium는 웹 브라우저를 구동하고 프로그램으로 조작하는 반면에 caspejs는 웹 브라우저 없이 작동합니다.  대신 casperjsphantomjs라는 프로그램 위에서 작동해야 합니다.  윈도우용으로   http://phantomjs.org/download.html 에서 MS 윈도우용 phantomjs를 먼저 구합니다.  압축을 풀면 example를 포함한 여러 파일이 있지만  bin 디렉토리의 phantomjs.exe 만 있으면 됩니다.  원도우 환경 변수 PATHphantomjs.exe가 있는 디렉토리를 지정하도록 합니다.  



윈도우용 casperjs  http://casperjs.org/  바로 첫 화면에서 다운로드 가능하고 모든 파일의 압축을 풀고 환경 변수 PATHcasterjs.exe가 있는 폴더 이름을 포함하도록 합니다.   도스창에서 “casper”만 실행해서 casperjsphanatomjs 버전이 나오면 설치는 성공입니다.



아래는http://backbonejs.org/examples/todos/index.html를 읽기 위한 casperjs 프로그램입니다. javascript인데 파일 이름은 todo3.js라면 도스창에서 “casterjs todo3.js”로 실행합니다.


 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
var casper = require('casper').create();

var bd = [];
var links = [];

function getBody () {
 var b = __utils__.findAll('body');
    return b;
}

function getTodos () {
    var todos = document.querySelectorAll('#todo-list li');
    return todos;
}

casper.start("http://backbonejs.org/examples/todos/index.html", function() {
 links = this.evaluate(getTodos);
    this.echo('num: ' + links.length);
});

// add new todo
casper.then(function() {
    this.sendKeys('#new-todo', 'book1');
    this.sendKeys('#new-todo', casper.page.event.key.Enter);
});

// add new todo
casper.then(function() {
    this.sendKeys('#new-todo', 'book2');
    this.sendKeys('#new-todo', casper.page.event.key.Enter);
});

casper.then(function() {
 bd = this.evaluate(getBody);
 this.echo(bd[0].outerHTML);
    links = this.evaluate(getTodos);
    this.echo('num: ' + links.length);
 this.echo("0: " + links[0].outerHTML);
 this.echo("1: " + links[1].outerHTML);
});

casper.run();

첫번째 라인은 casperjs를 시작하기 위한 casper 변수 선언입니다.  나중에 다시 언급하겠지만 casperjstest 모드에서 실행하려면 이 줄을 없애야 합니다.  라인6이하와 라인 11이하는  각각 <body><todo-list><li> 태그를 찾기 위한 함수 선언입니다.  라인 16에서  URL를 인수로 하는 casper.start()로 시작합니다. selenium와 달리 명시적으로 wait를 주지 않아도 페이지가 올라올 때까지 기다립니다.  casper API 실행 순서는 start() -> then() -> ... ->  run() 순입니다.  기본적으로 비동기로 실행되는 then이 순서를 정하고 run()은 반드시 존재해야 합니다.   라인 17에서  getTodos()를 호출해서  <todo-list><li> 태그를 가진 요소가 몇개인지 알아 봅니다.  라인 22이하와 라인 28이하를 통해 요소 두개를 추가합니다. 라인 34에서 <body> 태그에 요소를 찾아 HTML 형식으로 출력합니다. 라인 36이하는 다시 <todo-list><li> 태그를 가진 요소를 찾습니다.



여기에 casperjs의 버그가 있습니다. 첫번째 요소를 찾아 출력하는데에는 문제가 없는데 두번째 요소 경우  아예 에러도 없이 아무런 내용이 나오지 않습니다. 아무런 에러가 없다는 것이 더 문제인데 이 에러를 보려면 casperjstest 모드에서 실행해야 합니다.  test 모드는 자동적으로 변수 선언이 있기 때문에 첫번째 라인에서 선언문과 겹칩니다.  



그래서 첫번째 라인의 casper 변수 선언은 comment처리하고  “casperjs test todo3.js” 실행해야 비로소  links[1]null임을 알 수 있습니다.



seleniumjava와 파이썬과 같이 여러 언어도 지원하고 파싱을 위한 beautifulsoup까지 사용할 수 있는 반면에 casperjsjavascript로만 가능하고 따라서 웹 크롤링에 도움이 되는 지원 도구도 거의 없습니다. selenium와 파이썬으로 크롤링하다가 한계나 극복할 수 없는 에러만 생기지 않으면 계속 selenium를 사용할 것 같습니다.