μ½λ μΌ μ€λ§νΈ μλ§€(μ½λ μΌν‘) API λ₯Ό κ°μΌ λΉκ³΅μ ν΄λΌμ΄μΈνΈ
λ¬Έμ μ¬μ΄νΈ Β· λΉ λ₯Έ μμ Β· ν μ μλ κ² Β· API λ νΌλ°μ€ Β· κΈ°μ¬νκΈ°
pip install pykorailfrom datetime import datetime, timedelta, timezone
from pykorail import Korail
kst = timezone(timedelta(hours=9))
# λ΄μΌ μ€μ 9μ(KST) μΆλ°μ μ°Ύμ΅λλ€.
departure = (datetime.now(kst) + timedelta(days=1)).replace(hour=9, minute=0, second=0, microsecond=0)
with Korail.logged_in("me@example.com", "password") as korail:
trains = korail.trains.search("μμΈ", "λΆμ°", depart_after=departure)
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λΆ)
λΉλκΈ°λ‘λ μΈ μ μμ΅λλ€. 리μμ€μ μκ·Έλμ²κ° κ°κ³ , νΈμΆλ§ await μ
λλ€. μλλ
async ν¨μ μμμ λΆλ₯΄λ λΆλΆλ§ μ μμ΅λλ€.
from pykorail import AsyncKorail
async with await AsyncKorail.logged_in("me@example.com", "password") as korail:
trains = await korail.trains.search("μμΈ", "λΆμ°", depart_after=departure)asyncio μ μ©μ΄κ³ curl_cffi κ° νμν©λλ€. μ΄λ²€νΈ 루νΒ·λμμ± κ°μ λλ¨Έμ§ μ μ½μ
API λ νΌλ°μ€μ μμ΅λλ€.
Caution
μΉμ°¨κΆ μ¬νλ§€ λͺ©μ μΌλ‘ μ°μ§ λ§μΈμ. pykorail μ λ³ΈμΈμ΄ ν νλ₯Ό μλ§€νκΈ° μν λꡬμ λλ€. μΉμ°¨κΆμ μλμ λ°κ³ μμ΅μ μΌλ‘ λνλ νμλ μ² λμ¬μ λ²μΌλ‘ κΈμ§λμ΄ μκ³ , μμ μ μ΄λμ μ»λ λ° μ°λ κ²μ λΌμ΄μ μ€λ‘λ κΈμ§λ©λλ€.
μ½λ μΌκ³Ό μ무 κ΄λ ¨ μλ λΉκ³΅μ λΌμ΄λΈλ¬λ¦¬μ λλ€. λ¬Έμνλμ§ μμ μ± API λ₯Ό μ°κΈ° λλ¬Έμ μ½λ μΌμ΄ μ±μ λ°κΎΈλ©΄ μκ³ μμ΄ λ©μΆ μ μμ΅λλ€. μ΄μ© μ½κ΄κ³Ό κ΄λ ¨ λ²λ Ήμ μ§ν€λ κ²μ μ¬μ©μ μ± μμ΄λ©°, κ³Όλν μλ μμ²μ κ³μ μ μ¬λ‘ μ΄μ΄μ§ μ μμ΅λλ€.
from pykorail import AdultPassenger, Card, ChildPassenger
trains = korail.trains.search(
"μμΈ",
"λΆμ°",
depart_after=departure,
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"),
)λ€μͺ½ μ΄μ°¨κΉμ§ μ΄μ΄μ 보기
μλ²λ μ‘°ν ν λ²μ ν μͺ½(λ³΄ν΅ μ΄ νΈ)λ§ μ€λλ€. max_pages λ₯Ό μ¬λ¦¬λ©΄ λ§μ§λ§ μ΄μ°¨μ μΆλ°
μκ°λΆν° λ€μ μ‘°νν΄ μ΄μ΄ λΆμ
λλ€.
trains = korail.trains.search("μμΈ", "λΆμ°", include_no_seats=True, max_pages=5)
print(len(trains), trains[-1].dep_time)μͺ½λ§λ€ μμ²μ΄ ν λ²μ© λ λκ°λλ€. μ μ΄μ°¨κ° λ μμΌλ©΄ κ·Έ μ μ λ©μΆ₯λλ€.
λ§€μ§μΌ λ μμ½λκΈ° κ±ΈκΈ°
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 == "μ©μ°"]κ²°κ³Όκ° λν΄μ§λ κ²μ΄ μλλΌ νλ³΄κ° λμ΄μ§λ κ²μ΄λΌ, μΌλ©΄ μΈκ·Όμ νΈμ΄ μκ°μμΌλ‘ λΌμ΄λ€λ©° λ€μͺ½ μ§ν΅νΈμ λ°μ΄λΌ μ μμ΅λλ€. κ·Έλμ κΈ°λ³Έκ°μ κΊΌμ§μ λλ€.
νμΉ μ‘°ν Β· μλ§€νκΈ°
μ§ν΅μ΄ μλ ꡬκ°μ ν λ² κ°μνλ μ¬μ μΌλ‘ μ°Ύμ΅λλ€. μ‘°νλ λ‘κ·ΈμΈνμ§ μμλ λ©λλ€.
transfers = korail.trains.search_transfers("κ°λ¦", "λͺ©ν¬")
for transfer in transfers:
first, second = transfer.legs
print(first.train_no, "β", transfer.station_name, "β", second.train_no)
first, second = korail.reservations.create_transfer(transfers[0])
print(first.rsv_id == second.rsv_id) # True β μμ½λ²νΈλ νλμ
λλ€κ΅¬κ°μ λ°λ‘ create() νλ©΄ 첫 ꡬκ°λ§ μ‘νκ³ λμ§Έκ° λ§€μ§λ μ μμ΅λλ€. μ΄μͺ½μ ν ꡬκ°μ΄λΌλ
μ λλ©΄ μλ²κ° ν΅μ§Έλ‘ κ±°μ νκ³ , cancel() μ μμ½λ²νΈ λ¨μλΌ λ μ€ μ΄λ κ²μ λ겨λ λ
ꡬκ°μ΄ ν¨κ» μ·¨μλ©λλ€.
μ΄μ μ‘°ν
μμ½νκΈ° μ μ κ°μ λ΄ λλ€. λ‘κ·ΈμΈνμ§ μμλ λ©λλ€.
train = korail.trains.search("μμΈ", "λΆμ°")[0]
for fare in korail.trains.fares(train):
print(fare.seat_class_name, fare.total)
# μΌλ°μ€ 59800
# νΉμ€ 83700ν μΈ μ μ κΈ°μ€ μ΄μμ
λλ€. μ€μ λ‘ λΌ κΈμ‘μ μμ½ν λ€ Reservation.price μ μμ΅λλ€.
νΈμ°¨ Β· μ’μμ κ³¨λΌ μλ§€νκΈ°
create() λ μ’μμ μλ²κ° μ ν©λλ€. μ리λ₯Ό μ§μ κ³ λ₯΄λ €λ©΄ νΈμ°¨ λͺ©λ‘ λ€μμ μ’μ λ°°μΉλ₯Ό
λ°μ create_with_seats() μ λκΉλλ€. λ λ€ λ‘κ·ΈμΈμ΄ νμν©λλ€.
from pykorail import AdultPassenger
train = korail.trains.search("μμΈ", "λΆμ°")[0]
# μ’μμ μΉκ° ν λͺ
μ νλμ©μ΄λΌ, λ μλ¦¬κ° λ¨μ νΈμ°¨λ₯Ό κ³ λ¦
λλ€.
car = next(car for car in korail.trains.cars(train) if car.remaining_seats >= 2)
picked = [seat for seat in korail.trains.seats(train, car) if seat.is_available][:2]
reservation = korail.reservations.create_with_seats(train, picked, [AdultPassenger(2)])μ’μ μκ° μΉκ° μμ λ€λ₯΄κ±°λ, κ°μ μ’μμ λ λ² λκΈ°κ±°λ, κ°μ€μ΄ μμ΄λ©΄ μμ² μ μ
ValueError λ‘ λ§μ΅λλ€.
λ΄ μμ½Β·μΉμ°¨κΆ 보기
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) # νλΆκ³΅νλ²μ€ μκ°ν Β· μ’μ 보기
λ‘κ·ΈμΈ μμ΄ λ©λλ€. μ½λ μΌμ΄ νλ κ²μ κ΄λͺ μ β μΈμ²κ³΅ν(T1 Β· T2) λ°©ν₯λΏμ λλ€.
buses = korail.airport_bus.search("κ΄λͺ
μ", "μΈμ²κ³΅νT1")
bus = next(bus for bus in buses if bus.has_seat())
print(bus, bus.price)
print([seat.seat_no for seat in korail.airport_bus.seats(bus) if seat.is_available])λ§μΌλ¦¬μ§ Β· ν μΈμΏ ν° λ³΄κΈ°
mileage = korail.membership.mileage()
print(mileage.total, "λ§μΌλ¦¬μ§,", mileage.expiring, "μ΄λ² λ¬ μλ©Έ μμ ")
for entry in mileage.entries: # μ΅κ·Ό 30μΌμ μ 립 Β· μ¬μ© λ΄μ
print(entry.date, entry.movement, entry.amount) # 20261003 μ 립 2990
for coupon in korail.membership.coupons():
print(coupon.title, coupon.valid_until)κΈ°κΈ° νλ‘νμΌ κ³ μ νκΈ° β μ¬λ¬ λ² μ€ννλ€λ©΄
νλ‘νμΌλ§λ€ ν©μ± 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 β tz μλ
datetimeμ μ€ν λ¨Έμ μ νμμ‘΄κ³Ό 무κ΄νκ² νκ΅μκ°μΌλ‘ ν΄μλ©λλ€. - μ§λ μκ°μ κ±°λΆ β μλ²κ° κ³Όκ±°μλ λΉ κ²°κ³Όλ§ μ€μ "λ λ μ΄μ°¨"μ "μ΄μ°¨ μμ"μ΄ κ΅¬λΆλμ§ μμ΅λλ€.
- μ€ν¨λ μμΈλ‘ β
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 λͺ¨λ μ
λ ₯ν μ μμ΅λλ€.
ν΄λΌμ΄μΈνΈκ° ν΄λν° λ‘κ·ΈμΈμΌλ‘ λΆλ₯νκ³ νμ΄νμ μ κ±°ν΄ μ μ‘ν©λλ€.
PasswordChangeRequiredError κ° λλ€λ©΄ μμ΄λ Β· λΉλ°λ²νΈλ λ§μ΅λλ€. κ°μ λΉλ°λ²νΈλ₯Ό
6κ°μ λκ² μ¨μ μ½λ μΌμ΄ λ³κ²½ μλ΄λ₯Ό λμ΄ μνμ΄κ³ , κ·Έλμμ λ‘κ·ΈμΈμ΄ λλμ§ μμ΅λλ€.
μ½λ μΌν‘ μ±μ΄λ ννμ΄μ§μμ μλ΄λ₯Ό μ²λ¦¬ν λ€ λ€μ μ€ννμΈμ.
Windows μμ μ€μΉκ° μ λ©λλ€
curl_cffi μ libcurl DLL λ‘λκ° μ€ν¨νλ νκ²½μ΄ μμ΅λλ€.
pip install "pykorail[fallback]" λ‘ requests ν΄λ°±μ ν¨κ» μ€μΉνμΈμ.
λ€λ§ μμ μ μ μ΄μ λ‘ λ‘κ·ΈμΈμ΄ λ§ν μ μμ΅λλ€.
μ΄μ κΉμ§ λλ κ² μ€λ μ λ©λλ€
μ½λ μΌμ΄ μλ²λ₯Ό λ°κΏ¨μ μ μμ΅λλ€. API λ³κ²½ μ΄μ λ‘ μλ €μ£Όμλ©΄ λμνκ² μ΅λλ€. κ°μ₯ λμμ΄ λλ κΈ°μ¬μ λλ€.
μλ§€κ° νμ€ν λλμ?
μ΄ λΌμ΄λΈλ¬λ¦¬λ μ±κ³Ό κ°μ μμ²μ λ³΄λΌ λΏμ΄κ³ , μ’μ λ°°μ μ μ½λ μΌ μλ²κ° ν©λλ€. λͺ μ μλ§€μ²λΌ κ²½μμ΄ μ¬ν μν©μμ μ±κ³΅μ 보μ₯νμ§ μμ΅λλ€.
νλ‘μλ₯Ό λ£μ κ³νμ μλμ?
μκ³ , μμΌλ‘λ λ£μ§ μμ΅λλ€. νλ‘μλ‘ μΆμ²λ₯Ό λ°κΏ κ°λ©° μμ²νλ κ²μ μ½λ μΌ μλ²λ₯Ό μμ¬ μ μ μ΄μ©μλ³΄λ€ μ 리νκ² μλ§€νλ €λ λΆμ ν λ°©λ²μ λλ€.
νμ΄μ¬ 3.11 μ΄μ (3.11 β 3.15 μμ ν μ€νΈν©λλ€).
uv sync --all-extras
uv run ruff format && uv run ruff check && uv run ty check && uv run pytestλ€ κ°λ₯Ό μ λΆ ν΅κ³Όν΄μΌ ν©λλ€. 컀λ²λ¦¬μ§ νν 90%λ pytest κ° κ°μ νκ³ , μ§κΈ μμΉλ
CI μμ λ³Ό μ μμ΅λλ€.
κΈ°μ¬λ₯Ό νμν©λλ€ β CONTRIBUTING.md λΆν° 보μΈμ. μ μλν¬μΈνΈλ
곡μ μ½λ μΌν‘+ APK λ₯Ό λμ»΄νμΌν΄ κ²½λ‘μ νΌ νλλ₯Ό νμΈν λ€μλ§ μΆκ°ν©λλ€.
λ¦΄λ¦¬μ€ μ μ°¨λ CONTRIBUTING μ λ¦΄λ¦¬μ€ μ μ μμ΅λλ€.
MIT μ μμ μ μ΄λ κΈμ§ μ‘°νμ λν μ체 λΌμ΄μ μ€μ λλ€. κ·Έλμ MIT λ, OSI κΈ°μ€μ μ€νμμ€λ μλλλ€.
- κΈμ§ β μ΄ ν¨ν€μ§λ‘ ν보ν μΉμ°¨κΆ Β· μμ½μ μ° κ°λ³΄λ€ λΉμΈκ² λνλ κ², μμλ£ Β· ꡬλ λ£ Β· κ΄κ³ μμ΅μ λ°λ μλ§€ Β· μμ½λκΈ° Β· ν ν보 μλΉμ€, μ΄ ν¨ν€μ§μ μ λΆ λλ μλΉ λΆλΆ κΈ°λλ μν Β· μλΉμ€λ₯Ό νλ κ²
- νμ© β λ³ΈμΈμ μ¬ν, λ€λ₯Έ μ¬λμ νλ₯Ό μμλ£ μμ΄ μ κ°λ‘ λμ μλ§€νλ κ², λ¨μ²΄κ° μμμμ μ¬νμ μ°λ κ², μ°κ΅¬ Β· κ΅μ‘ Β· λΉμ리 κ°μΈ νλ‘μ νΈ
μ΄ μμ½μ μ°Έκ³ μ©μ΄κ³ μλ¬Έ LICENSE κ° μ°μ ν©λλ€. 0.3.0 κΉμ§μ λ°°ν¬λ³Έμ MIT κ·Έλλ‘μ λλ€.
μ½λ μΌκ³Ό 무κ΄ν λΉκ³΅μ νλ‘μ νΈ