25  웹 데이터

중앙선거관리위원회(이하 선관위)가 데이터를 엑셀, 웹, API 형식으로 제작하고 배포하는 이유는 국민들의 다양한 정보 접근성을 보장하고, 선거 정보의 활용도를 높이기 위해서다. 선관위의 다양한 형태 데이터 제작 및 배포 노력이 시민들의 선거 참여를 촉진하고, 민주주의 발전에 기여할 것으로 기대하고 있다. 다양한 형식의 데이터 제공은 시민들이 자신의 필요와 선호에 따라 선거 정보를 활용할 수 있도록 하며, 시민들의 정치적 권리 행사를 지원하는 데 도움이 된다.

엑셀 형식은 데이터 분석과 가공이 용이하므로, 연구자나 데이터 분석가들이 선거 데이터를 심층적으로 분석하고 새로운 통찰을 도출하는 데 도움이 된다. 또한, 엑셀 파일은 일반 국민들이 쉽게 다운로드하여 사용할 수 있어, 선거 정보의 접근성을 높이는 데 기여한다.

웹 형식은 일반 국민들이 쉽게 접근하고 이해할 수 있는 형태로 선거 정보를 제공한다. 시각화된 데이터와 인터랙티브한 웹 페이지를 통해 선거에 대한 국민들의 관심과 참여를 높일 수 있다. 웹 페이지를 통해 선거 정보가 실시간으로 업데이트되어, 시민들에게 최신 정보를 제공한다.

API 형식은 개발자들이 선거 데이터를 활용하여 다양한 애플리케이션과 서비스를 개발할 수 있도록 하여 선거 정보의 활용 범위를 넓히고, 시민들에게 더욱 편리하고 유용한 서비스를 제공하는 데 기여한다. API를 통해 선거 데이터를 다른 시스템과 연동하여 활용할 수 있어, 선거 정보를 다양한 제품과 서비스에 녹여내는 데 비용과 노력을 크게 낮출 수 있다.

graph LR
    subgraph 선관위 정보 제공 방식
        A[선관위 정보 제공] --> B[엑셀]
        A --> C[웹]
        A --> D[API]
    end

    subgraph 엑셀
        B --> E[다운로드 가능한 파일]
        E --> H[표 형식 데이터]
    end

    subgraph 웹
        C --> F[웹사이트 직접 접근]
        F --> I[동적 웹 인터페이스]
    end

    subgraph API
        D --> G[프로그래밍으로 데이터 접근]
        G --> J[JSON, XML 형식 데이터]
    end

    style A fill:#000,stroke:#fff,stroke-width:4px,color:#fff
    style B fill:#d3d3d3,stroke:#000,stroke-width:2px
    style C fill:#a9a9a9,stroke:#000,stroke-width:2px
    style D fill:#808080,stroke:#000,stroke-width:2px
    style E fill:#f2f2f2,stroke:#000,stroke-width:2px
    style F fill:#e6e6e6,stroke:#000,stroke-width:2px
    style G fill:#d9d9d9,stroke:#000,stroke-width:2px
    style H fill:#fff,stroke:#000,stroke-width:2px
    style I fill:#f9f9f9,stroke:#000,stroke-width:2px
    style J fill:#f5f5f5,stroke:#000,stroke-width:2px

그림 25.1: 선관위 제공 데이터

입장을 바꾸어 선관위에서는 데이터를 어떻게 제공할까? 선관위는 자체 선거데이터를 체계적으로 관리하는 데이터베이스와 웹서버, API 서버 등을 갖추고 PC뿐만 아니라 모바일에서도 선거 관련 다양한 정보를 제공하고 있다. 데이터 제공 측면에서 보면 선거 데이터를 수집, 정제, 가공한 후 엑셀 파일, 웹 페이지, API 형식으로 별도 개발을 통해 시민에게 다양한 형식으로 제공하고 있다.

