파이썬 크롤링: Daum 사전에서 어원과 예제 추출하기

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 에 공부할 단어의 예제나 어원 전체를 등록하려고 했습니다만…….
귀찮아서 그만 뒀습니다.

궁금한 점이 있으면 언제든 댓글 달아 주세요~

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