R Crawler 4
JavaScript 우회하기
Dr.Kevin 1/28/2018
왜 JavaScript를 알아야 할까요? 웹크롤링을 하다 보면 분명히 응답 상태코드가 200
으로 정상인데 찾는 HTML element
가 없는 경우가 매우 많습니다. 이런 경우, JavaScript를 의심해볼 필요가 있습니다.
HTML
을 보면 중간에 <script>
라는 tag
가 있고, 그 아래로 프로그래밍 코드 뭉치가 보이는 경우가 흔히 있습니다. 만약 우리가 찾는 HTML element
가 <script>
에 영향을 받으면 관련 HTML element
가 뒤늦게 불려오기 때문에 비록 HTML Response이 정상이어도 찾는 HTML element
가 응답 객체에 포함되지 않는 것입니다. (혹시 제가 잘못 기술한 부분이 있다면 피드백을 부탁 드립니다!)
위 문제를 해결하는 방법은 크게 2가지가 있습니다.
-
크롬의 사용자도구에서 ‘네트워크’ 탭에서 새로고침(F5)하여 원하는 데이터가 화면에 렌더링되는 시점을 포착하는 방식으로 관련 항목을 찾는다.
-
RSelenium을 사용하여 원하는 코드를 얻는다.
두 번째 방법은 속도가 느리다는 단점도 있지만 이 포스팅 다음에 소개해드릴 예정이므로, 이번 포스팅에서는 첫 번째 방법을 사용하여 원하는 데이터를 수집해 보겠습니다.
# 필요 패키지를 불러옵니다.
library(httr)
library(rvest)
library(dplyr)
library(stringr)
이번 예제에서는 2017년 프로야구 타자 스탯을 수집해보도록 하겠습니다. KBReport로 이동하여 화면 상단에 여러 메뉴가 있습니다. 여기에서 선수기록
을 클릭하면 선수 스탯이 테이블 형태로 출력됩니다. 데이터 조회 조건으로 아래와 같이 변경하였습니다.
- 팀 : “팀-전체”
- 포지션 : “포지션-전체”
- 시즌범위 : “시즌 시작-2017”
- TO : “종료 시즌-2017”
- 정규/포스트 시즌 구분 : “정규시즌”
- 분류1 : “분류-선택안함”
- 타석수 : “타석수-전체”
위와 같이 설정한 후 우측에 있는 결과
버튼을 클릭하면 아래에 테이블 형태로 데이터가 출력됩니다. URL
이 바뀌었으므로 바뀐 URL
을 GET()
함수에 할당하여 실행시키면 정상 응답을 받을 수 있지만 찾고자 하는 HTML element
는 없습니다.
다시 크롬으로 돌아가서 출력된 상태에서 크롬 개발자도구를 열고 Network 탭으로 이동한 다음 새로고침(F5)을 누릅니다. 매우 많은 항목들이 주르륵하고 생기는데, 이 때 주목해야 할 것은 개발자도구 위에서 세 번째 줄에 있는 메뉴입니다. 아마도 All이 선택되어 있을 것입니다. 이것을 XHR로 변경하면 항목이 크게 줄어듭니다.
XHR은 XML Http Request의 머릿글자로 AJAX 요청을 생성하는 JavaScript API라고 합니다[1]. 그리고 AJAX는 Asynchronos Javascript And XML의 머릿글자로 JavaScript와 XML을 의미한다고 합니다. 단순하게 요약하자면, JavaScript를 통해서 웹서버로부터 XML 데이터를 요청하는 것입니다[2].
저도 잘 모르는 걸 설명하려니 어렵네요. 아무튼 다시 크롬 개발자도구의 Network 탭으로 돌아가서, 세 번째 줄 메뉴에 있는 XHR로 이동하면 첫 번째 그림처럼 보일 것입니다.
이제 새로고침을 하면 두 번째 그림처럼 몇 가지 항목이 생성되는데 중간에 AJAX가 있고 POST 방식으로 요청한다는 것을 확인할 수 있습니다.
그리고 AJAX를 선택하면 오른쪽에서 Request와 Response에 관한 세부사항을 확인할 수 있는데요. General에서 Request URL을 복사하여 POST()
함수의 url
인자에 할당합니다.
그리고 나서 Form Data가 보일 때까지 화면을 아래로 내린 다음 인자명과 인자값을 복사해서 POST()
함수의 body
인자에 리스트 형태로 할당해주면 됩니다.
# HTML 요청 합니다.
resp <- POST(url = "http://www.kbreport.com/leader/list/ajax",
encode = "form",
body = list(
rows = 20,
order = "oWAR",
orderType = "DESC",
year_from = 2017,
year_to = 2017,
gameType = "R",
page = 1)
)
# 응답 상태코드를 확인합니다.
status_code(resp)
## [1] 200
응답 상태코드가 200
으로 정상입니다. 이제 크롬 개발자도구에서 Elements로 이동한 다음 데이터가 담긴 테이블의 HTML element
를 찾습니다. 타겟 element
를 찾는 방법은 이제 잘 아시겠죠? 해당 위치로 가서 마우스 오른쪽 버튼을 클릭한 후 검사(Inspect)를 선택하면 그 주변에서 찾을 수 있습니다. 이 웹페이지는 <table class="ltb-table responsive">
로 되어 있군요.
# 원하는 태그가 있는지 확인합니다.
read_html(resp) %>%
html_nodes(css = "table.ltb-table")
## {xml_nodeset (1)}
## [1] <table class="ltb-table responsive">\n<tr>\n<th width="20px">#</th>\ ...
# html_table() 함수를 이용하여 쉽게 정리하고 데이터프레임으로 변환합니다.
read_html(resp) %>%
html_nodes(css = "table.ltb-table") %>%
html_table() %>%
as.data.frame()
## X. 선수명 팀명 경기 타석 타수 안타 홈런 득점 타점 볼넷 삼진 도루
## 1 1 최정 SK 130 527 430 136 46 89 113 70 107 1
## 2 2 김재환* 두산 144 636 544 185 35 110 115 81 123 4
## 3 3 최형우 KIA 142 629 514 176 26 98 120 96 82 0
## 4 4 박건우 두산 131 543 483 177 20 91 78 41 64 20
## 5 5 로사리오 한화 119 510 445 151 37 100 111 50 61 10
## 6 6 나성범 NC 125 561 498 173 24 103 99 48 116 17
## 7 7 손아섭 롯데 144 667 576 193 20 113 80 83 96 25
## 8 8 김선빈 KIA 137 529 476 176 5 84 64 39 40 4
## 9 9 버나디나 KIA 139 621 557 178 27 118 111 41 112 32
## 10 10 박민우 NC 106 452 388 141 3 84 47 46 51 11
## 11 11 김하성 넥센 141 601 526 159 23 90 114 58 65 16
## 12 12 스크럭스 NC 115 518 437 131 35 91 111 65 134 4
## 13 13 러프 삼성 134 591 515 162 31 90 124 60 107 2
## 14 14 구자욱 삼성 144 647 564 175 21 108 107 63 138 10
## 15 15 안치홍 KIA 132 545 487 154 21 95 93 43 70 7
## 16 16 한동민 SK 103 414 350 103 29 64 73 46 79 2
## 17 17 서건창 넥센 139 615 539 179 6 87 76 67 68 15
## 18 18 나지완 KIA 137 551 459 138 27 85 94 62 105 1
## 19 19 이대호 롯데 142 608 540 173 34 73 111 50 84 1
## 20 20 박용택 LG 138 596 509 175 14 83 90 72 88 4
## BABIP 타율 출루율 장타율 OPS wOBA WAR
## 1 0.316 0.316 0.427 0.684 1.111 0.442 7.30
## 2 0.385 0.340 0.429 0.603 1.032 0.427 7.22
## 3 0.362 0.342 0.450 0.576 1.026 0.430 7.20
## 4 0.390 0.366 0.424 0.582 1.006 0.424 7.04
## 5 0.324 0.339 0.414 0.661 1.075 0.436 5.75
## 6 0.413 0.347 0.415 0.584 0.999 0.416 5.64
## 7 0.374 0.335 0.420 0.514 0.934 0.398 5.60
## 8 0.393 0.370 0.420 0.477 0.897 0.391 5.19
## 9 0.354 0.320 0.373 0.540 0.913 0.380 5.01
## 10 0.408 0.363 0.441 0.472 0.913 0.404 4.92
## 11 0.306 0.302 0.376 0.513 0.889 0.375 4.76
## 12 0.353 0.300 0.402 0.595 0.997 0.411 4.70
## 13 0.344 0.315 0.396 0.569 0.965 0.402 4.59
## 14 0.371 0.310 0.383 0.527 0.910 0.382 4.28
## 15 0.332 0.316 0.373 0.513 0.886 0.374 4.23
## 16 0.302 0.294 0.396 0.614 1.010 0.414 3.92
## 17 0.367 0.332 0.403 0.429 0.832 0.368 3.90
## 18 0.332 0.301 0.405 0.534 0.939 0.398 3.87
## 19 0.327 0.320 0.391 0.533 0.924 0.388 3.86
## 20 0.387 0.344 0.424 0.479 0.903 0.391 3.67
위와 같이 하면 아주 간단하게 원하는 데이터를 수집할 수 있습니다. 그런데 지금 우리는 겨우 20명의 타자 데이터만 수집하였습니다. 화면 맨 아래에 보면 페이지를 이동하는 네비게이션이 있습니다. 마지막 페이지로 이동해보니 모두 15개 페이지가 있음을 확인할 수 있었습니다.
페이지 이동은 어떻게 처리하면 좋을까요? 왠지 앞에서 POST()
함수의 body
인자를 설정할 때 page = 1
로 되어 있던 기억이 납니다. 그럼 여기에서 1
대신에 2
를 넣으면 2페이지가 되겠죠? 즉, page
에 할당되는 값을 1
부터 15
까지 순환하며 데이터를 수집하면 됩니다.
같은 명령을 반복실행하려면 for()
함수를 사용하면 됩니다. 그리고 각 페이지별로 수집한 데이터를 데이터 프레임으로 만든 후 rbind()
함수를 이용하여 행 기준으로 추가하면 간단하게 해결됩니다.
순환함수 실행에 앞서 최종 결과 객체인 hitterStat
을 빈 데이터 프레임으로 만들어 줍니다.
# 최종 결과 객체를 먼저 생성합니다.
hitterStat <- data.frame()
# 총 15 페이지를 순환 실행하여 수집합니다.
for (i in 1:15) {
resp <- POST(url = "http://www.kbreport.com/leader/list/ajax",
encode = "form",
body = list(
row = 20,
order = "oWAR",
orderType = "DESC",
year_from = 2017,
year_to = 2017,
gameType = "R",
page = i)
)
# 테이블에 있는 데이터를 데이터프레임에 할당합니다.
df <- read_html(resp) %>%
html_nodes(css = "table.ltb-table") %>%
html_table() %>%
as.data.frame()
# 새로 만든 데이터테이블을 결과 객체에 행 기준으로 추가합니다
hitterStat <- rbind(hitterStat, df)
}
모든 데이터를 다 수집하였으니 이제 데이터 정제과정을 거치겠습니다. 거의 모든 데이터가 숫자 데이터이므로 번거로운 작업은 그렇게 많지 않을 것 같습니다.
# 데이터 테이블 구조를 확인합니다.
str(hitterStat)
## 'data.frame': 292 obs. of 20 variables:
## $ X. : int 1 2 3 4 5 6 7 8 9 10 ...
## $ 선수명: chr "최정" "김재환*" "최형우" "박건우" ...
## $ 팀명 : chr "SK" "두산" "KIA" "두산" ...
## $ 경기 : int 130 144 142 131 119 125 144 137 139 106 ...
## $ 타석 : int 527 636 629 543 510 561 667 529 621 452 ...
## $ 타수 : int 430 544 514 483 445 498 576 476 557 388 ...
## $ 안타 : int 136 185 176 177 151 173 193 176 178 141 ...
## $ 홈런 : int 46 35 26 20 37 24 20 5 27 3 ...
## $ 득점 : int 89 110 98 91 100 103 113 84 118 84 ...
## $ 타점 : int 113 115 120 78 111 99 80 64 111 47 ...
## $ 볼넷 : int 70 81 96 41 50 48 83 39 41 46 ...
## $ 삼진 : int 107 123 82 64 61 116 96 40 112 51 ...
## $ 도루 : int 1 4 0 20 10 17 25 4 32 11 ...
## $ BABIP : chr "0.316" "0.385" "0.362" "0.39" ...
## $ 타율 : chr "0.316" "0.34" "0.342" "0.366" ...
## $ 출루율: chr "0.427" "0.429" "0.45" "0.424" ...
## $ 장타율: chr "0.684" "0.603" "0.576" "0.582" ...
## $ OPS : chr "1.111" "1.032" "1.026" "1.006" ...
## $ wOBA : chr "0.442" "0.427" "0.43" "0.424" ...
## $ WAR : chr "7.3" "7.22" "7.2" "7.04" ...
데이터 프레임 구조를 확인해보니 총 292명의 선수, 20개의 컬럼으로 구성되어 있습니다. 첫 번째 컬럼명이 현재 X.
로 되어 있는데 이것을 순위
로 변경해주는 것이 좋겠습니다.
# 첫 번째 컬럼명을 "순위"로 변경합니다.
colnames(hitterStat)[1] <- "순위"
14~20 번째 컬럼이 문자 벡터로 되어 있습니다. 이 부분을 보정하기 위해 먼저 요약정보를 확인하고 필요한 조치를 취하도록 하겠습니다.
# 14번째 컬럼 요약정보를 확인합니다.
table(hitterStat[14])
##
## - 0 0.000 0.1 0.143 0.15 0.159 0.167 0.179 0.182 0.185 0.191
## 11 21 8 1 2 1 1 2 1 3 1 1
## 0.2 0.200 0.205 0.211 0.214 0.218 0.222 0.231 0.233 0.238 0.242 0.244
## 3 1 1 1 1 1 1 1 1 1 1 1
## 0.246 0.25 0.250 0.252 0.257 0.259 0.26 0.263 0.264 0.267 0.268 0.269
## 1 6 2 1 1 1 1 1 1 3 1 1
## 0.272 0.273 0.275 0.276 0.277 0.279 0.28 0.282 0.283 0.285 0.286 0.287
## 1 2 2 2 1 2 1 1 2 1 1 1
## 0.288 0.289 0.29 0.290 0.291 0.292 0.293 0.294 0.297 0.299 0.3 0.300
## 2 3 1 1 2 3 2 5 2 1 5 3
## 0.301 0.302 0.303 0.305 0.306 0.308 0.31 0.311 0.313 0.314 0.316 0.317
## 1 2 1 2 4 1 1 1 4 2 2 2
## 0.318 0.32 0.321 0.323 0.324 0.326 0.327 0.328 0.329 0.33 0.331 0.332
## 1 1 3 1 6 3 3 2 1 2 1 3
## 0.333 0.334 0.335 0.337 0.338 0.34 0.340 0.341 0.342 0.343 0.344 0.346
## 16 1 2 1 1 2 1 1 1 1 1 3
## 0.347 0.348 0.35 0.351 0.352 0.353 0.354 0.355 0.356 0.358 0.359 0.362
## 2 3 1 2 1 6 3 1 1 3 2 4
## 0.363 0.364 0.365 0.367 0.37 0.371 0.374 0.375 0.378 0.381 0.383 0.384
## 2 2 1 2 1 2 1 1 1 2 1 1
## 0.385 0.387 0.388 0.39 0.393 0.394 0.4 0.408 0.41 0.413 0.417 0.418
## 1 2 1 1 1 1 2 1 1 1 1 1
## 0.423 0.429 0.455 0.474 0.5 0.6 0.75 1 1.000
## 1 3 1 1 5 1 1 3 1
15번째 페이지에서 보여지는 것처럼 일부 선수들의 데이터가 하이픈(-)
로 되어 있습니다. 이것 때문에 숫자가 아닌 문자 데이터로 강제전환된 것입니다. 이것을 제거하고 숫자 벡터로 변환하겠습니다.
# 데이터 중 "-"를 제거합니다.
hitterStat[hitterStat == "-"] <- NA
# 14~20 번째 컬럼을 숫자 벡터로 변환합니다.
hitterStat[, 14:20] <- data.matrix(hitterStat[, 14:20])
# 데이터테이블 구조를 다시 확인합니다.
str(hitterStat)
## 'data.frame': 292 obs. of 20 variables:
## $ 순위 : int 1 2 3 4 5 6 7 8 9 10 ...
## $ 선수명: chr "최정" "김재환*" "최형우" "박건우" ...
## $ 팀명 : chr "SK" "두산" "KIA" "두산" ...
## $ 경기 : int 130 144 142 131 119 125 144 137 139 106 ...
## $ 타석 : int 527 636 629 543 510 561 667 529 621 452 ...
## $ 타수 : int 430 544 514 483 445 498 576 476 557 388 ...
## $ 안타 : int 136 185 176 177 151 173 193 176 178 141 ...
## $ 홈런 : int 46 35 26 20 37 24 20 5 27 3 ...
## $ 득점 : int 89 110 98 91 100 103 113 84 118 84 ...
## $ 타점 : int 113 115 120 78 111 99 80 64 111 47 ...
## $ 볼넷 : int 70 81 96 41 50 48 83 39 41 46 ...
## $ 삼진 : int 107 123 82 64 61 116 96 40 112 51 ...
## $ 도루 : int 1 4 0 20 10 17 25 4 32 11 ...
## $ BABIP : num 0.316 0.385 0.362 0.39 0.324 0.413 0.374 0.393 0.354 0.408 ...
## $ 타율 : num 0.316 0.34 0.342 0.366 0.339 0.347 0.335 0.37 0.32 0.363 ...
## $ 출루율: num 0.427 0.429 0.45 0.424 0.414 0.415 0.42 0.42 0.373 0.441 ...
## $ 장타율: num 0.684 0.603 0.576 0.582 0.661 0.584 0.514 0.477 0.54 0.472 ...
## $ OPS : num 1.11 1.03 1.03 1.01 1.07 ...
## $ wOBA : num 0.442 0.427 0.43 0.424 0.436 0.416 0.398 0.391 0.38 0.404 ...
## $ WAR : num 7.3 7.22 7.2 7.04 5.75 5.64 5.6 5.19 5.01 4.92 ...
# 처음 10행만 미리보기 합니다.
head(x = hitterStat, n = 10L)
## 순위 선수명 팀명 경기 타석 타수 안타 홈런 득점 타점 볼넷 삼진 도루
## 1 1 최정 SK 130 527 430 136 46 89 113 70 107 1
## 2 2 김재환* 두산 144 636 544 185 35 110 115 81 123 4
## 3 3 최형우 KIA 142 629 514 176 26 98 120 96 82 0
## 4 4 박건우 두산 131 543 483 177 20 91 78 41 64 20
## 5 5 로사리오 한화 119 510 445 151 37 100 111 50 61 10
## 6 6 나성범 NC 125 561 498 173 24 103 99 48 116 17
## 7 7 손아섭 롯데 144 667 576 193 20 113 80 83 96 25
## 8 8 김선빈 KIA 137 529 476 176 5 84 64 39 40 4
## 9 9 버나디나 KIA 139 621 557 178 27 118 111 41 112 32
## 10 10 박민우 NC 106 452 388 141 3 84 47 46 51 11
## BABIP 타율 출루율 장타율 OPS wOBA WAR
## 1 0.316 0.316 0.427 0.684 1.111 0.442 7.30
## 2 0.385 0.340 0.429 0.603 1.032 0.427 7.22
## 3 0.362 0.342 0.450 0.576 1.026 0.430 7.20
## 4 0.390 0.366 0.424 0.582 1.006 0.424 7.04
## 5 0.324 0.339 0.414 0.661 1.075 0.436 5.75
## 6 0.413 0.347 0.415 0.584 0.999 0.416 5.64
## 7 0.374 0.335 0.420 0.514 0.934 0.398 5.60
## 8 0.393 0.370 0.420 0.477 0.897 0.391 5.19
## 9 0.354 0.320 0.373 0.540 0.913 0.380 5.01
## 10 0.408 0.363 0.441 0.472 0.913 0.404 4.92
이제까지 작업한 파일을 나중에 사용하기 위해 xlsx 파일로 저장하겠습니다. xlsx로 저장하려면 xlsx 패키지의 write.xlsx()
함수를 사용합니다.
# 필요 패키지를 불러옵니다.
library(xlsx)
## Loading required package: rJava
## Loading required package: xlsxjars
# 저장할 폴더를 지정합니다. 있는지 확인하고 없으면 새로 만듭니다.
newDir <- "./data"
if (dir.exists(paths = newDir) == FALSE) {
dir.create(path = newDir)
}
# xlsx 파일로 저장합니다.
write.xlsx(x = hitterStat,
file = "./data/2017_Baseball_hitter_stat.xlsx",
row.names = FALSE)
이상으로 JavaScript를 우회하여 웹데이터를 수집하는 방법에 대해 알아봤습니다. 다음 포스팅에서는 RSelenium을 이용하여 데이터를 수집하는 R Crawler 마지막 부분을 다루도록 하겠습니다.
[1] XHR에 대한 자세한 내용은 여기를 확인해보시기 바랍니다
[2] AJAX에 대한 자세한 내용은 여기를 참조하시기 바랍니다