graph LR
    subgraph 선관위 데이터 제작 과정
        AB[데이터 수집 <br> 데이터 정제 및 가공]
        AB --> C{데이터 <br> 형식 결정}
        C --> D[엑셀 파일 <br> 생성]
        C --> E[웹 페이지 <br> 생성]
        C --> F[API 개발]
    end

    subgraph 엑셀
        D --> G[데이터 시트 구성]
        G --> H[데이터 입력 <br> 서식 지정]
        H --> I[파일 저장 <br> 검토]
    end

    subgraph 웹
        E --> J[웹 페이지 디자인]
        J --> K[데이터 시각화 <br> 인터랙티브 요소 추가]
        K --> L[웹 페이지 <br> 테스트, 배포]
    end

    subgraph API
        F --> M[API 설계 <br> 문서화]
        M --> N[API 구현 <br> 테스트]
        N --> O[API 배포 <br> 모니터링]
    end

    style AB fill:#d9d9d9,stroke:#000,stroke-width:2px
    style C fill:#000,stroke:#fff,stroke-width:4px,color:#fff
    style D fill:#f2f2f2,stroke:#000,stroke-width:2px
    style E fill:#e6e6e6,stroke:#000,stroke-width:2px
    style F fill:#d9d9d9,stroke:#000,stroke-width:2px
    style G fill:#fff,stroke:#000,stroke-width:2px
    style H fill:#f9f9f9,stroke:#000,stroke-width:2px
    style I fill:#f5f5f5,stroke:#000,stroke-width:2px
    style J fill:#fff,stroke:#000,stroke-width:2px
    style K fill:#f9f9f9,stroke:#000,stroke-width:2px
    style L fill:#f5f5f5,stroke:#000,stroke-width:2px
    style M fill:#fff,stroke:#000,stroke-width:2px
    style N fill:#f9f9f9,stroke:#000,stroke-width:2px
    style O fill:#f5f5f5,stroke:#000,stroke-width:2px
그림 25.2: 선관위 데이터 제작 과정

25.1 엑셀

제21대 국회의원 선거 당선인 명부 데이터를 엑셀 형태로 구하려면, 선관위 자료공간 웹사이트 게시판을 통해 엑셀 파일을 다운로드할 수 있다.

그림 25.3: 선관위 제21대 국회의원 당선인 명부 엑셀파일

다운로드받은 엑셀 데이터를 가져오는 코드를 ?lst-excel 와 같이 작성할 수 있다. 먼저, library(readxl)library(tidyverse)를 통해 필요한 패키지를 로드한다. nec_sheets <- readxl::excel_sheets(...)는 엑셀 파일의 시트 이름을 가져오는 코드로 엑셀 파일에서 가져올 시트를 선택한다. winner_raw <- readxl::read_excel(...)는 선택한 시트의 데이터를 읽어오는 코드로 당선인 명부 데이터를 R 환경으로 불러온다.

이후, winner_raw |> count(소속정당명, name = "당선인수", sort = TRUE)는 파이프 연산자(|>)를 사용하여 데이터를 조작하여 정당별로 집계하고, 당선인 수를 계산한 후, 결과를 내림차순으로 정렬하여 당선인 수를 쉽게 파악할 수 있다. janitor::adorn_totals(where = "row", name = "합계")janitor 패키지 함수로, 집계 결과에 총합 행을 추가하여 전체 당선인 수를 확인할 수 있다.

library(readxl)
library(tidyverse)

nec_sheets <- excel_sheets("data/nec/제21대_국회의원선거(재보궐선거_포함)_당선인명부.xlsx")

winner_raw <- read_excel("data/nec/제21대_국회의원선거(재보궐선거_포함)_당선인명부.xlsx", 
                         sheet = nec_sheets[1])

winner_raw |> 
  count(소속정당명, name = "당선인수", sort = TRUE) |> 
    janitor::adorn_totals(where = "row", name = "합계")  
#>    소속정당명 당선인수
#>  더불어민주당      163
#>    미래통합당       84
#>        무소속        5
#>        정의당        1
#>          합계      253

25.2 API

공공데이터포털 중앙선거관리위원회 당선인정보 API를 활용하여 당선인 정보를 프로그래밍을 통해 직접 가져올 수 있다. 아래아한글로 작성된 당선인 정보 조회 서비스 API 명세서를 참고하여 Java, Javascript, C#, PHP, Curl, Objective-C, Python, Nodejs, R 언어로 예제 코드가 작성되어 있어 사용자가 편리하게 API를 활용할 수 있다.

그림 25.4: 국가선거정보 - 당선인 정보 조회 서비스 API 명세서

공공데이터포털 API 끝점(Endpoint)과 API KEY를 발급받고 API 서비스 신청을 하였다면 다음 단계로 아래아한글 당선인 정보 조회 서비스 API 명세서 내용을 참고하여 코드를 작성한다.

graph LR

  subgraph API 파악
    A[공공데이터포털<br>API 발급] --> B[API 명세서<br>참고]
  end
  
  subgraph 스크립트 작성
    B --> C[당선인 정보 API 호출<br>스크립트 작성]
    B --> D[선거구 정보 API 호출<br>스크립트 작성]
  end
  
  subgraph 함수 작성
    C --> E[당선인 정보 API 호출<br>함수 제작]
    D --> F[선거구 정보 API 호출<br>함수 제작]
  end
  
  subgraph 데이터 가져오기 및 저장
    F --> G[선거구 데이터프레임<br>제작]
    G --> H[선거구 당선인<br>데이터프레임 제작]
    H --> I[데이터 저장]
  end
  
