나만의 웹 크롤러 만들기(5): 웹페이지 업데이트를 알려주는 Telegram 봇

좀 더 보기 편한 깃북 버전의 나만의 웹 크롤러 만들기가 나왔습니다!

이전게시글: 나만의 웹 크롤러 만들기(4): Django로 크롤링한 데이터 저장하기

이번 가이드에서는 작업하는 컴퓨터가 아닌 원격 우분투16.04 서버(vps)에 올리는 부분까지 다룹니다. 테스트는 crontab -e 명령어를 사용할 수 있는 환경에서 가능하며, VISA/Master카드등 해외결제가 가능한 카드가 있다면 서비스 가입 후 실제로 배포도 가능합니다. 이 가이드에서는 Vultr VPS를 이용합니다.

앞서 Django를 이용해 크롤링한 데이터를 DB에 저장해 보았습니다.

하지만 크롤링을 할 때마다 동일한(중복된) 데이터를 DB에 저장하는 것은 바람직 하지 않은 일이죠.

또한, 크롤링을 자동으로 해 사이트에 변경사항이 생길 때 마다 내 텔레그램으로 알림을 받을 수 있다면 더 편리하지 않을까요?

이번 가이드에서는 클리앙 중고장터 등을 크롤링해 새 게시글이 올라올 경우 새글 알림을 텔레그램으로 보내는 것까지를 다룹니다.

다루는 내용:

  • Telegram Bot API
  • requests / BeautifulSoup
  • Crontab

시작하며

widgets:

텔레그램은 REST API를 통해 봇을 제어하도록 안내합니다.

물론 직접 텔레그램 api를 사용할 수도 있지만, 이번 가이드에서는 좀 더 빠른 개발을 위해 python-telegram-bot 패키지를 사용합니다.

python-telegram-bot은 Telegram Bot API를 python에서 쉽게 이용하기 위한 wrapper 패키지입니다.

python-telegram-bot 설치하기

widgets:

python-telegram-bot은 pip로 설치 가능합니다.

1
pip install python-telegram-bot

requests, bs4 역시 설치되어있어야 합니다!

텔레그램 봇 만들기 & API Key받기

widgets:

텔레그램 봇을 만들고 API키를 받아 이용하는 기본적인 방법은 python에서 telegram bot 사용하기에 차근차근 설명되어있습니다.

위 가이드에서 텔레그램 봇의 토큰을 받아오세요. 토큰은 aaaa:bbbbbbbbbbbbbb와 같이 생긴 문자열입니다.

이번 가이드는 텔레그램 봇을 다루는 내용보다는 Cron으로 크롤링을 하고 변화 발견시 텔레그램 메시지를 보내는 것에 초점을 맞췄습니다.

텔레그램 봇 API키를 받아왔다면 아래와 같이 크롤링을 하는 간단한 python파일을 작성해 봅시다. 클리앙에 새로운 글이 올라오면 “새 글이 올라왔어요!”라는 메시지를 보내는 봇을 만들어 보겠습니다.

클리앙 새글 탐지코드 만들기

widgets:

clien marcket web page list

우선 게시판의 글 제목중 첫번째 제목을 가져오고 txt파일로 저장하는 코드를 만들어 봅시다.

회원 장터 주소는 http://clien.net/cs2/bbs/board.php?bo_table=sold이고, 첫 게시글의 CSS Selector는 #content > div.board_main > table > tbody > tr:nth-child(3) > td.post_subject > a임을 알 수 있습니다. 따라서 아래와 같이 latest 변수에 담아 같은 폴더의 latest.txt 파일에 써 줍시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# clien_market_parser.py
import requests
from bs4 import BeautifulSoup
import os

# 파일의 위치
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

req = requests.get('http://clien.net/cs2/bbs/board.php?bo_table=sold')
req.encoding = 'utf-8' # Clien에서 encoding 정보를 보내주지 않아 encoding옵션을 추가해줘야합니다.

html = req.text
soup = BeautifulSoup(html, 'html.parser')
posts = soup.select('td.post_subject')
latest = posts[1].text # 0번은 회원중고장터 규칙입니다.

with open(os.path.join(BASE_DIR, 'latest.txt'), 'w+') as f:
f.write(latest)

위와 같이 코드를 구성하면 latest.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
# clien_market_parser.py
import requests
from bs4 import BeautifulSoup
import os

# 파일의 위치
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

req = requests.get('http://clien.net/cs2/bbs/board.php?bo_table=sold')
req.encoding = 'utf-8'

html = req.text
soup = BeautifulSoup(html, 'html.parser')
posts = soup.select('td.post_subject')
latest = posts[1].text

with open(os.path.join(BASE_DIR, 'latest.txt'), 'r+') as f_read:
before = f_read.readline()
if before != latest:
# 같은 경우는 에러 없이 넘기고, 다른 경우에만
# 메시지 보내는 로직을 넣으면 됩니다.
f_read.close()

with open(os.path.join(BASE_DIR, 'latest.txt'), 'w+') as f_write:
f_write.write(latest)
f_write.close()

새글이라면? 텔레그램으로 메시지 보내기!

widgets:

이제 메시지를 보내볼게요. telegram을 import하신 후 bot을 선언해주시면 됩니다. token은 위에서 받은 토큰입니다.

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
# clien_market_parser.py
import requests
from bs4 import BeautifulSoup
import os

import telegram

# 토큰을 지정해서 bot을 선언해 줍시다! (물론 이 토큰은 dummy!)
bot = telegram.Bot(token='123412345:ABCDEFgHiJKLmnopqr-0StUvwaBcDef0HI4jk')
# 우선 테스트 봇이니까 가장 마지막으로 bot에게 말을 건 사람의 id를 지정해줄게요.
# 만약 IndexError 에러가 난다면 봇에게 메시지를 아무거나 보내고 다시 테스트해보세요.
chat_id = bot.getUpdates()[-1].message.chat.id

# 파일의 위치
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

req = requests.get('http://clien.net/cs2/bbs/board.php?bo_table=sold')
req.encoding = 'utf-8'

html = req.text
soup = BeautifulSoup(html, 'html.parser')
posts = soup.select('td.post_subject')
latest = posts[1].text

with open(os.path.join(BASE_DIR, 'latest.txt'), 'r+') as f_read:
before = f_read.readline()
if before != latest:
bot.sendMessage(chat_id=chat_id, text='새 글이 올라왔어요!')
else: # 원래는 이 메시지를 보낼 필요가 없지만, 테스트 할 때는 봇이 동작하는지 확인차 넣어봤어요.
bot.sendMessage(chat_id=chat_id, text='새 글이 없어요 ㅠㅠ')
f_read.close()

with open(os.path.join(BASE_DIR, 'latest.txt'), 'w+') as f_write:
f_write.write(latest)
f_write.close()

이제 clien_market_parser.py파일을 실행할 때 새 글이 올라왔다면 “새 글이 올라왔어요!”라는 알림이, 새 글이 없다면 “새 글이 없어요 ㅠㅠ”라는 알림이 옵니다.

지금은 자동으로 실행되지 않기 때문에 python clien_market_parser.py명령어로 직접 실행해 주셔야 합니다.

자동으로 크롤링하고 메시지 보내기

widgets:

가장 쉬운방법: while + sleep

가장 쉬운 방법은 python의 while문을 쓰는 방법입니다. 물론, 가장 나쁜 방법이에요. 안전하지도 않고 시스템의 메모리를 좀먹을 수도 있어요.

하지만 테스트에서는 가장 쉽게 쓸 수 있어요.

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
# clien_market_parser.py
import requests
from bs4 import BeautifulSoup
import os
import time

import telegram

bot = telegram.Bot(token='123412345:ABCDEFgHiJKLmnopqr-0StUvwaBcDef0HI4jk')
chat_id = bot.getUpdates()[-1].message.chat.id

# 파일의 위치
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

while True:
req = requests.get('http://clien.net/cs2/bbs/board.php?bo_table=sold')
req.encoding = 'utf-8'

html = req.text
soup = BeautifulSoup(html, 'html.parser')
posts = soup.select('td.post_subject')
latest = posts[1].text

with open(os.path.join(BASE_DIR, 'latest.txt'), 'r+') as f_read:
before = f_read.readline()
if before != latest:
bot.sendMessage(chat_id=chat_id, text='새 글이 올라왔어요!')
else:
bot.sendMessage(chat_id=chat_id, text='새 글이 없어요 ㅠㅠ')
f_read.close()

with open(os.path.join(BASE_DIR, 'latest.txt'), 'w+') as f_write:
f_write.write(latest)
f_write.close()

time.sleep(60) # 60초(1분)을 쉬어줍니다.

파이썬 동작중에는 CTRL+C로 빠져나올수 있습니다.

추천: 시스템의 cron/스케쥴러를 이용하기

이번 가이드에서 핵심인 부분인데요, 이 부분은 이제 우분투 16.04 기준으로 진행할게요.

우선 Ubuntu 16.04가 설치된 시스템이 필요합니다. 이번 강의에서는 Vultr VPS를 이용합니다.

add deploy new vps

Vultr는 가상 서버 회사인데, Tokyo리전의 VPS를 제공해줘 빠르게 이용이 가능합니다. 트래픽도 굉장히 많이주고요.

가이드를 만드는 지금은 월 2.5달러 VPS는 아쉽게도 없어서, 월5달러 VPS로 진행하지만 월2.5달러 VPS로도 충분합니다!

아래의 Deploy Now를 누르면 새 Cloud Instance(VPS)가 생성되는데요, 서버가 생성된 후 들어가 보면 다음과 같이 id와 pw가 나와있습니다. 패스워드는 눈 모양을 누르면 잠시 보입니다.

이 정보로 ssh에 접속해 봅시다. (윈도는 putty이나 Xshell등을 이용해주세요.)

우분투 16.04버전에는 이미 Python3.5버전이 설치되어있기 때문에 pip3, setuptools을 설치해 주고 Ubuntu의 Locale을 설정해줘야 합니다. 아래 명령어를 한줄씩 순차적으로 치시면 완료됩니다.

1
2
3
sudo apt install python3-pip python3-setuptools build-essential
sudo locale-gen "ko_KR.UTF-8"
pip3 install requests bs4 python-telegram-bot

설치를 하신 후 코드를 테스트 해보려면 위 파일을 vi등으로 열어 위 코드들을 입력하시면 됩니다.

