Python 으로 Daum 사전에서 어원과 예제를 가져오는 크롤링, 스크래핑 코드와 설명 입니다.
실행 동영상 입니다.
Python 을 사용한 크롤링 스크래핑 예제 입니다.
Anki 를 사용해 영어 단어를 공부하기 위해 만들었습니다.
260 줄밖에 안되는 짧은 코드로 쉽게 이해 하실 수 있을겁니다.
처음 실행할 때는 라이브러리가 없어서 오류가 날 겁니다.
pip 를 사용해 없다고 하는 라이브러리를 설치 하시면 됩니다.
모든 코드는 복사가 가능합니다.
글에 코드를 복사 붙여넣기 하여 정상 동작 함을 테스트 했습니다.
함수를 찾을 수 없다는 오류가 나올 수 도 있습니다.
그 경우 찾지 못하는 함수를 오류가 난 함수 위로 올려주세요.
1. 선언
# -*- coding: cp949 -*-
import traceback
import sys
import requests
from bs4 import BeautifulSoup
from selenium import webdriver
import pandas as pd
import xlrd
from xlutils.copy import copy
urlBase = """https://dic.daum.net/search.do?q={0}&dic=eng"""
urlDetail = """https://dic.daum.net/word/view.do?wordid={0}"""
workPath = "d:\\DaumDic\\"
sourceExcelFileName = "wordbook.xls"
targetExcelFileName = "workbook_result.xls"
options = webdriver.ChromeOptions()
driver = webdriver.Chrome(options=options)
라이브러리를 import 해줍니다.
html 파싱을 위한 BeautifulSoup,
Chrome 사용을 위한 selenium
Excel 사용을 위한 pandas 와 xlrd, xlutils 를 가져옵니다.
단순 html 파싱을 한다면 BeautifulSoup 만으로 충분 하지만 Daum 사전은 javascript 로 동적으로 페이지를 읽어오는 부분이 있습니다.
동적 페이지는 페이지 로드 후 페이지를 동적으로 다시 가져오기 때문에 초기 html 에서는 읽을 수 없습니다.
때문에 실제 chrome 을 사용해 모든 페이지를 동적으로 가져온 후 파싱 하도록 합니다.
이렇게 해도 페이지 로딩이나 딜레이 등에 문제가 있을 수 있습니다. 이 경우 동적 페이지를 가져오는데 시간이 걸릴 수 있습니다.
이 경우 제대로 정보(여기서는 어원이나 예제)를 못 가져올 수가 있습니다.
때문에 여기서는 같은 동작을 세번 반복하여 오류가 있는지 확인하고 더 많은 정보를 가지고 있는 내용을 사용하도록 했습니다.
테스트를 해보니 3번으로는 100 번 중에 한두번은 실수 하는것 같습니다. 5 번 정도 반복 하시는게 안전할 듯 합니다.
선언부 코드는 소스에서 가장 위에 위치합니다.
2. 실행
if __name__ == "__main__":
main()
프로그램 실행부 입니다. 코드에 가장 아래에 위치해 줍니다.
3. main
프로그램 실행부 바로 위에 위치할 함수 입니다.
def main(args=None):
print("main: Start")
wordList = readXlsFile(workPath + sourceExcelFileName)
try:
# if simple list work
#wordList = [
# 'rupture',
# 'wander',
# 'terrain',
# 'debris',
# 'flaw',
# 'tip',
# 'hurl',
# 'refined',
# 'subtle',
# 'leverage',
# 'airborne',
# 'experiment',
# 'bunk',
# 'sublimate',
# ]
# or only single word
#wordList = ['inspire']
wordListLen = len(wordList)
I_IDX= wordListLen
J_IDX = 3
totalResult = [[0]*J_IDX for _ in range(I_IDX)]
finalResult = [[0]*J_IDX for _ in range(I_IDX)]
for j in range(0, J_IDX):
print("Work Index: ", j + 1)
result = doWork(wordList, j)
for i in range(0, wordListLen):
totalResult[i][j] = result[i]
for i in range(0, wordListLen):
IsSame = True
for j in range(0, J_IDX):
if totalResult[i][0][0] + totalResult[i][0][1] + totalResult[i][0][2] != totalResult[i][j][0] + totalResult[i][j][1] + totalResult[i][j][2]:
IsSame = False
break
if IsSame == True:
finalResult[i] = totalResult[i][0]
else:
print("Is Not Same: totalResult[i]: ", totalResult[i])
sortedResult = sorted(totalResult[i], reverse = True, key = lambda x:x[3])
finalResult[i] = sortedResult[0]
pass
driver.close()
saveWorkXls(finalResult)
except Exception as exptn:
print("main: Exception")
print(type(exptn))
print(exptn.args)
print(exptn)
print('exptn: ', traceback.format_exc())
else:
print("main: Ok")
print("main: End")
readXlsFile 함수를 사용해 xls 파일에서 단어 목록을 가져옵니다.
그 아래 #if simple list work 라고 주석이 되어 있는 부분으로 대체하면 xls 파일을 읽지 않습니다. 그냥 원하는 파일을 리스트로 찾을 때 사용하는 부분 입니다.
그 아래 # or only simgle word 부분은 목록이 아난 한 단어만 찾고 싶을때 사용 하시면 됩니다.
J_IDX 는 위에서 말씀드린 몇번을 검증할지 횟수 입니다.
3 번에서 5 번정도 사용 했는데, 3 번도 가끔 서로 안맞는 경우가 있으니 5 번 정도로 해주시는게 좋을 것 같습니다.
이후 for 문을 사용해 doWork 함수를 반복하는 코드가 있습니다.
실질적인 검색 및 크롤링, 스크래핑 작업은 doWork 에서 이루어 집니다.
그 다음 for 문에서는 검증 작업을 하게 됩니다.
J_IDX 회수만큼의 배열을 비교하여 완전히 동일하면 넘어가고 동일하지 않다면 가장 긴 결과값을 사용하게 됩니다.
마지막으로 그때까지 사용했던 chrome 를 driver.close() 해서 닫아주고,
saveWorkXls 함수를 호출하여 결과를 새로운 Excel 파일로 저장합니다.
4. 엑셀 파일 읽기
def readXlsFile(fileName):
df = pd.read_excel(fileName, usecols=['단어'])
result = df['단어'].tolist()
return result
fileName 에서 ‘단어’ 라는 컬럼의 값을 목록으로 가져와 반환합니다.
5. 엑셀 파일 저장
def saveWorkXls(list):
sourceFile = workPath + sourceExcelFileName
targetFile = workPath + targetExcelFileName
rb = xlrd.open_workbook(sourceFile, formatting_info=True)
sheet = rb.sheet_by_index(0)
wb = copy(rb)
ws = wb.get_sheet(0)
ws.write(0, 7, '어원')
ws.write(0, 8, '예제')
row = 1
for item in list:
ws.write(row, 7, item[1])
ws.write(row, 8, item[2])
row = row + 1
wb.save(targetFile)
원본 파일에서 첫번째 시트를 가져와 복사합니다.
그리고 0 번째 row, 7, 8 번째 컬럼에 ‘어원’ 과 ‘예제’ 를 입력합니다.
1 번 row 부터 넘겨받은 list 의 값을 입력하고 wb.save 명령으로 엑셀파일로 저장합니다.
6. doWork
def doWork(wordList, workIdx):
resultList = []
wordListLen = len(wordList)
for i in range(0, len(wordList)):
word = wordList[i]
print("workIdx: ", str(workIdx + 1), " | wordIdx: ", str(i + 1) + "/" + str(wordListLen), " | word: ", word)
urlBaseFormat = urlBase.format(word)
result = search_daum_dic_1(urlBaseFormat)
result[0] = word
resultList.append(result)
return resultList
main 에서 호출하는 실질적인 작업부 입니다만, 여기서도 단어 목록을 받아 search_daum_dic_1 로 주소를 조립해 넘겨주기만 합니다.
받아온 결과는 resultList 에 저장을 하고 반환합니다.
7. search_daum_dic_1, returnSoup
def returnSoup(getUrl):
driver.get(getUrl)
html = driver.page_source
soup = BeautifulSoup(html, "html.parser")
return soup
def search_daum_dic_1(getUrl):
soup = returnSoup(getUrl)
tit_cleansch = soup.find(attrs={'class':'tit_cleansch'})
if tit_cleansch != None:
data_tiara_id = tit_cleansch.attrs.get('data-tiara-id')
sendUrl = urlDetail.format(data_tiara_id)
soup = returnSoup(sendUrl)
return search_daum_dic_3(soup)
else:
return search_daum_dic_3(soup)
search_daum_dic 에서는 받은 주소를 returnSoup 함수로 넘깁니다.
returnSoup 에서는 chrome 로 주소를 불러오고, BeautifulSoup 로 html 을 파싱하여 반환합니다.
다시 search_daum_dic_1 에서는 받아온 파싱값을 가지고 html 코드를 분석합니다.
daum 사전에서 크롬 디버깅을 해보면 아시겠지만, tit_cleansch 값이 있다면 단어 설명이 바로 나온 경우니 해당 내용을 분석하면 됩니다.
만일 tit_cleansch 가 없다면 여러 단어가 나온 경우니 그중 data-tiara-id 주소로 한번 더 들어가서 분석 해야 합니다.
두 경우 모두 search_daum_dic_3 으로 해당 단어 설명이 나온 파싱 내용을 넘깁니다.
8. search_daum_dic_3
def search_daum_dic_3(soup):
arrResult = [''] * 4
try:
txt_refer = soup.find_all(attrs={'class':'ex_refer'})
if len(txt_refer) == 0:
arrResult[1] = ''
else:
for item in txt_refer:
parseText = item.get_text().strip()
if "어원" in parseText:
txt_refer = item.find_all(attrs={'class':'txt_refer on'})
if len(txt_refer) == 1:
parseText = txt_refer[0].get_text().strip()
parseText = parseText.replace('[어원] ', '')
else:
print('CHECK_THIS')
arrResult[1] = parseText
example = get_example(soup)
if len(example) != 0:
arrResult[2] = example
arrResult[3] = arrResult[1] + arrResult[2]
except Exception as exptn:
print("search_daum_dic_3 exception")
print(type(exptn))
print(exptn.args)
print(exptn)
print('exptn: ', traceback.format_exc())
else:
pass
return arrResult
단어 설명이 나온 내용에서 어원과 예제를 가져오는 부분 입니다.
예제는 get_example 함수에서 가져오는데 어원은 그냥 이 함수에서 파싱을 하는군요.
어원 부분도 따로 함수로 분리 하는게 나을 것 같습니다.
우선 ex_refer 부분이 없다면 어원이 없다는 이야기니 넘어갑니다.
있다면, ex_refer 목록에서 어원 이라는 단어가 있고, 그 밑에 txt_refer on 이 하나만 있는 경우 어원이라고 생각하고 저장합니다.
만일 어원이 더 있다면
을 삽입하고 덧붙입니다.
어원 검색이 끝나면 get_example 함수를 호출해 예제를 가져옵니다.
9. get_example
def get_example(soup):
try:
txt_example = soup.find_all(attrs={'class':'list_example'})
if len(txt_example) == 0:
return ''
examples = "";
for te in txt_example:
if len(te) == 0:
return ''
else:
box_example = te.find_all(attrs={'class':'box_example'})
box_exampleList = []
for be in box_example:
txt_ex = be.find_all(attrs={'class':'txt_ex'})
if len(txt_ex) == 2:
txt_example = txt_ex[0].get_text().strip()
mean_example = txt_ex[1].get_text().strip()
box_exampleList.append([txt_example, mean_example, len(txt_example) + len(mean_example)])
if len(box_exampleList) != 0:
sortedResult = sorted(box_exampleList, key = lambda x:x[2])
addItem = sortedResult[0][0] + '\n' + sortedResult[0][1]
examples = addItem
except Exception as exptn:
print("get_example exception")
print(type(exptn))
print(exptn.args)
print(exptn)
print('exptn: ', traceback.format_exc())
else:
pass
return examples
예제를 가져오는 부분이 복잡해서 뺄수밖에 없었군요.
list_example 목록을 가져와서 분석을 시작합니다.
만일 없다면 그냥 반환 합니다.
box_example 을 찾아서 box_exampleList 목록에 저장합니다.
그 후에 목록 길이가 0 이 아니라면 두번째 값(예제 내용) 으로 오름 차순으로 정렬 해줍니다.
가장 짧은 예제를 가져오기 위함 입니다.
여기까지 코드를 작성 하셨으면 정상적으로 동작함을 확인 할 수 있습니다.
원래 이후로 Web 을 구축해볼까 생각 했었습니다.
엑셀 파일을 올려서 DB 에 공부할 단어의 예제나 어원 전체를 등록하려고 했습니다만…….
귀찮아서 그만 뒀습니다.
궁금한 점이 있으면 언제든 댓글 달아 주세요~