前言

        不知道大家有没有跟我一样的经历,节假日或者日常通勤出差,需要频繁查 12306 高铁票。普通情况下打开 APP 刷刷车次看看有没有票就够用        了。但如果我想一次性拿到某条线路全部车次,做完整的余票统计、时段分析、筛选最优车次,纯靠 12306 手动翻页就非常折磨人。

        手动复制粘贴车次信息效率极低,几十上百趟车次,复制到表格里,再做统计画图,半天时间就搭进去了。作为程序员,我就在想,能不能把这套流程自动化:拿原始票务数据→清洗整理→图表可视化→直接输出一份完整分析报告。

        最近体验了 AI 工作台 WorkBuddy,它自带技能调用、脚本运行环境,不需要本地搭建复杂环境,就可以把整套流程串起来。今天就拿上海虹桥→南京南这条高铁高频线路作为案例,完整演示一遍从数据拉取到生成 Word 分析文档的完整流水线,全文会附带可直接复用的脚本片段,新手也可以跟着上手。

测试时间:2026‑08‑24 晚间,分析目标日期:2026‑08‑25,只统计 G 字头高速动车组。

一、要解决什么实际问题

        日常使用 12306APP或者网页,我们只能一页一页浏览车次,能看到单趟车的余票,但很难回答下面这些问题:

  1. 整条线路一天总共有多少趟 G 字头高铁?各个时间段运力分布怎么样?
  2. 二等座整体购票紧张程度如何,多少车次无票?无票车次集中在哪个时间段?
  3. 不同运行时长的车次占比多少?哪些标杆快车最难抢票?
  4. 如何把全部车次统计结果,输出一份带表格、图表,可存档、可转发的正式报告?

        如果手动做,要复制 200 + 车次信息,录入 Excel,做透视表,再用 Excel 画图,最后整理文档,工作量巨大。而借助 WorkBuddy 的能力,我们可以把「数据获取‑表格整理‑可视化‑文档生成」做成自动化流水线。

二、整体工具环境介绍

        本次实验全部在 WorkBuddy AI 工作台内部完成,不需要在自己电脑上安装 Python、Node 各种依赖包,平台提供隔离运行环境,开箱即用。

组件 作用说明
WorkBuddy AI 工作台 核心载体,支持技能调用、文件读写、Python/NodeJS 脚本执行
内置 12306 技能 封装 12306 官方查询接口,支持指定日期、车站、车次类型,获取车次、时刻、各席别余票结构化数据
Python3.13 + openpyxl 将原始 JSON 票务数据导出 Excel 明细表格
python‑docx + matplotlib 绘制统计图表,生成带图表、目录的 Word 正式报告

划重点:所有脚本运行在平台沙盒环境,本地不需要配置任何运行环境,这也是这套方案最大的优势。

1、 12036技能说明

        WorkBuddy其实内置了许多的技能,有些是免费的,有些是收费的。今天我们选用的主角就是免费的12306技能。在WorkBuddy的技能中可以搜索到:

        可以在WorkBuddy中查看这个技能具体是什么?

        将这个技能安装到项目空间后就可以正常使用了。

2、文档生成技能

        为了实现将获取的票务信息转换成Excel和Word文档。这里同时还需要安装docx技能。docx技能能力如下:

三、第一步:车次原始数据

        我们使用内置的脚本,传入出发站、到达站、出行日期、车次类型参数,直接输出 JSON 结构化数据,JSON 是后续所有数据分析的数据源。在安装好相应的技能之后,下面就可以正式开始获取数据,在WorkBuddy中输入以下提示词,这里使用的模型选用Hy3:

@skill:12306 查询明天上海虹桥到南京南的高铁时刻和二等座余票。

        输入后,在控制台开始数据获取:

        在其控制台中,可以看到截取的车次信息:

