
웹에서 정보를 긁어와 엑셀로 정리할 수 있다면 얼마나 많은 업무를 자동화하여 시간을 절약할 수 있을까?
오늘은 파이썬 selenium을 활용한 웹 크롤링을 적용하여 이메일로 받는 데이터를 엑셀에 저장하는 작업을 기록해 보겠다.
가상환경이 있고, 가상환경 하에서 진행한다고 가정해보겠다.
파이썬 설치 및 가상환경 설정, 모듈 설치하는 방법에 대한 글은 나중에 쓰겠다.
먼저 사용되는 모듈들을 설치해 보겠다.
- 크롤링에 활용되는 대표적인 모듈 selenium
- 복사 붙여넣기를 위한 모듈 pyperclip
- html 파일 파싱을 위한 모듈 bs4
- 엑셀 파일로 저장하는 기능을 위한 모듈 openpyxl
pip install selenium
pip install pyperclip
pip install bs4
pip install openpyxl
이 모듈들을 import해서 코드를 짜 보자.
우선 메일을 확인하기 위해서는 로그인이 필요하다. 로그인 기능부터 구현해야 하는데, 구글은 파이썬에서 웹을 켜고 자동 로그인을 하려 하니 불가능하다고 한다.

그래서 네이버 메일을 시도해 보려 한다. (만약 구글 메일을 긁어와야 하는 상황이라면 지메일의 자동 전달 기능을 통해 포워딩하여 긁어오는 것을 추천한다.)
우선 네이버 아이디와 비밀번호를 직접 입력하게 할 수도 있지만, 귀찮으므로 코드에 id와 pw를 저장한다.
id = 'your_id'
pw = 'your_password'
selenium으로 창을 열면 코드가 끝난 순간 창이 꺼진다. 좋은 기능이지만, 개발 중에는 진행도를 파악하기 위해 계속 켜져 있는 게 좋으므로 옵션을 설정해 준다.
chrome_options = Options()
chrome_options.add_experimental_option('detach', True)
참고로 크롬 드라이버의 경우 selenium이 업데이트되면서 굳이 링크 엔드포인트 찾아가며 다운받을 필요가 없게 되었다.
이 옵션을 이용해서 크롬 창을 열고, 로딩까지 암묵적으로 3초를 기다려준 후 네이버 로그인 주소를 입력한다.
driver = webdriver.Chrome(options=chrome_options)
driver.implicitly_wait(3)
driver.get('https://nid.naver.com/nidlogin.login?svctype=262144')
true가 대문자로 시작하거나, 시간 단위가 ms이 아닌 s인 파이썬을 주의한다.
이제 네이버 로그인 창을 실제로 열어, 아이디와 비밀번호를 입력하기 위해 구조를 파악해 보자.
네이버 로그인 창에서 개발자 도구 (윈도우는 f12, 맥은 options+command+I)를 열고 해당 버튼을 클릭한다.

이 버튼은 마우스로 클릭한 부분을 html 상에서 포커스해주는 툴로, 앞으로도 id나 name, xpath 등을 얻을 때 아주 유용하게 활용할 것이다.
저 버튼을 누른 상태로 우리가 가장 먼저 눌러야 하는, 아이디를 입력하는 input을 클릭한다.

그러면 오른쪽 elements 탭에 해당 input의 html 부분이 포커싱 될 것이다. 좀 더 크게 확대해 보겠다.

