Skip to content

Repository files navigation

πŸš„ pykorail

파이썬으둜 KTX ν‘œλ₯Ό μ‘°νšŒν•˜κ³  μ˜ˆλ§€ν•©λ‹ˆλ‹€

μ½”λ ˆμΌ 슀마트 예맀(μ½”λ ˆμΌν†‘) API λ₯Ό 감싼 비곡식 ν΄λΌμ΄μ–ΈνŠΈ

CI PyPI Python License

λΉ λ₯Έ μ‹œμž‘ Β· ν•  수 μžˆλŠ” 것 Β· API 레퍼런슀 Β· κΈ°μ—¬ν•˜κΈ°


λΉ λ₯Έ μ‹œμž‘

pip install pykorail
from datetime import datetime
from pykorail import Korail

with Korail.logged_in("me@example.com", "password") as korail:
    trains = korail.trains.search("μ„œμšΈ", "λΆ€μ‚°", depart_after=datetime(2026, 4, 1, 9, 0))
    for train in trains:
        print(train)
[KTX 101]  04/01 09:00~12:30  μ„œμšΈ~λΆ€μ‚°  νŠΉμ‹€ κ°€λŠ₯, μΌλ°˜μ‹€ κ°€λŠ₯ (3μ‹œκ°„ 30λΆ„)
[KTX 103]  04/01 10:00~13:30  μ„œμšΈ~λΆ€μ‚°  νŠΉμ‹€ λ§€μ§„, μΌλ°˜μ‹€ κ°€λŠ₯ (3μ‹œκ°„ 30λΆ„)
[KTX 105]  04/01 11:00~14:35  μ„œμšΈ~λΆ€μ‚°  νŠΉμ‹€ κ°€λŠ₯, μΌλ°˜μ‹€ λ§€μ§„ (3μ‹œκ°„ 35λΆ„)

Warning

μ½”λ ˆμΌκ³Ό 아무 κ΄€λ ¨ μ—†λŠ” 비곡식 λΌμ΄λΈŒλŸ¬λ¦¬μž…λ‹ˆλ‹€. λ¬Έμ„œν™”λ˜μ§€ μ•Šμ€ μ•± API λ₯Ό μ“°κΈ° λ•Œλ¬Έμ— μ½”λ ˆμΌμ΄ 앱을 λ°”κΎΈλ©΄ 예고 없이 멈좜 수 μžˆμŠ΅λ‹ˆλ‹€. 이용 μ•½κ΄€κ³Ό κ΄€λ ¨ 법령을 μ§€ν‚€λŠ” 것은 μ‚¬μš©μž μ±…μž„μ΄λ©°, κ³Όλ„ν•œ μžλ™ μš”μ²­μ€ 계정 제재둜 μ΄μ–΄μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.


ν•  수 μžˆλŠ” 것

ν‘œ μ˜ˆλ§€ν•˜κ³  κ²°μ œν•˜κΈ°

from pykorail import AdultPassenger, Card, ChildPassenger

trains = korail.trains.search(
    "μ„œμšΈ",
    "λΆ€μ‚°",
    depart_after=datetime(2026, 4, 1, 9, 0),
    passengers=[AdultPassenger(2), ChildPassenger(1)],
)

reservation = korail.reservations.create(trains[0])
print(reservation)
# [KTX 101] … 119600원(3석), κ΅¬μž…κΈ°ν•œ 4μ›” 1일 09:20

korail.reservations.pay(
    reservation,
    Card(number="1234567812345678", password="12", verify_number="900101", expire="2812"),
)
μ·¨μ†Œν‘œ 기닀리기
import time
from pykorail import NoResultsError, PastDepartureError

departure = datetime(2026, 4, 1, 9, 0)

while True:
    try:
        trains = korail.trains.search("μ„œμšΈ", "λΆ€μ‚°", depart_after=departure)
    except NoResultsError:
        time.sleep(30)  # μ„œλ²„μ— λΆ€λ‹΄ μ£Όμ§€ μ•Šκ²Œ λ„‰λ„‰νžˆ μ‰¬μ„Έμš”
        continue
    except PastDepartureError:
        break  # μ—΄μ°¨κ°€ 이미 λ– λ‚¬μŠ΅λ‹ˆλ‹€ β€” λ¬΄ν•œμ • λŒμ§€ μ•Šλ„λ‘

    korail.reservations.create(trains[0])
    break
맀진일 λ•Œ μ˜ˆμ•½λŒ€κΈ° κ±ΈκΈ°
trains = korail.trains.search("μ„œμšΈ", "λΆ€μ‚°", include_waiting_list=True)
waitable = [t for t in trains if not t.has_seat() and t.has_waiting_list()]

