R Crawler 2 부록

페이지 네비게이션 활용하기

Dr.Kevin 1/20/2018

R Crawler2 POST 함수로 수집하기 포스팅에서 조회 조건에 따라 1페이지에 출력된 데이터만 수집하는 것을 보여드렸는데요. 조회 조건에 해당되는 데이터가 2페이지 이상 출력되는 경우, 모든 데이터를 수집하려면 어떻게 해야 하는지 궁금하실 것 같아서 이번 포스팅을 추가하였습니다.

타겟 웹사이트에서 지역과 업종을 선택하면 1페이지에 최대 10개의 식당 리스트가 출력됩니다. 화면을 맨 밑으로 내리면 숫자가 적힌 버튼이 보이고, 이 숫자 버튼을 클릭하면 해당 페이지로 이동하는데요. 2페이지부터는 아래 방식으로 웹데이터를 수집할 수 있습니다.

먼저 크롬 개발자도구를 열고 네트워크 탭으로 이동합니다. clear 버튼을 눌러 아래 내용을 깨끗하게 지운 후, 네비게이션 위치로 가서 2를 클릭합니다. 그러면 여러 항목이 쭈욱 뜨는데 모두 GET 방식을 사용하고 있습니다. 좀 놀랍네요.

맨 위에 있는 항목을 유심히 살펴보니, 이전 포스팅에서 POST() 방식으로 html 요청할 때 Form Data의 요소로 쓰인 것들이 일부 보이네요. 이걸 클릭하고 들어가서 화면을 맨 아래로 내려보니 Form Data 대신 Query String Parameters가 보이고 세부 요소들이 Form Data의 요소들과 많이 일치하는 것을 알 수 있습니다. 그리고 page라는 요소가 추가로 보이는 군요.

일단 GET 방식이 사용된다는 것에 주목해봅시다. 화면을 다시 맨 위로 올려서 GeneralRequest URL을 살펴보면, POST() 함수의 인자로 사용된 요소들이 하나의 URL로 조립되어 있다는 것을 알 수 있습니다. 그렇다면 Request URL을 분해해보면 타겟 URL을 조립하는 방법을 알아낼 수 있을 것입니다!

타겟 URL 분해하고 조립하기

크롬 개발자도구에서 Request URL을 복사하여 붙인 후, 구분자(?, &)를 기준으로 분해하면 다음과 같습니다.

  • http://isuperpage.co.kr/search/s_pagedata_page.asp : 공통부분
  • ?x=37.5290841 : 사용자의 위도 좌표(로 추정)
  • &y=126.9293792 : 사용자의 경도 좌표(로 추정)
  • &searchWord=%C3%DF%C3%B5%B8%C0%C1%FD : 검색어 (여기서는 “추천맛집”)
  • &page=2 : 페이지 번호
  • &city=%BC%AD%BF%EF : 광역시도 (여기서는 “서울”)
  • &gu=%BF%B5%B5%EE%C6%F7%B1%B8 : 시구군 (여기서는 “영등포구”)
  • &dong=%BF%A9%C0%C7%B5%B5%B5%BF : 읍면동 (여기서는 “여의도동”)
  • &addr4= : 완성된 주소(로 보이는데 할당된 값이 없으므로 무시)
  • &pdc=0 : 모르겠음(역시 무시함)

searchWord 요소에 할당된 %C3%DF%C3%B5%B8%C0%C1%FD을 사람이 읽을 수 있도록 변환해보겠습니다. 먼저 적당한 객체명(이번 포스팅에서는 encoded)에 할당해줍니다. 그리고 R base 함수인 URLdecode() 함수를 사용하여 디코딩하겠습니다.

# Request URL에서 searchWord에 할당된 값을 저장합니다.
encoded <- "%C3%DF%C3%B5%B8%C0%C1%FD"

# %-인코딩된 문자열을 사람이 이해할 수 있도록 디코딩합니다.
URLdecode(encoded)
## [1] "\xc3\xdfõ\xb8\xc0\xc1\xfd"