车次 出发→到达 耗时 二等座
G2 06:43→07:58 1h15m
G4 07:00→08:11 1h11m 11 张
G1786 06:00→07:29 1h29m
G1970 06:09→07:36 1h27m
G94 08:55→09:54 59m 1 张
G100 10:04→11:03 59m
G18 12:55→14:07 1h12m
G22 15:00→15:59 59m
G24 16:00→16:59 59m
G26 17:00→18:07 1h07m

        WorkBuddy已经将完整的车次信息整理成Html,直接点击打开即可(是不是非常方便),如下图所示:

四、数据挖掘:背后的业务洞察

        拿到原始 JSON 之后,我们做统计分析,先看整体大盘数据。

指标 数值
G 字头总车次 261 趟
二等座显示有票 236 趟(约 90.4%)
二等座无票 25 趟(约 9.6%)
可正常购车次 259 趟
全天首末班车 05:45 / 22:05
标杆最短运行时间 59 分钟
展示真实具体余票张数车次 13 趟

从数据可以看出来,上海虹桥到南京南是典型公交化运营的高铁线路,全天 261 趟 G 车,平均间隔不到 4 分钟就发一班车。整体运力非常充足,大部分车次二等座都有票。少数无票车次大多集中在早高峰以及 59‑70 分钟的标杆直达快车,这种车耗时最短,非常抢手。

再看两个维度的细分分析:

  1. 时段分布 把车次按照出发小时分组统计。可以看到全天各个时间段都有充足车次投放。二等座无票车次是零散分布,没有出现某一个大时间段整体全部没票的情况。如果出行时间灵活,稍微错开热门标杆车次,很容易买到票。
  2. 运行时长分布 绝大多数车次耗时集中在 1‑2 小时区间。最快标杆车只要 59 分钟,个别过路绕行车次最长接近 6 小时。 其中运行时长≤70 分钟的标杆快车一共 26 趟,这类车时刻好、耗时短,但是抢票压力最大,经常一开售就被抢光。如果抢不到标杆车,可以退一步选择 1‑1.5 小时区间的车次,票源会宽裕很多。

1、将Html转为excel

将以上数据整理成一个Excel

        最终可以看到整理完成的Excel信息如下:

        具体生成的Python代码如下:

import json
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter

SRC = r"C:/Users/dbrg/WorkBuddy/2026-08-24-23-38-02/query.json"
OUT = r"C:/Users/dbrg/WorkBuddy/2026-08-24-23-38-02/上海虹桥-南京南-2026-08-25.xlsx"

with open(SRC, encoding="utf-8") as f:
    data = json.load(f)

# 中文列头
HEADERS = ["车次", "出发站", "到达站", "出发时间", "到达时间", "历时",
           "商务/特等", "一等座", "二等座", "无座", "状态"]

# 历时 H:MM -> XhYYm
def fmt_dur(d):
    if not d or d == "--":
        return d
    h, m = d.split(":")
    return f"{int(h)}h{int(m):02d}m"

# 状态映射
def fmt_status(v):
    return "可购" if v == "Y" else "不可购"

wb = Workbook()
ws = wb.active
ws.title = "高铁时刻表"

# 标题行
title_fill = PatternFill("solid", fgColor="1F4E78")
title_font = Font(bold=True, color="FFFFFF", size=11)
center = Alignment(horizontal="center", vertical="center")
thin = Side(style="thin", color="BFBFBF")
border = Border(left=thin, right=thin, top=thin, bottom=thin)

# 写表头
for c, h in enumerate(HEADERS, 1):
    cell = ws.cell(row=1, column=c, value=h)
    cell.fill = title_fill
    cell.font = title_font
    cell.alignment = center
    cell.border = border

# 颜色:二等座
green = PatternFill("solid", fgColor="C6EFCE")
red = PatternFill("solid", fgColor="FFC7CE")
green_font = Font(color="006100")
red_font = Font(color="9C0006")
ze_col = HEADERS.index("二等座") + 1
status_col = HEADERS.index("状态") + 1

