C#实现12306火车票实时查询系统(含GUI与数据解析)

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目基于C#语言开发,针对2013年改版后的12306官网接口,实现火车票信息的实时查询功能。系统运行于.NET平台,支持用户通过图形界面输入出发地、目的地和日期等参数,自动发送HTTP请求获取并解析车次、余票、价格等数据。项目涵盖网络编程、HTML/JSON数据解析、验证码处理、GUI设计及本地资源管理等关键技术,使用Newtonsoft.Json、HTML Agility Pack等常用库,具备完整的桌面应用架构和良好的用户体验。

C# 与 .NET 生态中的现代网络通信实战:从基础语法到 12306 抢票系统全栈实现

你有没有试过在春节前打开 12306,看着“余票查询中…”的转圈动画,心里默默祈祷?🤯 那种“差一点就能抢到”的感觉,简直像坐过山车。但你知道吗?背后其实是一整套复杂的前后端交互机制在运作——而我们今天要做的,就是 亲手揭开这层神秘面纱,用 C# 和 .NET 打造一个属于自己的智能查询工具

这不是简单的“爬虫教学”,而是一场完整的工程实践之旅:从面向对象设计、HTTP 协议底层原理,到反爬策略、UI 界面集成,我们将一步步构建一个 高可用、抗检测、可扩展的桌面级应用系统 。准备好了吗?Let’s go!🚀


🧱 面向对象不是口号,是代码的生命线

C# 作为一门强类型的现代语言,最大的优势之一就是它对 OOP(面向对象编程) 的原生支持。我们不写“面条代码”,我们要的是清晰、可维护、能复用的结构。

比如,在处理列车信息时,你会怎么组织数据?

public class TrainInfo
{
    public string TrainNumber { get; set; }
    public DateTime DepartureTime { get; set; }

    public TrainInfo(string number, DateTime time) =>
        (TrainNumber, DepartureTime) = (number, time);
}

看到没?短短几行,封装了属性、构造函数和初始化逻辑。这种模式贯穿整个项目——无论是请求模型还是响应解析,都基于类似的实体类展开。

💡 小贴士:别小看这个 => 表达式体成员写法,它不仅让代码更简洁,还能减少括号嵌套带来的视觉疲劳。而且一旦你开始使用这样的风格,后续所有业务逻辑都会自然地围绕这些“有生命的对象”来展开,而不是一堆零散的变量。

更重要的是,当你把数据抽象成类之后,未来的扩展就变得非常轻松。比如哪天需要加个“是否经停”字段,只需:

+ public bool IsStopover { get; set; }

完全不影响现有逻辑。这就是 OOP 的魅力所在: 变更是常态,设计要为变化而生


🌐 .NET 的网络通信能力有多强?强到让你怀疑人生!

说到网络通信,很多人第一反应是 HttpClient 。没错,它是核心,但它只是冰山一角。真正的高手,玩的是整套生态链。

🔗 HTTP 是什么?别再只会说“超文本传输协议”了!

HTTP 其实就是一个“对话规则”。客户端说:“我要这个资源!”服务器回:“给你,状态码 200。”但如果出错了呢?比如你输入了一个不存在的 URL,服务器就会甩你一句 404 Not Found

在 .NET 中,我们通过 HttpClient 发起请求,并接收一个 HttpResponseMessage 对象。关键就在于如何正确解读它的状态码:

var response = await client.GetAsync("https://api.example.com/data");

switch (response.StatusCode)
{
    case HttpStatusCode.OK:
        Console.WriteLine("✅ 请求成功");
        break;
    case HttpStatusCode.NotFound:
        Console.WriteLine("🚫 资源未找到");
        break;
    case HttpStatusCode.TooManyRequests:
        Console.WriteLine("⏳ 请求太频繁,请稍后再试");
        break;
    default:
        Console.WriteLine($"⚠️ 其他错误: {response.ReasonPhrase}");
        break;
}

你会发现,光会发请求还不够, 懂状态码才是调试的关键 。特别是面对像 12306 这种复杂系统时,一个 401 Unauthorized 可能意味着你的 Cookie 过期了;一个 429 Too Many Requests 则说明 IP 已被限流。

⚠️ 特别提醒:永远不要频繁 new HttpClient()!这会导致套接字耗尽。要用 IHttpClientFactory 或至少复用实例。


🚀 异步不是装饰品,是性能的命脉

如果你还在用同步方式调用 Web API,那你的程序迟早会“卡死”。为什么?因为主线程被阻塞了啊!

正确的姿势是使用 async/await

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0");

var payload = new { username = "test", password = "123456" };
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");

try
{
    var response = await client.PostAsync("https://example.com/api/login", content);
    response.EnsureSuccessStatusCode(); 
    var responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"❌ 请求失败: {ex.Message}");
}

这段代码虽然不长,但包含了几个重要概念:
- async/await 实现非阻塞调用;
- StringContent 正确设置 MIME 类型;
- EnsureSuccessStatusCode() 自动抛异常;
- ReadAsStringAsync() 异步读取结果。

整个流程就像一条流水线,每个环节都能并行推进,效率直接起飞 💥

下面是这个异步请求的生命周期图示:

sequenceDiagram
    participant Client
    participant HttpClient
    participant Server

    Client->>HttpClient: 发起 PostAsync()
    activate HttpClient
    HttpClient->>Server: 发送 HTTP POST 请求
    Server-->>HttpClient: 返回响应(含状态码)
    HttpClient->>Client: 返回 HttpResponseMessage
    alt 状态码为2xx
        Client->>Client: 处理响应数据
    else 其他状态
        Client->>Client: 捕获异常或错误处理
    end
    deactivate HttpClient

你看,控制流清晰明了,异常传播路径也一目了然。这才是专业级的思维建模方式。


🎭 用户代理伪装?这只是反检测的第一步!

很多新手以为,只要换个 User-Agent 就能绕过反爬,too young too simple 😅

现实是:现在的网站不仅能识别 UA,还会检查一大堆头部字段。比如缺少 Accept-Encoding: gzip ,可能会导致返回压缩失败;没有 Referer ,可能直接拒绝服务。

所以,我们要做的不是“改一个头”,而是“模拟真实浏览器”。

public static class HttpHeaderExtensions
{
    public static void AddCommonBrowserHeaders(this HttpClient client, string referer = null)
    {
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));

        client.DefaultRequestHeaders.Add("Accept-Language", "zh-CN,zh;q=0.9");
        client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
        client.DefaultRequestHeaders.Add("Cache-Control", "no-cache");
        client.DefaultRequestHeaders.Connection.Add("keep-alive");

        if (!string.IsNullOrEmpty(referer))
            client.DefaultRequestHeaders.Referrer = new Uri(referer);
    }
}

现在你可以这样调用:

client.AddCommonBrowserHeaders("https://12306.cn");

一句话搞定全套伪装 👌

更进一步,还可以搞个 UA 池轮换,降低风险:

private static readonly string[] UserAgents = {
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36..."
};

public void SetRandomUserAgent(HttpClient client)
{
    var random = new Random();
    var ua = UserAgents[random.Next(UserAgents.Length)];
    client.DefaultRequestHeaders.Remove("User-Agent");
    client.DefaultRequestHeaders.UserAgent.ParseAdd(ua);
}

这样一来,每次请求看起来都不一样,风控系统很难判定你是机器人 🤖➡️👤


🍪 Cookie 容器的设计,决定了你能不能“登录”

HTTP 是无状态的,但我们人类是有记忆的。怎么办?靠 Cookie 维持会话。

但在 C# 中有个坑: HttpClient 默认不自动管理 Cookie!必须手动绑定 CookieContainer

var cookieContainer = new CookieContainer();
var handler = new HttpClientHandler
{
    CookieContainer = cookieContainer,
    UseCookies = true,
    AllowAutoRedirect = true
};

using var client = new HttpClient(handler);

这样配置后,所有进出的请求都会自动带上 Cookie。登录成功后,后续操作也能保持身份认证状态。

为了复用方便,我们可以封装成一个会话客户端:

public class SessionClient : IDisposable
{
    private readonly CookieContainer _cookieContainer;
    private readonly HttpClientHandler _handler;
    private readonly HttpClient _client;

    public SessionClient()
    {
        _cookieContainer = new CookieContainer();
        _handler = new HttpClientHandler
        {
            CookieContainer = _cookieContainer,
            UseCookies = true,
            AllowAutoRedirect = true
        };
        _client = new HttpClient(_handler);
    }

    public async Task<HttpResponseMessage> LoginAsync(string username, string password)
    {
        var form = new Dictionary<string, string>
        {
            {"username", username},
            {"password", password}
        };

        var content = new FormUrlEncodedContent(form);
        return await _client.PostAsync("https://example.com/login", content);
    }

    public HttpClient Client => _client;

    public void Dispose()
    {
        _client?.Dispose();
    }
}

这个类保证了同一个用户的多次请求共享同一份 Cookie,非常适合做长期任务,比如定时查票。

甚至还能持久化保存 Cookie,下次启动直接恢复登录态:

// 保存
File.WriteAllText("cookies.json", JsonSerializer.Serialize(GetAllCookies(cookieContainer)));

// 恢复
var savedCookies = JsonSerializer.Deserialize<List<Cookie>>(json);
foreach (var c in savedCookies)
{
    cookieContainer.Add(new Uri($"https://{c.Domain}{c.Path}"), c);
}

再也不用手动登录了,爽歪歪~ 😎


🛡️ 异常处理不到位?等着被封 IP 吧!

网络环境千变万化,DNS 解析失败、连接超时、SSL 握手异常……这些都是家常便饭。你不处理?那就等着程序崩溃吧。

⏱️ 超时控制 + 重试策略 = 稳定性的双保险

默认超时是 100 秒,太久了!我们应该主动设短一些:

_client = new HttpClient(_handler)
{
    Timeout = TimeSpan.FromSeconds(30)
};

但这还不够。遇到临时故障怎么办?上重试机制!

推荐使用 Polly 库,它是 .NET 生态中最流行的弹性策略框架:

var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
    .WaitAndRetryAsync(3, i => TimeSpan.FromSeconds(Math.Pow(2, i)));

await retryPolicy.ExecuteAsync(async () =>
{
    var response = await _client.GetAsync("/api/data");
    return response;
});

这段代码的意思是:如果失败,分别等待 2s、4s、8s 再试,总共三次。

指数退避策略可以有效缓解服务器压力,避免雪崩效应。


📝 日志记录不能少,不然出了问题都不知道哪错了

建议用结构化日志,比如 Serilog 或 NLog:

try
{
    var response = await _client.GetAsync(url);
}
catch (HttpRequestException ex) when (ex.InnerException is SocketException sockEx)
{
    logger.LogError(ex, "Socket 错误 [{Code}] 发生于 {Url}", sockEx.SocketErrorCode, url);
}
catch (TaskCanceledException)
{
    logger.LogWarning("请求超时: {Url}", url);
}

有了这些日志,排查问题就像侦探破案一样精准 🔍


🏗️ 最终成果:一个坚如磐石的高可用客户端

把这些能力整合起来,我们可以写出这样一个“终极武器”:

public class ResilientApiClient : IDisposable
{
    private readonly HttpClient _client;
    private readonly AsyncRetryPolicy<HttpResponseMessage> _retryPolicy;

    public ResilientApiClient(Uri baseAddress)
    {
        var handler = new HttpClientHandler
        {
            UseCookies = true,
            CookieContainer = new CookieContainer()
        };

        _client = new HttpClient(handler)
        {
            BaseAddress = baseAddress,
            Timeout = TimeSpan.FromSeconds(30)
        };

        _retryPolicy = BuildRetryPolicy();
    }

    private AsyncRetryPolicy<HttpResponseMessage> BuildRetryPolicy()
    {
        return Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests || 
                           (int)r.StatusCode >= 500)
            .WaitAndRetryForeverAsync(
                sleepDurationProvider: _ => TimeSpan.FromSeconds(5),
                onRetry: (outcome, timeSpan) =>
                {
                    Console.WriteLine($"🔁 请求失败,{timeSpan}s 后重试...");
                });
    }

    public async Task<string> GetStringAsync(string endpoint)
    {
        var response = await _retryPolicy.ExecuteAsync(
            () => _client.GetAsync(endpoint));

        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }

    public void Dispose() => _client?.Dispose();
}

这个类集成了:
- 自动 Cookie 管理 ✅
- 超时防护 ✅
- 永不停止的重试 ✅
- 友好提示 ✅

拿来就能用,嵌入任何项目都没问题!


🔍 12306 接口逆向分析:别怕,跟着我一步步来

你以为 12306 很神秘?其实只要你打开 Chrome 的开发者工具,一切真相大白。

🕵️‍♂️ 抓包第一步:F12 → Network → XHR/Fetch

登录 12306 官网,按 F12,切换到 Network 标签页,然后点击“查询车票”。

你会看到一堆请求,重点关注那些返回 JSON 的 XHR 请求。其中最有可能的就是这个:

GET https://kyfw.12306.cn/otn/leftTicket/queryE?
  leftTicketDTO.train_date=2025-04-05&
  leftTicketDTO.from_station=BJP&
  leftTicketDTO.to_station=SHH&
  purpose_codes=ADULT

参数含义如下:

参数 示例 说明
train_date 2025-04-05 出发日期
from_station BJP 北京北站电报码
to_station SHH 上海虹桥站电报码
purpose_codes ADULT 成人票

注意:这些车站码不是拼音首字母!必须查表转换。


🗺️ 车站编码映射表从哪来?JS 文件里藏着宝藏!

12306 前端 JS 文件中有一个全局变量叫 station_names ,长得像这样:

var station_names ='@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京东|BOP|beijingdong|bjd|1@...';

我们可以在 C# 中解析它:

public static Dictionary<string, string> ParseStationData(string rawData)
{
    var result = new Dictionary<string, string>();
    var entries = rawData.Split('@');

    foreach (var entry in entries)
    {
        if (string.IsNullOrEmpty(entry)) continue;

        var parts = entry.Split('|');
        if (parts.Length >= 5)
        {
            string stationName = parts[1]; // 中文名
            string teleCode = parts[2];    // 电报码
            result[stationName] = teleCode;
        }
    }

    return result;
}

以后用户输入“北京西”,我们就查表得到 BJP ,完美转换!


🔄 动态参数也不能放过:时间戳、随机数、Referer……

有些接口还带 _=1712284800123 这样的参数,其实是毫秒级时间戳,用来防缓存。

C# 获取很简单:

public static long GetCurrentTimestampMs() =>
    DateTimeOffset.Now.ToUnixTimeMilliseconds();

再加上 Referer 轮换、URL 参数扰动,伪装度直接拉满:

var queryParams = HttpUtility.ParseQueryString("");
queryParams["leftTicketDTO.train_date"] = "2025-04-05";
queryParams["leftTicketDTO.from_station"] = "BJP";
queryParams["leftTicketDTO.to_station"] = "SHH";
queryParams["purpose_codes"] = "ADULT";
queryParams["_"] = GetCurrentTimestampMs().ToString();

string finalUrl = $"{baseUrl}?{queryParams}";

🧩 数据提取:正则 vs DOM 解析器,谁赢了?

拿到 HTML 怎么办?别急着写正则!

正则虽然快,但极其脆弱。一旦网页改了个 class 名,你就得重新写规则。

真正靠谱的是 Html Agility Pack(HAP) ,它能把 HTML 当成 XML 来操作:

using HtmlAgilityPack;

var doc = new HtmlDocument();
doc.LoadHtml(htmlContent);

var nodes = doc.DocumentNode.SelectNodes("//div[@class='train-item']");
foreach (var node in nodes)
{
    var trainNo = node.SelectSingleNode(".//strong[@class='train-no']")?.InnerText.Trim();
    var startTime = node.SelectSingleNode(".//span[@class='start-time']")?.InnerText.Trim();
    var seats = node.SelectSingleNode(".//em[@class='seats']")?.InnerText.Trim();

    Console.WriteLine($"🚄 {trainNo} | {startTime} | {seats}");
}

XPath 查询精准定位,不怕标签顺序乱、属性换行等问题,稳定性杠杠的!


💾 本地数据管理:让程序更快、更聪明

频繁解析车站文件?没必要!内存缓存走起:

public sealed class StationManager
{
    private static readonly Lazy<StationManager> _instance = 
        new(() => new StationManager());

    public static StationManager Instance => _instance.Value;

    private readonly StationIndex _index;

    private StationManager()
    {
        var parser = new StationParser();
        var stations = parser.ParseStations();
        _index = new StationIndex(stations);
    }

    public Station GetStationByCode(string code) => _index.GetByCode(code);
    public List<Station> SearchStations(string name) => _index.FuzzySearchByName(name);
}

Trie 前缀树 + 哈希索引,搜索“北京”自动提示“北京南”、“北京西”,用户体验瞬间提升 🚀


🖼️ WinForms 界面开发:别再说 WinForm 过时了!

谁说桌面应用一定要丑?WinForms 也能做出漂亮的 UI!

主窗体包含:
- 两个联动 ComboBox(选出发地/目的地)
- DateTimePicker(选日期)
- DataGridView(展示车次)
- ProgressBar + GIF 动画(加载状态)

关键代码:

private void cmbFromStation_SelectedIndexChanged(object sender, EventArgs e)
{
    UpdateDestinationItems();
}

private void UpdateDestinationItems()
{
    var selected = cmbFromStation.SelectedItem?.ToString();
    var items = StationManager.Instance.SearchStations("")
                       .Where(s => s.Name != selected).ToList();

    cmbToStation.DataSource = items;
}

还有这个小技巧,开启双缓冲防止闪烁:

typeof(DataGridView).InvokeMember("DoubleBuffered", 
    BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty, 
    null, dgvTrainList, new object[] { true });

整个界面流畅丝滑,毫无卡顿感~


🎯 总结:这不仅仅是一个“抢票工具”

我们完成的不是一个简单的爬虫脚本,而是一个完整的工程系统:

  • ✅ 面向对象设计保障可维护性
  • ✅ 异步 + Polly 提升稳定性
  • ✅ Cookie + UA + Header 全方位伪装
  • ✅ Trie + 缓存加速本地查询
  • ✅ WinForms 构建友好交互界面

这套架构完全可以迁移到其他场景:航班查询、酒店预订、股票行情监控……只要是涉及 Web 交互的地方,都能派上用场!

最后送大家一句话:

“技术的价值,不在于你能爬多少数据,而在于你能否构建一个可持续运行的系统。”

希望这篇实战指南,能帮你迈出自动化系统的第一步 🚀

🎉 加油,下一个做出智能出行助手的人,就是你!

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目基于C#语言开发,针对2013年改版后的12306官网接口,实现火车票信息的实时查询功能。系统运行于.NET平台,支持用户通过图形界面输入出发地、目的地和日期等参数,自动发送HTTP请求获取并解析车次、余票、价格等数据。项目涵盖网络编程、HTML/JSON数据解析、验证码处理、GUI设计及本地资源管理等关键技术,使用Newtonsoft.Json、HTML Agility Pack等常用库,具备完整的桌面应用架构和良好的用户体验。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值