E --> H

linkStyle 8 stroke:red,stroke-width:4px

style A fill:#f0f0f0,stroke:#333,stroke-width:2px
style B fill:#f0f0f0,stroke:#333,stroke-width:2px
style C fill:#e0e0e0,stroke:#333,stroke-width:2px
style D fill:#e0e0e0,stroke:#333,stroke-width:2px
style E fill:#d0d0d0,stroke:#333,stroke-width:2px
style F fill:#d0d0d0,stroke:#333,stroke-width:2px
style G fill:#c0c0c0,stroke:#333,stroke-width:2px
style H fill:#c0c0c0,stroke:#333,stroke-width:2px
style I fill:#c0c0c0,stroke:#333,stroke-width:2px
그림 25.5: 공공데이터포털 API 명세서 참고 및 코드 작성

공공데이터포털에서 제공하는 API를 활용하여 당선인 정보와 선거구 정보를 수집하고 분석에 용이한 형태로 가공하는 과정은 다음과 같다. API 사용을 위해 활용신청과 API KEY 발급을 진행한 후 명세서를 참고하여 API 호출 방법과 반환되는 데이터 형식을 파악한다.

당선인 정보와 선거구 정보를 가져오기 위한 API 호출 스크립트를 작성하고, 반복을 줄이고 재사용성을 높이기 위해 함수로 변환하는 작업을 수행한다. 선거구 데이터프레임에서 기본 정보, 즉 시도명과 선거구명을 작성한 함수 get_winner()에 전달하여 선거구별 당선인 정보를 데이터프레임으로 가져온다.

25.2.1 스크립트

library(httr)
library(jsonlite)
library(tidyverse)

response <- GET("http://apis.data.go.kr/9760000/WinnerInfoInqireService2/getWinnerInfoInqire",
                query = list(sgId = "20200415",
                              sgTypecode = "2",
                              sdName = "서울특별시",
                              sggName = "종로구",
                              pageNo = 1,
                              numOfRows = 10,
                              resultType = "json",
                              serviceKey = Sys.getenv('DATA_GO_DECODE_KEY')))

print(status_code(response))
#> [1] 200

response_list <- content(response, "text") |> 
  fromJSON()

response_tbl <- response_list$response$body$items$item

response_tbl |> 
    select(sgId, sggName, sdName, giho, jdName, name)

#>       sgId sggName     sdName giho       jdName   name
#> 1 20200415  종로구 서울특별시    1 더불어민주당 이낙연

25.2.2 함수

get_winner <- function(sdName = "서울특별시", sggName = "종로구") {
    response <- GET("http://apis.data.go.kr/9760000/WinnerInfoInqireService2/getWinnerInfoInqire",
                query = list(sgId = "20200415",
                              sgTypecode = "2",
                              sdName = sdName,
                              sggName = sggName,
                              pageNo = 1,
                              numOfRows = 1000,
                              resultType = "json",
                              serviceKey = Sys.getenv('DATA_GO_DECODE_KEY')))

    response_list <- content(response, "text") |> 
      fromJSON()
    
    response_tbl <- response_list$response$body$items$item |> 
        select(sgId, sggName, sdName, giho, jdName, name)
    
    return(response_tbl)
}

get_winner("서울특별시", "종로구")

#>       sgId sggName     sdName giho       jdName   name
#> 1 20200415  종로구 서울특별시    1 더불어민주당 이낙연

25.2.3 선거구

중앙선거관리위원회 코드정보 API를 활용하여 선거구 정보를 프로그래밍을 통해 가져올 수 있다. 당선인 명부 데이터를 불러올 때 선거구 정보가 필수적이라 이 과정을 생략할 수는 없다. 당선인 정보와 동일하기 때문에 스크립트 제작 과정은 생략하고 명세서에 나와 있는 내용을 바탕으로 R 코드를 작성해서 선거구 데이터프레임을 제작한다.

get_precinct <- function(pageNo = 1) {
  response <- GET("http://apis.data.go.kr/9760000/CommonCodeService/getCommonSggCodeList",
              query = list(sgId = "20200415",
                            sgTypecode = "2",
                            pageNo =  pageNo,
                            numOfRows = 1000,
                            resultType = "json",
                            serviceKey = Sys.getenv('DATA_GO_DECODE_KEY')))
  
  response_list <- content(response, "text") |> 
    fromJSON()
  
  response_tbl <- response_list$response$body$items$item
  
  return(response_tbl)
}

