Skip to content

Commit

Permalink
Refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
amazingguni committed Feb 2, 2020
1 parent 48802b7 commit f9422f1
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 45 deletions.
6 changes: 4 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ jobs:
name: Reserve pilates
command: |
. venv/bin/activate
python pila.py -u "${USER}" -p "${PASSWORD}" -w "${WEEKDAYS}" -t "${TIME}" << parameters.extra-opts >>
python pila.py -u "${USER}" -p "${PASSWORD}" \
-w "${WEEKDAYS}" -t "${TIME}" \
-s "${SLACK_TOKEN}" -c "${SLACK_CHANNEL}" << parameters.extra-opts >>
workflows:
commit:
Expand All @@ -43,7 +45,7 @@ workflows:
openning-soyeon:
triggers:
- schedule:
cron: "50 2 * * *"
cron: "50 2 * * 0,2,4"
filters:
branches:
only:
Expand Down
105 changes: 105 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Pila Bot

필라테스 예약을 하기 위해 만들어진 Repository입니다. CircleCI를 통해 주기적으로 구동되며 여러대의 워커에서 Selenium으로 브라우저를 열어 예약 작업을 진행합니다.

## How to use

먼저 GitHub Repository를 자신의 계정으로 Fork합니다.

![fork1](img/fork1.png)

Fork한 Repository에 들어가서 `.circleci/config.yml`의 내용을 다음과 같이 바꿔줍니다.

> 기존에는 2명 이상을 지원하기 위해 `context`라는 기능을 사용하고 있지만 fork해서 구성하는 경우에는 불필요합니다. 그 부분을 제거합니다.
```yml
version: 2.1
jobs:
pila:
docker:
- image: circleci/python:3.6-browsers
parallelism: 2
parameters:
extra-opts:
type: string
default: ""
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "requirements.txt" }}
- v1-dependencies-

- run:
name: install dependencies
command: |
python3 -m venv venv
. venv/bin/activate
pip install -r requirements.txt
- save_cache:
paths:
- ./venv
key: v1-dependencies-{{ checksum "requirements.txt" }}

- run:
name: Reserve pilates
command: |
. venv/bin/activate
python pila.py -u "${USER}" -p "${PASSWORD}" -w "${WEEKDAYS}" -t "${TIME}" << parameters.extra-opts >>
workflows:
commit:
jobs:
- pila
openning:
triggers:
- schedule:
cron: "50 2 * * *"
filters:
branches:
only:
- master
jobs:
- pila:
extra-opts : "--wait-opening"
```
Fork한 Repository를 연동하기 위해 CircleCI에 접속합니다.
- https://circleci.com/dashboard
좌측 상단에 자신의 아이디가 선택되었는지 확인한 이후에 좌측의 `ADD PROJECT`를 클릭합니다.

![add_projects_1.png](img/add_projects_1.png)

`pila-bot`을 찾고 우측의 `Set Up Project` 버튼을 클릭합니다.

![add_projects_2.png](img/add_projects_2.png)

`Start Building` 버튼을 클릭합니다.

![add_projects_3.png](img/add_projects_3.png)

여기까지 진행하면 CircleCI가 첫번째 작업을 실행합니다. 하지만 계정정보를 입력하지 않았기 때문에 에러가 발생할 것입니다. 다시 CircleCI로 접속해 계정과 예약 정보를 입력해줍니다.

먼저 [CircleCI로 다시 접속](https://circleci.com/dashboard)하고 `pila-bot`의 설정 페이지로 접속(톱니 아이콘)합니다.

![setting.png](img/setting.png)

좌측의 `BUILD SETTINGS - Environment Variables` 메뉴를 선택합니다.

Environment Variables 페이지에서 `Add Variables` 버튼을 눌러 하나씩 환경 변수를 추가해줍니다.

|Name|Desc|Example|
|---|---|---|
|USER|계정 이름|1234|
|PASSWORD|접속 비밀번호|1234|
|TIME|예약을 원하는 날짜와 시간(여러개일 경우 ,로 구분)|월20:00,목19:30|

그리고 좌측의 `JOBS` 메뉴를 누르고 실패했던 빌드를 클릭해 `Rerun workflow`를 클릭하면 완료됩니다

> 혹시 모르니.. 자동 예약이 수행될때 잘 되는지 모니터링해주세요 :)





