devxoul
1/21/2014 - 7:19 PM

[Deprecated] 코레일 승차권 검색 및 예매 프로그램. https://github.com/devxoul/korail

[Deprecated] 코레일 승차권 검색 및 예매 프로그램. https://github.com/devxoul/korail

# -*- coding: utf-8 -*-

"""
This code won't be updated anymore.
Project has moved to repository: https://github.com/devxoul/korail
"""

import requests
from bs4 import BeautifulSoup
from datetime import datetime

session = requests.session()


class Train(object):
    #: 기차 종류
    train_type = None

    #: 출발역 코드
    dep_code = None

    #: 출발날짜 (yyyyMMdd)
    dep_date = None

    #: 출발시각 (hhmmss)
    dep_time = None

    #: 도착역 코드
    arr_code = None

    #: 도착 시각
    arr_time = None

    #: 인원
    count = 0

    #: 특실 예약가능 여부
    first_class = False

    #: 일반실 예약가능 여부
    general_admission = False

    def __repr__(self):
        return '[%s] %s~%s(%s~%s) [특실:%d][일반실:%d]' % (
            self.train_type.encode('utf-8'),
            self.dep_code.encode('utf-8'),
            self.dep_time.encode('utf-8'),
            self.arr_code.encode('utf-8'),
            self.arr_time.encode('utf-8'),
            self.first_class,
            self.general_admission,
        )


def all_stations():
    stations = []
    for i in range(14):
        url = 'http://www.korail.com/servlets/pr.pr11100.sw_pr11111_f1Svt'
        params = {
            'hidKorInx': i,
        }
        r = requests.get(url, params=params)
        html = r.text.split('<table class="s-view">')[3]
        rows = html.split("javascript:putStation('")[1:]
        for row in rows:
            name = row.split("'")[0]
            id = row.split(",'")[1].split("'")[0]
            stations.append((id, name))
    return stations


def login(id, password, use_phone=False):
    """
    :param id: 코레일 멤버십 번호 또는 휴대전화번호
    :param password: 비밀번호
    :param use_phone: 휴대전화번호 로그인 여부
    """
    url = 'https://www.korail.com/servlets/hc.hc14100.sw_hc14111_i2Svt'
    data = {
        'selInputFlg': '4' if use_phone else '2',  # 멤버십번호: 2, 휴대전화로그인: 4
        'UserId': id,
        'UserPwd': password,
        'hidMemberFlg': '1',  # 없으면 입력값 오류
    }
    r = session.post(url, data=data)
    if 'w_mem01106' in r.text:
        return True
    return False


def logout():
    url = 'http://www.korail.com/2007/mem/mem01000/w_mem01102.jsp'
    session.get(url)


def euckr(s):
    """
    문자열을 EUC-KR 유니코드로 변환하여 리턴한다.
    """
    return unicode(s, 'utf-8').encode('euc_kr')


def search(dep_code, arr_code, date, time='000000', train='05', count=1):
    """
    :param dep_code: 출발역 코드
    :param arr_code: 도착역 코드
    :param date: 날짜 (yyyyMMdd)
    :param time: 시간 (hhmmss)
    :param train: 기차 종류
                  - 00: KTX
                  - 01: 새마을호
                  - 02: 무궁화호
                  - 03: 통근열차
                  - 04: 누리로
                  - 05: 전체 (기본값)
                  - 06: 공학직통
                  - 09: ITX-청춘
    :param count: 인원
    """
    url = 'http://www.korail.com/servlets/pr.pr21100.sw_pr21111_i1Svt'
    params = {
        'txtGoAbrdDt': date,
        'txtGoHour': time,
        'txtGoStartCode': dep_code,
        'txtGoEndCode': arr_code,
        'checkStnNm': 'N',
        'radJobId': '1',  # 직통
        'txtPsgCnt1': count,
    }
    r = session.get(url, params=params)

    html = BeautifulSoup(r.text)
    error = html.select('.point02')
    if error:
        print error[0].string.strip()
        return

    rows = html.select('table.list-view tr')[1:]

    trains = []
    train_info = r.text.split('new train_info(')[1:]
    i = 0
    for info in train_info:
        obj = info.split(',')
        train = Train()
        train.train_type = obj[22].strip()[1:-1]
        train.dep_code = obj[18].strip()[1:-1]
        train.dep_date = obj[24].strip()[1:-1]
        train.dep_time = obj[25].strip()[1:-1]
        train.arr_code = obj[19].strip()[1:-1]
        train.arr_time = obj[27].strip()[1:-1]
        train.count = count

        td7s = rows[i].select('td[width=7%]')

        #특실
        img = td7s[0].select('img')
        content = img[0] if img else td7s[0].contents[0]
        train.first_class = 'yes' in content.__str__()

        #일반실
        img = td7s[1].select('img')
        content = img[0] if img else td7s[0].contents[0]
        train.general_admission = 'yes' in content.__str__()

        print train.first_class, train.general_admission
        trains.append(train)
        i += 1

    return trains


