시작하며

 

3학년 1학기에 수강하는 데이터베이스 과목의 최종 프로젝트는 공공데이터 포털의 공공 데이터를 활용하여 서비스를 제작하는 것이었습니다. 한학기 동안 진행되는 3개월 프로젝트였으며 저는 제 동기 한명과 팀을 이뤄서 서비스 제작을 했습니다. 프로젝트를 구상하던 당시에 서울특별시장 선거를 포함한 보궐선거가 진행되던 시기라 선거정보 및 국회의원 정보를 제공하는 서비스를 제작하기로 했습니다. 공공데이터 포털에 중앙선거관리위원회의 선거 정보들과 국회 사무처 제공의 국회의원 정보 데이터를 활용했습니다. 

서비스는 100% 웹 어플리케이션으로 기획하였으며 기반 백엔드언어는 Node.js를 활용하였습니다. 실제 임시 배포를 하고자 GCP를 사용하였고 서버를 기반으로 하는 GCP OS는 우분투로 하였습니다.

제가 맡은 부분은 데이터베이스 구축과 이를 위한 데이터 파싱작업과 백엔드 API 제작이었습니다.

 


디렉토리 구조

 

├── README.md
├── node_modules
│   └── ...
│   ...
│   └── ...
├── imgs
│   ├── title1.png
│   └── title2.png
├── models
├── package-lock.json
├── package.json
├── public
│   └── src
│       ├── readmore.min.js
│       ├── search.png
│       ├── sidebar.css
│       ├── sidebar_bgd.png
│       ├── stamp.png
│       ├── style.css
│       └── w3_sidebar.css
├── router
│   └── main.js
├── server.js
└── views
    ├── CandInfo.html
    ├── CandProm.html
    ├── Member.html
    ├── PollPlace.html
    ├── PrevElec.html
    ├── about.html
    └── index.html

 


데이터 수집 및 파싱

 

데이터베이스는 MySQL을 사용하여 저장했습니다. 이에 따라 테이블 설계를 진행했고 총 6개의 테이블이 설계되었습니다. 각 테이블에 필요한 데이터를 공공데이터 포털의 API를 통해 불러오고 파싱하는 작업을 했습니다. 데이터 파싱과 데이터프레임 제작은 재사용성 향상과 수정의 편의를 위해 모두 모듈화를 통해 제작했습니다.

 

1. 사용 라이브러리

import json
import urllib
import pandas as pd
from urllib.parse import urlencode, quote_plus, unquote, quote
from urllib.request import urlopen
from pandas.io.json import json_normalize
from sqlalchemy import create_engine
import pymysql
import datetime as dt
import sklearn
from sklearn.preprocessing import LabelEncoder
import json
import xmltodict
import multiprocessing as mp
import cryptography

pymysql.install_as_MySQLdb()
import MySQLdb

 

모든 API는 json형식으로 로드하였지만 일부 API는 xml만 한정적으로 로드하기 때문에 xmltodict라이브러리를 추가로 사용했습니다. mysql연결을 위해 sqlalchemy와 pymysql을 활용했습니다.

 

 

2. 데이터 파싱 및 데이터프레임 제작

def get_election_df(pg_num = 1, num_row = 100):
  election_base_url = 'http://apis.data.go.kr/9760000/CommonCodeService/getCommonSgCodeList'

  page_no = str(pg_num)
  num_of_rows = str(num_row)                                  # maximum 34 datas

  queryParams = '?' + urlencode({
      quote_plus('pageNo') : page_no,
      quote_plus('numOfRows') : num_of_rows,
      quote_plus('resultType') : 'json',
      quote_plus('ServiceKey') : service_key 
  })

  API_election_code_url = election_base_url + unquote(queryParams)

  # Election code load

  '''
  < election code >
  (0)대표선거명               
  (1)대통령,(2)국회의원,(3)시도지사,(4)구시군장,(5)시도의원,(6)구시군의회의원
  (7)국회의원비례대표,(8)광역의원비례대표,(9)기초의원비례대표,(10)교육의원,(11)교육감
  '''

  response = urlopen(API_election_code_url)
  json_str = response.read()

  json_object = json.loads(json_str)

  body = [json_object['getCommonSgCodeList']['item']]

  election_code_data = pd.json_normalize(json_object['getCommonSgCodeList']['item'])
  
  return election_code_data