reservation = korail.reservations.create(waitable[0])
assert reservation.is_waiting
μΈκ·Όμ—­μ—μ„œ μΆœλ°œΒ·λ„μ°©ν•˜λŠ” μ—΄μ°¨κΉŒμ§€ 보기

μ•±μ˜ "인접역" μ˜΅μ…˜μž…λ‹ˆλ‹€. μš©μ‚° β†’ λŒ€μ „μ„ 찾으면 μ„œμšΈ β†’ λŒ€μ „ Β· μš©μ‚° β†’ μ„œλŒ€μ „ νŽΈλ„ ν•¨κ»˜ λ‚˜μ˜΅λ‹ˆλ‹€. 직톡이 μ—†λŠ” ꡬ간은 이걸 μΌœμ§€ μ•ŠμœΌλ©΄ μ‘°νšŒν•  방법이 μ—†μŠ΅λ‹ˆλ‹€.

trains = korail.trains.search("μš©μ‚°", "λŒ€μ „", include_nearby_stations=True)

for train in trains:
    print(train.dep_name, "β†’", train.arr_name)
# μ„œμšΈ β†’ λŒ€μ „ / μš©μ‚° β†’ μ„œλŒ€μ „ / μ„œμšΈ β†’ μ„œλŒ€μ „ …

# μΆœλ°œμ€ μš©μ‚°μ—μ„œλ§Œ ν•˜κ³  μ‹Άλ‹€λ©΄ κ²°κ³Όμ—μ„œ κ³ λ₯΄λ©΄ λ©λ‹ˆλ‹€
from_yongsan = [t for t in trains if t.dep_name == "μš©μ‚°"]

κ²°κ³Όκ°€ λ”ν•΄μ§€λŠ” 것이 μ•„λ‹ˆλΌ 후보가 λ„“μ–΄μ§€λŠ” 것이라, 켜면 인근역 편이 μ‹œκ°„μˆœμœΌλ‘œ 끼어듀며 λ’€μͺ½ μ§ν†΅νŽΈμ„ λ°€μ–΄λ‚Ό 수 μžˆμŠ΅λ‹ˆλ‹€. κ·Έλž˜μ„œ 기본값은 κΊΌμ§μž…λ‹ˆλ‹€.

λ‚΄ μ˜ˆμ•½Β·μŠΉμ°¨κΆŒ 보기
for reservation in korail.reservations.all():
    print(reservation.train.dep_name, "β†’", reservation.train.arr_name, reservation.price)

for ticket in korail.tickets.all():
    print(ticket.ticket_no, ticket.car_no, ticket.seat_no)
    korail.tickets.refund(ticket)  # ν™˜λΆˆ
κΈ°κΈ° ν”„λ‘œνŒŒμΌ κ³ μ •ν•˜κΈ° β€” μ—¬λŸ¬ 번 μ‹€ν–‰ν•œλ‹€λ©΄

ν”„λ‘œνŒŒμΌλ§ˆλ‹€ ν•©μ„± Android IDκ°€ ν•˜λ‚˜ μƒμ„±λ˜λ©°, 같은 ν”„λ‘œνŒŒμΌμ„ μ“°λŠ” ν΄λΌμ΄μ–ΈνŠΈλŠ” κ·Έ IDλ₯Ό μž¬μ‚¬μš©ν•©λ‹ˆλ‹€. μž¬μ‹œμž‘ 후에도 μœ μ§€ν•˜λ €λ©΄ id와 android_idλ₯Ό ν•¨κ»˜ μ €μž₯ν•˜μ„Έμš”. pykorail은 νŒŒμΌμ„ μžλ™μœΌλ‘œ μ½κ±°λ‚˜ μ“°μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

from pykorail.device import profile_by_id, random_profile

profile = profile_by_id(saved_id, android_id=saved_android_id) or random_profile()
# 호좜 μ•±μ˜ 섀정에 profile.id와 profile.android_idλ₯Ό ν•¨κ»˜ μ €μž₯ν•©λ‹ˆλ‹€.
korail = Korail(device_profile=profile)