위와 같이 디코딩하면 Windows 사용자들은 추천맛집이라고 잘 보일 것입니다. 하지만 Mac 사용자들은 \xc3\xdfõ\xb8\xc0\xc1\xfd으로 출력되어 마치 외계어처럼 보일 것입니다. 그 이유를 짐작하실 수 있나요? 바로 R이 인코딩하는 방식에 차이가 있습니다. Windows 사용자들이 RStudio에서 localeToCharset()을 실행하면 CP949로 출력될 것입니다. 이번 예제의 타겟 웹사이트가 인코딩 방식으로 EUC-KR을 사용하고 있고, EUC-KRCP949의 부분집합임을 고려할 때 서로 인코딩 방식이 맞는 거죠. 그런데 Mac 사용자라면 localeToCharset() 결과로 UTF-8이 출력될 것이고, 이것은 EUC-KR과 다르기 때문에 인코딩 방식이 충돌하는 거죠. 그래서 한글이 이상하게 외계어처럼 보이는 것입니다.

이 문제를 해결하려면? 아래와 같이 iconv()를 사용하면 됩니다. Mac 사용자만 해보세요.

# 인코딩 방식을 변환합니다. (Mac 사용자만 실행해보세요)
iconv(x = URLdecode(encoded), from = "CP949", to = "UTF-8")
## [1] "추천맛집"

Mac 사용자도 추천맛집이라고 잘 보일 것입니다. 이제 %-디코딩하는 방법을 알았으니 다음으로는 %-인코딩하는 방법을 알아보겠습니다. 이 방법을 알아야 GET()url 인자에 할당할 타겟 URL을 제대로 조립할 수 있습니다.

추천맛집decoded에 할당하고, %-인코딩은 R base 함수인 URLencode() 함수를 사용하겠습니다.

# 한글 문자열을 지정합니다.
decoded <- "추천맛집"

# %-인코딩 합니다.
URLencode(decoded)
## [1] "%EC%B6%94%EC%B2%9C%EB%A7%9B%EC%A7%91"

출력된 결과를 살펴보면, Windows 사용자는 encoded와 똑같다는 것을 확인할 수 있을 것입니다. 하지만 Mac 사용자는 %EC%B6%94%EC%B2%9C%EB%A7%9B%EC%A7%91로 출력되어 encoded와 많이 다를 것입니다.

참고로 두 문자열이 정확하게 일치하는 지 확인하려면 비교연산자 ==를 사용하면 됩니다. 만약 두 개의 문자열이 똑같다면 TRUE를 출력할 것입니다.

# 두 문자열 일치 여부를 확인합니다.
# Windows 사용자는 TRUR, Mac 사용자는 FAlSE가 출력됩니다.
encoded == URLencode(decoded)
## [1] FALSE

Mac 사용자의 경우, 역시 인코딩 문제 때문에 이런 현상이 발생합니다. 만약 iconv()를 이용해서 문자열의 인코딩 방식을 UTF-8에서 CP949로 바꾸어주면 어떨까요? 한 번 해보겠습니다.

# 문자열의 인코딩 방식을 확인합니다. (Mac 사용자만 실행해보세요)
Encoding(decoded)
## [1] "unknown"
# 문자열의 인코딩 방식을 변경합니다.
iconv(x = decoded, from = "UTF-8", to = "CP949")
## [1] "\xc3\xdfõ\xb8\xc0\xc1\xfd"
# 다시 %-인코딩 합니다.
URLencode(iconv(x = decoded, from = "UTF-8", to = "CP949"))
## Warning in strsplit(URL, ""): input string 1 is invalid in this locale

## [1] "NA"

마지막 스크립트 실행 결과로 NA가 출력되었고, 결과 메세지를 읽어보니 로케일이 맞지 않다고 합니다. 그럼 로케일을 변경한 후, 다시 %-인코딩을 해보겠습니다. Windows 사용자는 실행할 필요 없습니다(만 로케일 변경 방법이 궁금하면 따라해 보세요).

# 로케일을 초기화합니다.
Sys.setlocale(category = "LC_ALL")
## [1] "en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8"
# 현재 로케일을 확인합니다.
Sys.getlocale()
## [1] "en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8"
# UTF-8에서 CP949로 로케일을 변경합니다.
# Windows에서는 이 스크립트가 실행되지 않습니다.
# Windows 사용자는 locale에 "ko_KR.CP949" 대신에
# "C"를 할당해야 "UTF-8"를 처리할 수 있습니다. 
Sys.setlocale(category = "LC_ALL", locale = "ko_KR.CP949")
## [1] "ko_KR.CP949/ko_KR.CP949/ko_KR.CP949/C/ko_KR.CP949/en_US.UTF-8"
# 다시 %-인코딩 합니다.
URLencode(iconv(x = decoded, from = "UTF-8", to = "CP949"))
## [1] "%C3%DF%C3%B5%B8%C0%C1%FD"
# 두 문자열 일치 여부를 확인합니다.
encoded == URLencode(iconv(x = decoded, from = "UTF-8", to = "CP949"))
## [1] TRUE

이제야 비로소 두 문자열이 일치합니다. 로케일에 익숙하지 않은 분들은 이 방법 말고 다음에 소개해드리는 쉬운 방법을 사용하시기 바랍니다. 바로 urltools 패키지의 url_encode() 함수를 사용하는 것입니다. Windows 사용자는 실행하지 않습니다.

# Mac에서 로케일을 초기화합니다.
Sys.setlocale(category = "LC_ALL")
## [1] "en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8"
# 필요 패키지를 불러옵니다.
library(urltools)

# %-인코딩 합니다.
url_encode(iconv(x = decoded, from = "UTF-8", to = "CP949"))
## [1] "%c3%df%c3%b5%b8%c0%c1%fd"

출력된 결과가 encoded와 비슷해 보이지만, 대문자가 아닌 소문자로 출력되었습니다. 두 문자열이 똑같은 지 확인하기 위해 방금 실행한 스크립트를 toupper() 함수에 할당하여 모두 대문자로 변환한 후, 일치 여부를 확인해보겠습니다.

# 대문자로 변환합니다.
toupper(url_encode(iconv(x = decoded, from = "UTF-8", to = "CP949")))
## [1] "%C3%DF%C3%B5%B8%C0%C1%FD"
# 일치 여부를 확인합니다.
encoded == toupper(url_encode(iconv(x = decoded, from = "UTF-8", to = "CP949")))
## [1] TRUE

먼 길을 돌아왔지만 결국 해결했습니다. 참고로 URL은 대소문자를 구분하지 않기 때문에 타겟 URL이 소문자로 되어 있어도 제대로 실행될 것입니다. 따라서 url_encode() 함수를 이용하여 한글 문자열을 %-인코딩 처리하고 toupper() 함수로 소문자를 대문자로 변환한 후 타겟 URL을 조립하면 됩니다.

타겟 URL을 조립할 때 필요한 요소만 사용하겠습니다. 그러니까 x, y, addr4pdc는 제외하고 searchWord, city, gu, dongpage만 추가하겠습니다.

# 조회 기준을 새로 설정합니다.
upjong <- "한식"
sido <- "서울"
gugn <- "강남구"
dong <- "압구정동"

# 타겟 URL 요소를 설정합니다. (Windows 사용자만 실행하세요!)
url <- paste0(
  "http://isuperpage.co.kr/search/s_pagedata_page.asp",
  paste0("?searchWord=", URLencode(upjong)),
  paste0("&city=", URLencode(sido)),
  paste0("&gu=", URLencode(gugn)),
  paste0("&dong=", URLencode(dong)),
  paste0("&page=", 1)
  )