Binary file added img/add_projects_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/add_projects_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/add_projects_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/fork1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/setting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
105 changes: 62 additions & 43 deletions pila.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,21 @@

from pytz import timezone
from datetime import datetime, timedelta
from slacker import Slacker

TIMEOUT = 20
LONG_TIMEOUT = 30

# 화요일에는 수목가 열리고, 목요일에는 금토이 열리고, 일요일에는 월화
OPENING_HOUR = 12
OPENING_MINUTE = 0
# OPENING_HOUR = 21
# OPENING_MINUTE = 57

# 화요일에는 수목가 열리고, 목요일에는 금토이 열리고, 일요일에는 월화
KST = timezone('Asia/Seoul')

def get_indexes_of_target_weekday_indexes(target_weekday_str):
WEEKDAY_STRS = '월화수목금토일'
weekday_indexes = []
for each in target_weekday_str:
if each not in WEEKDAY_STRS:
print(f'[ignored] {each} is not weekday string')
weekday_indexes.append(WEEKDAY_STRS.index(each))
return weekday_indexes
KST = timezone('Asia/Seoul')

def get_target_dates(target_weekday_indexes):
now = datetime.now().astimezone(KST)
# today_weekday = now.weekday()
# open_weekday = [1, 3, 6]
# if today_weekday not in open_weekday:
# return []
target_dates = [now + timedelta(days=1), now + timedelta(days=2)]
return list(filter(lambda x: x.weekday() in target_weekday_indexes, target_dates))
def get_index_of_weekday(weekday_str):
WEEKDAY_STRS = '일월화수목금토'
return WEEKDAY_STRS.index(weekday_str)

def init_browser():
options = Options()
Expand Down Expand Up @@ -76,13 +62,16 @@ def is_reserved_before(reservelist_element):
def is_openned(reservelist_element):
return '관련내용이 존재하지 않습니다' not in reservelist_element.text

def reserve_date_class(browser, target_date, target_time):
target_date_str = target_date.strftime("%Y-%m-%d")
print(f'Try to reserve {target_date_str} {target_date.strftime("%A")}')
def reserve_date_class(browser, target_datetime):
target_date_str = target_datetime.strftime("%Y-%m-%d")
target_time_str = target_datetime.strftime("%H:%M")
target_weekday = target_datetime.strftime("%A")
print(f'Try to reserve {target_date_str} {target_weekday}')
TOTAL_RETRY_CNT = 30
for i in range(TOTAL_RETRY_CNT):
browser.execute_script(f"funcSearch01('{target_date_str}','C')")
reservelist_element = WebDriverWait(browser, TIMEOUT).until(EC.presence_of_element_located((By.CSS_SELECTOR, '#reserveList')))
reservelist_element = WebDriverWait(browser, TIMEOUT).until(
EC.presence_of_element_located((By.CSS_SELECTOR, '#reserveList')))
if is_reserved_before(reservelist_element):
print(f'You have a reservation for this day.({target_date_str})')
return []
Expand All @@ -98,7 +87,7 @@ def reserve_date_class(browser, target_date, target_time):
class_name = li.find_element_by_css_selector('.mName div').text
class_time = li.find_element_by_css_selector('.rTime div').text
class_num = li.find_element_by_css_selector('.rNum div').text
if target_time not in class_time:
if target_time_str not in class_time:
continue
if '(정원초과)' in class_num:
print(f'Can not reserve {class_name} {class_time}(정원초과)')
Expand All @@ -111,7 +100,8 @@ def reserve_date_class(browser, target_date, target_time):
button_element = li.find_element_by_css_selector('.rbutton .complete5')
button_element.click()
# 상세 보기 팝업에서 예약 버튼 클릭
reserve_button_element = WebDriverWait(browser, TIMEOUT).until(EC.element_to_be_clickable((By.CSS_SELECTOR, '.AVBtn')))
reserve_button_element = WebDriverWait(browser, TIMEOUT).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, '.AVBtn')))
reserve_button_element.click()

