[입 개발] 오픈 소스로 문서의 텍스트 추출 서비스 만들기 #1

해당 포스트는 KT Olleh Ucloud Biz의 지원을 받고 작성되었습니다.

갑자기 Apache Tika를 보다가 문서에서의 텍스트 추출 서비스를 간단히 만들 수 있겠다라는 생각이 들어서 시도를 해보게 되었습니다.  이에 대한 개발을 위해 사용된 기술들은 Python, Python Flask, Apache Tika(java), Celery(Python), Redis(c) 등입니다. 파일을 등록하면 여기서 텍스트만 추출해서 보여주는 것입니다.  간단한 UI는 다음과 같습니다.

text_extraction_1_1

예전 같았다면 문서에 대한 텍스트 변환 코드를 다 작성했어야 했지만(실제로 5~8년 전에는 문서에서 텍스트 추출을 직접 구현했던 경험이… 그때는 한글 포맷도 없던 시절에 T.T) 이제는 실제로 이런 것들을 해주는 오픈소스들이 대부분 존재합니다. 여기서 사용하는 Apache Tika의 경우도 그런 라이브러리 중에 하나이고, 꽤 많은 문서를 지원해주고 있습니다.

참고로 모든 소스는 https://github.com/charsyam/celery_with_tika/ 에서 받으실 수 있습니다.

 

Apache Tika는 http://tika.apache.org 에서 받을 수 있고, 라이브러리 형태로도 사용할 수 있고, tika-app 을 이용하면 그냥 커맨드 쉘 형태로도 사용이 가능합니다.
Python Flask 는 Python에서 사용할 수 있는 web framework 입니다.(django가 더 유명하지만, 전 웬지 반창고파라 ㅋㅋㅋ, 이유는 없습니다. 그냥 Flask를 먼저 배워서 ㅋㅋ)
Python Celery 는 Python 기반의 분산 작업 처리 솔루션입니다. 사실 여기서 핵심은 Tika(문서 추출), Celery(분산 작업) 입니다.

왜냐하면, 궁극적인 이 프로젝트의 목적은 text extraction as a service 형태인데, 최대한 간단하게 작업 자체를 확장하거나, 줄일 수 잇는게 목적입니다. 그런데 이런 작업을 잘 만들려면
많은 노력이 들지만, Celery를 통해서 누워서 떡먹기식으로 해결이 가능합니다. 텍스트 추출은 tika가 처리를 해주고요.

그래서 시스템의 간단한 아키텍처는 다음과 같습니다.
text_extraction_1_2

구조를 보면 매우 단순합니다. 하지만, 조금만 생각해보면, 아주 확장에 자유로운 구조가 될 수 있습니다.
text_extraction_1_3

여기서 다시 웹서버만 늘리면 다음 구조가 되게 됩니다.
text_extraction_1_4

굉장히 확장이 쉬워보입니다.(물론, 여기에는 함정이 있습니다. 마지막 아키텍처 처럼 될려면, 그 후방이 분산 파일 시스템처럼 구성이 되어야 합니다. 즉, 웹서버에서는 파일을 분산 파일 시스템에 저장하고, Celery에서 이를 읽어서 처리하는 방법이 되어야 합니다. 그러나 그림은 더 이상은 생략)

최초에 이를 만들때 생각한 것은 최대한 쉽게 만들 수 있도록 하자였습니다. 그래서 결론적으로 tika를 이용하는 방법은 os.system 이나 subprocess를 이용해서 그냥 커맨드를 실행시키고 이 결과를 이용해서 보여주는 방식입니다.(중간에 pyjnius 등의 이야기도 하겠지만…)

그리고 이를 구현하는데 든 코드는 몇줄 되지 않습니다. 일단 웹 페이지를 보여주는 python flask 관련 코드는 다음과 같습니다.

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        file = request.files['file']
        if file:
            workid = id_generator();
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], workid))
            result = textextractor.delay(workid)
            return render_template('result.html',task_id=result.task_id)
        else:
            return render_template('index.html')
    else:
        return render_template('index.html')

if __name__ == '__main__':
    app.run(host="0.0.0.0", debug=True)

Celery 를 사용하기 때문에(그것도 비동기로) celery 큐에 집어넣는 작업만 하면 끝납니다.
이 때, KT Ucloud를 사용하신다면 Flask가 기본적으로 5000번 포트를 이용하는데(지정가능)
Port Forwarding에서 다음과 같이 5000번 포트를 추가하셔야 정상적으로 서비스가 가능합니다.

text_extraction_1_5

그리고 실제 Celery 를 이용한 부분은 다음과 같습니다.

@celery.task
def textextractor(workid):
    filename = "%s/%s"%(Config.UPLOAD_FOLDER, workid)
    builds = "%s/%s"%(Config.BUILD_FOLDER, workid)
    status = 0

    command = "/usr/bin/java -jar %s -t %s"%(Config.TIKA_CLASSPATH,filename)

    args = shlex.split(command)
    try:
        contents = subprocess.Popen(args, stdout=subprocess.PIPE).communicate()[0]
    except:
        contents = "tika error"
        status = -1

    os.remove(filename)
    return (contents, status)

실제로 tika를 실행시켜주고 그 결과를 가져오는 것이기 때문에, 아주 간단합니다. 다만, 눈에 보이는 버그도 숨어있습니다.(귀찮아서 아직 처리를 못한…)

즉, tika만 익숙하면 구현하는데는 거의 1시간도 채 걸리지 않습니다. 그렇다면, 그렇게 쉽게 걸릴일을 왜 이렇게 오래걸렸냐라고 물어보면, 딴 삽질들에 대해서 얘기를 해야합니다. 비동기 작업(Celery가 해주지만, 결과를 polling 하는 방법), 좀 더 깔끔하게 하기 위한 방법, 뒤에 hwp를 추가하기 위한 간단한 삽질등은 다음번에 올리도록 하겠습니다. 다만, 위에 코드만으로도 아주 간단히 -_- 문서에서의 text 추출을 만들 수 있다는게, 오픈소스의 대단함이지 않을가 싶습니다.