# 数据行
r = 2
for t in data:
    row = [
        t.get("trainCode", ""),
        t.get("fromStation", ""),
        t.get("toStation", ""),
        t.get("departTime", ""),
        t.get("arriveTime", ""),
        fmt_dur(t.get("duration", "")),
        t.get("swz", "--"),
        t.get("zy", "--"),
        t.get("ze", "--"),
        t.get("wz", "--"),
        fmt_status(t.get("canBuy", "")),
    ]
    for c, v in enumerate(row, 1):
        cell = ws.cell(row=r, column=c, value=v)
        cell.alignment = center
        cell.border = border
    # 二等座颜色
    ze_val = row[ze_col - 1]
    zc = ws.cell(row=r, column=ze_col)
    if ze_val == "无":
        zc.fill = red
        zc.font = red_font
    elif ze_val not in ("--", ""):
        zc.fill = green
        zc.font = green_font
    # 状态颜色
    sc = ws.cell(row=r, column=status_col)
    if row[status_col - 1] == "可购":
        sc.fill = green
        sc.font = green_font
    else:
        sc.fill = red
        sc.font = red_font
    r += 1

# 列宽
widths = [9, 11, 11, 10, 10, 9, 11, 9, 9, 8, 9]
for i, w in enumerate(widths, 1):
    ws.column_dimensions[get_column_letter(i)].width = w

# 冻结首行 + 自动筛选
ws.freeze_panes = "A2"
ws.auto_filter.ref = f"A1:{get_column_letter(len(HEADERS))}{r-1}"

wb.save(OUT)
print(f"已生成: {OUT}")
print(f"车次总数: {len(data)}")
print(f"二等座有票: {sum(1 for t in data if t.get('ze') not in ('无','--',''))}")
print(f"二等座无票: {sum(1 for t in data if t.get('ze') == '无')}")

五、生成完整分析报告

        图表、表格、文字分析全部准备好了,接下来调用docx中文排版组件,直接生成一份结构完整的 Word 文档。在WorkBuddy中输入以下提示词:

基于docx技能基于excel数据对明日火车票余票情况进行一个全面的解读,形成一份完整的票务说明文档。

        代码片段如下:

# -*- coding: utf-8 -*-
import json, sys
from collections import Counter, defaultdict

BASE = r"C:/Users/dbrg/WorkBuddy/2026-08-24-23-38-02"
SRC = f"{BASE}/query.json"
sys.path.insert(0, r"C:/Users/dbrg/.workbuddy/skills/docx-cjk-report-builder/references")
from docx_cjk_builder import (setup_document, setup_header_footer, add_toc,
                              render_blocks, add_pagebreak, add_caption)
from docx.shared import Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import os, tempfile

plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'SimSun', 'Arial']
plt.rcParams['axes.unicode_minus'] = False
_PIE_COLORS = ['#4472C4', '#ED7D31', '#A5A5A5', '#FFC000', '#5B9BD5', '#70AD47']

def add_chart(doc, kind, categories, series, caption=None, height_cm=7.5, width_cm=13.5):
    """series: list of (name, tuple_of_values); kind: 'pie' | 'bar'"""
    if caption:
        add_caption(doc, caption)
    fig, ax = plt.subplots(figsize=(width_cm / 2.54, height_cm / 2.54), dpi=150)
    if kind == 'pie':
        vals = series[0][1]
        ax.pie(vals, labels=categories, autopct='%1.1f%%', startangle=90,
               colors=_PIE_COLORS[:len(vals)], textprops={'fontsize': 9})
        ax.axis('equal')
    else:  # bar (clustered)
        n = len(series)
        w = 0.8 / n
        x = list(range(len(categories)))
        for i, (name, vals) in enumerate(series):
            pos = [xj + (i - (n - 1) / 2) * w for xj in x]
            ax.bar(pos, vals, w, label=name, color=_PIE_COLORS[i % len(_PIE_COLORS)])
        ax.set_xticks(x)
        ax.set_xticklabels(categories, rotation=40, ha='right', fontsize=8)
        ax.legend(fontsize=8, loc='best')
        ax.set_ylabel('车次数量', fontsize=9)
        ax.spines['top'].set_visible(False)
        ax.spines['right'].set_visible(False)
    fig.tight_layout()
    fd, fname = tempfile.mkstemp(suffix='.png', prefix='chart_')
    os.close(fd)
    fig.savefig(fname, dpi=150)
    plt.close(fig)
    # 图片放入单独段落并居中(add_picture 会自建段落,需显式获取再设对齐)
    para = doc.add_paragraph()
    para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = para.add_run()
    run.add_picture(fname, width=Cm(width_cm))
    os.remove(fname)
    return fname

