Github 으로 데이터 공유 API 만들기

폴더 내 파일 구성이 달라질 수 있는 데이터셋을 공유할 일이 생겼습니다. 이를 이용할 사람들이 링크로 파일을 다운로드 받아 폴더에 파일을 더하는 방식이라면 귀찮을 뿐더러 의도치 않게 파일을 잘못된 폴더에 저장할 위험도 있습니다. 이러한 위험을 방지하기 위하여 데이터셋을 파이썬 패키지로 만들자는 생각을 했습니다. 파이썬으로 파일을 다운로드 받고 압축을 푸는 기능만 구현하면 github 과 url 링크를 이용하여 데이터셋을 공유할 수 있습니다.

데이터셋을 파이썬 패키지로 만들기

이 포스트는 https://github.com/lovit/textmining_dataset/movie_comments 데이터셋 패키지를 만드는 과정에 대하여 기술하고 있습니다. movie_comments 의 내용에 대해서는 README 를 참고하세요.

최근 텍스트 마이닝 공부를 위한 튜토리얼 코드를 정리하고 있습니다. 정리를 하다보니 데이터 로딩 함수 기능을 넣고 싶어졌습니다. 예를 들어 아래는 세 개의 column 이 tap separated 로 분리된 테이블 형식의 데이터를 로딩하는 코드 입니다.

data_path = '...'
with open(data_path, encoding='utf-8') as f:
    docs = [doc.strip().split('\t') for doc in f]
    idxs, texts, rates = zip(*docs)

이 코드는 여러 실습 튜토리얼 코드에서 반복됩니다. 그렇다면 매번 지저분하게 코드를 넣는 것이 아니라 아래처럼 dataset/loader.py 에 함수를 만들어두고 이를 이용하면 편리하겠단 생각이 들었습니다.

def load_movie_comments():
    data_path = '...'
    with open(data_path, encoding='utf-8') as f:
        docs = [doc.strip().split('\t') for doc in f]
        idxs, texts, rates = zip(*docs)

여기에 데이터셋의 종류, 전처리에 이용한 토크나이저의 종류까지 선택할 수 있도록 옵션을 주면 좋겠다는 생각도 들었습니다. 샘플로 이용할 데이터의 크기도 num_doc 으로 조절하는 기능도 넣었습니다. os 를 이용하면 이 파일의 패키지가 설치된 절대 주소를 얻을 수 있습니다.

import os

installpath = os.path.sep.join(
    os.path.dirname(os.path.realpath(__file__)).split(os.path.sep)[:-1])

def load_movie_comments(large=False, tokenize=None, num_doc=-1, directory=None):

    # set default directory
    if directory is None:
        directory = '{}/movie_comments/data/'.format(installpath)

    # set data size
    size = 'large' if large else 'small'

    # set tokenizer type
    if tokenize is 'komoran':
        tokenization = '_komoran'
    elif tokenize is 'soynlp':
        tokenization = '_soynlp'
    else:
        tokenization = ''

    # set data path
    path = '{}/data_{}{}.txt'.format(directory, size, tokenization)

이제 새로운 토크나이저를 이용한 데이터가 추가된다면 tokenize option 만 추가하여 구현하면 됩니다.

이후로 이 데이터셋을 이용하기 위해서 아래와 같이 몇 줄의 코드만 적어도 됩니다.

from movie_comments import load_movie_comments

idxs, texts, rates = load_movie_comments()
idxs, texts, rates = load_movie_comments(large=False, tokenize='komoran')

영화 평점 분류를 위하여 binary classification 용 데이터셋도 만들어 뒀습니다. 데이터 사이즈와 토크나이저 별로 학습용 데이터 x, yx 의 텍스트인 texts, 그리고 Bag of Words Model 의 각 column index 에 해당하는 단어가 저장되어 있는 idx_to_vocab 을 return 하는 함수도 만들었습니다. 데이터를 고정했기 때문에 실습 때마다 동일한 결과를 재현할 수 있으며, 토크나이징과 벡터라이징을 반복하지 않기 때문에 전처리와 같은 반복 작업을 하지 않아도 됩니다.

x, y, idx_to_vocab = load_sentiment_dataset(model_name='10k', tokenize='komoran')

Github 과 url 을 이용하여 데이터셋 업데이트 하기

처음에는 데이터셋을 패키지로 만들어 압축파일로 공유하였습니다. 그런데 같은 데이터셋에 몇 개의 파일만 추가하거나, 새로운 토크나이저로 전처리를 완료한 데이터를 추가하고 싶어졌습니다. 매번 링크로 압축 파일을 공유한다면 이를 해제하는 과정에서 잘못된 폴더에 파일을 저장할 수도 있고, 매번 해당 폴더에 파일을 복사하고 압축을 풀어야 하는 번거로움도 있기 때문입니다.

그래서 데이터셋 패키지를 github 에 올리기로 결정하였습니다. 그런데 github 에 데이터까지 모두 올리면 git 이 데이터셋까지 버전관리를 하기 때문에 파이썬의 API 코드들만 github 을 이용하여 공유하고, 데이터 자체는 외부 서버를 이용하면 좋겠다는 생각을 했습니다. 그리고 이는 url 로 다운로드 받을 수 있는 형태로 만든다면 이후에 데이터를 보관하는 서버를 바꾸더라도 url 만 수정하면 되기 때문에 편리하겠다는 생각도 하였습니다. 저는 처음 드랍박스를 이용한 데이터 공유를 생각했습니다.