precinct_raw <- tibble(page = 1:3) |> 
  mutate(data = map(page, get_precinct)) 

precinct_tbl <- precinct_raw |> 
  unnest(data)

precinct_tbl  |> 
  select(sgId, sdName, sggName, sggJungsu)

#> # A tibble: 30 × 4
#>    sgId     sdName     sggName      sggJungsu
#>    <chr>    <chr>      <chr>        <chr>    
#>  1 20200415 서울특별시 종로구       1        
#>  2 20200415 서울특별시 중구성동구갑 1        
#>  3 20200415 서울특별시 중구성동구을 1        
#>  4 20200415 서울특별시 용산구       1        
#>  5 20200415 서울특별시 광진구갑     1        
#>  6 20200415 서울특별시 광진구을     1        
#>  7 20200415 서울특별시 동대문구갑   1        
#>  8 20200415 서울특별시 동대문구을   1        
#>  9 20200415 서울특별시 중랑구갑     1        
#> 10 20200415 서울특별시 중랑구을     1        
#> # ℹ 20 more rows
#> # ℹ Use `print(n = ...)` to see more rows

25.2.4 선거구 당선인

winners_raw <- precinct_tbl |> 
  mutate(winner = map2(sdName, sggName, get_winner))

winners_tbl <-winners_raw |> 
  select(winner) |> 
  unnest(winner)

winners_tbl

#> # A tibble: 253 × 6
#>    sgId     sggName      sdName     giho  jdName       name  
#>    <chr>    <chr>        <chr>      <chr> <chr>        <chr> 
#>  1 20200415 종로구       서울특별시 1     더불어민주당 이낙연
#>  2 20200415 중구성동구갑 서울특별시 1     더불어민주당 홍익표
#>  3 20200415 중구성동구을 서울특별시 1     더불어민주당 박성준
#>  4 20200415 용산구       서울특별시 2     미래통합당   권영세
#>  5 20200415 광진구갑     서울특별시 1     더불어민주당 전혜숙
#>  6 20200415 광진구을     서울특별시 1     더불어민주당 고민정
#>  7 20200415 동대문구갑   서울특별시 1     더불어민주당 안규백
#>  8 20200415 동대문구을   서울특별시 1     더불어민주당 장경태
#>  9 20200415 중랑구갑     서울특별시 1     더불어민주당 서영교
#> 10 20200415 중랑구을     서울특별시 1     더불어민주당 박홍근
#> # ℹ 243 more rows
#> # ℹ Use `print(n = ...)` to see more rows

25.2.5 데이터 저장

winners_tbl |> 
  write_csv("data/21st_election_winners.csv")

25.3 동적 웹 페이지

중앙선거관리위원회 선거통계시스템에서 제공하는 제21대 국회의원선거 당선인 명단을 R과 RSelenium을 활용한 동적 웹 크롤링을 통해 수집하고 정제하는 과정을 살펴본다. RSelenium 패키지를 설치하고 불여우(Firefox) 브라우저를 제어하기 위해 드라이버를 설치하고 선관위 웹사이트로 이동한다.

다음으로 CSS 선택자(selector)를 이용하여 선거유형, 선거명, 선거코드, 시도 등의 조회조건을 순차적으로 선택하고 클릭 이벤트를 실행하여 검색조건을 완성한 후 검색 버튼을 클릭하여 해당 조건에 맞는 당선인 명단이 포함된 HTML 표를 브라우저에 렌더링한다. 마지막 단계로, 다시 CSS 선택자로 해당 표를 선택하고 getElementAttribute() 함수를 통해 HTML 표를 추출한 다음, rvest 패키지의 read_html(), html_table() 함수를 이용하여 HTML 표를 데이터프레임으로 변환하여 tibble 형태로 크롤링 작업을 마무리 한다.

과거에는 RSeleniuminstall.packages() 명령어를 통해서 CRAN에서 다운로드를 할 수 없었으나 이제는 CRAN, RSelenium에서 직접 설치가 가능하고 GitHub rOpenSci 저장소에서 devtools로 설치할 수 있다.

# devtools::install_github("ropensci/binman")
# devtools::install_github("ropensci/wdman")
# devtools::install_github("ropensci/RSelenium")

library(RSelenium)
library(tidyverse)

# 1. 데이터 ----

rem_driver <- rsDriver(browser = "firefox", port = 4568L)
remdrv_client <- rem_driver[["client"]]
remdrv_client$navigate("http://info.nec.go.kr/main/showDocument.xhtml?electionId=0000000000&topMenuId=EP&secondMenuId=EPEI01")