with open(SRC, encoding="utf-8") as f:
    data = json.load(f)

def dur_min(d):
    if not d or d == "--":
        return None
    h, m = d.split(":")
    return int(h) * 60 + int(m)

def fmt_dur(m):
    return f"{m//60}小时{m%60}分" if m is not None else "—"

total = len(data)
ze_ok = [t for t in data if t.get("ze") not in ("无", "--", "")]
ze_no = [t for t in data if t.get("ze") == "无"]
can_y = [t for t in data if t.get("canBuy") == "Y"]
can_n = [t for t in data if t.get("canBuy") == "N"]

# 出发时段分布(按小时)
hour_cnt = Counter()
hour_no = Counter()
for t in data:
    h = int(t["departTime"].split(":")[0])
    hour_cnt[h] += 1
    if t.get("ze") == "无":
        hour_no[h] += 1

# 历时分布
def bucket(m):
    if m is None: return "其他"
    if m < 60: return "不足1小时"
    if m <= 90: return "1–1.5小时"
    if m <= 120: return "1.5–2小时"
    if m <= 180: return "2–3小时"
    return "3小时以上"
dur_buckets = Counter(bucket(dur_min(t["duration"])) for t in data)

# 最快车次
min_dur = min(dur_min(t["duration"]) for t in data)
fastest = [t for t in data if dur_min(t["duration"]) == min_dur]

# 首末班
first = min(data, key=lambda t: t["departTime"])
last = max(data, key=lambda t: t["departTime"])

# 具体余票数(数字)的车次
numeric_ze = [(t["trainCode"], t["departTime"], t["ze"]) for t in data
              if t.get("ze") not in ("无", "有", "--", "")]

# 标杆车(<=70分钟)及其二等座
benchi = [t for t in data if dur_min(t["duration"]) <= 70]
benchi.sort(key=lambda t: dur_min(t["duration"]))

# 推荐车次:运行时间短且二等座有票,取前若干
recs = [t for t in data if t.get("ze") not in ("无", "--", "") and dur_min(t["duration"]) <= 80]
recs.sort(key=lambda t: (dur_min(t["duration"]), t["departTime"]))

print("total", total, "ze_ok", len(ze_ok), "ze_no", len(ze_no))
print("can_y", len(can_y), "can_n", len(can_n))
print("min_dur", min_dur, "first", first["departTime"], "last", last["departTime"])
print("numeric_ze count", len(numeric_ze))
print("benchi count", len(benchi))

# 预建查找表(避免函数定义顺序问题)
arr_map = {t["trainCode"]: t["arriveTime"] for t in data}
dur_map = {t["trainCode"]: (lambda m: f"{m//60}h{m%60:02d}m")(dur_min(t["duration"]))
           for t in data if dur_min(t["duration"]) is not None}

# ============ 构建文档 BLOCKS ============
BLOCKS = []