Binary 형식의 파일들도 있지만 텍스트로 이뤄진 테이블 데이터가 많기 때문에 통신 용량을 줄이기 위하여 zip 으로 압축하기로 결정했습니다.

그렇다면 이제 구현해야 하는 코드는 (1) zip 파일을 다운로드 받는 함수와 (2) 이를 적절한 폴더에 unzip 하는 함수 입니다.

download 함수는 requests 패키지를 이용하여 url 과의 통로를 열고 (stream=True), 이로부터 데이터를 다운로드 받아 local 파일로 저장하면 됩니다. 이 때 파일의 형태는 binary 이기 때문에 open(path, 'wb') 를 이용하여 기록합니다.

import requests

def download(url, fname):
    headers = {'user-agent': 'Wget/1.16 (linux-gnu)'}
    try:
        r = requests.get(url, stream=True, headers=headers)
        with open(fname, 'wb') as f:
            for chunk in r.iter_content(chunk_size=1024):
                if chunk:
                    f.write(chunk)
        return True
    except Exception as e:
        print(e)
        return False

드랍박스의 링크는 wget 을 이용하여 다운로드가 가능합니다. 터미널에서 이를 확인한 뒤, 처음에는 user-agent 를 설정하지 않고서 다운로드를 하였습니다. 그런데, user-agent 설정이 없으면 다운로드 도중 연결이 끊어집니다. 이유는 확인하지 않았습니다만, 이를 해결하기 위해서 Wget 으로 user-agent 를 설정하여 headers 에 추가하였습니다. 다운로드가 완료되면 True 를 return 합니다.

두번째로 다운로드 된 zip 파일의 압축을 푸는 함수를 만들었습니다. 이 역시 파일의 압축 해제가 성공하면 True 를 return 합니다.

import zipfile

def unzip(source, destination):
    abspath = os.path.abspath(destination)
    dirname = os.path.dirname(abspath)
    if not os.path.exists(dirname):
        os.makedirs(dirname)

    try:
        downloaded = zipfile.ZipFile(source)
        downloaded.extractall(destination)
        return True
    except Exception as e:
        print(e)
        return False

downloadunzip 을 이용하여 링크로부터 데이터를 다운로드 받아 적절한 폴더에 압축을 푸는 _fetch 함수를 만듭니다. os 패키지를 이용하여 해당 패키지가 설치되어있는 절대 주소를 찾아 installpath 에 저장합니다. 링크를 installpath/movie_comments 폴더 내에 다운로드 합니다. 압축을 풀 위치 역시 installpath/movie_comments/data 와 같이 지정할 수 있기 때문에 패키지를 설치하는 위치에 파일을 다운 받을 수 있습니다. 다운로드를 받고 압축을 푸는 과정에서 예상하지 못한 문제가 발생한다면 raise 를 이용하여 Exception 을 일으킵니다. 다운로드 한 파일은 압축을 푼 뒤 더 이상 필요하지 않기 때문에 os.remove 를 이용하여 다운로드한 파일을 제거합니다.

import os

installpath = os.path.sep.join(
    os.path.dirname(os.path.realpath(__file__)).split(os.path.sep)[:-1])

def _fetch(url, download_fname, directory):
    download_path = os.path.abspath('{}/movie_comments/{}'.format(installpath, download_fname))
    if download(url, download_path):
        print('Success to download {} {}'.format(dataset, download_fname))
    else:
        raise IOError('Failed to download {} '.format(dataset, download_fname))

    unzip_path = os.path.abspath('{}/movie_comments/{}'.format(installpath, directory))
    if unzip(download_path, unzip_path):
        print('Success to unzip {} {}'.format(dataset, download_fname))
    else:
        raise IOError('Failed to unzip {} {}'.format(dataset, download_fname))

    os.remove(download_path)

저는 datamodels 폴더를 각각 업데이트 하기를 원합니다. _fetch 함수를 이용하여 각각을 업데이트 하는 함수를 따로 만듭니다.

def fetch_data(data_url):
    _fetch(data_url, 'data.zip', 'data/')

def fetch_model(model_url):
    _fetch(model_url, 'models.zip', 'models/')

사용법

완성된 데이터셋은 https://github.com/lovit/textmining_dataset 에 올려뒀습니다. 코드 설치는 git clone 으로 가능합니다.

cd directory
git clone https://github.com/lovit/textmining_dataset.git

파이썬을 실행시켜 아래의 스크립트와 링크를 이용하여 데이터를 다운로드 받습니다.

import sys

data_url = 'https://www.dropbox.com/s/.../data.zip?dl=0'
model_url = 'https://www.dropbox.com/s/.../models.zip?dl=0'
directory_path = 'GIT_LOCAL_DIRECTORY'

sys.path.append(directory_path)

from movie_comments import fetch_data, fetch_model
fetch_data(data_url)
fetch_model(model_url)

수업 시 모두가 동일한 데이터와 pre-trained models 를 이용할 수 있도록 하기 위해서 데이터를 공유하고, 데이터나 학습된 모델들을 손쉽게 로딩하는 함수들을 만들어 반복적인 작업을 하지 않아도 되도록 하는 패키지를 만들었습니다.