대부분의 데이터 파싱 및 데이터프레임 제작 모듈은 위의 코드를 기반으로 제작되어 있습니다. 기본적인 API를 통해 데이터를 로드하기 전에 필요한 query를 설정하고 서비스키와 함께 요청을 보냅니다.

수신한 정보의 딕셔너리의 키값 중 'getCommonSgCodeList'의 'item'에서 데이터를 가져옵니다. 그렇게 불러온 데이터를 모두 DataFrame형태로 변환하여 데이터 전처리를 쉽게 진행하였습니다.

 

 

3. 데이터 전처리

def candidate_code_preprocessing():
  election_code = election_code_preprocessing()
  data = election_code.reset_index()  
  election_data = data[['sgId', 'sgTypecode']]

  candidate_df = pd.DataFrame()

  # concat all candidate df
  for i in range(len(election_data)):
    curr_id = election_data['sgId'][i]
    curr_code = election_data['sgTypecode'][i]
    curr_df = pd.DataFrame()

    if curr_code != 0:
      curr_df = get_candidate_df(curr_id, curr_code)
    
    candidate_df = pd.concat([candidate_df, curr_df], ignore_index=True)
  
  # candidate column change
  candidate = candidate_df.drop(['NUM', 'GIHO_SANGSE', 'HANJA_NAME', 'JOB_ID', 'EDU_ID', 'EDU', 'CAREER1', 'CAREER2', 'AGE'], axis=1)
  candidate.columns = ['sgId', 'sgTypecode', 'cnddtId', 'sggName', 'sdName', 'wiwName', 'giho', 'partyName', 'name', 'gender', 'birthday','address', 'job', 'status']

  # encoding data
  le = LabelEncoder()

  for category in ['gender', 'status']:
    candidate[category] = le.fit_transform(candidate[category])

  # giho null value
  candidate[candidate['giho'] == ''] = 1

  # type matching to sql column type
  candidate = candidate.astype({'sgId':int, 'sgTypecode':int, 'cnddtId':int, 'gender':int, 'status':int, 'giho':int})
  candidate['birthday'] = pd.to_datetime(candidate['birthday'], format='%Y-%m-%d')
  candidate.set_index('cnddtId', inplace=True)

  cndd_idx = candidate[candidate['sgId']==1].index
  candidate = candidate.drop(cndd_idx)

  return candidate

데이터베이스를 설계할 때 필요한 컬럼들을 미리 설정했습니다. 따라서 데이터프레임의 구조를 파악하고 필요한 컬럼을 추출하거나 상황에 따라서는 scikit-learn의 LabelEncoder를 통해 데이터 식별을 했습니다. 사실상 설계와 제작에 가장 오랜시간이 걸린 부분이 데이터 전처리 파트였습니다.

 


서버

 

서버는 배포를 위해 GCP (Google Cloud Platform)를 활용했습니다. 모든 API는 Node.js와 express로 제작되었으며 GET방식으로 송수신을 하게 설계하였습니다. 

 

 app.get("/CandInfo_do", function (req, res) {
    var sgId = req.query.date;
    var sggName = req.query.elecplace
    var name = req.query.name;

    if (sgId === undefined) {
      sgId = '';
    }

    var sql = 'SELECT DISTINCT sgId FROM election_code; '
    var sql1 = "SELECT * FROM candidate WHERE sgId LIKE '%"+sgId+"%' AND sggName LIKE '%"+sggName+"%' AND name LIKE '%"+name+"%' ORDER BY giho ASC; ";

    conn.query(sql + sql1, function (err, result) {
      if (err) {
        console.log(err);
      }else {
        var resultArray1 = Object.values(JSON.parse(JSON.stringify(result[0])));
        var resultArray2 = Object.values(JSON.parse(JSON.stringify(result[1])));
        
        res.render("CandInfo.html", { codes : resultArray1, candList : resultArray2 , searchSgId:sgId, searchKey: sggName})
      }
    })
  })

페이지를 이동하는 API가 아닌 유저가 특정 동작을 수행할 경우 반환하는 API입니다. 

 

 

github - https://github.com/cow-coding/V.O.T.E

반응형