将会议/活动嘉宾行程 Excel 源表数据,按标准映射规则批量写入车辆预约业务模板(业务导入模版.xlsx)。
模板路径:C:\Users\Administrator\Desktop\业务导入模版.xlsx
核心脚本:scripts/import_itinerary.py
列映射参考:references/column_mapping.md
城市减时参考:references/city_time_rules.md
运行 scripts/explore_source.py 探查源表的 sheet 列表、每个 sheet 的行数、第一行和表头行(通常为 Row 1 或有明显标题的行)。找出包含行程数据的主 sheet。
隐藏 Sheet 跳过:ws.sheet_state != 'visible' 的 sheet 跳过,不处理。隐藏列、隐藏行照常读取。
⚠️ 不要自行添加用户未要求的过滤逻辑(如按医院名、航班状态过滤行)。
关键信息:
读取 references/city_time_rules.md。
如源表回程出发地涉及已知城市(上海/长沙/扬州泰州),直接使用已有规则。
如出现未记录城市,必须先暂停,向用户询问该城市的减时规则(机场/高铁站分别多少小时),获取答复后再继续。
按 references/column_mapping.md 规则逐行提取来程和回程数据,应用过滤、本地场景处理、中转号码提取等逻辑。
具体处理逻辑参见下方 关键规则 章节。
以 scripts/import_itinerary.py 为基础,复制原始模板 → 按规则写入数据(从 Row 2 开始,来程在前,回程在后)→ 验证 Row 1 表头未变动 → 保存结果到桌面 → 通过 deliver_attachments + open_result_view 交付给用户。
sheet1.xml 数据行 → 保存为新文件| 模板列/字段 | 固定值 | 规则 |
|---|---|---|
| ------------ | -------- | ------ |
| A 序号* | 无"车型"列:同航班/车次同序号 | ⚠️ 非简单递增;相同序号的行全部保留 |
| A 序号* | 有"车型"列:每人独立序号 | 车型已指定车辆级别,无需统计人数 |
| C 城市* | 上海(或源表城市) | 默认上海,其他城市由用户指定 |
| T 付费方式* | 记账 | 固定值 |
| G 用车类型* | 单程 | 固定 |
| 用车类型 | 单程 | 固定 |
| 订车公司/订车人 | 用户提供 | 每次处理时询问或从源表提取 |
| 客户负责人/受理人 | 用户提供 | 每次处理时询问或从源表提取 |
| 车辆级别 | 无"车型"列:1-2人=B级轿车 / 3-4人=商务车 / >4人=均分 | 每序号≤4人;拆分后子组独立判断:≤2人=B级,3-4人=商务 |
| 车辆级别 | 有"车型"列:直接使用源表值 | 小车/轿车→B级轿车,商务/商务车→商务车 |
| 项目名称 / 用车天数 / 接机人(填手机号) / 乘客级别 / 出发地类型 / 目的地类型 / 客户要求 / 车型 / 联系人 | 不填 | 留空 |
如果源表有"车型"列,直接按单元格内容导出:
如果源表有"酒店"列,按同样方式提取到模板 I/J 列(每人可能不同,替代统一的酒店名称)。
> 优先级:源表"车型"列 > 同序号人数统计算法。
| 源表表头关键字 | 模板列 | 备注 |
|---|---|---|
| -------------- | -------- | ------ |
| 来程日期 / 去程日期 | D 用车日期* | ⚠️ 格式:YYYY/MM/DD(如 2025/07/12),YYYY-MM-DD 也可 |
| 回程日期 | D 用车日期* | ⚠️ 必须 YYYY-MM-DD(同上) |
| 到达时间 | E 预订时间* | 合并格式取后段 |
| 航班号 / 航班 / 车次号 / 车次 | H 航班/车次 | 中转取最后一个标准号码 |
| 到达地 | I 出发地* | |
| 酒店名称 | J 目的地* | 用户提供/源表酒店列提取 |
| 姓名 | M 乘客姓名* | |
| 电话 / 手机号 | N 乘客联系方式 |
> ⚠️ 注意:"上车/下车地"指的是乘客搭乘火车/飞机的上车点和下车点,不是用车的上车点。
当源表存在表头含"上车/下车地"或"上车/下车"字样的列时,该列格式为 上车地-下车地(如 宁波-上海南站):
| 方向 | 模板列 I(出发地*) | 模板列 J(目的地*) | 说明 |
|---|---|---|---|
| ------ | :---: | :---: | ------ |
| 来程 | -后面(下车地=抵达站) | 酒店名称 | 用车从抵达站接到酒店 |
| 回程 | 酒店名称 | -前面(上车地=出发站) | 用车从酒店送到出发站 |
处理规则:
- 拆分 → part[1](下车地)→ I列;J列 → 用户提供的酒店名称- 拆分 → I列 → 酒店名称;part[0](上车地)→ J列-(如只有 虹桥高铁站):> v1.2(已废弃):错误地把 - 前面→I、后面→J,不符合来程"从抵达站接人→送酒店"的业务逻辑。
> v1.3 修正(2026-05-27):来程 I=下车地(抵达站)、J=酒店;回程 I=酒店、J=上车地(出发站)。
| 源表表头关键字 | 模板列 | 备注 |
|---|---|---|
| -------------- | -------- | ------ |
| 回程日期 | D 用车日期* | ⚠️ 格式:YYYY/MM/DD(同来程) |
| 出发时间 | E 预订时间* | 减时后输出;合并格式取前段 |
| 航班号 / 航班 / 车次号 / 车次 | H 航班/车次 | 前加"送";中转取第一个 |
| 出发地 | J 目的地* | J列全称:目的地*(多个目的地,第一列序号一致) |
| 酒店名称 | I 出发地* | 用户提供/源表酒店列提取 |
| 姓名 | M 乘客姓名* | |
| 电话 / 手机号 | N 乘客联系方式 |
> ⚠️ 如果源表没有对应表头关键字,模板对应列留空,不要乱填。
> ⚠️ 2026-05-28 新增:当源表有"属地城市""去程车接时间""去程(地点-地点)""返程车接时间""返程(地点-地点)"等列时,表示这是属地接送场景。
> 属地接送的特点是:用车从乘客家庭地址出发送站/接站,不是从酒店出发。每人地址不同,不能拼车。
识别条件:
属地接送字段映射(覆盖默认映射):
| 方向 | 模板列 | 源表字段 | 说明 |
|---|---|---|---|
| ------ | -------- | --------- | ------ |
| 来程 | C 城市* | 属地城市列 | ⚠️ 不用默认"上海",直接填源表城市 |
| 来程 | D 用车日期* | 出发日期 | 同默认 |
| 来程 | E 预订时间* | 去程车接时间 | ⚠️ 不是航班到达时间,是约定的接车时间 |
| 来程 | H 航班/车次 | 航班号/车次 | 前加"送",同默认 |
| 来程 | I 出发地* | 去程(地点-地点)-前部分 | ⚠️ 家庭地址(不是到达站) |
| 来程 | J 目的地* | 出发机场/火车站 | ⚠️ 送站目的地(不是酒店) |
| 返程 | C 城市* | 属地城市列 | ⚠️ 同来程 |
| 返程 | D 用车日期* | 返程日期 | 同默认 |
| 返程 | E 预订时间* | 返程车接时间 | ⚠️ 不是航班出发时间;为空时用返程到达时间 |
| 返程 | H 航班/车次 | 返程航班号 | ⚠️ 不加"送"!接站不需要"送"前缀 |
| 返程 | I 出发地* | 返程到达机场/火车站(AA列) | ⚠️ 属地到达站,车在属地城市的到达站接人, 不是Z列(返程出发地=会议城市出发站!) |
| 返程 | J 目的地* | 返程(地点-地点) | ⚠️ 家庭地址(不是出发站) |
属地序号规则(与默认不同!):
| 场景 | 序号规则 | 原因 |
|---|---|---|
| ------ | --------- | ------ |
| 属地接送 | 每人独立序号 | 每人家庭地址不同,即使同航班同时间也无法拼车 |
| 普通接送(非属地) | 同航班/车次同序号 | 都是从酒店出发/到达,可以拼车 |
> ⚠️ 2026-05-28 教训(B7451023):吴芳和刘瑭谐乐返程同G223、同时间19:02,但因目的地地址不同(梓园路86号 vs 金泽园),不能拼车同序号。属地接送的核心判断标准是地址是否相同,不是航班/时间是否相同。
> 🚨 2026-05-28 教训2(B7451023修正2):返程I列错误填成了Z列"返程出发地"(会议城市站如"上海虹桥")。属地返程的逻辑是:人到属地城市站 → 车在属地到达站接人 → 送回家。所以I列=AA列(返程到达地=属地到达站),不是Z列(返程出发地=会议城市出发站)。绝不能再搞反!
> 🚨 2026-05-28 教训3(C4531002/B7451023修正3):H列"航班/车次"的"送"前缀规则:
> - 去程(送站):从家送到机场/车站 → H列加"送"(如"送G226""送MU5336")
> - 返程(接站):从机场/车站接回家 → H列不加"送"(如"G223""MU5351")
>
> 逻辑很简单:接站不需要"送",只有送站才需要"送"。绝不能再给返程加"送"!
"去程(地点-地点)"列拆分规则:
家庭地址-出发车站/机场(如 长沙市望城区黄金中路1号金泽园(西门)-长沙南高铁站)-前 → I列(出发地*,即家庭地址)-后 → 参考值(但J列用出发机场/火车站列的标准值)- → 整段为家庭地址属地接送过滤规则:
| 字段 | 值 | 处理 |
|---|---|---|
| ------ | ----- | ------ |
| 去程车接时间 | NA / N/A | 跳过来程(无需安排车辆) |
| 去程(地点-地点) | 含"自行" | 跳过来程(自行前往) |
| 返程车接时间 | NA / N/A | 跳过返程(无需安排车辆) |
| 返程航班号 | 含"不返回属地""自行购票""自驾"等 | 跳过返程 |
| 返程车接时间 | 空(None)但返程日期+地址完整 | 用返程到达时间作默认,不跳过 |
> ⚠️ 注意区分:返程车接时间=None(空,用默认值) vs "NA"(明确跳过)。
> ⚠️ 2026-05-27 新增:输出到模板 I 列(出发地)和 J 列(目的地)的地点名称必须统一格式。全国通用。
此标准化在字段映射和"上车/下车地"拆分之后执行,作为最终的 I/J 列输出清洗。无法匹配的保留源表原始内容。
> ⚠️ 判断顺序关键:必须先匹配火车(GDC/KTZ/纯数字),再用飞机正则。否则 [A-Z0-9]{2,3} 会把 D2828 的 "D2" 误判为航司代码。
| H 列内容 | 判断 | 正则特征 | 示例 | |
|---|---|---|---|---|
| --------- | ------ | --------- | ------ | |
| G/D/C 单字母 + 数字 | 高铁/动车 | ^[GCD]\d+$ | G2025, D530, C3864 | |
| K/T/Z 单字母 + 数字 | 普通火车 | ^[KTZ]\d+$ | K1234, T110 | |
| 纯数字 | 普通火车 | ^\d+$ | 1462 | |
| 航空公司代码(2位字母或1数字+1字母)+ 数字 | 飞机 | `^(?:[A-Z]{2} | [0-9][A-Z])\d{2,4}$` | MU5640, 9C8856, CZ3501 |
| 待定 / 本地 / 市内 | 不处理 | — | 保留原文 |
> 中转/换乘场景:提取所有标准号码后按多数判断(如"MU5640转G2025"中飞机和火车各一,来程取最后一个(MU5640)→按飞机处理)。
| 源表名 | → 统一名 | 规则 |
|---|---|---|
| -------- | --------- | ------ |
上海浦东T1、浦东T1 | 浦东机场T1 | 关键字"浦东"→浦东机场 |
上海虹桥T2、虹桥T2 | 虹桥机场T2 | 关键字"虹桥"(飞机)→虹桥机场 |
浦东 | 浦东机场 | 无航站楼省略 T 后缀 |
黄花T2、长沙黄花T2 | 黄花机场T2 | |
龙嘉 | 龙嘉机场 | |
扬泰 | 扬泰机场 |
全国所有机场映射见 references/location_names.md。通用提取规则:
{地名}机场{Tn}(无航站楼省略 Tn)| 规则 | 格式 | 示例 |
|---|---|---|
| ------ | ------ | ------ |
| 与机场重名的站 | {地名}高铁站 | 上海虹桥 → 虹桥高铁站 |
| 不冲突的站 | {原名}站(已有"站"不重复加) | 长沙南 → 长沙南站 |
| 源表名 | → 统一名 |
|---|---|
| -------- | --------- |
上海南 | 上海南站 |
上海 | 上海站 |
"虹桥"同时存在机场和高铁站 → 根据 H 列交通工具类型判断:
虹桥机场Tn虹桥高铁站> 代码实现见 references/location_names.md 中的 normalize_location_name() 函数模板。
⚠️ 此过滤必须在 unmerge_sheet() 之后执行! 详见"合并单元格处理"章节。
1. 空行程过滤:仅有姓名+电话但以下字段全部为空的行直接跳过,不导入:
> ⚠️ v3.0 修正:航班/车次为"待定"但日期+出发地+时间均完整 → 不跳过,正常导入,H列填"待定"(回程不前缀"送")。仅过滤"停运""取消""自驾""返程自理""自订"等明确不要的记录。
2. 🆕 "接机接站"是/否过滤(v3.4):源表有"是否需接机"/"是否需送机"等列,含 是/否 值:
3. 回程"不用送"过滤:回程备注列命中任一关键词 → 跳过:
4. 回程"取消/自驾/自理"过滤:回程航班号/备注列含以下内容 → 跳过:
支持多种时间格式(自动识别):
| 输入格式 | 说明 |
|---|---|
| --------- | ------ |
16:30 / 16:30 | 标准时间(全角半角冒号均支持) |
16:30-18:35 | 合并格式,来程取后段,回程取前段 |
16:30~18:35 | 合并格式(波浪号分隔) |
16.30 | 小数点格式 |
16/30 | 斜杠格式 |
0.694444(Excel 小数) | Excel 时间序列值,乘以 1440 转分钟 |
下午4点30分 | 中文时间标签 |
⚠️ 时间解析顺序(2026-05-26 教训):
去秒逻辑必须在合并格式拆分之后执行,否则会把合并格式中的冒号误判为秒分隔符:
❌ 错误:先 count(':') → "13:26-15:36" 有2个冒号 → 截断 → "13:26-15" → 后段="15"
✅ 正确:先按 '-' 拆分 → 后段="15:36" → count(':')=1 → 不去秒
正确流程:
- / ~)→ 取后段(来程)或前段(回程)count(':') > 1 且不含分隔符)20:28:00 的单独时间值执行去秒⚠️ HH.MM 与 Excel 序列值辨析(2026-05-27 长春站教训):
Excel 中 13.25 可能被 openpyxl 读取为 float 13.25,与真正的 Excel 时间序列值(如 0.694444)难以区分。解析优先级:
✅ 正确:
1. datetime 对象 → strftime('%H:%M')
2. 匹配 ^\d{1,2}\.\d{2}$ → HH.MM 格式(如 13.25 → 13:25)
3. 纯小数 0<val<1 → Excel 时间序列值(如 0.694444 → 16:40)
4. 否则按字符串处理
❌ 错误:对所有 float 一律按 Excel 序列值处理 → 13.25 × 24 × 60 = 318:00
> 2026-05-27:长春表 13.25(D530到达时间)被误判为 Excel 序列值,得 318 小时。
正则提取所有标准格式号码(字母+数字,如 G2025、MU5640、9C8856):
9C8856)也要正确提取⚠️ 执行顺序铁律:先 unmerge → 再判断过滤 → 再提取数据。绝不能先过滤再 unmerge。
当源表存在合并单元格时,必须先展开合并单元格再处理数据。
unmerge 函数模板:
def unmerge_sheet(ws):
"""将合并单元格值展开到所有覆盖行/列"""
data = {}
for row in range(1, ws.max_row + 1):
for col in range(1, ws.max_column + 1):
data[(row, col)] = ws.cell(row=row, column=col).value
for mc in ws.merged_cells.ranges:
val = ws.cell(row=mc.min_row, column=mc.min_col).value
for r in range(mc.min_row, mc.max_row + 1):
for c in range(mc.min_col, mc.max_col + 1):
data[(r, c)] = val
return data
🆕 Union-Find 合并组算法(v3.2 核心):
> ⚠️ v3.2 修正(2026-05-27 Longchamp 教训):v3.0 的"无合并组时用航班/车次作 key"是错误的!这会导致同航班但各自独立单元格的人也被统一序号。同序号的前提仅限真正共享了合并单元格的行。
规则:
序号分配:仅 merge_group_id 作 key,无合并组时每人独立序号。禁止用航班/车次作为序号 key。(v3.2 修正)
Union-Find 代码模板:
def build_merge_groups(ws, sheet_name):
parent = {}
def find(x):
if x not in parent: parent[x] = x
if parent[x] != x: parent[x] = find(parent[x])
return parent[x]
def union(a, b):
ra, rb = find(a), find(b)
if ra != rb: parent[ra] = rb
for mc in ws.merged_cells.ranges:
rows_in = list(range(mc.min_row, mc.max_row + 1))
if len(rows_in) > 1:
for i in range(1, len(rows_in)):
union(rows_in[0], rows_in[i])
group_map = {}; gid = 0
for row in range(1, ws.max_row + 1):
root = find(row)
if root not in group_map:
gid += 1; group_map[root] = f"{sheet_name}_G{gid}"
return {(sheet_name.strip(), row): group_map[find(row)] for row in range(1, ws.max_row + 1)}
典型场景:
| 场景 | 合并列 | 处理 |
|---|---|---|
| ------ | -------- | ------ |
| 同航班两人成对排列 | 日期/航班/出发地/到达地/车型/司机等全部列 | unmerge → 同序号 |
| 仅车型/酒店/司机列合并 | 车型/司机/车牌/备注,但日期/航班各自独立 | unmerge → Union-Find 同序号 |
| 分段标题行 | 整行合并 | 通过 header 判断过滤 |
| 仅姓名+电话无任何合并 | 无 | 正常过滤跳过 |
> 另见 references/merge_cell_handling.md
当航班号字段含"本地"或"市内"字眼时:
来程本地:
| 源表字段 | 模板列 | 备注 |
|---|---|---|
| --------- | -------- | ------ |
| 出发地 | I 出发地* | |
| 到达地 | J 目的地* | 空时输出"需要手动填写" |
| 出发/到达时间 | E 预订时间* | 不减时 |
| — | H 航班/车次 | 输出"本地" |
回程本地:
| 源表字段 | 模板列 | 备注 |
|---|---|---|
| --------- | -------- | ------ |
| 出发地 | I 出发地* | |
| 到达地 | J 目的地* | 空时输出"请留意检查" |
| 出发时间 | E 预订时间* | 不减时 |
| — | H 航班/车次 | 输出"本地" |
> 本地行中出发地/时间/到达地任意一个有内容时,其余空字段统一输出"请留意检查"
源表日期列可能包含多行日期(如 05月28日\n05月29日),按方向取不同行:
| 方向 | 取值 | 说明 |
|---|---|---|
| ------ | ------ | ------ |
| 来程 | 取最后一个日期 | 最后一段行程的到达日期 |
| 回程 | 取第一个日期 | 第一段行程的出发日期 |
> 多行中转换照此规则:来程取最后一行日期+最后一站的下车地;回程取第一行日期+第一站的出发地。
源表中常有"是否需接机"/"是否需送机"列,取值"是"/"否":
| 列含义 | 值=否 | 处理 |
|---|---|---|
| -------- | ------ | ------ |
| 接机(来程方向) | 否 | 跳过该人的来程导入 |
| 送机(回程方向) | 否 | 跳过该人的回程导入 |
> ⚠️ 此过滤在数据提取阶段执行,不同于回程备注/航班号关键词过滤。两者都要检查。
> ⚠️ v3.3(2026-05-27):取消 v3.2 的"无合并组→每人独立序号"。改为看"车型"列。
| 源表情况 | 序号规则 | 车辆级别规则 |
|---|---|---|
| --------- | --------- | ------------ |
| 源表有"车型"列 | 每人独立序号 | 直接取源表车型值(小车→B级轿车,商务→商务车) |
| 源表无"车型"列 | 同航班/车次 = 同序号 | 1-2人→B级轿车;≥3人→商务车;>3人→均分 |
拆分/均分逻辑(同航班 >4人时,v3.8):
> ⚠️ v3.8(2026-05-28):4人不拆→1台商务;7人拆4+3→2台商务。阈值从>3改为>4,每车容量从3改为4。
>
> ⚠️ v3.7:拆分后每个子组按自身人数独立判断车辆级别。2人用B级轿车,不用商务车。
>
> ⚠️ v3.6(2026-05-28):取消旧的"6人一车"规则,改为每车≤3人,>3人全部均分。6人也拆成2台车(3+3),避免拥挤。
>
> ⚠️ v3.5(2026-05-28):航班号比较必须大小写不敏感(如 TV9832 = tv9832)。H 列保留原始大小写,分组时统一转大写。序号在所有数据提取后统一处理。
每次处理回程时间时,必须先向用户确认减时规则,再执行计算。
即使城市已有历史规则记录,也要向用户确认"本次使用 [城市] [机场Xh/高铁站Xh] 的减时规则,是否正确?"得到确认后再继续。
> ⚠️ 之前多次因未确认减时规则导致返工。此后每次导入都必须确认。
每次处理完毕,必须同时:
C:\Users\Administrator\Desktop\{会议名}-业务导入.xlsxdeliver_attachments 弹窗推送文件open_result_view 打开结果面板禁止仅存入中间目录而不交付。
每次保存结果文件前,必须逐列验证以下 6 列全部非空:
| 检查项 | 模板列 | 预期值 |
|---|---|---|
| --- | --- | --- |
| 订车公司* | Col18 | 用户提供(如"柠檬") |
| 订车人* | Col19 | 用户提供(如"柠檬记账") |
| 付费方式* | Col20 | "记账" |
| 车辆级别* | Col25 | "B级轿车" / "商务车" |
| 客户负责人* | Col27 | 用户提供(如"张晓东") |
| 受理人* | Col28 | 用户提供(如"张晓东") |
| D列日期格式 ⚠️ | Col4 | 必须 YYYY/MM/DD(YYYY-MM-DD 也可,不可 YYYYMMDD) |
> ⚠️ 2026-05-26 教训:大肠癌国际论坛导入时 Col18/19/27/28 全部漏填,导致返工。此后每次导入必须逐列检查,发现空值立即补填再交付。
>
> ⚠️ 2026-05-28 教训:研究者会导入 D 列用了 YYYYMMDD 格式(如 20250711),应为 YYYY/MM/DD。优先 YYYY/MM/DD,YYYY-MM-DD 也可接受。
每次处理完成后,在回复中主动说明本次使用的:
共 1 个版本