# 선거유형 "electionType2" 선택 후 클릭
electionType2 <- remdrv_client$findElement(using = "css selector", "#electionType2")
electionType2$clickElement()

# 조회조건: 제21대 선택
electionName <- remdrv_client$findElement(using = "css selector", "#electionName > option:nth-child(2)")
electionName$clickElement()

# 조회조건: 제21대 선택 > 국회의원선거
electionCode <- remdrv_client$findElement(using = "css selector", "#electionCode > option:nth-child(2)")
electionCode$clickElement()

# 조회조건: 제21대 선택 > 국회의원선거 > 시도 > 서울특별시
cityCode <- remdrv_client$findElement(using = "css selector", "#cityCode > option:nth-child(2)")
cityCode$clickElement()

# 검색 실행
runButton <- remdrv_client$findElement(using = "css selector", "#searchBtn")
runButton$clickElement()

# HTML 표 --> 데이터프레임 변환
winner_html <- remdrv_client$findElement("css", "#table01")$getElementAttribute("outerHTML")[[1]]

winner_table <- read_html(winner_html) %>%
  html_table(fill = TRUE) %>%
  .[[1]] 

remdrv_client$close()

winner_table |> 
  select(-직업, -학력, -경력)

#> # A tibble: 49 × 6
#>    선거구명     정당명       `성명(한자)`   성별  `생년월일(연령)` `득표수(득표율)`
#>    <chr>        <chr>        <chr>          <chr> <chr>            <chr>           
#>  1 종로구       더불어민주당 이낙연(李洛淵) 남    1952.12.20(67세) 54,902(58.38)   
#>  2 중구성동구갑 더불어민주당 홍익표(洪翼杓) 남    1967.11.20(52세) 70,387(54.25)   
#>  3 중구성동구을 더불어민주당 박성준(朴省俊) 남    1969.04.23(50세) 64,071(51.96)   
#>  4 용산구       미래통합당   권영세(權寧世) 남    1959.02.24(61세) 63,891(47.80)   
#>  5 광진구갑     더불어민주당 전혜숙(全惠淑) 여    1955.05.05(64세) 56,608(53.68)   
#>  6 광진구을     더불어민주당 고민정(高旼廷) 여    1979.08.23(40세) 54,210(50.37)   
#>  7 동대문구갑   더불어민주당 안규백(安圭伯) 남    1961.04.29(58세) 51,551(52.72)   
#>  8 동대문구을   더불어민주당 장경태(張耿態) 남    1983.10.12(36세) 55,230(54.54)   
#>  9 중랑구갑     더불어민주당 서영교(徐瑛敎) 여    1964.11.11(55세) 55,185(57.76)   
#> 10 중랑구을     더불어민주당 박홍근(朴洪根) 남    1969.10.08(50세) 74,131(59.28)   
#> # ℹ 39 more rows
#> # ℹ Use `print(n = ...)` to see more rows
그림 25.6: 동적 웹 페이지 데이터 추출과정
자바(Java) 설치 오류

RSelenium 패키지를 사용하기 위해서는 자바(Java)가 설치되어 있어야 한다. 데이터 과학 PC (Java) - 윈도우 블로그 게시글을 참조하거나 library(multilinguer); install_java() 함수를 사용해서 설치하여 사용할 수 있다.

java_check()에서 다음과 같은 에러가 발생했습니다: PATH to JAVA not found. Please check JAVA is installed.

25.4 요약

웹은 방대한 데이터의 보고이지만, 데이터 형식과 구조가 제각각으로 데이터를 수집하고 정제하는 작업이 쉽지 않다. 하지만 이번 장에서 소개한 엑셀, API, 동적 웹 페이지 데이터 수집 방법을 활용한다면 웹 데이터도 충분히 가치 있는 분석 자료로 활용할 수 있다.

선관위는 시민들의 정보 접근성을 높이고 선거 정보 활용도를 제고하기 위해 엑셀, 웹, API 등 다양한 형식으로 데이터를 제공하고 있다. 엑셀 파일은 readxl 패키지를 사용하여 쉽게 불러올 수 있고, API를 활용하면 프로그래밍을 통해 원하는 데이터를 선택적으로 가져올 수 있다. 또한 RSelenium을 사용하면 웹 브라우저를 제어하여 동적 웹 페이지의 데이터까지도 수집할 수 있다.

웹 데이터를 수집하고 정제하는 과정은 데이터 과학 프로젝트에서 중요한 부분을 차지한다. 데이터의 형식과 구조를 파악하고, 적절한 도구와 방법을 선택하여 효율적으로 데이터를 수집하는 것이 핵심이다.