코드 상에서 이 input을 찾아 누르려면 id, name, xpath등을 활용할 수 있는데 여기서는 id나 name으로도 충분히 찾을 수 있을 것 같다. 다시 코드로 돌아와서 id가 'id'인 element를 찾고 클릭한다.
id_input = driver.find_element('id', 'id')
id_input.click()
id_input.send_keys()를 활용해도 되지만 키를 너무 빨리 입력하면 네이버에서 자동 로그인인 것을 감지해 captcha를 걸기 때문에 아까 저장해놓은 id를 복사 후 붙여넣기 하도록 하겠다.
pyperclip.copy(id)
actions = webdriver.ActionChains(driver)
actions.key_down(Keys.COMMAND).send_keys('v').key_up(Keys.COMMAND).perform()
time.sleep(1)
command (윈도우면 control) 키를 누르고 - v를 누르고 - command 키를 떼는 action을 한다. 혹시 모르니 1초 기다려주자.
pw도 같은 방식으로 복사 후 붙여넣기하면 된다.
pw_input = driver.find_element('id', 'pw')
pw_input.click()
pyperclip.copy(pw)
actions = webdriver.ActionChains(driver)
actions.key_down(Keys.COMMAND).send_keys('v').key_up(Keys.COMMAND).perform()
time.sleep(1)
마지막으로 로그인 버튼을 클릭한다.

driver.find_element('id', 'submit_btn').click()
여기까지 한 후 한번 실행해 보자.
python3 mailCrawling.py
네이버 로그인 창에 진입해 아이디와 비밀번호를 붙여넣고 로그인하는 과정을 성공했다면, 드디어 메일함으로 진입한다.
driver.get('https://mail.naver.com/v2/folders/0')
메일함에서 모든 메일을 볼 것이 아니라, 검색을 통해 필터링을 한 후 목록을 추출할 것이다.
메일 검색창 클릭 - 검색어 입력 - 검색 버튼 클릭 3가지 행동을 이어서 실행시키면 된다.
검색창을 찾아 보니 id가 search인 input이다.

driver.find_element('id', 'search').send_keys('검색어')
어라? 그런데 검색 버튼에 써먹을 만한 특성이 보이지 않는다.

2가지 방법이 있는데, command + c, command + v를 눌렀던 것처럼 enter키를 누르는 방법이 있고, xpath를 이용하는 방법이 있다.
html의 해당 button element에서 오른쪽 마우스 클릭, Copy > Copy XPath를 누르면 해당 검색버튼의 xpath가 복사된다.

다음은 똑같이 xpath로 해당 element를 찾아서 누르면 된다.
driver.find_element('xpath', '//*[@id="search-bar"]/div/button').click()
time.sleep(1)
이제 검색까지 했으니 원하는 메일 목록이 쭉 떠 있을 것이다.
이제는 해당 페이지 전체의 html을 긁어와 beautifulsoup를 통해 파싱하는 것이 더 편할 것이다.
네이버 메일에서 각 메일에 부여한 숫자만 알아내면 각 메일의 상세 화면을 띄울 수 있기 때문이다.
soupForMailList = BeautifulSoup(driver.page_source, 'html.parser')
class가 search-result로 되어 있는 ul 아래에 각 메일이 있으므로 이 ul element를 선택하고, 읽지 않은 메일만 파싱하기 위해 반복문을 돌릴 것이다. 아래 사진을 확인해보면, 읽은 메일의 class에는 read가 들어있는 것을 볼 수 있다.

우리가 원하는 것은 mail-뒤에 있는 숫자이므로 구조를 파악한 후 파싱한다.
mailIdList = []
# 검색 결과인 ul로 반복문
mails = soupForMailList.find('ul', {'class': 'search_result'})
for mail in mails:
# id 파싱, 잘 안되면 이 부분 형식 확인
className = str(mail).split('<div')[0].split('=')[1].replace('"', '').replace('draggable', '').replace('id', '')
# 읽은 메일 제외
if ('read' in className):
continue
mailIdList.append(className.split('-')[1].replace(' ', ''))
엑셀 파일에 결과를 저장할 것이다. 엑셀 파일을 생성하고 연다.
# 새 엑셀 파일 생성 혹은 덮어쓰기
file_name = './메일 크롤링 결과 데이터.xlsx'
wb = openpyxl.Workbook()
sheet = wb.active
이제 아까 반복문으로 얻은 메일 아이디 리스트에서 다시 반복문을 돌리며 세부 메일 화면으로 들어가, 원하는 데이터를 찾을 것이다.
for i in range(len(mailIdList)):
# 각 메일의 세부 페이지
driver.get('https://mail.naver.com/v2/read/-1/' + mailIdList[i])
time.sleep(2)
# 각 메일의 세부 페이지 파싱
html = driver.page_source
soupForEachMail = BeautifulSoup(html, 'html.parser')
time.sleep(1)
필자가 원하는 데이터는 html을 확인해보니 table 안에 있으므로 모든 테이블을 찾아 보았다.
table = soupForEachMail.find_all('table')
마찬가지로 print를 통해 원하는 테이블이 2번째에 있음으로 확인했기 때문에 2번째 테이블을 저장하고, 그 안에서 td를 찾는다.
사진이 없는 이유는 전부 가려야 하는 개인정보라서...데이터 구조를 확인하고 싶다면 그냥 print(soupForEachMail)을 해보자.
td의 순서에 따라 엑셀 파일에 헤더를 작성해준다.
table = soupForEachMail.find_all('table')[1]
td = table.find_all('td')
'''
table에서 원하는 데이터 파싱 => 고객명과 비고 추출
0: 일시
1: data a
2: data b
3: data c
4: data d
5: data e
6: data f
7: 비고
'''
# 헤더
sheet.cell(row=1, column=1).value = '메일번호'
sheet.cell(row=1, column=2).value = '일시'
sheet.cell(row=1, column=3).value = 'data a'
sheet.cell(row=1, column=4).value = 'data b'
sheet.cell(row=1, column=5).value = 'data c'
sheet.cell(row=1, column=6).value = 'data d'
sheet.cell(row=1, column=7).value = 'data e'
sheet.cell(row=1, column=8).value = 'data f'
sheet.cell(row=1, column=9).value = '비고'
업무 메일에 형식이 지정된 테이블이 온다면 보통 순서나 내용이 잘 바뀌지 않으므로 이렇게 주석 처리해놓는 것도 좋다.
엑셀 시트에 각 테이블을 저장하고, 각 메일을 구분하기 위해 네이버 메일 아이디를 쓴 다음 파일을 저장한다.
# 메일 번호
sheet.cell(row=i+2, column=1).value = mailIdList[i]
# 저장하기
wb.save(file_name)
실행하면 긁어온 메일들이 읽음처리 되면서 엑셀파일에 저장된 것을 확인할 수 있다.
개발자만 사용하는 게 아니므로 운영자에게 실행 파일로 넘기려면 .exe 파일이나 .app 파일로 만들어주는 게 좋다.
언젠가의 다음 글에서 다루겠다.
코드 전문
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
import time
import pyperclip
from bs4 import BeautifulSoup
import openpyxl
# 네이버 계정 아이디, 비밀번호
id = 'your_id'
pw = 'your_pw'
# # 완료해도 창 꺼지지 않게
# chrome_options = Options()
# chrome_options.add_experimental_option("detach", True)
# driver = webdriver.Chrome(options=chrome_options)
driver = webdriver.Chrome()
driver.implicitly_wait(3)
# 네이버 로그인 화면
driver.get('https://nid.naver.com/nidlogin.login?svctype=262144')
# id 선택 후 붙여넣기 (mac이면 command, windows면 control)
id_input = driver.find_element('id', 'id')
id_input.click()
pyperclip.copy(id)
actions = webdriver.ActionChains(driver)
actions.key_down(Keys.COMMAND).send_keys('v').key_up(Keys.COMMAND).perform()
time.sleep(1)
# pw 선택 후 붙여넣기
pw_input = driver.find_element('id', 'pw')
pw_input.click()
pyperclip.copy(pw)
actions = webdriver.ActionChains(driver)
actions.key_down(Keys.COMMAND).send_keys('v').key_up(Keys.COMMAND).perform()
time.sleep(1)
# 로그인 버튼 클릭
driver.find_element('id', 'submit_btn').click()
# 메일함으로 이동
driver.get('https://mail.naver.com/v2/folders/0')
# 검색어 입력 후 검색
driver.find_element('id', 'search').send_keys('검색어')
driver.find_element('xpath', '//*[@id="search-bar"]/div/button').click()
time.sleep(1)
# 메일 목록 파싱 (id)
soupForMailList = BeautifulSoup(driver.page_source, 'html.parser')
mailIdList = []
# 검색 결과인 ul로 반복문
mails = soupForMailList.find('ul', {'class': 'search_result'})
for mail in mails:
# id 파싱, 잘 안되면 이 부분 형식 확인
className = str(mail).split('<div')[0].split('=')[1].replace('"', '').replace('draggable', '').replace('id', '')
# 읽은 메일 제외
if ('read' in className):
continue
mailIdList.append(className.split('-')[1].replace(' ', ''))
# 새 엑셀 파일 생성 혹은 덮어쓰기
file_name = './메일 크롤링 결과 데이터.xlsx'
wb = openpyxl.Workbook()
sheet = wb.active
for i in range(len(mailIdList)):
# 각 메일의 세부 페이지
driver.get('https://mail.naver.com/v2/read/-1/' + mailIdList[i])
time.sleep(2)
# 각 메일의 세부 페이지 파싱
html = driver.page_source
soupForEachMail = BeautifulSoup(html, 'html.parser')
time.sleep(1)
table = soupForEachMail.find_all('table')[1]
td = table.find_all('td')
'''
table에서 원하는 데이터 파싱 => 고객명과 비고 추출
0: 일시
1: data a
2: data b
3: data c
4: data d
5: data e
6: data f
7: 비고
'''
# 헤더
sheet.cell(row=1, column=1).value = '메일번호'
sheet.cell(row=1, column=2).value = '일시'
sheet.cell(row=1, column=3).value = 'data a'
sheet.cell(row=1, column=4).value = 'data b'
sheet.cell(row=1, column=5).value = 'data c'
sheet.cell(row=1, column=6).value = 'data d'
sheet.cell(row=1, column=7).value = 'data e'
sheet.cell(row=1, column=8).value = 'data f'
sheet.cell(row=1, column=9).value = '비고'
# 메일 번호
sheet.cell(row=i+2, column=1).value = mailIdList[i]
# 데이터
for col in range(8):
sheet.cell(row=i+2, column=col+2).value = str(td[col]).split('>')[1].split('<')[0].strip()
# 저장하기
wb.save(file_name)
글에서 느꼈을지 모르겠지만, 이제껏 쓴 게시글 중 가장 일반적이지 못한 글 같다. (사진도 많이 없고...)
하지만 웹 크롤링이 그렇다. 지금 만들어낸 프로그램마저도 네이버 메일에서 html 구성을 바꿔버리면 아마 무용지물이 될 것이다.
웹 크롤링은 html을 보고 직접 어떻게 긁어올지 판단해야 하는 만큼 형식이 예측 가능하고 변동이 많지 않은 곳에서 사용하는 게 좋다.
한번 만들어서 오래 쓰는 기술은 못 되지만, 그만큼 특정 분야에서의 공수를 많이 줄여줄 것이다. 사용법만 알면 여기저기써 쉽게 만들어 쓸 수 있다는 장점도 있다.
한번 따라해보면서 지금 시도하는 작업을 성공하는 것을 목적으로 하지 말고, 어떻게 하는지를 얻어가길 바란다.
'개발 및 알고리즘 > 알고리즘' 카테고리의 다른 글
| [React Native] 리액트 네이티브에서 keychain store 구현하기 (react-native-keychain) (0) | 2024.08.16 |
|---|---|
| [React Native] 리액트 네이티브에서 지문 인식 구현하기 (react-native-biometrics) (0) | 2024.08.14 |
| [AWS] CloudWatch Alarm 설정해서 Slack에 알림 보내기 (1) | 2024.08.07 |
| [React Native] 리액트 네이티브에서 커스텀 폰트 일괄 적용하기 (0) | 2024.07.30 |
| [React Native] 리액트 네이티브에서 구글로그인 구현하기 (@react-native-google-signin/google-signin) (2) | 2024.07.23 |