# Mac용 %-인코딩 함수를 생성합니다. (Mac 사용자만 실행하세요!)
pcntEncoding4Mac <- function(string) {
  encoded <- toupper(url_encode(iconv(x = string, from = "UTF-8", to = "CP949")))
  return(encoded)
}

# 타겟 URL 요소를 설정합니다. (Mac 사용자만 실행하세요!)
url <- paste0(
  "http://isuperpage.co.kr/search/s_pagedata_page.asp",
  paste0("?searchWord=", pcntEncoding4Mac(upjong)),
  paste0("&city=", pcntEncoding4Mac(sido)),
  paste0("&gu=", pcntEncoding4Mac(gugn)),
  paste0("&dong=", pcntEncoding4Mac(dong)),
  paste0("&page=", 1)
  )

# 타겟 URL을 출력합니다.
cat(url)
## http://isuperpage.co.kr/search/s_pagedata_page.asp?searchWord=%C7%D1%BD%C4&city=%BC%AD%BF%EF&gu=%B0%AD%B3%B2%B1%B8&dong=%BE%D0%B1%B8%C1%A4%B5%BF&page=1

타겟 URL을 복사하여 웹브라우저에서 접속해보겠습니다. 화면에 텍스트만 잔뜩 출력되네요. 이런 형태를 JSON이라고 합니다. 이번 포스팅은 R에서 JSON을 처리하는 간단한 방법을 안내하는 것으로 마무리하겠습니다.

JSON 데이터 처리하기

데이터를 주고 받는 목적으로 사용되는 형태로는 크게 XMLJSON을 들 수 있는데요. XMLR에서 Open API 활용하기에서 소개해드릴 예정입니다. JSONJavaScript 방식으로 처리된 데이터 교환 형태라고 생각하면 쉽습니다. XML에 비해 가볍고, 데이터 처리가 좀 더 쉽다는 장점이 있습니다. 이번 예제에서 필요한 함수는 jsonlite 패키지의 fromJSON()입니다. 먼저 타겟 URL로 html 요청한 후 JSON 데이터를 다뤄보겠습니다.

# 필요 패키지를 불러옵니다.
library(httr)

# GET() 함수로 html 요청합니다.
resp <- GET(url)

# html을 텍스트 형태로 출력합니다.
cat(content(x = resp, as = 'text'))
## {"totalCount":"7","pageSize":"10","currentPage":"1","poiList":[{ "num":"261694" ,"clas_name":"음식점-부페(BUFFET)" ,"tel":"02-3446-5400" ,"t_addr":"서울 강남구 압구정동 152" ,"road_addr":"" ,"city":"서울" ,"gu":"강남구" ,"lat":"37.5305954" ,"lng":"127.0306881" ,"sangho":"드마리스","level":"" }  , { "num":"2505428" ,"clas_name":"음식점-한식" ,"tel":"02-549-7870" ,"t_addr":"서울 강남구 압구정동 224-11" ,"road_addr":"압구정로 165" ,"city":"서울" ,"gu":"강남구" ,"lat":"37.5312368" ,"lng":"127.0277169" ,"sangho":"전주관","level":"" }  , { "num":"21484" ,"clas_name":"음식점-한식" ,"tel":"02-542-0669" ,"t_addr":"서울 강남구 압구정동 352-1 쇼핑센타2층" ,"road_addr":"" ,"city":"서울" ,"gu":"강남구" ,"lat":"37.5312368" ,"lng":"127.0277169" ,"sangho":"녹원","level":"" }  , { "num":"2127192" ,"clas_name":"음식점-냉면" ,"tel":"02-547-0145" ,"t_addr":"서울 강남구 압구정동 352-1" ,"road_addr":"압구정로11길 17" ,"city":"서울" ,"gu":"강남구" ,"lat":"37.5312368" ,"lng":"127.0277169" ,"sangho":"태백산칡냉면한식","level":"" }  , { "num":"2286175" ,"clas_name":"음식점-한식" ,"tel":"02-540-3894" ,"t_addr":"서울 강남구 압구정동 369-1" ,"road_addr":"압구정로29길 71" ,"city":"서울" ,"gu":"강남구" ,"lat":"37.5327792" ,"lng":"127.0280865" ,"sangho":"충무키친","level":"" }  , { "num":"2444658" ,"clas_name":"음식점-한식" ,"tel":"02-542-5829" ,"t_addr":"서울 강남구 압구정동 369-2" ,"road_addr":" " ,"city":"서울" ,"gu":"강남구" ,"lat":"37.5327792" ,"lng":"127.0280865" ,"sangho":"춘풍월","level":"" }  , { "num":"2193359" ,"clas_name":"음식점-한식" ,"tel":"02-543-1890" ,"t_addr":"서울 강남구 압구정동 454" ,"road_addr":"압구정로29길 72" ,"city":"서울" ,"gu":"강남구" ,"lat":"37.53228178" ,"lng":"127.0284076" ,"sangho":"향촌면옥","level":"" } ]}
# 필요 패키지를 불러옵니다.
library(jsonlite)