# 封面
BLOCKS.append(('p', "上海虹桥至南京南高速铁路", {'align':'center','size':14,'bold':True,'space_after':6}))
BLOCKS.append(('p', "明日(2026年8月25日)车票余票情况说明", {'align':'center','size':22,'bold':True,'space_after':24}))
BLOCKS.append(('p', "(基于12306官方接口实时余票数据)", {'align':'center','size':12,'color':'595959','space_after':40}))
BLOCKS.append(('p', "编制日期:2026年8月24日", {'align':'center','size':11,'space_after':4}))
BLOCKS.append(('p', "数据基准日:2026年8月25日(周二)", {'align':'center','size':11,'space_after':4}))
BLOCKS.append(('p', "数据来源:中国铁路12306官方售票接口", {'align':'center','size':11,'space_after':4}))
BLOCKS.append(('p', "区间:上海虹桥站 → 南京南站(G字头高速动车组)", {'align':'center','size':11,'space_after':4}))

BLOCKS.append(('pagebreak',))

# 目录(真实 TOC 域,Word 中右键“更新域”生成)
BLOCKS.append(('toc',))
BLOCKS.append(('pagebreak',))

# 一、总体情况概述
BLOCKS.append(('h1', "一、总体情况概述"))
BLOCKS.append(('p',
    f"本报告基于中国铁路12306官方售票接口于2026年8月24日晚间获取的实时数据,"
    f"对2026年8月25日(周二)由上海虹桥站开往南京南站的G字头高速动车组列车车票余票情况进行了全面梳理与解读。"
    f"当日该区间共开行高速动车组列车 {total} 趟,覆盖自清晨 {first['departTime']} 至晚间 {last['departTime']} 的长时间段,"
    f"基本实现了从早到晚的高密度公交化开行,能够充分满足当日商务、通勤及旅游等各类出行需求。"))
BLOCKS.append(('p',
    f"从余票总体态势看,当日二等座(日常出行主力席别)整体供给充裕:在 {total} 趟列车中,"
    f"二等座显示“有票”或具体剩余张数的车次达 {len(ze_ok)} 趟,占比约 {len(ze_ok)*100//total}%;"
    f"仅 {len(ze_no)} 趟车次二等座显示“无票”,且多集中于部分早班及标杆直达车次。"
    f"整体而言,明日该区间高铁运力充足,旅客购票压力较小,选择空间大。"))

# 二、车次总体规模与开行特征
BLOCKS.append(('h1', "二、车次总体规模与开行特征"))
BLOCKS.append(('p',
    f"本次统计区间(上海虹桥→南京南)当日开行的 {total} 趟列车均为G字头高速动车组,"
    f"其中包含少量经由其他线路、历时较长的过路车次。从可售状态看,"
    f"显示“可购”的车次 {len(can_y)} 趟,“不可购”(多为已停止发售或限售车次){len(can_n)} 趟。"))
BLOCKS.append(('p', "列车开行呈现以下特征:"))
BLOCKS.append(('bullets', [
    f"开行密度高:自 {first['departTime']} 首班车至 {last['departTime']} 末班车,平均发车间隔不足 4 分钟,呈典型公交化运营格局。",
    f"历时跨度大:最短运行时间仅 {fmt_dur(min_dur)}(标杆直达车次),最长达 6 小时(个别经停绕行过路车次),主流车次集中于 1 至 2 小时之间。",
    "席别结构完整:各车次普遍设有商务/特等座、一等座、二等座及无座,部分车次另设动卧等席别,可满足差异化需求。",
]))

# 三、二等座余票总体态势
BLOCKS.append(('h1', "三、二等座余票总体态势"))
BLOCKS.append(('p',
    f"二等座作为日常出行的主力席别,其供给情况最能反映购票难易程度。统计显示,"
    f"{total} 趟车次中二等座有余票(含“有票”与具体剩余张数)共 {len(ze_ok)} 趟,"
    f"无票 {len(ze_no)} 趟,余票覆盖率约 {len(ze_ok)*100//total}%。"))
BLOCKS.append(('p',
    f"需说明的是,12306系统在余票充足时统一显示“有”而不显示具体张数,仅当余票较少时才会给出具体数值。"
    f"当日共有 {len(numeric_ze)} 趟车次显示了具体剩余二等座张数(个位至十余张不等),"
    f"这些车次通常临近发车或属于热门标杆车次,提示旅客尽早下手。"))
# 具体余票数的车次表(取部分代表)
BLOCKS.append(('table', {
    'caption': '表1  显示具体剩余二等座张数的部分车次',
    'headers': ['车次', '出发时间', '到达时间', '历时', '二等座余票'],
    'rows': [[c, d, arr_map[c], dur_map[c], n] for (c, d, n) in numeric_ze[:18]],
    'widths': [2.2, 2.2, 2.2, 2.0, 2.4],
    'align_center_cols': {0,1,2,3,4},
}))
BLOCKS.append(('chart', {
    'kind': 'pie',
    'categories': ['二等座有票', '二等座无票'],
    'series': [('车次数量', (len(ze_ok), len(ze_no)))],
    'caption': '图1  二等座余票总体态势(有票 vs 无票)',
    'height_cm': 7.5, 'width_cm': 9.5,
}))

# 四、出发时段分布
BLOCKS.append(('h1', "四、出发时段分布与余票紧张度分析"))
BLOCKS.append(('p', "将全天车次按出发小时归并,可观察各时段的运力投放与二等座余票紧张程度:"))
hour_rows = []
for h in sorted(hour_cnt):
    cnt = hour_cnt[h]
    no = hour_no[h]
    ok = cnt - no
    tag = "紧张" if no > 0 and ok > 0 and no >= max(1, cnt*0.3) else ("充裕" if no == 0 else "部分紧张")
    hour_rows.append([f"{h:02d}:00–{h+1:02d}:00", str(cnt), str(ok), str(no), tag])
BLOCKS.append(('table', {
    'caption': '表2  各出发时段车次数量与二等座余票情况',
    'headers': ['出发时段', '车次总数', '二等座有票', '二等座无票', '余票态势'],
    'rows': hour_rows,
    'widths': [3.2, 2.2, 2.4, 2.4, 2.2],
    'align_center_cols': {1,2,3,4},
}))
BLOCKS.append(('p',
    "分析可见,全天各时段均有稳定车次投放,二等座“无票”车次零散分布,并未出现整段时段的系统性紧张,"
    "说明运能调度较为均衡,旅客在不同时段均可获得较为充分的购票机会。"))
hour_labels = [f"{h:02d}:00" for h in sorted(hour_cnt)]
ok_list = [hour_cnt[h] - hour_no[h] for h in sorted(hour_cnt)]
no_list = [hour_no[h] for h in sorted(hour_cnt)]
BLOCKS.append(('chart', {
    'kind': 'bar',
    'categories': hour_labels,
    'series': [('二等座有票', tuple(ok_list)), ('二等座无票', tuple(no_list))],
    'caption': '图2  各出发时段车次数量与二等座余票分布',
    'height_cm': 8, 'width_cm': 15.5,
}))

# 五、运行时长分布与标杆车次
BLOCKS.append(('h1', "五、运行时长分布与标杆车次"))
order = ["不足1小时","1–1.5小时","1.5–2小时","2–3小时","3小时以上","其他"]
dur_rows = [[k, str(dur_buckets.get(k,0))] for k in order if dur_buckets.get(k,0)>0]
BLOCKS.append(('table', {
    'caption': '表3  车次运行时长分布',
    'headers': ['运行时长区间', '车次数量'],
    'rows': dur_rows,
    'widths': [5.0, 3.0],
    'align_center_cols': {1},
}))
BLOCKS.append(('p',
    f"从运行效率看,当日最快车次为运行仅 {fmt_dur(min_dur)} 的标杆直达列车,"
    f"此类车次多停靠站少、时刻优越,是商务出行的首选,但二等座往往开售即被抢购,余票波动较大。"))
