首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 串接 12306 火車票查詢 API:餘票與票價即時查詢完整實作

     編輯:PHP 相關知識

前言

中國鐵路 12306 官方未公開開放 API,但其前端網頁呼叫的內部介面可被逆向用於查詢車次、餘票與票價。本文提供一套完整的 PHP 實作,包含車站代碼對照表、查詢邏輯、CURL 請求封裝與結果格式化輸出。

架構概覽

  • name.php:車站中文名 ↔ 電報碼 映射表(超過 3000 筆)
  • train.php:核心查詢類別,封裝 HTTP 請求、參數組裝、資料解析
  • 查詢流程:站名轉代碼 → 呼叫查車次 API → 逐筆呼叫查票價 API → 合併輸出

核心程式碼實作

1. 車站代碼對照表 (name.php)

因篇幅極大,此處僅節錄關鍵結構:

<?php
return [
    "北京"       => "BJP",
    "北京西"     => "BXP",
    "上海"       => "SHH",
    "上海虹橋"   => "AOH",
    "廣州"       => "GZQ",
    "深圳北"     => "IOQ",
    "成都東"     => "ICW",
    "西安北"     => "EAY",
    // ... 超過 3000 個車站
    "鄭州東"     => "ZAF",
    "武漢"       => "WHN",
];
?>

維護建議:定期從 12306 官網 station_name.js 同步更新,或建立資料表由排程自動同步。

2. 核心查詢類別 (train.php)

<?php
/**
 * 12306 火車票查詢類別
 * 支援:車次查詢、餘票查詢、票價查詢
 */
class Train12306
{
    private const BASE_URL = 'https://kyfw.12306.cn/otn';
    private const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36';
    private const REFERER = 'https://kyfw.12306.cn/otn/lcxxcx/init';
    
    private $stationMap;
    private $ch;

    public function __construct($stationFile = __DIR__ . '/name.php')
    {
        $this->stationMap = include $stationFile;
        $this->initCurl();
    }

    private function initCurl()
    {
        $this->ch = curl_init();
        curl_setopt_array($this->ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_USERAGENT      => self::USER_AGENT,
            CURLOPT_REFERER        => self::REFERER,
            CURLOPT_ENCODING       => 'gzip,deflate',
            CURLOPT_HTTPHEADER     => [
                'Accept: */*',
                'Accept-Language: zh-CN,zh;q=0.9',
                'Host: kyfw.12306.cn',
                'Connection: keep-alive',
            ],
        ]);
    }

    /**
     * 統一 GET 請求並解析 JSON
     */
    private function get($endpoint, array $params = [])
    {
        $url = self::BASE_URL . $endpoint . '?' . http_build_query($params);
        curl_setopt($this->ch, CURLOPT_URL, $url);
        $response = curl_exec($this->ch);
        
        if ($response === false) {
            throw new RuntimeException('CURL 錯誤: ' . curl_error($this->ch));
        }
        
        $data = json_decode($response, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new RuntimeException('JSON 解析失敗: ' . json_last_error_msg());
        }
        return $data;
    }

    /**
     * 解析日期參數:支援 "mm-dd" 或 "yyyy-mm-dd"
     */
    private function parseDate($dateStr = '')
    {
        if (empty($dateStr)) {
            return date('Y-m-d');
        }
        $parts = explode('-', $dateStr);
        if (count($parts) === 2) {
            return date('Y') . '-' . $dateStr;      // 補上当年年份
        }
        if (count($parts) === 3) {
            return $dateStr;
        }
        throw new InvalidArgumentException('日期格式錯誤,請使用 mm-dd 或 yyyy-mm-dd');
    }

    /**
     * 查詢車次與餘票
     */
    public function queryTrains($fromStation, $toStation, $date = '')
    {
        $fromCode = $this->stationMap[$fromStation] ?? null;
        $toCode   = $this->stationMap[$toStation] ?? null;
        
        if (!$fromCode || !$toCode) {
            throw new InvalidArgumentException('車站名稱錯誤或不支援:' . $fromStation . ' / ' . $toStation);
        }

        $date = $this->parseDate($date);
        
        // 1. 查車次與餘票
        $listData = $this->get('/lcxxcx/query', [
            'purpose_codes' => 'ADULT',
            'queryDate'     => $date,
            'from_station'  => $fromCode,
            'to_station'    => $toCode,
        ]);

        if (empty($listData['status']) || empty($listData['data']['datas'])) {
            return [];
        }

        $trains = $listData['data']['datas'];

        // 2. 逐筆查票價並合併
        foreach ($trains as &$train) {
            $priceData = $this->get('/leftTicket/queryTicketPrice', [
                'train_no'      => $train['train_no'],
                'from_station_no' => $train['from_station_no'],
                'to_station_no'   => $train['to_station_no'],
                'seat_types'      => $train['seat_types'],
                'train_date'      => $date,
            ]);

            $price = $priceData['data'] ?? [];
            
            // 座位類型代碼映射
            $seatMap = [
                'gr_num' => ['A6', '高級軟臥'],
                'qt_num' => ['OT', '其他'],
                'rw_num' => ['A4', '軟臥'],
                'rz_num' => ['A2', '軟座'],
                'tz_num' => ['P',  '特等座'],
                'wz_num' => ['WZ', '無座'],
                'yw_num' => ['A3', '硬臥'],
                'yz_num' => ['A1', '硬座'],
                'ze_num' => ['O',  '二等座'],
                'zy_num' => ['M',  '一等座'],
                'swz_num'=> ['A9', '商務座/特等座'],
            ];

            foreach ($seatMap as $field => [$priceKey, $label]) {
                $num = $train[$field] ?? '--';
                $cost = $price[$priceKey] ?? '';
                // 12306 回傳「有」或數字,無票時為「無」或空
                $train[$field] = $num . ($cost ? " ($cost)" : '');
            }
        }

        return $trains;
    }

    public function __destruct()
    {
        if ($this->ch) curl_close($this->ch);
    }
}

3. 使用範例

<?php
require_once 'train.php';

try {
    $query = new Train12306();
    
    // 查詢北京 → 天津,明天(預設今天)
    $trains = $query->queryTrains('北京', '天津', '01-03');
    
    if (empty($trains)) {
        echo '查無車次或當日停駛';
        exit;
    }

    foreach ($trains as $t) {
        echo "=== 車次: {$t['station_train_code']} ===" . PHP_EOL;
        echo "出發: {$t['start_time']} {$t['start_station_name']} → 抵達: {$t['arrive_time']} {$t['end_station_name']}" . PHP_EOL;
        echo "歷時: {$t['lishi']}" . PHP_EOL;
        
        // 輸出有票的座位
        $seats = [
            'swz_num'=>'商務座', 'tz_num'=>'特等座', 'zy_num'=>'一等座',
            'ze_num'=>'二等座', 'gr_num'=>'高級軟臥', 'rw_num'=>'軟臥',
            'yw_num'=>'硬臥', 'rz_num'=>'軟座', 'yz_num'=>'硬座', 'wz_num'=>'無座'
        ];
        
        foreach ($seats as $field => $label) {
            $val = $t[$field] ?? '--';
            if ($val !== '--' && !str_contains($val, '無') && !str_contains($val, '--')) {
                echo "  $label: $val" . PHP_EOL;
            }
        }
        echo PHP_EOL;
    }
    
} catch (Exception $e) {
    echo '查詢失敗: ' . $e->getMessage();
}

關鍵 API 端點說明

功能端點關鍵參數
車次/餘票查詢/lcxxcx/querypurpose_codes, queryDate, from_station, to_station
票價查詢/leftTicket/queryTicketPricetrain_no, from_station_no, to_station_no, seat_types, train_date
車站代碼 JS/resources/js/framework/station_name.js定期同步更新 name.php

座位類型代碼對照表

代碼座位類型備註
A9 / P商務座 / 特等座高鐵專用
M一等座高鐵/動車
O二等座高鐵/動車
A6高級軟臥部分直達特快
A4軟臥普快/特快
A3硬臥普快/特快
A2軟座動車/城際
A1硬座普快/特快
WZ無座站票

效能優化與生產環境建議

  1. 快取車站代碼:將 name.php 改為 Redis/APCu 快取,避免每次 include 大陣列
  2. 快車次查詢結果:同一區間、同一天的查詢結果可快取 1-5 分鐘
  3. 並行請求票價:使用 curl_multi 或 Swoole 協程並行查詢 10+ 筆票價,大幅降低延遲
  4. 錯誤重試與熔斷:網路波動時指數退避重試,連續失敗觸發熔斷
  5. IP 輪換/代理池:高頻查詢易觸發 12306 風控,需配合代理 IP 池
  6. 合規性提醒:僅供個人學習研究,商業用途請申請官方合作介面,遵守 robots.txt 與服務條款

常見問題排查

現象可能原因解決方案
回傳空資料/狀態 falseCookie 過期、Referer 驗證、IP 封鎖更新 Cookie、檢查 Header、更換代理 IP
車站找不到代碼name.php 版本過舊從 station_name.js 同步最新資料
票價欄位為空該車次無此座位類型、或售罄正常現象,前端需判斷顯示「--」或「售罄」
SSL 憑證驗證失敗本地 CA 憑證庫過舊CURLOPT_SSL_VERIFYPEER = false(僅測試環境)

總結

本文提供了完整的 PHP 12306 查詢實作,涵蓋車站代碼管理、雙 API 串接、CURL 封裝、異常處理與生產環境優化建議。核心難點在於 12306 的反爬蟲機制與介面穩定性,實際部署時需配合代理池、快取策略與監控告警,並嚴格遵守法律法規與平台規範。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved