KorpuSQL 다운로드는 아래 페이지에서 가능합니다.
2016/01/27 - [프로그래밍] - 코퍼스 분석용 SQL도구 KorpuSQL 개발!
KorpuSQL을 개발한게 간편한 코퍼스 구축을 위해서였죠. 구슬이 서 말이라도 꿰어야 보배인것처럼 코퍼스 처리 프로그램이 서 말이라도 직접 구축해보지 않으면 의미가 없어요. 그래서 한 번 코퍼스 구축을 해보기로 했습니다. 텍스트를 구하기가 가장 쉬운 방법이 뭐가 있을까 고민하다가, 한국어 위키백과의 문서들을 가져와서 코퍼스로 구축해보기로 했습니다. 일단 위키백과가 백과사전을 지향하는 위키 사이트이다보니, 다른 인터넷 페이지들과는 달리 어느 정도 검증된 표현을 사용하고, 구어나 인터넷에서 유행하는 이상한 말들보다는 표준어 문어체로 작성되어 있어서 이 친구를 고른 것이지요.
자, 일단 목표를 설정했으니 방법을 강구해봅시다.
일단 한국어 위키백과의 모든 문서를 긁어와야하는데, 모든 문서 목록은 여기에서 볼 수 있습니다. 일단 Python을 이용해서 이 문서 목록을 가져오는일을 해보죠. 해당 페이지의 HTML소스코드를 살펴봅시다.
<span class="mw-htmlform-submit-buttons"> <input class="mw-htmlform-submit" type="submit" value="보기" /> </span> </fieldset> </form><div class="mw-allpages-nav"><a href="/w/index.php?title=%ED%8A%B9%EC%88%98:%EB%AA%A8%EB%93%A0%EB%AC%B8%EC%84%9C&from=.jar" title="특수:모든문서">다음 문서 (.jar)</a></div><div class="mw-allpages-body"><ul class="mw-allpages-chunk"><li class="allpagesredirect"><a href="/wiki/!" class="mw-redirect" title="!">!</a></li><li><a href="/wiki/!!" title="!!">!!</a></li> <li><a href="/wiki/!!!" title="!!!">!!!</a></li> <li class="allpagesredirect"><a href="/wiki/!%3F" class="mw-redirect" title="!?">!?</a></li> <li class="allpagesredirect"><a href="/wiki/!Democracia_Real_YA!" class="mw-redirect" title="!Democracia Real YA!">!Democracia Real YA!</a></li> <li><a href="/wiki/!_(%EB%8F%99%EC%9D%8C%EC%9D%B4%EC%9D%98)" title="! (동음이의)">! (동음이의)</a></li> <li><a href="/wiki/!_-attention-" title="! -attention-">! -attention-</a></li> <li class="allpagesredirect"><a href="/wiki/!%EC%BF%B5%EC%96%B4" class="mw-redirect" title="!쿵어">!쿵어</a></li>
앞뒤 자르고 중요한 부분만 가져왔습니다. 일단 문서 목록은 <ul class="mw-allpage-chunk"> 에서 시작됩니다. 끝나는 부분은 </ul>이겠죠. 정규식을 이용해서 이 사이에 있는 녀석들을 가져오면 목록은 구할 수 있겠죠. 그리고 보시면 class="mw-redirect" 로 리다이렉트 문서는 별도로 표시가 되어있습니다. 리다이렉트 문서 목록까지 다 가져오면 겹치는 문서가 생길테므로 이 녀석들은 제외하고 가져옵시다.
그리고 다음 페이지로 넘어가는 링크는 녹색으로 칠해진 부분에 있습니다. 친절하게도 다음 문서라고 써져있기까지 하네요. HTML소스에서 이 부분이 있으면 다음 페이지로 넘어가서 계속 파싱하고, 없으면 끝내는 것으로 코드를 작성하면 되겠습니다.
자 그럼 이제 코딩 시작해볼까요. 시간이 오래걸릴거 같아서 아저씨는 미리 만들어왔어요. 파이썬3용 코드입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# -*- coding: utf-8 -*- import urllib.request import urllib.parse import html import re def getPageList(url, output): # getPageList 함수 # url주소에 있는 위키백과 문서 목록 페이지를 파싱해 목록을 추려냅니다. # 그 결과를 output 파일에 씁니다. while True : print ( "Processing: %s" % url) try : f = urllib.request.urlopen(url) except : # 웹페이지를 가져올 수 없을땐 에러를 띄웁니다 # 인터넷 연결을 확인해보거나, URL 주소를 확인해보세요. raise ( "% error" % url) return None data = f.read().decode( 'utf-8' ) # 읽은 HTML소스 코드를 utf-8로 해석합니다. # 위키백과 사이트는 utf-8을 쓰기때문에! match = re.search( """<ul class="mw-allpages-chunk">(.+?)</ul>""" , data, re.DOTALL | re. UNICODE ) ws = re.findall( """<a\s+href="([^"]+)"([^>]*)>([^<]*)</a>""" , match.group( 1 )) for w in ws: if re.search( """class="mw-redirect""" , w[ 1 ]): continue # class="mw-redirect"가 있다면 리다이렉트 문서이므로 패스. output.write( "%s\t%s\n" % (w[ 0 ], html.unescape(w[ 2 ]))) # 아닐 경우 해당 페이지의 URL과 페이지 제목을 output에 씁니다. # 이때 페이지 제목의 HTML Entity가 이스케이프되어있을 수으므로 # html.unescape로 언이스케이프해줍니다. nextPage = re.search( """<a\s+href="([^"]+)"[^>]*>다음 문서""" , data, re.DOTALL | re. UNICODE ) # 다음 페이지가 있는지 추출합니다. if nextPage: url = urllib.parse.urljoin(url, html.unescape(nextPage.group( 1 ))) # 있다면 url을 업데이트하고 반복. else : return # 없다면 끝. file = open ( 'list.txt' , 'wt' , encoding = 'utf-8' ) getPageList( "https://ko.wikipedia.org/wiki/%ED%8A%B9%EC%88%98:%EB%AA%A8%EB%93%A0%EB%AC%B8%EC%84%9C" , file ) file .close() |
문서가 30만개가 넘다보니 목록만 가져오는데도 좀 시간이 걸립니다. 게임이나 하면서 잠시 기다려주세요.
목록을 모두 가져왔다면 본격적으로 페이지를 가져와봅시다. 한국어 위키백과 문서들의 HTML소스코드를 잘 들여다보면 일반적인 위키백과 페이지 문서의 본문은 <div id="mw-content-text" lang="ko" dir="ltr" class="mw-content-ltr"> 태그에서 시작해서 <div class="printfooter"> 태그 앞에서 끝난다는 것을 알수있습니다. 여기를 끄집어내온다음 각종 태그들은 지워버리면 문서 내 텍스트만 뽑아올 수 있겠죠! HTML 문서에서는 공백문자가 두 개 이상 등장해도 공백 한 칸으로만 해석합니다. (원래 약속이 그래요.) 그리고 몇몇 태그는 block속성을 지니고 있어서 그 태그가 끝나면 자동으로 줄(문단)이 바뀌죠. 그리고 위키백과의 특성상 문단마다 [편집]이라는 링크가 있는데 이 녀석들도 모두 지워버리지요. (그렇지 않으면 [편집]이 코퍼스내 빈도순위 1위 단어가 될겁니다.) 이 점을 고려해서 적절하게 텍스트를 뽑아봅시다. 역시나 아저씨는 미리 만들어왔어요.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# -*- coding: utf-8 -*- import urllib.request import urllib.parse import html import re def deleteTag(x): if x = = None : return "" else : return re.sub( "<[^>]*>" , "", x) def getPage(url): try : f = urllib.request.urlopen(url) except : print ( "% error" % url) return None data = f.read().decode( 'utf-8' ) match = re.search( """<div id="mw-content-text" lang="ko" dir="ltr" class="mw-content-ltr">(.*?)<div class="printfooter">""" , data, re.DOTALL | re. UNICODE ) # 아까 얘기했던 방법으로 본문을 뽑아냅니다. if not match: return None # 일치하는 결과가 없으면 뭔가 잘못된 문서이므로 여기서 끝 s = re.sub( """(\s| )+""" , " ", match.group( 1 )) # 공백문자가 1개 이상 등장하는 경우 모두 스페이스 하나로 바꿔치우겠습니다. s = re.sub( """<span class="mw-editsection-bracket">(.+?)</span>""" , "", s) s = re.sub( """<a href="[^"]*edit[^"]*"[^>]*>편집</a>""" , "", s) # [편집] 을 모두 날려버립니다. s = re.sub( """<(h[1-6]|div|li|p|td|th)(\s[^>]*)?>""" , "\n", s) # block 태그들은 줄바꿈문자로 대체합니다. s = html.unescape(deleteTag(s)) # 나머지 태그들은 그냥 다 삭제하고, HTML Entity 해석. s = re.sub( """(\n[\r\t ]*)+""" , " .\n", s) # 마지막으로 줄이 바뀔때마다 마침표를 찍어줍니다. 이는 KorpuSQL에 문장이 끝났음을 알려주기 위한 신호입니다. # 그렇지 않으면 문장이 바뀌었는데도 마침표가 없다며 다 한 문장으로 이어버리게됩니다. return s |
이 함수를 실행하면 다음과 같은 결과를 얻을 수 있습니다. 예제로 사용한 페이지는 고대 한국어 페이지입니다.
음 잘 작동합니다. 이제 이 함수를 아까 만들어 저장했던 list.txt 목록 파일에 들어있는 모든 페이지에 대해서 수행해야 합니다. 이렇게 스크립트를 짜봅시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# -*- coding: utf-8 -*- import urllib.request import urllib.parse import html import re import os.path def do_work(item): fpath = 'doc\\%s.txt' % item[ 1 ] if os.path.isfile(fpath): return # 만일 해당 폴더에 이미 문서가 저장되어있다면 패스합니다. try : print (item[ 0 ], item[ 1 ]) except : None try : file = open (fpath, 'wt' , encoding = 'euc-kr' , errors = 'replace' ) # EUC-KR밖에 모르는 불쌍한 UTagger를 위해 euc-kr 인코딩으로 저장합니다. # 인코딩 변화에 실패한 경우(euc-kr로 표현 불가능한 문자) ?로 대체됩니다. file .write(page) file .close() except : print ( "Error" , item[ 0 ], item[ 1 ]) doclist = open ( 'list.txt' , 'rt' , encoding = 'utf-8' ) for l in doclist: t = l.replace( '\n' , ' ').split(' \t') t[ 1 ] = re.sub( """[<>:"/\\|?*]""" , '', t[ 1 ]) do_work(t) doclist.close() |
짠! 장담컨대 이 코드는 매우 잘 돌아가지만 하루종일 돌려도 프로그램이 안끝날 수 있습니다. 컴퓨터 속도가 느려서 그런게 아니라 웹페이지를 가져오는데 시간이 걸리기 때문입니다. 웹페이지를 가져올때까지 기다린다음 처리해서 저장하고, 또 다음 웹페이지를 가져올때까지 기다리고... 비효율적이죠. 쓰레드와 큐를 이용해서 여러 웹페이지를 가져오게 한다음 받은 웹페이지부터 처리하도록 수정해봅시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# -*- coding: utf-8 -*- import urllib.request import urllib.parse import html import re import os.path import threading import queue def deleteTag(x): if x = = None : return "" else : return re.sub( "<[^>]*>" , "", x) def getPage(url): try : f = urllib.request.urlopen(url) except : print ( "% error" % url) return None data = f.read().decode( 'utf-8' ) match = re.search( """<div id="mw-content-text" lang="ko" dir="ltr" class="mw-content-ltr">(.*?)<div class="printfooter">""" , data, re.DOTALL | re. UNICODE ) if not match: return None s = re.sub( """(\s| )+""" , " ", match.group( 1 )) s = re.sub( """<span class="mw-editsection-bracket">(.+?)</span>""" , "", s) s = re.sub( """<a href="[^"]*edit[^"]*"[^>]*>편집</a>""" , "", s) s = re.sub( """<(h[1-6]|div|li|p|td|th)(\s[^>]*)?>""" , "\n", s) s = html.unescape(deleteTag(s)) s = re.sub( """(\n[\r\t ]*)+""" , " .\n", s) return s lock = threading.Lock() # 출력을 위해 lock을 만듭니다. def do_work(item): fpath = 'doc\\%s.txt' % item[ 1 ] if os.path.isfile(fpath): return try : with lock: print (item[ 0 ], item[ 1 ]) # 출력시는 항상 락을 걸고 출력 except : None try : file = open (fpath, 'wt' , encoding = 'euc-kr' , errors = 'replace' ) file .write(page) file .close() except : with lock: print ( "Error" , item[ 0 ], item[ 1 ]) def worker(): while True : item = q.get() # 큐에 작업할 녀석이 있다면 가져옵니다. do_work(item) # 가져온 작업목록을 수행 q.task_done() # 작업이 끝났다고 큐에다가 알려줌 q = queue.Queue() for i in range ( 16 ): t = threading.Thread(target = worker) t.daemon = True t.start() # 스레드 16개를 만들어서 시작시킵니다. doclist = open ( 'list.txt' , 'rt' , encoding = 'utf-8' ) for l in doclist: t = l.replace( '\n' , ' ').split(' \t') t[ 1 ] = re.sub( """[<>:"/\\|?*]""" , '', t[ 1 ]) q.put(t) # 가져올 사이트 목록을 차례대로 큐에 넣어줍니다. q.join() # 큐의 모든 작업이 끝날때까지 대기 doclist.close() |
스레드 16개를 돌리며 웹페이지를 가져오게 수정했습니다. 이정도면 컴퓨터 놀리지 않고 웹 페이지를 가져올 수 있을겁니다. (스레드 숫자를 더 올려도 괜찮을거같은데, 위키백과 쪽에서 DDoS공격 같은걸로 오인해서 차단시켜버릴까봐 이 정도에서 만족하기로 했습니다.)
이렇게 밤새 돌리면 30만 페이지를 모두 가져올 수 있습니다. 다 가져왔다면? 바로 UTagger를 실행합시다.
UTagger 설정은 위와 같이 해줍니다. 파일 형태: [원시말뭉치], 출력 형식: [한줄에 한어절]
그리고 폴더 분석을 눌러서 우리가 긁어온 위키백과 문서들이 있는 doc 폴더를 골라줍니다.
문서가 많죠? 시간이 꽤 걸립니다. 이번엔 컴퓨터 켜놓고 식사하고 오시면 될듯합니다.
다 됐나요? 그러면 이제 KorpuSQL을 실행합시다. 이 프로그램은 태깅 코퍼스를 DB화하여 통계를 비롯한 각종 처리를 용이하게 도와줍니다.
[파일] - [데이터베이스 연결]
내장 DB로 [새로 만들기/열기...] 를 누릅니다.
외부 데이터베이스를 사용할 수도 있지만, 그럴려면 별도로 MySQL이나 MariaDB 등을 설치해야합니다. 컴퓨터 메모리만 풍족하다면 내장 DB를 메모리에 적재하고 사용하는게 가장 빠릅니다. (물론 외부 DB를 메모리 테이블로 만들어서 쓰는게 그것보다 빠르긴하지만 번거로우므로)
디비 생성이 완료되었다면 이제 태깅 코퍼스를 입력할 차례입니다.
[데이터베이스]-[태그 코퍼스 일괄 입력]을 누르세요
[패턴 추가]로 가져올 파일 목록을 추가할 수 있습니다. [파일 추가]로 여러 파일을 동시에 추가할 수도 있지만, 아시다시피 파일갯수가 30만개가 넘기 때문에 번거롭습니다. 인코딩은 EUC-KR로 선택해주세요.
태그 파일이 생성된 폴더의 경로를 적어주면 됩니다. [확인]을 누르면 디비 입력이 시작됩니다. 컴퓨터 사양에 따라 소요 시간이 달라지겠지만, 이번에는 차 한잔 하고 오시면 될거같습니다.
디비 입력이 끝나면, 이제 코퍼스 분석을 즐기면 됩니다. 코퍼스 전체 용량이 크다보니 쿼리 수행시간이 좀 걸리네요 더 단축시킬 방법 없는지 고민해봐야겠습니다.
코넥티아 M Stylus SSD 교체기 (6) | 2016.06.05 |
---|---|
태블릿 PC 구매 후기 - 성우모바일 코넥티아 M Stylus (5) | 2016.03.12 |
전역 보고! + 군생활에서 얻을 것들! (2) | 2016.02.11 |
라틴어의 L자도 모르던 사람이 라틴어 덕후가 되기까지. (19) | 2015.12.17 |
[적분스페셜] 교과서 논쟁을 통해 본 역사학 경시 풍조 (3) | 2015.10.29 |
수수사 과제에 도움이 될만한 (0) | 2015.09.10 |
댓글 영역