dur_cats = [k for k in order if dur_buckets.get(k, 0) > 0]
dur_vals = [dur_buckets[k] for k in dur_cats]
BLOCKS.append(('chart', {
    'kind': 'pie',
    'categories': dur_cats,
    'series': [('车次数量', tuple(dur_vals))],
    'caption': '图3  车次运行时长分布',
    'height_cm': 8, 'width_cm': 11,
}))
BLOCKS.append(('p', "主要标杆车次(运行时间≤70分钟)及其二等座余票情况如下:"))
benchi_rows = []
for t in benchi:
    benchi_rows.append([t["trainCode"], t["departTime"], t["arriveTime"],
                        fmt_dur(dur_min(t["duration"])), t.get("ze","—")])
BLOCKS.append(('table', {
    'caption': '表4  标杆车次(运行≤70分钟)二等座余票',
    'headers': ['车次', '出发', '到达', '历时', '二等座'],
    'rows': benchi_rows,
    'widths': [2.0, 2.0, 2.0, 2.4, 2.4],
    'align_center_cols': {0,1,2,3,4},
}))

# 六、重点推荐车次
BLOCKS.append(('h1', "六、重点推荐车次"))
BLOCKS.append(('p',
    "综合运行时间与二等座余票情况,以下车次兼顾“快捷”与“有票”,建议作为优先购票选项"
    "(运行时间≤80分钟且二等座有余票,按出发时间排列,取前15趟):"))
rec_rows = []
for t in recs[:15]:
    rec_rows.append([t["trainCode"], t["departTime"], t["arriveTime"],
                     fmt_dur(dur_min(t["duration"])), t.get("ze","—")])
BLOCKS.append(('table', {
    'caption': '表5  优先推荐购票车次',
    'headers': ['车次', '出发', '到达', '历时', '二等座'],
    'rows': rec_rows,
    'widths': [2.0, 2.0, 2.0, 2.4, 2.4],
    'align_center_cols': {0,1,2,3,4},
}))

# 七、购票与出行建议
BLOCKS.append(('h1', "七、购票与出行建议"))
BLOCKS.append(('bullets', [
    "尽早锁定标杆车次:运行1小时左右的直达车次(如G2、G4、G18、G22等)最为抢手,建议提前购票或第一时间候补。",
    "错峰出行更从容:早晚高峰及热门整点车次余票消耗快,若时间灵活,可选择平峰时段车次,余票更充裕。",
    "关注具体余数车次:凡显示具体剩余张数(个位至十余张)的车次,提示余票有限,宜尽快下单,切勿犹豫。",
    "善用候补功能:心仪车次无票时,及时提交候补订单,系统在退票或增开时优先兑现。",
    "留意过路长历时车次:个别经停绕行车次历时可达数小时,非必要不建议选择,以免耽误行程。",
    "以官方为准:余票为动态数据,出行前请务必于12306官方渠道再次核实最新余票与开行信息。",
]))

# 八、数据说明与注意事项
BLOCKS.append(('h1', "八、数据说明与注意事项"))
BLOCKS.append(('bullets', [
    "数据时点:本报告数据抓取自2026年8月24日晚间12306官方接口,余票为实时动态值,至出行日可能发生变化。",
    "余票含义:“有”表示余票充足但未公开具体张数;“数字”为系统显示的具体剩余张数;“无”表示当前无票(含候补可能)。",
    "站点说明:查询结果以“上海虹桥→南京南”为区间,个别车次因经停或售票系统展示原因,起讫站名显示为“上海/南京”,实为同一区间车次。",
    "统计口径:本报告仅统计G字头高速动车组;不含D字头动车及其他普速列车。",
    "免责声明:本报告仅供参考,不构成任何购票承诺,最终以铁路部门官方发售信息为准。",
]))