# JSON 데이터를 처리합니다.
resp %>% as.character() %>% fromJSON()
## $totalCount
## [1] "7"
## 
## $pageSize
## [1] "10"
## 
## $currentPage
## [1] "1"
## 
## $poiList
##       num           clas_name          tel
## 1  261694 음식점-부페(BUFFET) 02-3446-5400
## 2 2505428         음식점-한식  02-549-7870
## 3   21484         음식점-한식  02-542-0669
## 4 2127192         음식점-냉면  02-547-0145
## 5 2286175         음식점-한식  02-540-3894
## 6 2444658         음식점-한식  02-542-5829
## 7 2193359         음식점-한식  02-543-1890
##                                   t_addr       road_addr city     gu
## 1               서울 강남구 압구정동 152                 서울 강남구
## 2            서울 강남구 압구정동 224-11    압구정로 165 서울 강남구
## 3 서울 강남구 압구정동 352-1 쇼핑센타2층                 서울 강남구
## 4             서울 강남구 압구정동 352-1 압구정로11길 17 서울 강남구
## 5             서울 강남구 압구정동 369-1 압구정로29길 71 서울 강남구
## 6             서울 강남구 압구정동 369-2                 서울 강남구
## 7               서울 강남구 압구정동 454 압구정로29길 72 서울 강남구
##           lat         lng           sangho level
## 1  37.5305954 127.0306881         드마리스      
## 2  37.5312368 127.0277169           전주관      
## 3  37.5312368 127.0277169             녹원      
## 4  37.5312368 127.0277169 태백산칡냉면한식      
## 5  37.5327792 127.0280865         충무키친      
## 6  37.5327792 127.0280865           춘풍월      
## 7 37.53228178 127.0284076         향촌면옥

fromJSON() 함수를 실행한 결과, 4개의 요소를 갖는 리스트가 출력되었습니다. 각 요소가 의미하는 것은 다음과 같습니다.

  • totalCount : 조회 조건에 맞는 식당은 총 7개
  • pageSize : 한 페이지당 출력하는 식당의 개수
  • currentPage : 현재 응답받은 페이지 위치
  • poiList : 현재 응답받은 식당 정보를 데이터 프레임으로 생성

첫 번째 요소인 totalCount를 두 번째 요소인 pageSize로 나누면 순환 실행할 전체 페이지 수를 계산해낼 수 있습니다. 그리고 각 페이지에서 출력되는 식당 정보는 네 번째 요소로 깔끔하게 정리되어 있으므로 네 번째 요소만 가져와서 rbind()로 처리하면 간단하게 해결될 것입니다.

# 최종 데이터 객체를 생성합니다.
result <- resp %>% as.character() %>% fromJSON() %>% `[[`(4)

이상으로 페이지 네비게이션을 활용하는 포스팅을 마무리하겠습니다.

Written on January 20, 2018