전체 ν”„λ‘œνŒŒμΌμ„ μ €μž₯ν•˜λ €λ©΄ dataclasses.asdict(profile)을 μ§λ ¬ν™”ν•œ λ’€ DeviceProfile(**saved_data)둜 볡원할 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. IDλŠ” Android 14의 SSAID 계산 방식과 μ½”λ ˆμΌ 곡개 μ„œλͺ… μΈμ¦μ„œλ‘œ μƒμ„±ν•œ ν•©μ„± 값이며, μ‹€μ œ κΈ°κΈ°μ—μ„œ μΆ”μΆœν•œ 값은 μ•„λ‹™λ‹ˆλ‹€. ν”„λ‘œνŒŒμΌμ„ μƒλž΅ν•˜λ©΄ ν΄λΌμ΄μ–ΈνŠΈλ§ˆλ‹€ μƒˆ IDκ°€ μƒμ„±λ©λ‹ˆλ‹€. μ΄λ•Œλ„ korail.android_idλ₯Ό μ €μž₯ν•˜κ³  Korail(android_id=saved_android_id)둜 볡원할 수 μžˆμŠ΅λ‹ˆλ‹€. ν”„λ‘œνŒŒμΌ μœ λ¬΄μ™€ λ¬΄κ΄€ν•˜κ²Œ User-AgentλŠ” korailtalkμž…λ‹ˆλ‹€.


μ•Œμ•„λ‘λ©΄ 쒋은 것

역은 μ΄λ¦„μœΌλ‘œ "μ„œμšΈμ—­" 이 μ•„λ‹ˆλΌ "μ„œμšΈ". μ˜€νƒ€λ©΄ μš”μ²­ 전에 막고 λΉ„μŠ·ν•œ 역을 μ•Œλ €μ€λ‹ˆλ‹€.
μ‹œκ°μ€ 항상 KST datetime(2026, 4, 1, 9) λŠ” μ„œλ²„κ°€ μ–΄λŠ νƒ€μž„μ‘΄μ΄λ“  ν•œκ΅­μ‹œκ°„ μ˜€μ „ 9μ‹œ.
μ§€λ‚œ μ‹œκ°μ€ κ±°λΆ€ μ„œλ²„κ°€ 과거에도 빈 결과만 μ€˜μ„œ "λ– λ‚œ μ—΄μ°¨"와 "μ—΄μ°¨ μ—†μŒ"이 κ΅¬λΆ„λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
μ‹€νŒ¨λŠ” μ˜ˆμ™Έλ‘œ cancel() Β· pay() λŠ” 성곡 μ‹œ 아무것도 λ°˜ν™˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
from pykorail import KorailError, SoldOutError

try:
    korail.reservations.create(train)
except SoldOutError:
    ...  # λ§€μ§„ β€” λ‹€μŒ μ—΄μ°¨λ‘œ
except KorailError as exc:
    print(exc.msg, exc.code)  # μ½”λ ˆμΌμ΄ μ€€ λ©”μ‹œμ§€μ™€ μ½”λ“œ

λ¬Έμ„œ

πŸ“– API 레퍼런슀 전체 λ©”μ„œλ“œ Β· λͺ¨λΈ Β· μ˜΅μ…˜ Β· μ˜ˆμ™Έ
🀝 κΈ°μ—¬ κ°€μ΄λ“œ 개발 ν™˜κ²½, ν…ŒμŠ€νŠΈ κ·œμΉ™, PR 절차
πŸ› μ•„ν‚€ν…μ²˜ κ·œμ•½ 계측 ꡬ쑰, μ™ΈλΆ€ API λΆˆλ³€μ‹
πŸ€– μ—μ΄μ „νŠΈ μ„€μ • Codex Β· Claude Code Β· Pi Β· Cursor Β· OpenCode
πŸ” λ³΄μ•ˆ μ •μ±… Β· 행동 κ°•λ Ή

자주 λ¬»λŠ” 것

둜그인이 μ•ˆ λ©λ‹ˆλ‹€

μ„€μΉ˜ν•  λ•Œ κ²½κ³ κ°€ λ–΄λ‹€λ©΄ curl_cffi λŒ€μ‹  requests 둜 돌고 μžˆμ„ 수 μžˆμŠ΅λ‹ˆλ‹€. μ½”λ ˆμΌμ€ TLS 지문을 보기 λ•Œλ¬Έμ— μ΄λ•Œ 둜그인이 거뢀될 수 μžˆμŠ΅λ‹ˆλ‹€. pip install curl_cffi 둜 ν•΄κ²°λ˜λŠ” κ²½μš°κ°€ λŒ€λΆ€λΆ„μž…λ‹ˆλ‹€.

νœ΄λŒ€ν° λ²ˆν˜ΈλŠ” 010-1234-5678κ³Ό 01012345678 λͺ¨λ‘ μž…λ ₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ν΄λΌμ΄μ–ΈνŠΈκ°€ νœ΄λŒ€ν° 둜그인으둜 λΆ„λ₯˜ν•˜κ³  ν•˜μ΄ν”ˆμ„ μ œκ±°ν•΄ μ „μ†‘ν•©λ‹ˆλ‹€.

Windows μ—μ„œ μ„€μΉ˜κ°€ μ•ˆ λ©λ‹ˆλ‹€

curl_cffi 의 libcurl DLL λ‘œλ“œκ°€ μ‹€νŒ¨ν•˜λŠ” ν™˜κ²½μ΄ μžˆμŠ΅λ‹ˆλ‹€. pip install "pykorail[fallback]" 둜 requests 폴백을 ν•¨κ»˜ μ„€μΉ˜ν•˜μ„Έμš”. λ‹€λ§Œ μœ„μ— 적은 이유둜 둜그인이 λ§‰νž 수 μžˆμŠ΅λ‹ˆλ‹€.

SRT 도 λ˜λ‚˜μš”?

μ•„λ‹ˆμš”. SRT(μˆ˜μ„œκ³ μ†μ² λ„)λŠ” λ‹€λ₯Έ νšŒμ‚¬μ˜ λ‹€λ₯Έ μ‹œμŠ€ν…œμž…λ‹ˆλ‹€.

μ–΄μ œκΉŒμ§€ 되던 게 였늘 μ•ˆ λ©λ‹ˆλ‹€

μ½”λ ˆμΌμ΄ μ„œλ²„λ₯Ό 바꿨을 수 μžˆμŠ΅λ‹ˆλ‹€. API λ³€κ²½ 이슈 둜 μ•Œλ €μ£Όμ‹œλ©΄ λŒ€μ‘ν•˜κ² μŠ΅λ‹ˆλ‹€. κ°€μž₯ 도움이 λ˜λŠ” κΈ°μ—¬μž…λ‹ˆλ‹€.

μ˜ˆλ§€κ°€ ν™•μ‹€νžˆ λ˜λ‚˜μš”?

이 λΌμ΄λΈŒλŸ¬λ¦¬λŠ” μ•±κ³Ό 같은 μš”μ²­μ„ 보낼 뿐이고, μ’Œμ„ 배정은 μ½”λ ˆμΌ μ„œλ²„κ°€ ν•©λ‹ˆλ‹€. λͺ…μ ˆ 예맀처럼 경쟁이 μ‹¬ν•œ μƒν™©μ—μ„œ 성곡을 보μž₯ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.


개발

파이썬 3.10 이상 (3.10 – 3.14 μ—μ„œ ν…ŒμŠ€νŠΈν•©λ‹ˆλ‹€).

uv sync --all-extras
uv run ruff format && uv run ruff check && uv run ty check && uv run pytest

λ„€ 개λ₯Ό μ „λΆ€ 톡과해야 ν•©λ‹ˆλ‹€. 컀버리지 ν•˜ν•œ 90%, ν˜„μž¬ 96%.

κΈ°μ—¬λ₯Ό ν™˜μ˜ν•©λ‹ˆλ‹€ β€” CONTRIBUTING.md λΆ€ν„° λ³΄μ„Έμš”. μƒˆ μ—”λ“œν¬μΈνŠΈλŠ” 곡식 μ½”λ ˆμΌν†‘+ APK λ₯Ό λ””μ»΄νŒŒμΌν•΄ κ²½λ‘œμ™€ 폼 ν•„λ“œλ₯Ό ν™•μΈν•œ λ’€μ—λ§Œ μΆ”κ°€ν•©λ‹ˆλ‹€.

릴리슀

λ²„μ „μ˜ μœ μΌν•œ μΆœμ²˜λŠ” git νƒœκ·Έμž…λ‹ˆλ‹€. νŒŒμΌμ„ μ†μœΌλ‘œ κ³ μΉ  ν•„μš”κ°€ μ—†μŠ΅λ‹ˆλ‹€.

git tag v0.2.0 && git push origin v0.2.0

νƒœκ·Έλ₯Ό λ°€λ©΄ μ „ 버전 게이트 β†’ λ³΄μ•ˆ μŠ€μΊ” β†’ λΉŒλ“œ β†’ PyPI μ—…λ‘œλ“œ β†’ 릴리슀 λ…ΈνŠΈ 생성이 μžλ™μœΌλ‘œ λ•λ‹ˆλ‹€.


MIT License Β· μ½”λ ˆμΌκ³Ό λ¬΄κ΄€ν•œ 비곡식 ν”„λ‘œμ νŠΈ

About

Unofficial Korail python SDK

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

26 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages