简介:本项目基于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 交互的地方,都能派上用场!
最后送大家一句话:
“技术的价值,不在于你能爬多少数据,而在于你能否构建一个可持续运行的系统。”
希望这篇实战指南,能帮你迈出自动化系统的第一步 🚀
🎉 加油,下一个做出智能出行助手的人,就是你!
简介:本项目基于C#语言开发,针对2013年改版后的12306官网接口,实现火车票信息的实时查询功能。系统运行于.NET平台,支持用户通过图形界面输入出发地、目的地和日期等参数,自动发送HTTP请求获取并解析车次、余票、价格等数据。项目涵盖网络编程、HTML/JSON数据解析、验证码处理、GUI设计及本地资源管理等关键技术,使用Newtonsoft.Json、HTML Agility Pack等常用库,具备完整的桌面应用架构和良好的用户体验。
1603

被折叠的 条评论
为什么被折叠?