이제 코드를 약간 바꿔볼게요. 새 글이 올라올때만 파일을 다시 쓰도록 해요.

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
# clien_market_parser.py
import requests
from bs4 import BeautifulSoup
import os

import telegram

bot = telegram.Bot(token='123412345:ABCDEFgHiJKLmnopqr-0StUvwaBcDef0HI4jk')
chat_id = bot.getUpdates()[-1].message.chat.id

# 파일의 위치
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

req = requests.get('http://clien.net/cs2/bbs/board.php?bo_table=sold')
req.encoding = 'utf-8'

html = req.text
soup = BeautifulSoup(html, 'html.parser')
posts = soup.select('td.post_subject')
latest = posts[1].text

with open(os.path.join(BASE_DIR, 'latest.txt'), 'r') as f_read:
before = f_read.readline()
f_read.close()
if before != latest:
bot.sendMessage(chat_id=chat_id, text='새 글이 올라왔어요!')
with open(os.path.join(BASE_DIR, 'latest.txt'), 'w+') as f_write:
f_write.write(latest)
f_write.close()

이제 이 clien_market_parser.py 파일을 python3으로 실행해야 하기 때문에, python3이 어디 설치되어있는지 확인 해 봅시다.(아마 /usr/bin/python3일거에요!)

1
2
root@vultr:~# which python3
/usr/bin/python3

이제 Crontab에 이 파이썬으로 우리 파일을 매 1분마다 실행하도록 만들어 봅시다.

crontab 수정은 crontab -e명령어로 사용 가능합니다. 만약 에디터를 선택하라고 한다면 초보자는 Nano를, vi를 쓰실수 있으시다면 vi를 이용하세요.

이 한줄을 crontab 마지막에 추가해 주세요.

1
* * * * * /usr/bin/python3 /root/clien_market_parser.py

힌트: 매 12분마다로 하시려면 */12 * * * * /usr/bin/python3 /root/clien_market_parser.py로 하시면 됩니다.

이제 여러분의 휴대폰으로 새 글이 올라올 때 마다 알람이 올라올 거랍니다 :)

마무리

widgets:

이번편 가이드는 DB를 이용하지 않고 단순하게 새로운 글이 왔다는 사실만을 메시지로 알려주는 봇을 만들어서 뭔가 아쉬움이 있을겁니다.

다음편 가이드는 multiprocessing을 이용한 N배 빠른 크롤러을 만들어 봅니다.

다음 가이드: 나만의 웹 크롤러 만들기(6): N배빠른 크롤링, multiprocessing!

편리한 깃헙페이지 블로깅을 위한 이미지서버, 구글드라이브: 앱으로 만들고 키보드 단축키 연결하기

본 가이드는 MacOS에서 이용가능합니다.

이전 가이드: 편리한 깃헙페이지 블로깅을 위한 이미지서버, 구글드라이브: 업로드 ShellScript편

터미널에서 gdrive list라고 했을때 에러가 나지 않는 상태에서 아래 가이드를 진행해주세요.

들어가며

이전 가이드에서 스크린샷을 찍고 구글드라이브에 올린 후 이미지의 공유 URL을 가져오는 스크립트를 작성했습니다.

하지만 키보드 Shortcuts를 이용한 편리성에는 따라가기가 어렵죠. .sh스크립트를 키보드로 연동하는 방법 중 여러가지 방법이 있지만, 이번에는 MacOS App으로 만든 후 앱을 실행하는 것을 서비스에 등록하고 Automator를 통해 키보드와 앱실행 서비스를 연동하는 과정을 다룹니다.

만약 잘 동작하는 맥용 앱을 바로 다운받으시려면 CaptureToGdrive.zip을 받아주신 후 압축을 푸신 후 앱을 Application폴더로 옮기신 후 [백투더맥 Q&A] 키보드 단축키로 응용 프로그램을 실행하는 바로 가기 만들기 과정을 따라가시면 됩니다.

SH파일을 앱으로 만들기

우선 .sh파일로 된 스크립트를 맥용 앱으로 Wrapping해주는 작업이 필요합니다. 이번 가이드에서는 이 작업을 간소화해주는 platypus를 이용합니다.

platypus는 platypus.zip을 받고 압축을 풀어 사용하시면 됩니다.

앱을 실행하면 아래와 같은 화면이 뜹니다.

App Name을 CaptureToGdrive로, Script Type을 bash로, Script Path는 아래의 +New를 눌러 아래와 같이 코드를 입력해 줍시다.

1
2
3
4
5
6
7
8
#!/bin/bash

screencapture -tpng -i /tmp/temp_shot_gdrive.png
DATEFILENAME=`date +"%Y%m%d%H%M"`
ID=`/usr/local/bin/gdrive upload /tmp/temp_shot_gdrive.png --name screenshot${DATEFILENAME}.png --share | egrep "^Uploaded" | awk '{print $2}'`
URL="https://drive.google.com/uc?id=${ID}"

echo ${URL} | pbcopy

위 코드는 이전 가이드에서 다뤘던 것과 약간 다른데요, gdrive명령어의 위치를 명확히 /usr/local/bin/gdrive로 바꿔준점이 다릅니다. 쉘 스크립트를 사용할 때 명확히 하지 않으면 gdrive의 PATH를 잡지 못해 에러가 나기 때문입니다.

만약 다른 위치에 까셨다면 which gdrive명령을 통해 그 위치로 변경해주시면 됩니다.

스크립트를 입력하고 나면 AppName이 초기화되는 사소한 문제가 있으니 다시 AppName을 등록해 줍시다.

스크린샷 촬영은 인터페이스가 필요없기 때문에 InterfaceNone으로, root권한이 필요없고 백그라운드일 필요도 없고 프로그램이 굳이 계속 떠 있을 필요가 없기때문에 모든 체크박스는 아래와 같이 체크해제 해두시면 됩니다.

이제 CreateApp을 클릭하고 아래와 같이 클릭한 후 Create를 누르면 앱이 만들어집니다 :)

만들어진 앱을 실행해 보시고 잘 되시는지 확인해보세요.

앱을 키보드로 연결하기

이 부분은 좀 더 잘 정리되어있는 [백투더맥 Q&A] 키보드 단축키로 응용 프로그램을 실행하는 바로 가기 만들기을 참고하시기 바랍니다.

마치며

이번 가이드에서는 업로드 되는 폴더를 정확히 명시하지는 않았습니다. gdrive패키지에서 -p를 이용하면 폴더를 지정가능하다고 하지만 테스트 결과 제대로 업로드 되지 않는 것을 확인했기 때문에, 현재 주로 사용하지는 않는 다른 구글 아이디에 gdrive를 연결해 두었습니다.

Imgur, Dropbox등 여러 이미지 Serving 업체들이 있지만, 구글이 가진 구글 Fiber망과 서비스의 안정성은 여타 서비스들이 따라가기 어려운 점이라고 생각합니다 :)

편리한 깃헙페이지 블로깅을 위한 이미지서버, 구글드라이브: 업로드 ShellScript편

본 가이드는 MacOS에서 이용가능합니다.

들어가며

깃헙 페이지를 Jekyll등을 이용해 Markdown파일을 이용하다보면 스크린샷을 저장하고 깃헙 레포 폴더에 옮긴 후 수동으로 url을 추가해 주는 작업이 상당히 귀찮고, 심지어 깃헙 레포당 저장공간은 1G로 제한됩니다.

Dropbox의 경우에는 MacOS 내장 스크린샷(CMD+Shift+4)를 이용할 경우 파일을 자동으로 dropbox에 올린 후 공유 url이 나옵니다. 하지만 일반 유저는 용량 제한도 있고, 트래픽 제한도 있습니다.

따라서 무료로 15G의 용량과 명시적 트래픽 제한이 없는 구글드라이브를 이용하는 방안을 고려해보았습니다.

정확히는 Github은 레포당 용량을 명시적으로 제한하지는 않지만 1G가 넘어가는 경우 스토리지를 이용하도록 가이드합니다. Dropbox링크를 통한 트래픽은 무료 유저의 경우 일 20G, 유료플랜 유저의 경우 일 200G를 줍니다. GoogleDrive의 경우 무료 계정도 일 100G(추정치)의 트래픽을 제공하기 때문에 큰 무리는 따르지 않는다 생각합니다.

Gdrive 설치하기

이번 가이드에서는 Gdrive를 이용합니다.

Homebrew를 통해 간단히 설치할 수 있습니다. 터미널에서 아래와 같이 입력해 주세요.

1
brew install gdrive

Gdrive AUTH

gdrive를 설치하고 나서, gdrive가 구글드라이브에 액세스 할 수 있도록 권한을 부여해야 합니다.

아래 명령어는 구글드라이브의 최상위 디렉토리를 리스팅 하는 명령어인데, 이 과정에서 드라이브 액세스 권한을 요구하기 때문에 자연스럽게 권한 등록이 가능합니다.

1
gdrive list

명령어를 입력시 아래와 같은 창이 뜹니다. 절대 창을 끄지 마시고 아래 안내되는 구글 링크로 들어가세요.

보안을 위해 키 일부를 지웠습니다. 원래는 회색 빈칸이 없습니다 :)

Console: GoogleDrive auth link

링크를 따라가시면 구글 로그인을 요구합니다. 로그인을 하시면 아래와 같은 권한 요구 창이 뜨는데요, ‘허용’을 눌러주시면 됩니다.

허용을 누르면 아래와 같은 코드가 나옵니다. 이 코드를 아까 터미널 창에 복사-붙여넣기를 해주세요.

만약 코드가 정상적이었다면 아래와 같이 최상위 디렉토리의 폴더/파일 리스트가 나타납니다.

capture.sh파일 만들기

Gdrive가 정상적으로 구글 계정과 연결되었다면, 이제 capture.sh파일을 만들어야 합니다. 파일의 코드는 아래와 같습니다. 복사 하신 후 원하시는 위치에 넣어주세요. (저는 ~/capture.sh로 두었습니다.)

1
2
3
4
5
6
7
8
9
#!/bin/bash
# ~/capture.sh
screencapture -tpng -i /tmp/temp_shot_gdrive.png
DATEFILENAME=`date +"%Y%m%d%H%M"`
# use -p id to upload to a specific folder
ID=`gdrive upload /tmp/temp_shot_gdrive.png --name screenshot${DATEFILENAME}.png --share | egrep "^Uploaded" | awk '{print $2}'`
URL="https://drive.google.com/uc?id=${ID}"

echo ${URL} | pbcopy

우선 이 스크립트 파일에 실행권한을 줘야 합니다.

1
chmod +x capture.sh

이제 ./capture.sh명령을 입력하면 캡쳐메뉴로 진입하고, 캡쳐를 진행하고 잠시 기다리면(업로드 시간) 클립보드에 구글드라이브로 공유된 파일의 URL이 복사됩니다.

rc파일(.zshrc/.bashrc)에 alias걸기

capture.sh파일을 둔 위치가 ~/capture.sh라고 가정하고, ~/.zshrc(혹은~/.bashrc) 파일을 수정해 주겠습니다.

항상 ./capture.sh라고 입력하는 것은 귀찮은 일이기 때문에, alias를 통해 cap라는 명령어를 캡쳐 명령어로 지정해 봅시다.

.zshrc.bashrc파일 제일 아래에 아래 코드를 덧붙여주고 저장해줍시다.

1
alias cap="~/capture.sh"

터미널을 재실행한 후 cap라는 명령을 치면 캡쳐 도구가 뜹니다!

다음 가이드: 앱으로 만들어 단축키로 연결하기

기본 스크린샷처럼 키보드 단축키 만으로 스크린샷 링크를 가져올 수 있다면 훨씬 편리하겠죠?

다음 가이드에서는 이번에 만든걸 앱으로 만들어 스크린샷 단축키로 연결하는 과정을 다룹니다.

다음가이드: 편리한 깃헙페이지 블로깅을 위한 이미지서버, 구글드라이브: 앱으로 만들고 키보드 단축키 연결하기

완성된 앱도 함께 제공합니다!

Django에 Social Login와 Email유저 함께 이용하기

django-custom-usersocial-auth-app-django(구 python-social-auth)를 이용해 이메일 기반 유저와 소셜 로그인으로 로그인 한 유저를 하나처럼 사용하는 방법입니다.

장고에 소셜 로그인을 붙이는 가이드는 Django에 Social Login 붙이기: Django세팅부터 Facebook/Google 개발 설정까지 포스팅에서 찾으실 수 있습니다.

Django + SocialLogin + Email as User

웹 서비스를 제공할 때 여러가지 로그인 방법을 구현할 수 있습니다. 아이디/패스워드 기반의 방식, 페이스북과 구글등의 OAuth를 이용한 소셜 로그인 방식 등이 있습니다.

장고 프로젝트를 만들 때 django-custom-user등의 패키지를 이용하면 이메일 주소를 Unique Key로 사용해 이메일 주소로 로그인을 할 수 있도록 만들어 줍니다.

django-custom-user에 관한 문서는 Django Custom User GitHub에서 확인하실 수 있습니다.

하지만, social-auth-app-django를 통해 유저를 생성 할 경우 OAuth Provider에 따라 다른 User를 생성합니다. 즉, 같은 이메일 주소를 가지고 있는 유저라 하더라도 페이스북을 통해 로그인 한 유저와 구글을 통해 로그인 한 유저는 다르게 다뤄진다는 뜻입니다.

사실 이메일 주소를 신뢰하지 않고 Provier마다 다른 유저로 생성하는 것이 기본으로 되어있는 이유는 Oauth Provier의 신뢰 문제입니다. 모든 Oauth Provier가 가입한 유저의 Email의 실 소유권을 확인하지는 않기 때문입니다.

이를 해결하고 같은 이메일을 통해 로그인한 유저는 모두 같은 유저로 취급하기 위해서는 장고 프로젝트 폴더의 settings.py파일 안에서 social-auth-app-django의 Pipeline설정을 변경해 줘야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.auth_allowed',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.social_auth.associate_by_email', # <--- 이 줄이 핵심입니다.
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
)

SOCIAL_AUTH_PIPELINEsettings.py내에는 기본적으로 지정이 해제되어있습니다. 따라서 변수가 없는 경우 위 코드 전체를 settings.py파일 끝에 덧붙이시면 됩니다.

참고: Python Social Auth: Associate users by Email

React+JSX(ES6)를 빌드 없이 사용하기: browser.js

Babel: ES6를 ES5로

바벨(Babel)은 ES6(ECMAScript6)을 ES5 문법으로 변환시켜 오래된 브라우저들에서도 ES6의 기능을 이용할 수 있도록 도와주는 자바스크립트 모듈이다. React는 개발시 ES6 문법을 주로 이용하기 때문에 이러한 Babel은 필수적이라고 말할 수 있다.

그러나…

React에서 공식적인 이용 방법 중 하나인 CDN을 이용할 경우 (아래 사진처럼) 실제 첫 튜토리얼을 할 경우 JavaScript에서 JSX를 공식적으로 지원하지 않기 때문에 JS문법 에러가 난다.

아래 코드는 작동하지 않는다.

1
2
3
4
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);

물론 실제 배포시에는 빌드 과정을 거쳐 나온 파일을 관리해야 한다. 하지만 React를 처음 배우는 과정에서는 로컬의 한 HTML파일 안에서 모든 과정이 작동하기를 원하게 된다. 따라서 클라이언트 렌더링을 고려할 수 있다.

물론 클라이언트 렌더링은 성능 이슈가 있기 때문에 실 배포시에는 사용하지 않아야 한다.

Browser.js 사용하기

Babel은 6버전부터 Browser.js를 업데이트 하지 않았다. 하지만 정상적으로 동작하는 파일이 CDN에 존재하기 때문에, HTML문서에 다음 세 줄을 추가해 주면 script태그에 type="text/babel"이라는 타입을 가진 코드들을 ES6로 간주하고 ES5로 변환해 준다.

1
2
3
<script src="https://unpkg.com/react@15/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.js"></script>

Browser.js파일을 추가해서 우리는 위 코드를 아래와 같이 쓸 수 있게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
<script src="https://unpkg.com/react@15/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.js"></script>

<div id="root"></div>

<script type="text/babel">
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
</script>

위 코드를 살펴보면 정상적으로 동작한다는 것을 알 수 있다.

입문/개발 전용!!

단, 이 방법은 React에 입문하는 사람이 HTML 문서 하나만으로, 그리고 NPM을 사용하지 않고 작업할 경우에 사용할 수 있는 방법이며, 실 프로젝트에서 이와 같이 사용하는 것은 여러 문제를 일으킬 수 있다.

그러니 입문/개발때에만 사용하자 :)

Fabric으로 Django 배포하기

이번 가이드는 완성된 상태의 Django 프로젝트가 있다고 가정합니다. 예제로 https://github.com/Beomi/irkshop 을 배포해 봅니다.

https://gist.github.com/Beomi/945cd905175c3b21370f8f04abd57404의 예제를 설명합니다.

Fabric으로 Django 배포하기

Django는 내장된 runserver라는 개발용 웹 서버가 있습니다. 하지만 개발용 웹 서버를 상용 환경에서 사용하는 것은 여러가지 문제를 가져옵니다. 메모리 문제등의 성능 이슈부터 Static file서빙의 보안 문제까지 다양한데요, 이 때문에 Django는 웹 서버(ex: Apache2 NginX등)를 통해 배포하게 됩니다.

하지만 이러한 배포작업은 아마존 EC2등의 VPS나 리얼 서버에서 Apache2를 깔고, python3mod_wsgi등을 깔아야만 동작하기 때문에 배포 자체가 어려움을 갖게 됩니다. 또한 SSH에 접속히 직접 명령어를 치는 경우 오타나 실수등으로 인해 정상적으로 작동하지 않는 경우도 부지기수입니다.

따라서 이러한 작업을 자동화해주는 도구가 바로 Fabric이고, 이번 가이드에서는 Django 프로젝트를 Vultr VPS, Ubuntu에 올리는 방법을 다룹니다.

Vultr VPS 생성하기

Vultr는 VPS(가상서버) 제공 회사입니다. 최근 가격 인하로 유사 서비스 대비 절반 가격에 이용할 수 있어 가성비가 좋습니다.

사용자가 많지 않은 (혹은 혼자 사용하는..) 서비스라면 최소 가격인 1cpu 512MB의 월 2.5달러짜리를 이용하시면 됩니다.

Vultr는 일본 Region에 서버가 있어 한국에서 사용하기에도 핑이 25ms정도로 양호합니다.

VPS하나를 만든 후 root로 접속해 장고를 구동할 사용자를 만들어 봅시다.

예제VPS

django 유저 만들기(sudo권한 가진 유저 만들기)

Fabric을 사용할 때 초기에 apt를 이용해 패키지를 설치해야 할 필요가 있습니다.

하지만 처음에 제공되는 root계정은 사용하지 않는 것을 보안상 추천합니다. 따라서 우리는 sudo권한을 가진 django라는 유저를 생성하고 Fabric으로 진행해 보겠습니다.

1
2
adduser django # `django`라는 유저를 만듭니다.
adduser django sudo # django유저를 `sudo`그룹에 추가합니다.

비밀번호를 만드는 것을 제외하면 나머지는 빈칸으로 만들어 두어도 무방합니다.

Fabric 설치하기

Fabric은 기본적으로 서버가 아닌 클라이언트에 설치합니다. 개념상 로컬에서 SSH로 서버에 접속해 명령을 처리하는 것이기 때문에 당연히 SSH 명령을 입려하는 로컬에 설치되어야 합니다.

Fabric은 공식적으로는 Python2.7만을 지원합니다. 하지만 이 프로젝트를 Fork해서 Python3을 지원하는 프로젝트인 Fabric3이 있습니다. 이번 가이드에서는 이 Fabric3을 설치합니다.

1
2
3
pip3 install fabric3
# 혹은
python3 -m pip install fabric3

fabfile 만들기

Fabric import하기

Fabric을 설치하시면 fab이라는 명령어를 사용할 수 있습니다. 이 명령어는 fab some_func라는 방식을 통해 fabfile.py파일 안의 함수를 실행할 수 있습니다.

fabfile은 기본적으로 manage.py파일와 같은 위치인 프로젝트 폴더에 두시는 것을 권장합니다.

1
2
from fabric.contrib.files import append, exists, sed, put
from fabric.api import env, local, run, sudo

우선 fabric에서 사용하는 API들을 import해줍니다.

fabric.contrib.files에서는 원격(혹은 로컬)의 파일을 관리하는 API입니다. fabric.api는 Fabric에서 사용하는 환경이나, SSH로 연결한 원격 서버에서 명령어를 실행하는 API입니다.

PROJECT_DIR, BASE_DIR 지정하기

장고의 settings.py파일에 기본적으로 지정된 BASE_DIR와 같은 장고 프로젝트의 폴더 위치를 지정하는 PROJECT_DIRBASE_DIR을 지정해 줍니다.

PROJECT_DIRsettings.py가 있는 폴더의 위치이고, BASE_DIRmanage.py가 있는 폴더입니다.

1
2
3
4
5
6
7
8
from fabric.contrib.files import append, exists, sed, put
from fabric.api import env, local, run, sudo
import random
import os
import json

PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(PROJECT_DIR)

배포용 변수 불러오기

서버에 배포를 하기 위해 git을 이용합니다. 따라서 소스가 올라가 있는 깃헙(혹은 gitlab, bitbucket 등)의 주소(REPO_URL)가 필요합니다.

그리고 원격으로 SSH접속을 하기 때문에 원격 서버에 접속할 수 있는 SSH용 주소(REMOTE_HOST_SSH), 원격 계정 ID(REMOTE_USER), 원격 계정 비밀번호(REMOTE_PASSWORD)가 필요합니다.

또한, 장고 settings.pyALLOWED_HOSTS에 추가할 도메인(REMOTE_HOST)이 필요합니다.

이러한 변수들은 보통 json파일에 저장하고 .gitignore에 이 json파일을 지정해 git에 올라가지 않도록 관리합니다. 이번 가이드에서는 deploy.json이라는 파일에 아래 변수들을 저장하고 관리해 보겠습니다.

deploy.json파일을 fabfile.py파일이 있는 곳에 아래 내용을 담고 저장해주세요.

REPO_URLPROJECT_NAME을 제외한 설정은 위 Vultr에서 만들어준 대로 진행해주세요. 단, REMOTE_USER는 root이면 안됩니다!

1
2
3
4
5
6
7
8
{
"REPO_URL":"https://github.com/Beomi/irkshop.git",
"PROJECT_NAME":"irkshop",
"REMOTE_HOST_SSH":"45.77.20.73",
"REMOTE_HOST":"45.77.20.73",
"REMOTE_USER":"django",
"REMOTE_PASSWORD":"django_pwd123"
}

만약 SSH 포트가 다르다면 REMOTE_HOST_SSH 뒤 포트를 :으로 붙여주면 됩니다. (ex: 45.77.20.73:22)

REMOTE_HOST는 도메인 주소(ex: irkshop.testi.kr)일 수 있습니다. 하지만 이번 배포에서는 도메인을 다루지 않으므로 IP주소로 대신합니다.

json파일을 만들었다면 이 파일을 이제 fabfile.py에서 불러와 사용해 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from fabric.contrib.files import append, exists, sed, put
from fabric.api import env, local, run, sudo
import random
import os
import json

PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# deploy.json파일을 불러와 envs변수에 저장합니다.
with open(os.path.join(PROJECT_DIR, "deploy.json")) as f:
envs = json.loads(f.read())

REPO_URL = envs['REPO_URL']
PROJECT_NAME = envs['PROJECT_NAME']
REMOTE_HOST_SSH = envs['REMOTE_HOST_SSH']
REMOTE_HOST = envs['REMOTE_HOST']
REMOTE_USER = envs['REMOTE_USER']
REMOTE_PASSWORD = envs['REMOTE_PASSWORD']
# 아래 부분은 Django의 settings.py에서 지정한 STATIC_ROOT 폴더 이름, STATID_URL, MEDIA_ROOT 폴더 이름을 입력해주시면 됩니다.
STATIC_ROOT_NAME = 'static_deploy'
STATIC_URL_NAME = 'static'
MEDIA_ROOT = 'uploads'

Fabric 환경 설정하기

이제 Fabric이 사용할 env를 설정해 줍시다. 대표적으로 env.userenv.hosts, env.password가 있습니다.

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
from fabric.contrib.files import append, exists, sed, put
from fabric.api import env, local, run, sudo
import random
import os
import json

PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

with open(os.path.join(PROJECT_DIR, "deploy.json")) as f:
envs = json.loads(f.read())

REPO_URL = envs['REPO_URL']
PROJECT_NAME = envs['PROJECT_NAME']
REMOTE_HOST_SSH = envs['REMOTE_HOST_SSH']
REMOTE_HOST = envs['REMOTE_HOST']
REMOTE_USER = envs['REMOTE_USER']
REMOTE_PASSWORD = envs['REMOTE_PASSWORD']
STATIC_ROOT_NAME = 'static_deploy'
STATIC_URL_NAME = 'static'
MEDIA_ROOT = 'uploads'

# Fabric이 사용하는 env에 값들을 저장합니다.
env.user = REMOTE_USER
username = env.user
env.hosts = [
REMOTE_HOST_SSH, # 리스트로 만들어야 합니다.
]
env.password = REMOTE_PASSWORD
# 원격 서버에서 장고 프로젝트가 있는 위치를 정해줍니다.
project_folder = '/home/{}/{}'.format(env.user, PROJECT_NAME)

이와 같이 설정시 fab명령어를 실행할 경우에 추가적인 값을 입력할 필요가 없어집니다.

APT 설치 목록 지정하기

VPS에 따라 설치되어있는 리눅스 패키지가 다릅니다. 이번 가이드에서는 Apache2mod-wsgi-py3을 사용하기 때문에 이 패키지와 파이썬 의존 패키지들을 설치해 줘야 합니다.

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
from fabric.contrib.files import append, exists, sed, put
from fabric.api import env, local, run, sudo
import random
import os
import json

PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

with open(os.path.join(PROJECT_DIR, "deploy.json")) as f:
envs = json.loads(f.read())

REPO_URL = envs['REPO_URL']
PROJECT_NAME = envs['PROJECT_NAME']
REMOTE_HOST_SSH = envs['REMOTE_HOST_SSH']
REMOTE_HOST = envs['REMOTE_HOST']
REMOTE_USER = envs['REMOTE_USER']
REMOTE_PASSWORD = envs['REMOTE_PASSWORD']
STATIC_ROOT_NAME = 'static_deploy'
STATIC_URL_NAME = 'static'
MEDIA_ROOT = 'uploads'

env.user = REMOTE_USER
username = env.user
env.hosts = [REMOTE_HOST_SSH,]
env.password = REMOTE_PASSWORD
project_folder = '/home/{}/{}'.format(env.user, PROJECT_NAME)

# APT로 설치할 목록을 정해줍니다.
apt_requirements = [
'ufw', # 방화벽
'curl',
'git', # 깃
'python3-dev', # Python 의존성
'python3-pip', # PIP
'build-essential', # C컴파일 패키지
'python3-setuptools', # PIP
'apache2', # 웹서버 Apache2
'libapache2-mod-wsgi-py3', # 웹서버~Python3 연결
# 'libmysqlclient-dev', # MySql
'libssl-dev', # SSL
'libxml2-dev', # XML
'libjpeg8-dev', # Pillow 의존성 패키지(ImageField)
'zlib1g-dev', # Pillow 의존성 패키지
]

Fab 함수 만들기

Fabric은 fabfile있는 곳에서 fab 함수이름의 명령어로 실행 가능합니다. 단, _로 시작하는 함수는 Fabric이 관리하지 않습니다.

이제 서버에서 실행할 SSH를 캡슐화한다고 보면 됩니다. 크게 setupdeploy로 나눌 수 있다고 봅니다. Setup은 장고 코드와 무관한 OS의 패키지 관리와 VirtualEnv관리, Deploy는 장고 소스가 변화할 경우 업데이트 되어야 하는 코드입니다.

Setup에는 APT 최신 패키지 설치와 apt_requirements설치, 그리고 virtualenv를 만드는 것까지를 다룹니다.

Deploy에서는 Git에서 최신 소스코드를 가져오고, Git에서 관리되지 않는 환경변수 파일을 서버에 업로드하고, 장고 settings.py파일을 상용 환경으로 바꿔주고, virtualenv로 만든 가상환경에 pip 패키지를 설치하고, StaticFile들을 collect하고, DB를 migrate해주고, Apache2의 VirtualHost에 장고 웹 서비스를 등록하고, 폴더 권한을 잡아주고, 마지막으로 Apache2 웹서버를 재부팅하는 과정까지를 다룹니다.

여기서부터는 코드가 너무 길어지는 관계로 apt_requirements 포함한 윗부분을 생략합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 앞부분 생략
def new_server():
setup()
deploy()

def setup():
_get_latest_apt() # APT update/upgrade
_install_apt_requirements(apt_requirements) # APT install
_make_virtualenv() # Virtualenv

def deploy():
_get_latest_source() # Git에서 최신 소스 가져오기
_put_envs() # 환경변수 json파일 업로드
_update_settings() # settings.py파일 변경
_update_virtualenv() # pip 설치
_update_static_files() # collectstatics
_update_database() # migrate
_make_virtualhost() # Apache2 VirtualHost
_grant_apache2() # chmod
_grant_sqlite3() # chmod
_restart_apache2() # 웹서버 재시작

이와 같이 함수를 등록해주면 fab new_server, fab setup, fab deploy를 통해 바로바로 배포를 할 수 있습니다.

이제 _로 시작하는, 진짜 Fabric함수들을 만들어 보겠습니다.

_ 로 시작하는 함수들을 설명할 때는 함수만 각각 설명합니다. 모두 모인 코드는 글 상단의 GIST를 참고해주세요.

  • _get_latest_apt: APT 업데이트 & 업그레이드
1
2
3
4
def _get_latest_apt():
update_or_not = input('would you update?: [y/n]')
if update_or_not=='y':
sudo('sudo apt-get update && sudo apt-get -y upgrade')
  • _install_apt_requirements: apt_requirements에 적은 패키지들을 설치합니다.
1
2
3
4
5
def _install_apt_requirements(apt_requirements):
reqs = ''
for req in apt_requirements:
reqs += (' ' + req)
sudo('sudo apt-get -y install {}'.format(reqs))
  • _make_virtualenv: 원격 서버에 ~/.virtualenvs폴더가 없는 경우 virtualenv와 virtualenvwrapper를 설치하고 .bashrc파일에 virtualenvwrapper를 등록해 줍니다.
1
2
3
4
5
6
7
8
9
def _make_virtualenv():
if not exists('~/.virtualenvs'):
script = '''"# python virtualenv settings
export WORKON_HOME=~/.virtualenvs
export VIRTUALENVWRAPPER_PYTHON="$(command \which python3)" # location of python3
source /usr/local/bin/virtualenvwrapper.sh"'''
run('mkdir ~/.virtualenvs')
sudo('sudo pip3 install virtualenv virtualenvwrapper')
run('echo {} >> ~/.bashrc'.format(script))
  • _get_latest_source: 만약 .git폴더가 없다면 원격 git repo에서 clone해오고, 있다면 fetch해온 후 최신 커밋으로 reset합니다.
1
2
3
4
5
6
7
def _get_latest_source():
if exists(project_folder + '/.git'):
run('cd %s && git fetch' % (project_folder,))
else:
run('git clone %s %s' % (REPO_URL, project_folder))
current_commit = local("git log -n 1 --format=%H", capture=True)
run('cd %s && git reset --hard %s' % (project_folder, current_commit))
  • _put_envs: 로컬의 envs.json이름의 환경변수를 서버에 업로드 합니다.

Apache2는 웹서버가 OS의 환경변수를 사용하지 않기 때문에 json와 같은 파일을 통해 환경변수를 관리해 줘야 합니다. 저는 envs.json이라는 파일을 manage.py파일이 있는 프로젝트 폴더에 만든 후 환경변수를 장고의 settings.py에서 불러와 사용합니다.

1
2
def _put_envs():
put(os.path.join(PROJECT_DIR, 'envs.json'), '~/{}/envs.json'.format(PROJECT_NAME))
  • _update_settings: settings.py파일을 바꿔줍니다. DEBUG를 False로 바꾸고, ALLOWED_HOSTS에 호스트 이름을 등록하고, 장고에서 만들어준 키 파일이 아니라 서버에서 랜덤으로 만든 Secret KEY를 사용하게 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
def _update_settings():
settings_path = project_folder + '/{}/settings.py'.format(PROJECT_NAME)
sed(settings_path, "DEBUG = True", "DEBUG = False")
sed(settings_path,
'ALLOWED_HOSTS = .+$',
'ALLOWED_HOSTS = ["%s"]' % (REMOTE_HOST,)
)
secret_key_file = project_folder + '/{}/secret_key.py'.format(PROJECT_NAME)
if not exists(secret_key_file):
chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
key = ''.join(random.SystemRandom().choice(chars) for _ in range(50))
append(secret_key_file, "SECRET_KEY = '%s'" % (key,))
append(settings_path, '\nfrom .secret_key import SECRET_KEY')
  • _update_virtualenv: virtualenv에 requirements.txt파일로 지정된 pip 패키지들을 설치합니다.
1
2
3
4
5
6
7
def _update_virtualenv():
virtualenv_folder = project_folder + '/../.virtualenvs/{}'.format(PROJECT_NAME)
if not exists(virtualenv_folder + '/bin/pip'):
run('cd /home/%s/.virtualenvs && virtualenv %s' % (env.user, PROJECT_NAME))
run('%s/bin/pip install -r %s/requirements.txt' % (
virtualenv_folder, project_folder
))
  • _update_static_files: CollectStatic을 해줍니다.
1
2
3
4
5
def _update_static_files():
virtualenv_folder = project_folder + '/../.virtualenvs/{}'.format(PROJECT_NAME)
run('cd %s && %s/bin/python3 manage.py collectstatic --noinput' % (
project_folder, virtualenv_folder
))
  • _update_database: DB migrate를 해줍니다.
1
2
3
4
5
def _update_database():
virtualenv_folder = project_folder + '/../.virtualenvs/{}'.format(PROJECT_NAME)
run('cd %s && %s/bin/python3 manage.py migrate --noinput' % (
project_folder, virtualenv_folder
))
  • _make_virtualhost: Apache2가 관리하는 VirtualHost를 만들어줍니다. 80포트로 지정하고 Static파일을 Apache2가 서빙합니다.

만약 SSL을 사용하고 싶으시다면 *:443으로 관리되는 파일을 추가적으로 만드셔야 합니다. 이번 가이드에서는 다루지 않습니다.

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
def _make_virtualhost():
script = """'<VirtualHost *:80>
ServerName {servername}
Alias /{static_url} /home/{username}/{project_name}/{static_root}
Alias /{media_url} /home/{username}/{project_name}/{media_url}
<Directory /home/{username}/{project_name}/{media_url}>
Require all granted
</Directory>
<Directory /home/{username}/{project_name}/{static_root}>
Require all granted
</Directory>
<Directory /home/{username}/{project_name}/{project_name}>
<Files wsgi.py>
Require all granted
</Files>
</Directory>
WSGIDaemonProcess {project_name} python-home=/home/{username}/.virtualenvs/{project_name} python-path=/home/{username}/{project_name}
WSGIProcessGroup {project_name}
WSGIScriptAlias / /home/{username}/{project_name}/{project_name}/wsgi.py
{% raw %}
ErrorLog ${{APACHE_LOG_DIR}}/error.log
CustomLog ${{APACHE_LOG_DIR}}/access.log combined
{% endraw %}
</VirtualHost>'""".format(
static_root=STATIC_ROOT_NAME,
username=env.user,
project_name=PROJECT_NAME,
static_url=STATIC_URL_NAME,
servername=REMOTE_HOST,
media_url=MEDIA_ROOT
)
sudo('echo {} > /etc/apache2/sites-available/{}.conf'.format(script, PROJECT_NAME))
sudo('a2ensite {}.conf'.format(PROJECT_NAME))
  • _grant_apache2: 프로젝트 폴더내 파일을 www-data그룹(Apache2)이 관리할 수 있도록 소유권을 변경합니다.
1
2
def _grant_apache2():
sudo('sudo chown -R :www-data ~/{}'.format(PROJECT_NAME))
  • _grant_sqlite3: 만약 Sqlite3을 그대로 이용할 경우 www-data가 쓰기 권한을 가져야 합니다.
1
2
def _grant_sqlite3():
sudo('sudo chmod 775 ~/{}/db.sqlite3'.format(PROJECT_NAME))
  • _restart_apache2: 모든 설정을 마친 후 Apache2 웹서버를 재실행해 설정을 적용해줍니다.
1
2
def _restart_apache2():
sudo('sudo service apache2 restart')

배포해보기

이제 manage.py파일이 있는 곳에서 아래 명령어를 입력해 봅시다.

1
fab new_server

이 명령어를 치면 자동으로 모든 과정이 완료되고 서버가 뜹니다.

만약 파일을 수정하고 커밋했다면, Github Repo에 올린 후 deploy 명령어를 통해 새 코드를 서버에 배포할 수 있습니다.

1
fab deploy

짜잔!

프로젝트 하나가 배포가 완료되었습니다! 아무것도 없어보이지만, DB에 자료를 추가하면 IRKSHOP 예제처럼 예쁘게 생성됩니다.

Simple IRKSHOP

[번역]셀러리: 시작하기

글 작성 시점 최신 버전 v4.0.2의 문서입니다.

원문: http://docs.celeryproject.org/en/latest/getting-started/index.html

셀러리: 시작하기

출시버전: v4.0.2

출시일: 2017. 03. 15.

번역일: 2017. 03. 19. ~

셀러리 입문하기

  • 태스크 큐란 무엇인가? (What’s a Task Queue?)

  • 뭐가 필요한가요? (What do I need?)

  • 시작하기 (Get Started)

  • 셀러리는.. (Celery is..)

  • 셀러리의 기능들 (Features)

  • 프레임워크와 함께 이용하기 (Framework Integration)

  • 빠르게 찾아보기 (Quick Jump)

  • 셀러리 설치하기 (Installation)

브로커 (Brokers)

  • 브로커 가이드 (Broker Instructions)

    • RabbitMQ 사용하기 (Using RabbitMQ)
    • Redis 사용하기 (Using Redis)
    • Amazon SQS 사용하기 (Using Amazon SQS)
  • 브로커간 기능 (Broker Overview)

셀러리 한 발자국 내밀기 (First Steps with Celery)

  • 브로커 선택하기 (Choosing a Broker)

  • 셀러리 설치하기 (Installing Celery)

  • 앱 (Application)

  • 셀러리 워커 서버 띄우기 (Running the Celery worker server)

  • 태스크 호출하기 (Calling the task)

  • 결과 유지하기 (Keeping Results)

  • 설정하기 (Configuration)

  • 더 나아가기 (Where to go from here)

  • 문제 해결하기 (Troubleshooting)

더 알아보기 (Next Steps)

  • 셀러리를 기존 앱에서 사용하기 (Using Celery in your Application)

  • 태스크 호출하기 (Calling Tasks)

  • Canvas: 워크플로 디자인하기 (Canvas: Designing Work-flows)

  • 라우팅 (Routing)

  • 원격 제어 (Remote Control)

  • 타임존 (Timezone)

  • 최적화 (Optimization)

  • 더, 더, 더. (What to do now?)

[번역]셀러리 입문하기

글 작성 시점 최신 버전 v4.0.2의 문서입니다.

원문: http://docs.celeryproject.org/en/latest/getting-started/introduction.html

셀러리 입문하기

widgets:

  • 태스크 큐란 무엇인가? (What’s a Task Queue?)

  • 뭐가 필요한가요? (What do I need?)

  • 시작하기 (Get Started)

  • 셀러리는.. (Celery is..)

  • 셀러리의 기능들 (Features)

  • 프레임워크와 함께 이용하기 (Framework Integration)

  • 셀러리 설치하기 (Installation)

태스크 큐란 무엇인가?

태스크 큐는 스레드간 혹은 기계 간 업무를 분산하는 목적으로 만들어진 메커니즘입니다.

태스크 큐에 들어가는 일거리들은 태스크(Task)라고 불리고 각각 독립된 워커(Worker)프로세스들은 새로운 일거리(Task)가 없는지 지속적으로 태스크 큐를 감시합니다.

셀러리는 메시지(message)를 통해 통신하는데요, 보통 브로커(Broker)가 클라이언트와 워커 사이에서 메시지를 중계해줍니다. 브로커는 클라이언트가 큐에 새로 추가한 태스크를 메시지로 워커에 전달해줍니다.

셀러리 시스템에서는 여러개의 워커와 브로커를 함께 사용할 수 있는데요, 이 덕분에 높은 가용성과 Scaling이 가능합니다.

셀러리는 Python으로 짜여져 있습니다. 하지만 어떤 언어를 통해서도 프로토콜을 통해 셀러리를 사용할 수 있습니다. 예를들어, Node나 PHP를 위한 node-celeryPHP client도 있답니다.

또, HTTP endpoint를 통해 webhook으로 태스크를 요청하는 것도 가능하답니다.

뭐가 필요한가요?

셀러리를 사용하려면 메시지를 주고받아주는 브로커 등이 필요합니다. RabbitMQ나 Redis같은 브로커가 가장 좋은 선택이지만, 개발환경에서는 Sqlite와 같이 수많은 실험적인 대안도 있습니다.

셀러리는 단일 머신, 복수 머신, 혹은 데이터센터간에서도 사용 가능합니다.

시작하기

만약 여러분이 셀러리를 처음 이용하시거나 3.1버전 같은 이전 버전을 이용했다면, 셀러리 한 발자국 내밀기더 알아보기 튜토리얼을 먼저 해보세요.

버전 확인

셀러리 4.0은 Python2.7/3.4/3.5 PyPy5.4/5.5에서 정상적으로 동작합니다.

셀러리는..

  • 단순해요!

    셀러리는 사용하기도 쉽고 관리하기도 쉽습니다. 설정 파일도 필요하지 않아요!

    가장 단순한 셀러리 앱은 아래와 같이 만들 수 있어요.

    1
    2
    3
    4
    5
    6
    7
    from celery import Celery

    app = Celery('hello', broker='amqp://guest@localhost//')

    @app.task
    def hello():
    return 'hello world'
  • 높은 가용성

    워커와 클라이언트는 연결이 끊어지거나 실패하면 자동으로 다시 연결을 시도합니다. 그리고 몇몇 브로커들은 Primary/Primary나 Primary/Replica 의 복제방식을 통해 고가용성을 제공합니다.

  • 빨라요!

    하나의 셀러리 프로세스는 1분에 수십만개의 태스크를 처리할 수 있고, ms초 이하의 RTT(왕복지연시간)로 태스크를 처리 가능하답니다. (RabbitMQ, librabbitmq와 최적화된 설정을 할 경우)

  • 유연해요!

    셀러리의 대부분 파트는 그 자체로 이용할 수도 있고 원하는 만큼 확장도 가능합니다. Custom pool implementations, serializers, compression schemes, logging, schedulers, consumers, producers, broker transports을 포함해 더 많이요.

셀러리의 기능들

  • 모니터링

    모니터링 이벤트 스트림은 각 워커에서 나오고, 클러스터에서 수행하는 작업을 실시간으로 알려줍니다.

    더 알아보기..

  • 워크 플로우

    간단하거나 복잡한 워크플로우를 “캔버스”라는 도구를 통해 그룹핑, 체이닝, 청킹등을 할 수 있습니다.

    더 알아보기..

  • 시간 / 비율 제한

    태스크가 시간당 얼마나 실행 될지 제어할 수 있고, 한 태스크가 얼마나 오랫동안 실행될지 허용할 수 있습니다. 각각의 워커마다 다르게 설정하거나 각각의 태스크마다도 다르게 설정할 수 있답니다.

    더 알아보기..

  • 스케쥴링

    어떤 태스크를 정해진 시간에 실행할 수 있습니다. 또, 정해진 주기로 태스크를 실행 할 수도 있습니다. Crontab에서 사용하는 방식(분/시간/요일등등)을 이용할 수도 있습니다.

    더 알아보기..

  • 사용자 컴포넌트

    각각의 워커 컴포넌트들을 커스터마이징해 사용할 수 있습니다. 그리고 추가적인 컴포넌트도 커스터마이징해 사용할 수 있습니다. 워커는 내부 구조를 세밀하게 제어할수있는 종속성 그래프인 “bootsteps”를 사용하여 빌드됩니다.

프레임워크와 함께 이용하기

셀러리는 웹 프레임 워크와 쉽게 함께 사용할 수 있고, 일부는 합쳐진 패키지도 있습니다.

Django의 경우에는 장고와 함께하는 셀러리 첫걸음을 참고하세요.

통합 패키지가 굳이 필요하지는 않지만 개발을 더 쉽게 해주고 DB 커넥션 등에서 중요한 hook를 추가하기도 하기때문에 이용하는 편이 낫습니다.

설치하기

셀러리는 PyPI를 통해 쉽게 설치할 수 있습니다.

1
pip install -U pip

나만의 웹 크롤러 만들기(4): Django로 크롤링한 데이터 저장하기

좀 더 보기 편한 깃북 버전의 나만의 웹 크롤러 만들기가 나왔습니다!

(@2017.03.18) 본 블로그 테마가 업데이트되면서 구 블로그의 URL은 https://beomi.github.io/beomi.github.io_old/로 변경되었습니다. 예제 코드에서는 변경을 완료하였지만 캡쳐 화면은 변경하지 않았으니 유의 바랍니다.

이전게시글: 나만의 웹 크롤러 만들기(3): Selenium으로 무적 크롤러 만들기

Python을 이용해 requestsselenium을 이용해 웹 사이트에서 데이터를 크롤링해 보았습니다.

하지만 이러한 데이터를 체계적으로 관리하려면 DB가 필요하고, 이러한 DB를 만들고 관리하는 방법이 여러가지가 있지만 이번 가이드에서는 Python 웹 프레임워크인 django의 Database ORM을 이용해 DB를 만들고 데이터를 저장해 보려 합니다.

이번 가이드에서는 1회차 가이드였던 이 블로그를 크롤링해서 나온 결과물을 Django ORM으로 Sqlite DB에 저장해보는 것까지를 다룹니다.

이번 가이드는 기본적으로 Django의 Model에 대해 이해하고 있는 분들에게 추천합니다.

만약 django가 처음이시라면 DjangoGirls Tutorial: DjangoTube를 따라해보시면 기본적인 이해에 도움이 되시리라 생각합니다. 30분 내외로 따라가실 수 있습니다.

Django 프로젝트 만들기

widgets:

우선 크롤링 한 데이터를 저장할 Django 프로젝트와 앱을 만들고, Model을 통해 DB를 만들어야 합니다.

Django 설치하기

Django는 pip로 간편하게 설치할 수 있습니다.

가상환경을 이용해 설치하는 것을 추천합니다. 가상환경은 python3.4이후부터는 python3 -m venv 가상환경이름으로 만드실 수 있습니다.

1
pip install django

django install success

글 작성 시점인 2017.02.28 기준 1.10.5 버전이 최신버전입니다.

Django Start Project | 프로젝트 만들기

django가 성공적으로 설치되면 django-admin이라는 명령어로 장고 프로젝트를 생성할 수 있습니다.

이번 가이드에서는 websaver라는 이름의 프로젝트를 만들어보겠습니다.

1
django-admin startproject websaver

startproject websaver

성공적으로 생길 경우 어떠한 반응도 나타나지 않습니다.

위 명령어를 치면 명령어를 친 위치에 websaver라는 폴더가 생기고, 그 안의 구조는 아래와 같습니다.

cd websaverwebsaver폴더 안으로 진입한 상태입니다.

tree websaver

tree 명령어는 mac에서 brew install tree로 설치한 명령어입니다. 기본적으로는 깔려있지 않습니다.

이와 같이 manage.py파일과 프로젝트 이름인 websaver라는 이름의 폴더가 함께 생성됩니다.

Django Start App | 장고 앱 만들기

Django는 프로젝트와 그 안의 으로 관리됩니다. 이 은 하나의 기능을 담당하는 단위로 보시면 됩니다.

앱은 manage.py파일을 통해 startapp이라는 명령어로 생성 가능합니다. parsed_data라는 이름의 앱을 만들어보겠습니다.

1
python manage.py startapp parsed_data

manage.py 파일이 있는 곳에서 실행합니다. django가 설치된 가상환경에 진입해 있는지 꼭 확인하세요!

이제 아래와 같은 구조로 앱이 생겼을 것인데, 이 앱을 Django가 관리하도록 websaver폴더 안의 settings.py파일의 INSTALLED_APPS에 추가해줘야 합니다.

startapp parsed_data

유의: .pyc파일은 python실행시 생기는 캐싱 파일입니다. 없으셔도 전혀 문제는 발생하지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
# websaver/settings.py
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'parsed_data', # 앱을 추가해 줍시다.
]
...

Django First Migration | 첫 마이그레이션


장고는 python manage.py migrate이라는 명령어로 DB를 migrate합니다.

1
python manage.py migrate

위 명령어를 입력하면 아래와 같이 Django에서 사용하는 기본적인 DB가 생성됩니다.

First migrate

parsed_data App Model | parsed_data 앱 모델 만들기

이제 DB구조를 관리해주는 Model을 만들어 줘야 합니다.

Django에서 모델은 앱 단위로 만들어지고 구성됩니다. 따라서 앞서 만들어준 parsed_data앱 안의 models.py파일을 수정해줘야 합니다.

이 모델 파일은 크롤링해온 데이터를 필드별로 저장하는 것이 목적입니다. 따라서 크롤링한 데이터를 파이썬이 관리할 수 있는 객체로 만들어두는 것이 중요합니다.

이번 가이드에서는 나만의 웹 크롤러 만들기 With Requests/BeautifulSoup에서 만든 parser.py파일을 수정해 게시글의 title와 link를 DB에 저장해보겠습니다.

따라서 이번 앱의 모델에서는 title와 link라는 column을 가진 BlogData라는 이름의 Table을 DB에 만들면 됩니다.

django models의 class는 DB의 Table이 됩니다.

1
2
3
4
5
6
7
# parsed_data/models.py
from django.db import models


class BlogData(models.Model):
title = models.CharField(max_length=200)
link = models.URLField()

이와 같이 만들어주면 title은 200글자 제한의 CharField로, link는 URLField로 지정됩니다.

parsed_data App Makemigrations & Migrate | 앱 DB 반영하기

이제 해야 할 일은 Django가 모델을 관리하도록 하려면 makemigrations를 통해 DB의 변경 정보를 정리하고, migrate를 통해 실제 DB에 반영하는 과정을 진행해야 합니다.

django가 설치된 가상환경에서 실행하도록 합시다. 명령어의 실행 위치는 manage.py파일이 있는 곳입니다.

1
2
python manage.py makemigrations parsed_data
python manage.py migrate parsed_data

각 명령어 입력시 아래와 같이 결과가 나타난다면 성공적으로 DB에 반영된 것입니다.

parsed_data app migration

크롤링 함수 만들기

widgets:

나만의 웹 크롤러 만들기 With Requests/BeautifulSoup에서 만든 parser.py파일을 수정해보겠습니다.

이번 파일은 manage.py가 있는 위치에 parser.py라는 이름으로 저장해보겠습니다.

만약 requestsbs4가 설치되어있지 않다면 pip로 설치해주세요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# parser.py
import requests
from bs4 import BeautifulSoup
import json
import os

# python파일의 위치
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

req = requests.get('https://beomi.github.io/beomi.github.io_old/')
html = req.text
soup = BeautifulSoup(html, 'html.parser')
my_titles = soup.select(
'h3 > a'
)

data = {}

for title in my_titles:
data[title.text] = title.get('href')

with open(os.path.join(BASE_DIR, 'result.json'), 'w+') as json_file:
json.dump(data, json_file)

이전의 parser.py파일은 위와 같습니다. 이제 이 파일을 parse_blog라는 함수로 만들고, {‘블로그 글 타이틀’: ‘블로그 글 링크’}로 이루어진 딕셔너리를 반환하도록 만들어 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# parser.py
import requests
from bs4 import BeautifulSoup

def parse_blog():
req = requests.get('https://beomi.github.io/beomi.github.io_old/')
html = req.text
soup = BeautifulSoup(html, 'html.parser')
my_titles = soup.select(
'h3 > a'
)
data = {}
for title in my_titles:
data[title.text] = title.get('href')
return data

이제 parse_blog라는 함수를 다른 파일에서 import해 사용할 수 있습니다.

또한, 현재 프로젝트 폴더의 구조는 아래와 같습니다.

project folder tree

하지만 현재 parse_blog함수는 Django에 저장하는 기능을 갖고 있지 않습니다. 따라서 약간 더 추가를 해줘야 합니다.

Django 환경 불러오기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# parser.py
import requests
from bs4 import BeautifulSoup
# 아래 4줄을 추가해 줍니다.
import os
# Python이 실행될 때 DJANGO_SETTINGS_MODULE이라는 환경 변수에 현재 프로젝트의 settings.py파일 경로를 등록합니다.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "websaver.settings")
# 이제 장고를 가져와 장고 프로젝트를 사용할 수 있도록 환경을 만듭니다.
import django
django.setup()

def parse_blog():
req = requests.get('https://beomi.github.io/beomi.github.io_old/')
html = req.text
soup = BeautifulSoup(html, 'html.parser')
my_titles = soup.select(
'h3 > a'
)
data = {}
for title in my_titles:
data[title.text] = title.get('href')
return data

위 코드에서 아래 4줄을 추가해 줄 경우, 이 파일을 단독으로 실행하더라도 마치 manage.py을 통해 django를 구동한 것과 같이 django환경을 사용할 수 있게 됩니다.

Django ORM으로 데이터 저장하기

1
2
3
4
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "websaver.settings")
import django
django.setup()

python manage.py shell을 실행하는 것과 비슷한 방법입니다.

이제 models에서 우리가 만든 BlogData를 import해 봅시다.

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
# parser.py
import requests
from bs4 import BeautifulSoup
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "websaver.settings")
import django
django.setup()
# BlogData를 import해옵니다
from parsed_data.models import BlogData

def parse_blog():
req = requests.get('https://beomi.github.io/beomi.github.io_old/')
html = req.text
soup = BeautifulSoup(html, 'html.parser')
my_titles = soup.select(
'h3 > a'
)
data = {}
for title in my_titles:
data[title.text] = title.get('href')
return data

# 이 명령어는 이 파일이 import가 아닌 python에서 직접 실행할 경우에만 아래 코드가 동작하도록 합니다.
if __name__=='__main__':
blog_data_dict = parse_blog()
for t, l in blog_data_dict.items():
BlogData(title=t, link=l).save()

위와 같이 parser.py를 수정한 후 터미널에서 parser.py파일을 실행해 봅시다.

1
python parser.py

아무런 에러가 나지 않는다면 성공적으로 저장된 것입니다.

저장된 데이터 Django Admin에서 확인하기

SuperUser | 관리자계정 만들기

Django는 Django Admin이라는 강력한 기능을 제공합니다.

우선 Admin 계정을 만들어야 합니다. createsuperuser 명령어로 만들 수 있습니다.

createsuperuser

기본적으로 유저이름, 이메일, 비밀번호를 받습니다.

이메일은 입력하지 않아도 됩니다.

앱에 Admin 등록하기

Django가 어떤 앱을 admin에서 관리하도록 하려면 앱 폴더(parsed_data) 안의 admin.py파일을 수정해줘야 합니다.

1
2
3
4
5
6
7
# parsed_data/admin.py
from django.contrib import admin
# models에서 BlogData를 import 해옵니다.
from .models import BlogData

# 아래의 코드를 입력하면 BlogData를 admin 페이지에서 관리할 수 있습니다.
admin.site.register(BlogData)

Django Runserver | 장고 서버 실행하기

이제 manage.py가 있는 위치에서 runserver명령어로 장고 개발 서버를 실행해 봅시다.

1
python manage.py runserver

아래와 같이 나타난다면 성공적으로 서버가 실행된 것입니다.

django runserver

이제 http://localhost:8000/admin/로 들어가 봅시다.

Django admin login page

아까 createsuperuser로 만든 계정으로 로그인 해 봅시다.

django admin page

우리가 만든 parsed_data앱 안에 BlogData라는 항목이 나와있는 것을 볼 수 있습니다.

blogdata admin list

BlogData object라는 이름으로 데이터들이 들어와 있는 것을 확인할 수 있습니다. 하나를 클릭해 들어가 보면 아래와 같이 title와 link가 성공적으로 들어와 있는 것을 볼 수 있습니다.

blogdata specific data

약간 더 나아가기

widgets:

위에서 Admin페이지에 들어갈 때 모든 데이터들의 이름이 BlogData object로 나와있는 것을 볼 수 있습니다.

우리가 만들어 준 parsed_data/models.py파일의 BlogData Class를 살펴보면 models.Model클래스를 상속받아 만들었고, 이 클래스는 기본적으로 ClassName + object라는 값을 반환하는 __str__함수를 내장하고 있습니다.

따라서 models.Model을 상속받은 BlogData__str__함수에서는 BlogData object라는 값을 반환합니다. 이 __str__함수를 오버라이딩해 사용하면 Admin에서 데이터의 이름을 좀 더 직관적으로 알 수 있습니다.

str 함수 오버라이딩하기

parsed_data앱 폴더 안의 models.py파일을 아래와 같이 수정해 봅시다.

1
2
3
4
5
6
7
8
9
10
# parsed_data/models.py
from django.db import models


class BlogData(models.Model):
title = models.CharField(max_length=200)
link = models.URLField()

def __str__(self):
return self.title

위 코드는 BlogData 데이터 객체의 title 값을 반환합니다.

이제 장고 서버를 다시 켜주고 BlogData admin page로 들어가면 타이틀 이름으로 된 데이터들을 볼 수 있습니다.

title list admin page

현재 models.py파일을 수정했지만 DB에 반영되는 사항이 아니기 때문에 makemigrationsmigrate를 해줄 필요가 없습니다.

다음 가이드에서는..

widgets:

다음 가이드는 주기적으로 데이터를 크롤링 해, 새로운 데이터가 생기는 경우 텔레그램 봇으로 메시지 알림을 보내주는 과정을 다룰 예정입니다.

다음 가이드: 나만의 웹 크롤러 만들기(5): 웹페이지 업데이트를 알려주는 Telegram 봇

나만의 웹 크롤러 만들기(3): Selenium으로 무적 크롤러 만들기

좀 더 보기 편한 깃북 버전의 나만의 웹 크롤러 만들기가 나왔습니다!

Updated @ 2019.10.10. Typo/Layout fix, 네이버 로그인 Captcha관련 수정 추가

이전게시글: 나만의 웹 크롤러 만들기(2): Login with Session

Selenium이란?

Selenium은 주로 웹앱을 테스트하는데 이용하는 프레임워크다. webdriver라는 API를 통해 운영체제에 설치된 Chrome등의 브라우저를 제어하게 된다.

브라우저를 직접 동작시킨다는 것은 JavaScript를 이용해 비동기적으로 혹은 뒤늦게 불러와지는 컨텐츠들을 가져올 수 있다는 것이다. 즉, ‘눈에 보이는’ 컨텐츠라면 모두 가져올 수 있다는 뜻이다. 우리가 requests에서 사용했던 .text의 경우 브라우저에서 ‘소스보기’를 한 것과 같이 동작하여, JS등을 통해 동적으로 DOM이 변화한 이후의 HTML을 보여주지 않는다. 반면 Selenium은 실제 웹 브라우저가 동작하기 때문에 JS로 렌더링이 완료된 후의 DOM결과물에 접근이 가능하다.

어떻게 설치하나?

pip selenium package

Selenium을 설치하는 것은 기본적으로 pip를 이용한다.

1
pip install selenium

참고: Selenium의 버전은 자주 업데이트 되고, 브라우저의 업데이트 마다 새로운 Driver를 잡아주기 때문에 항상 최신버전을 깔아 주는 것이 좋다.

이번 튜토리얼에서는 BeautifulSoup이 설치되어있다고 가정합니다.

BeautifulSoup은 pip install bs4로 설치 가능합니다.

webdriver

Selenium은 webdriver라는 것을 통해 디바이스에 설치된 브라우저들을 제어할 수 있다. 이번 가이드에서는 Chrome을 사용해 볼 예정이다.

Chrome WebDriver

크롬을 사용하려면 로컬에 크롬이 설치되어있어야 한다.

그리고 크롬 드라이버를 다운로드 받아주자.

https://sites.google.com/a/chromium.org/chromedriver/downloads

글 수정일자인 2019년 10월 10일에는 크롬 77버전이 최신이며, 해당하는 크롬 드라이버를 받아야 한다.

Update @ 2019.10.10

크롬에서는 현재 버전별 지정된 chromedriver를 받도록 안내하며, 버전에 일치하지 않는 드라이버를 사용하면 에러가 납니다.

현재 사용하는 크롬의 버전은 크롬 창에 👉 chrome://version 👈 이 URL을 주소창에 그대로 입력하면(http없이) 버전을 확인할 수 있습니다.

스크린샷 2019-10-10 오후 5.49.07

스크린샷 2019-10-10 오후 5.53.25

버전을 클릭하면 아래와 같은 OS별 Driver파일이 나열되어있다. 사용하는 OS에 따른 driver를 받아주자.

스크린샷 2019-10-10 오후 5.56.03

zip파일을 받고 풀어주면 chromedriver라는 파일이 저장된다.

위 폴더를 기준으로 할 경우 /Users/beomi/Downloads/chromedriver가 크롬드라이버 파일의 위치가 된다.

이 경로를 나중에 Selenium 객체를 생성할 때 지정해 주어야 한다. (그래야 python이 chromedriver를 통해 크롬 브라우저를 조작할 수 있다!)

PhantomJS webdriver

단, 2018년+ 기준 PhantomJS는 더이상 개발되지 않고 있기 때문에 앞으로는 크롬의 headless모드를 사용하는 것을 추천합니다.

PhantomJS는 기본적으로 WebTesting을 위해 나온 Headless Browser다.(즉, 화면이 존재하지 않는다)

하지만 JS등의 처리를 온전하게 해주며 CLI환경에서도 사용이 가능하기 때문에, 만약 CLI서버 환경에서 돌아가는 크롤러라면 PhantomJS를 사용하는 것도 방법이다.

PhantomJS는 공식 프로젝트의 PhantomJS Download Page에서 받을 수 있다.

Binary 자체로 제공되기 때문에, Linux를 제외한 OS에서는 외부 dependency없이 바로 실행할 수 있다.

Extracted PhantomJS Zip file

압축을 풀어주면 아래와 같은 많은 파일들이 있지만, 우리가 사용하는 것은 bin폴더 안의 phantomjs파일이다.

위 폴더 기준으로 할 경우 /Users/beomi/Downloads/phantomjs-2.1.1-macosx/bin/phantomjs가 PhantomJS드라이버의 위치다.

Selenium으로 사이트 브라우징

Selenium은 webdriver api를 통해 브라우저를 제어한다.

우선 webdriver를 import해주자.

1
from selenium import webdriver

이제 driver라는 이름의 webdriver 객체를 만들어 주자.

이름이 꼭 driver일 필요는 없다.

이번 가이드에서는 크롬을 기본적으로 이용할 예정이다.

1
2
3
4
5
6
from selenium import webdriver

# Chrome의 경우 | 아까 받은 chromedriver의 위치를 지정해준다.
driver = webdriver.Chrome('/Users/beomi/Downloads/chromedriver')
# PhantomJS의 경우 | 아까 받은 PhantomJS의 위치를 지정해준다.
# driver = webdriver.PhantomJS('/Users/beomi/Downloads/phantomjs-2.1.1-macosx/bin/phantomjs')

Selenium은 기본적으로 웹 자원들이 모두 로드될때까지 기다려주지만, 암묵적으로 모든 자원이 로드될때 까지 기다리게 하는 시간을 직접 implicitly_wait을 통해 지정할 수 있다.

1
2
3
4
5
from selenium import webdriver

driver = webdriver.Chrome('/Users/beomi/Downloads/chromedriver')
# 암묵적으로 웹 자원 로드를 위해 3초까지 기다려 준다.
driver.implicitly_wait(3)

이제 특정 url로 브라우저를 켜 보자.

1
2
3
4
5
6
from selenium import webdriver

driver = webdriver.Chrome('/Users/beomi/Downloads/chromedriver')
driver.implicitly_wait(3)
# url에 접근한다.
driver.get('https://google.com')

만약 chromedriver의 위치가 정확하다면 새 크롬 화면이 뜨고 구글 첫 화면으로 들어가질 것이다.

Selenium은 driver객체를 통해 여러가지 메소드를 제공한다.

아래의 메소드들은 보통 driver.~~~ 방식으로 사용합니다.

URL에 접근하는 메소드,

  • get('http://url.com')

페이지의 단일 element에 접근하는 메소드,

  • find_element_by_name('HTML_name')
  • find_element_by_id('HTML_id')
  • find_element_by_xpath('/html/body/some/xpath')
  • find_element_by_css_selector('#css > div.selector')
  • find_element_by_class_name('some_class_name')
  • find_element_by_tag_name('h1')

페이지의 여러 elements에 접근하는 메소드 등이 있다. (대부분 elementelements 로 바꾸기만 하면 된다.)

  • find_elements_by_css_selector('#css > div.selector')

위 메소드들을 활용시 HTML을 브라우저에서 파싱해주기 때문에 굳이 Python와 BeautifulSoup을 사용하지 않아도 된다.

하지만 Selenium에 내장된 함수만 사용가능하기 때문에 좀더 사용이 편리한 soup객체를 이용하려면 driver.page_source API를 이용해 현재 렌더링 된 페이지의 Elements를 모두 가져올 수 있다.

driver.page_source: 브라우저에 보이는 그대로의 HTML, 크롬 개발자 도구의 Element 탭 내용과 동일.

requests 통해 가져온 req.text: HTTP요청 결과로 받아온 HTML, 크롬 개발자 도구의 페이지 소스 내용과 동일.

위 2개는 사이트에 따라 같을수도 다를수도 있습니다.

네이버 로그인 하기

네이버는 requests를 이용해 로그인하는 것이 어렵다. 프론트 단에서 JS처리를 통해 로그인 처리를 하기 때문인데, Selenium을 이용하면 보다 쉽게 로그인을 할 수 있다.

1
2
3
4
5
6
from selenium import webdriver

driver = webdriver.Chrome('/Users/beomi/Downloads/chromedriver')
driver.implicitly_wait(3)
# url에 접근한다.
driver.get('https://nid.naver.com/nidlogin.login')

네이버 로그인 화면을 확인 해 보면 아이디를 입력받는 부분의 name이 id, ​비밀번호를 입력받는 부분의 name이 pw인 것을 알 수 있다.

Naver Login Page

find_element_by_name을 통해 아이디/비밀번호 input 태그를 잡아주고, 값을 입력해 보자.

1
2
3
4
5
6
7
8
from selenium import webdriver

driver = webdriver.Chrome('/Users/beomi/Downloads/chromedriver')
driver.implicitly_wait(3)
driver.get('https://nid.naver.com/nidlogin.login')
# 아이디/비밀번호를 입력해준다.
driver.find_element_by_name('id').send_keys('naver_id')
driver.find_element_by_name('pw').send_keys('mypassword1234')

Naver Login Input

성공적으로 값이 입력된 것을 확인할 수 있다.

이제 Login버튼을 눌러 실제로 로그인이 되는지 확인해 보자.

1
2
3
4
5
6
7
8
9
from selenium import webdriver

driver = webdriver.Chrome('/Users/beomi/Downloads/chromedriver')
driver.implicitly_wait(3)
driver.get('https://nid.naver.com/nidlogin.login')
driver.find_element_by_name('id').send_keys('naver_id')
driver.find_element_by_name('pw').send_keys('mypassword1234')
# 로그인 버튼을 눌러주자.
driver.find_element_by_xpath('//*[@id="frmNIDLogin"]/fieldset/input').click()

Update @ 2019.10.10.

하지만 로그인이 이전처럼 잘 되지 않고 Captcha를 요구하는 창이 뜰 수 있다.

네이버에서

성공적으로 로그인이 되는 것을 확인할 수 있다.

Naver Login Success

로그인이 필요한 페이지인 네이버 페이의 주문내역 페이지를 가져와보자.

Naver Pay Order

네이버 페이의 Url은 https://order.pay.naver.com/home 이다. 위 페이지의 알림 텍스트를 가져와 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from selenium import webdriver
from bs4 import BeautifulSoup

driver = webdriver.Chrome('/Users/beomi/Downloads/chromedriver')
driver.implicitly_wait(3)
driver.get('https://nid.naver.com/nidlogin.login')
driver.find_element_by_name('id').send_keys('naver_id')
driver.find_element_by_name('pw').send_keys('mypassword1234')
driver.find_element_by_xpath('//*[@id="frmNIDLogin"]/fieldset/input').click()

# Naver 페이 들어가기
driver.get('https://order.pay.naver.com/home')
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')
notices = soup.select('div.p_inr > div.p_info > a > span')

for n in notices:
print(n.text.strip())

로그인이 잘 되고, 성공적으로 리스트를 받아오는 것을 확인해 볼 수 있다.

Result

정리하기

Selenium은 웹 테스트 자동화 도구이지만, 멋진 크롤링 도구로 사용할 수 있다.

또한, BeautifulSoup와 함께 사용도 가능하기 때문에 크롤링을 하는데 제약도 줄어 훨씬 쉽게 크롤링을 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from selenium import webdriver
from bs4 import BeautifulSoup

# setup Driver|Chrome : 크롬드라이버를 사용하는 driver 생성
driver = webdriver.Chrome('/Users/beomi/Downloads/chromedriver')
driver.implicitly_wait(3) # 암묵적으로 웹 자원을 (최대) 3초 기다리기
# Login
driver.get('https://nid.naver.com/nidlogin.login') # 네이버 로그인 URL로 이동하기
driver.find_element_by_name('id').send_keys('naver_id') # 값 입력
driver.find_element_by_name('pw').send_keys('mypassword1234')
driver.find_element_by_xpath(
'//*[@id="frmNIDLogin"]/fieldset/input'
).click() # 버튼클릭하기
driver.get('https://order.pay.naver.com/home') # Naver 페이 들어가기
html = driver.page_source # 페이지의 elements모두 가져오기
soup = BeautifulSoup(html, 'html.parser') # BeautifulSoup사용하기
notices = soup.select('div.p_inr > div.p_info > a > span')

for n in notices:
print(n.text.strip())

다음 가이드

Selenium으로 많은 사이트에서 여러 정보를 가져와 볼 수 있게 되었습니다.

하지만 가져온 데이터를 DB에 저장하려면 약간의 어려움이 따르게 됩니다.

다음 시간에는 Django의 ORM을 이용해 sqlite3 DB에 데이터를 저장해보는 방법에 대해 알아보겠습니다.

다음 가이드: 나만의 웹 크롤러 만들기(4): Django로 크롤링한 데이터 저장하기

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×