def reserve(train):
    """
    승차권 예약

    :param train: `Train` 인스턴스
    """
    session.headers = {
        'Referer': 'http://www.korail.com/servlets/pr.pr21100.sw_pr21111_i1Svt'
    }
    url = 'http://www.korail.com/servlets/pr.pr12100.sw_pr12111_i1Svt'
    data = {
        'txtCompaCnt1': train.count,  # 인원
        'txtDptRsStnCd1': train.dep_code,  # 출발역 코드
        'txtDptDt1': train.dep_date,  # 출발날짜
        'txtDptTm1': train.dep_time,  # 출발시각
        'txtArvRsStnCd1': train.arr_code,  # 도착역 코드
        'txtTrnClsfCd1': train.train_type,  # 열차 종류 (Train Class Code인듯)
        'txtSeatAttCd4': '15',  # ??? - 요구속성 (다른 번호는 창측/내측 이런거던데...)
        'txtPsgTpCd1': '1',  # ??? - 단체예약, 개인예약 구분이라는데...
        'txtJobId': '1101',  # 1101: 개인예약, 1102: 예약대기, 1103: SEATMAP예약
        'txtJrnyCnt': '1',  # 환승 횟수 (1이면 편도)
        'txtPsrmClCd1': '1',  # 1: 일반실, 2: 특실
        'txtJrnySqno1': '001',  # ???
        'txtJrnyTpCd1': '11',  # 편도
    }
    r = session.post(url, data=data)
    if 'w_mem01100.jsp' in r.text:
        print 'Need login!!'
        return False

    elif u'홈페이지주소를 잘못 입력하셨습니다.' in r.text:
        print u'Referer 오류'
        return False

    elif u'20분 이내 열차는' in r.text:
        print '20분 이내 열차는 예약하실 수 없습니다. 역창구 및 자동발매기를 이용하시기 바랍니다.'
        return False

    elif u'오류' in r.text:
        html = BeautifulSoup(r.text)
        error = html.select('.point02')
        print error[0].string.strip()
        return False

    elif 'w_adv03100.gif' in r.text:
        print 'Reservation Success!!'
        return True

    print 'Unhandled Error.'
    print r.text
    return False


def ticket_ids():
    """
    승차권 id 목록 조회
    """
    url = 'http://www.korail.com/pr/pr13500/w_pr13510.jsp'
    r = session.get(url)
    tickets = []
    pnrs = r.text.split("new pnr_info( '")
    for pnr in pnrs[1:]:
        ticket_id = pnr.split("'")[0]
        tickets.append(ticket_id)
    return tickets


def cancel(pnr_id):
    """
    예약취소

    :param pnr_id: 예약취소할 승차권 id
    """
    url = 'http://www.korail.com/servlets/pr.pr14500.sw_pr14514_i1Svt?'
    data = {
        'txtPnrNo': pnr_id,
        'txtLngScnt': '01',
        'txtJrnySqno': '001',
    }
    r = session.get(url, params=data)
    return u'정상적으로 취소가 완료되었습니다.' in r.text


phone = ''
password = ''
stations = all_stations()
dep_code = [station for station in stations if station[1] == u'서울'][0][0]
arr_code = [station for station in stations if station[1] == u'동대구'][0][0]
dep_date = datetime.strftime(datetime.now(), '%Y%m%d')
dep_time = datetime.strftime(datetime.now(), '%H%M%S')

print '서울역:', dep_code
print '동대구역:', arr_code

print 'Logging in...'
r = login(phone, password, True)
if not r:
    print 'Login failed.'
    exit()
print 'Login succeeded.'

trains = search(dep_code, arr_code, dep_date, dep_time, 2)
for t in trains:
    print t

reserve(trains[-1])

tickets = ticket_ids()
for ticket_id in tickets:
    if cancel(ticket_id):
        print 'Canceled reservation.'