# 수강 신청 완료 확인 버튼 클릭
Expand All @@ -138,43 +128,65 @@ def wait_for_openning_time(hour=OPENING_HOUR, minute=OPENING_MINUTE):
if remain_seconds < 10: break
time.sleep(remain_seconds / 2)

def main(user, password, weekdays, target_time, wait_opening, slack_token, slack_channel):

def send_slack_message(token, channel, message):
if not slack_token:
return
slack = Slacker(slack_token)
slack.chat.post_message(channel, message)

def main(user, password, target_datetimes, wait_opening, slack_token, slack_channel):
try:
browser = init_browser()
print('Start crawling')
print(f'Login {user}')
login(browser, user, password)
display_date = get_display_date(browser)
print(f'Today: {display_date}')
target_weekday_indexes = get_indexes_of_target_weekday_indexes(weekdays)
target_dates = get_target_dates(target_weekday_indexes)
if not target_dates:
print(f'There is not date to reservation.({weekdays})')
return
target_dates_str = ''.join([d.strftime("%Y-%m-%d %H:%M:%S") for d in target_dates])
print(f'target_dates: {target_dates_str}')
if wait_opening:
print('Wait opening time')
wait_for_openning_time()
reserved_classes = []
for date in target_dates:
reserved_classes += reserve_date_class(browser, date, target_time)
for target_datetime in target_datetimes:
reserved_classes += reserve_date_class(browser, target_datetime)

if reserved_classes:
print()
print(f'[Reserved classes]')
for each in reserved_classes:
print(f' - {each}')
message = f'[{user}] Successfully book pilates classes :dancer::dancer:\n' \
+ '\n'.join([f'- {each}' for each in reserve_clasees])
print(message)
send_slack_message(slack_token, slack_channel, message)
else:
print('Couldn\'t book pilates classes')
message = f'[{user}] Couldn\'t book pilates classes :sob::sob:'
print(message)
send_slack_message(slack_token, slack_channel, message)

finally:
pass

def get_target_datetimes(time_str):
now = datetime.now().astimezone(KST)
ret = []
for each in time_str.split(','):
weekday_idx = get_index_of_weekday(each[0])
hour, minute = each[1:].split(':')
hour = int(hour)
minute = int(minute)
diff_day = (weekday_idx - now.weekday() ) % 7
if diff_day > 2: continue
target = now + timedelta(days=(weekday_idx - now.weekday() ) % 7)
target = target.replace(hour=hour, minute=minute, second=0, microsecond=0)
ret.append(target)
return ret


if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--user', '-u', required=True, help='Username of pilates homepage')
parser.add_argument('--password', '-p', required=True, help='Password of pilates homepage')
parser.add_argument('--weekdays', '-w', required=True, help='Day of the class you want to book(e.g, 월수금)')
parser.add_argument('--time', '-t', required=True, help='The time of the class you want to book(e.g, 20:00)')
parser.add_argument('--slack-token', '-s', required=False, help='Slack token')
parser.add_argument('--slack-channel', '-c', required=False, help='Slack channel')
Expand All @@ -183,13 +195,20 @@ def main(user, password, weekdays, target_time, wait_opening, slack_token, slack

user = args.user
password = args.password
weekdays = args.weekdays
target_time = args.time
target_datetimes = get_target_datetimes(args.time)
slack_token = args.slack_token
slack_channel = args.slack_channel
wait_opening = args.wait_opening
now = datetime.now().astimezone(KST)

print(f'Now: {now.strftime("%Y-%m-%d %H:%M:%S")}')
print(f'Book pilates class at {target_time} on {weekdays} now.({user})')
main(user, password, weekdays, target_time, wait_opening, slack_token, slack_channel)
print(f'Book pilates class at {args.time} now.({user})')
if not target_datetimes:
print(f'There is not date to reservation.({args.time})')
exit(1)
for target in target_datetimes:
print(f'Target Datetime')
print(f' - {target.strftime("%Y-%m-%d %H:%M %A")}')

main(user, password, target_datetimes, wait_opening, slack_token, slack_channel)

0 comments on commit f9422f1

Please sign in to comment.