# 附录:全部车次明细
BLOCKS.append(('pagebreak',))
BLOCKS.append(('h1', "附录:上海虹桥至南京南全部G字头车次余票明细"))
BLOCKS.append(('p', f"下表汇总当日该区间全部 {total} 趟G字头车次的核心信息(按发车时间排序)。", {'size':10,'color':'595959'}))
all_rows = []
for t in sorted(data, key=lambda x: x["departTime"]):
    all_rows.append([t["trainCode"], t["departTime"], t["arriveTime"],
                     dur_map[t["trainCode"]], t.get("swz","—"), t.get("zy","—"),
                     t.get("ze","—"), t.get("wz","—"),
                     "可购" if t.get("canBuy")=="Y" else "不可购"])
BLOCKS.append(('table', {
    'caption': f'表6  全部 {total} 趟车次余票明细(按发车时间)',
    'headers': ['车次','出发','到达','历时','商务/特等','一等座','二等座','无座','状态'],
    'rows': all_rows,
    'widths': [1.6,1.5,1.5,1.4,1.7,1.4,1.4,1.3,1.5],
    'align_center_cols': set(range(9)),
    'fontsize': 8.5,
}))

# 生成
doc = setup_document()
setup_header_footer(doc, "上海虹桥至南京南高铁余票情况说明(2026-08-25)")
# 渲染:遇到 toc / chart 哨兵特殊处理
for blk in BLOCKS:
    if blk[0] == 'toc':
        add_toc(doc)
    elif blk[0] == 'chart':
        c = blk[1]
        add_chart(doc, c['kind'], c['categories'], c['series'],
                  caption=c.get('caption'), height_cm=c.get('height_cm', 7.5),
                  width_cm=c.get('width_cm', 13.5))
    else:
        render_blocks(doc, [blk])
out = f"{BASE}/上海虹桥-南京南-2026-08-25-票务说明.docx"
doc.save(out)
print("SAVED", out)

输出的 Word 文档结构非常完整,拿来直接存档或者简单修改就可以做汇报:

  • 封面 + Word 自动目录(支持更新域刷新页码)

  • 总体情况概述

  • 车次规模和开行特征
  • 二等座余票整体态势(附带明细表格 + 占比饼图)

  • 分出发时段余票分析(时段统计表 + 柱状图)

  • 运行时长 & 标杆车次分析

  • 重点车次推荐
  • 购票出行建议

  • 数据来源与免责说明附录:全部 261 趟车次完整余票明细,以上信息是不是准备得特别详细,拿来即可用。

六、整体流程复盘

        完整自动化流水线链路:

12306 技能查询接口 → JSON 原始数据 → openpyxl 输出 Excel 明细 → python‑docx 生成完整 Word 报告

        整套流程脚本大部分由 AI 辅助生成迭代,我们只需要少量修改参数,接近零代码就完成整套数据分析。对比手动处理,收益非常明显:

  1. 数据完整度高:一次性获取全天全部 261 趟车次,不用 APP 一页页手动翻找,不会漏掉车次;
  2. 量化分析:从时段、运行时长、席别多个维度做统计,所有结论都有原始数据支撑,不是主观感受;
  3. 产出物可交付:直接输出带图表表格的正式文档,适合存档、内部参考;
  4. 高度可复用:换出发站、到达站、出行日期,只修改少量参数,整套流水线直接重新跑一遍,不需要重新写大量代码。

        不仅仅是高铁票务,很多类似场景:交通数据统计、公开接口数据采集分析、自动生成报告,都可以套用这套 “技能拉取数据‑脚本处理‑可视化‑文档输出” 的模式。

七、重要免责与数据说明

  1. 本次数据采集时间为 2026‑08‑24 晚间,12306 余票属于实时动态数据,随着时间会不断变化,出行当天实际余票会和采集时刻存在差异。
  2. “有” 代表余票充足但是接口没有返回具体张数;数字代表接口返回的真实剩余票数;“无” 代表当前无票。
  3. 本次只统计 G 字头高速动车组,D 字头动车不在统计范围内。
  4. 本方案仅做技术学习演示,所有购票最终以铁路 12306 官方实际发售信息为准。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。
Logo

一站式 AI 云服务平台

更多推荐