AOE 瀚忠空运价格提取 Skill
提取 AOE 瀚忠空运多航司运价表 → 24列标准输出 → 上传 WPS。
⚠️ 铁律:仅提取用户最新提交文件,禁止复制历史数据
- 禁止查看/引用/复制任何历史脚本中的 price/data 数组
- 禁止从历史输出文件直接读取数据
- 禁止硬编码历史文件路径作为源文件
- 每次提取必须从用户当前提交文件全新读取
- 有就是有,没有就是没有:不得用历史数据填补缺失值
一、核心参数
| 参数 | 值 |
|---|---|
| 源文件 | 用户提交(命名 AOE XXMAY GARY.xlsx) |
| 格式 | Excel .xlsx,多 Sheet(12个价格Sheet) |
| 起始港 | 多港口:PVG/HGH/HAK/NKG/CGO/KMG |
| 价格编号 | aoe_{sheet}_{carrier}_001 |
| 来源 | AOE |
| 货型 | 1:1(默认)+ 子行货型(1:300/1:500等) |
| 引擎 | pandas + openpyxl(GF需要 openpyxl 检测填充色) |
二、Sheet 清单与特征
| Sheet | 航司 | 格式特征 | 特殊处理 |
|---|---|---|---|
| EY PVG | EY | 简单, +250KG列 | +250→300KG槽位, PVG起运 |
| EY SZX | EY | 同上 | SZX起运 |
| EY EHU | EY | 同上 | EHU起运 |
| 印度专线 | CX/AI/NH/GI/JD/O3/RH | 主行+子行, 多货型 | 合并单元格, 全角冒号, 货型子行 |
| ~~AI~~ | ~~AI~~ | ~~副本版已移除~~ | - |
| 6E印度散货 | 6E | 简单 | 生效日期=05-29 |
| CX CGK | CX | 主行+子行 | 合并单元格 |
| 南非CXEYVN | CX/VN | 主行+子行 | 卡车行跳过 |
| D0中东 欧洲 | D0 | 双段式:中东直价+Zone表 | 🚨 新增,col1=目的港,同行多比率共价,Zone表跳过 |
| VN | VN | 多目的地 | 生效日期=01-Jun |
| HU | HU | 副本版格式:区域+航线+卡车Zone | 🚨 列映射变更,col0=区域,col1=航线,col4=目的站 |
| ~~GF~~ | ~~GF~~ | ~~副本版已移除~~ | - |
| ~~UA~~ | ~~UA~~ | ~~副本版已移除~~ | 0608恢复 |
| ~~QF~~ | ~~QF~~ | ~~副本版已移除~~ | 0608恢复 |
| ~~KTM~~ | ~~KTM~~ | ~~副本版已移除~~ | 0608恢复 |
| GF | GF | 多目的地,黑底跳过 | 0608恢复,openpyxl检测 |
| ~~C6~~ | ~~C6~~ | ~~副本版已移除~~ | 0608新增OSR HGH |
| ~~LH~~ | ~~LH~~ | ~~副本版已移除~~ | 0608新增FRA PVG |
| ~~CX RUH 停飞中~~ | ~~CX~~ | ~~副本版已移除~~ | 0608恢复,停飞仍提取 |
| ~~D0美国~~ | - | ~~图片无法OCR~~ | 0608跳过 |
三、关键规则
3.1 合并单元格价格传播(⚠️ 高频坑)
印度专线、CX CGK、南非 等Sheet中,子行的300/500/1000KG常为合并单元格,只有col8(300KG)有值。
# 子行价格读取后统一处理
if p300 and not p500: p500 = p300
if p500 and not p1000: p1000 = p500
3.2 全角冒号检测
印度专线 Sheet 中货型列存在全角冒号:1:260、1::500。
def is_ratio_str(val):
s = str(val).strip() if pd.notna(val) else ''
return bool(re.match(r'^1[::]+(\d+)', s))
所有 startswith('1:') 判断必须替换为 is_ratio_str()。
3.3 货型子行处理
印度专线中,主行下可能有「散货」「带电托盘」「1M以下矮托盘」等货型子行——这些行的col4有货型描述,col5为/或Q100起...(非1:XXX)。
处理逻辑:
1. col4有货型描述 + col6-10有价格 → 提取为1:1行
2. 更新 current_cargo 供后续子行继承
3. st_type 从 col4 解析
3.4 散托类型(多表述)
def st_type_from_cargo(s):
has_tuo = '托' in s
has_san = '散' in s
if has_tuo and has_san: return '散货/托盘'
if has_tuo: return '托盘' # 含 矮托盘/超高/带电托盘
if has_san: return '散货' # 含 带电散货
return '散货/托盘'
常见货型表述:
- 托/散 / 托盘/散货 → 散货/托盘
- 托盘 / 托/超高 → 托盘
- 散货 / 1:300散货 → 散货
- 带电托盘 → 托盘
- 1M高度以下矮托盘 → 托盘
3.5 黑底单元格过滤(GF专有)
GF Sheet 使用 openpyxl 读取,价格区域 col C-G 有 patternType='solid' 的行整行跳过。
c45_cell = ws.cell(row=row, column=3)
fill = c45_cell.fill
if fill and fill.patternType == 'solid':
continue # 黑底,不提取
3.6 HU 卡车/飞机Zone + 托盘列(⚠️ 托盘增量不提取)
HU Sheet 中 托盘 列(col12=col M)为合并单元格(如 M8:M14 包含 BRU主行+6卡车区)。但 HU 的托盘增量不提取,散托增量统一留空。
中转区有两种格式:
- 卡车N区:公路转运,col4 列出目的站
- 飞机N区:空运转运,col4 列出目的站(结构完全相同,必须同时提取)
# 必须同时处理卡车和飞机区
is_truck = routing.startswith('卡车') or routing.startswith('飞机')
# HU 所有行:散托增量留空
'散托增量': ''
3.7 目的地继承
印度专线中,同一目的地下多个航司行(col0为空)→ 继承 current_dest:
if dest:
current_dest = dest
# 后续无dest的航司行使用 current_dest
3.8 QF 双区块
QF Sheet 有两个独立格式块(SYD + JFK/ORD/DFW),通过 'AOE报价' / 'valid to' 行切换生效日期。
3.9 EY +250KG 特殊列
EY Sheet 价格列为 +100/+250/+500/+1000(无+300)。+250KG 值填入标准 300KG 列。
全局规则
- 频次为空时默认填"Daily":频次列如果为空则填入 Daily(0727全局规则)
- 分泡列不提取:分泡列始终留空,不提取任何分泡数据(0720全局规则)
四、提取流程
1. 确认源文件路径(命名含 AOE + 日期)
2. pandas 读取各 Sheet 结构
3. 逐 Sheet 调用对应提取函数
4. GF 使用 openpyxl 检测填充色
5. 应用合并单元格传播 + 货型判断
6. 生成 24 列 DataFrame
7. validate_columns() 验证
8. 保存 → 自动上传 WPS → 删除临时文件
五、脚本模板
当前脚本:/Users/hantao/WorkBuddy/oone/extract_aoe_0525.py
源文件:0525/AOE 25MAY GARY.xlsx
输出:aoe_0525_output.xlsx(临时,上传后删除)
新批次时需更新:
- file_path 指向新源文件
- 各Sheet 生效日期
- 新增/停飞 Sheet 的处理
六、常见坑
坑1:合并单元格价格(印度/CGK/南非)
子行 300/500/1000KG 合并为单值,只有 col8 有数据。
→ 修复:if p300 and not p500: p500 = p300; if p500 and not p1000: p1000 = p500
坑2:全角冒号(印度专线)
1:260 / 1::500 导致 startswith('1:') 失败。
→ 修复:用 is_ratio_str() 正则 ^1[::]+(\d+)
坑3:货型子行跳过(印度专线)
「散货」「带电托盘」等行 col5 为 / 不是 1:XXX,被跳过。
→ 修复:检测 col4 有货型描述 + col6-10 有价格 → 提取为 1:1 行
坑4:黑底单元格误提取(GF)
GF 部分价格单元格黑色填充表示废弃。
→ 修复:用 openpyxl 读取,patternType=='solid' → 跳过整行
坑5:HU 托盘增量不提取
HU Sheet 的托盘列有 +2/+1 合并单元格,但不提取散托增量,统一留空。
坑6:目的地跨行继承(印度专线)
DEL块下 NH/AI 行的 col0 为空,dest 丢失。
→ 修复:if dest: current_dest = dest + 记录用 current_dest
坑7:子行 100KG 错位
子行记录中 '100KG': p45 应为 p100。
→ 修复:改为 '100KG': p100
坑8:GF openpyxl 警告
DrawingML support is incomplete 不影响数据读取,可忽略。
坑9:D0中东欧洲 多比率同行共用价格(0603新增)
D0 Sheet 的 RATIO col 可能含多级比率(如 1:100;1:300),同行所有比率共用同一价格。
❌ 错误:尝试从下一行读价格 → BAH 1:300 取了 R7 的 38
✅ 正确:同行多比率全部使用该行价格 → BAH 1:100 和 1:300 都是 37
坑10:D0 Zone参考表误提取(0603新增)
D0 Sheet R23+ 是 Zone 参考表(Region/PVG/DEST/add-on),col0='Europe'/'Africa'。
✅ 在循环中检测到 col0 in ('Europe','Africa','Region','ME','General Cargo') → break 终止
坑11:D0 目的港解析(0603新增)
目的港在 col1(非 col0),多目的港用 / 或 , 分隔(如 BAH/AUH/DWC/SHJ)。
排除 PVG(3字母但为起运港)和含 to/+ 的 add-on 字符串。
坑15:EY PVG/SZX/EHU 起始行错误(0603修正)
EY 三个 Sheet 数据起始行均为 R6(df.iloc[5]),不是 R10(df.iloc[9])。
❌ 错误:range(9, len(df)) → 跳过前3~4行有效数据(AMM/AUH/BAH 等)
✅ 正确:range(5, len(df)) → 从 R6 开始
坑16:EY 三个 Sheet 生效日期不同(0603修正)
- EY PVG:R3=
AOE报价6.1 报价6.1-6.15→2026-06-01 00:00:00 - EY SZX:R3=
AOE报价 6.8-6.14→2026-06-08 00:00:00(与PVG/EHU不同!) - EY EHU:R3=
AOE报价 6.1→2026-06-01 00:00:00
每次提取必须先读 R3 内容确认生效日期,不能用datetime.now()代替。
坑12:HU 列映射差异(0603新增)
副本版 HU Sheet 格式与 0525 不同:
- col0=区域描述(加拿大区域/美东区域...)
- col1=航线(PEK-YYZ),起运港从航线首段提取
- col4=目的站 / 卡车区列表(用逗号分隔多个3字码)
- col6-10=价格(+45KG/+100KG/+300KG/+500KG/+1000KG)
- 直飞行 routing='直飞',卡车子行 routing='卡车N区'
坑13:D0 散托增量(0603新增)
D0 备注 托盘+N → 解析为散货+增量N。
def parse_d0_increment(remark_str):
m = re.search(r'托盘\+(\d+(?:\.\d+)?)', remark_str)
if m: return m.group(1)
m = re.search(r'\+(\d+(?:\.\d+)?)', remark_str)
if m and float(m.group(1)) <= 20: return m.group(1)
return ''
坑17:EY 三个 Sheet 频次处理(0603新增)
- EY PVG/SZX:表格无明确频次标注 → 全部默认
Daily,不能留空 - EY EHU:频次记录在第一个价格行(R6)的备注列,如
*ETD/EHU: D1467(货机) - 解析规则:从第一行备注中
re.search(r'D(\d+)', first_remark)提取,整个Sheet统一使用 - 当前值:
D1467(后续批次可能变化,每次需读 R6 备注确认) - 如未匹配到则回退为
Daily
first_row = df.iloc[5] # R6是第一个数据行
first_remark = safe_str(first_row[6]) # col6 = 备注列
m = re.search(r'D(\d+)', first_remark)
sheet_freq = 'D' + m.group(1) if m else 'Daily'
坑14:印度专线「航司」空值跳过(0603修正)
印度专线中 JIA/CCU 行 MIN 值特殊(MIN 1.7/KG OR 3900/CNY),col2='CX' 有航司但 col0 有其他目的港码。
✅ 修正:if 'MIN' in min_val.upper() or 'KG' in min_val.upper(): i += 1; continue 在主行提取中已覆盖
坑18:印度专线频次清洗 + 生效日期 + ratio双列 + cargo传播(0603修正)
18a. 生效日期读取:
- 印度专线 R7 'AOE报价6.2调整' → 正则提取 → 2026-06-02
- 6E印度散货 R1 'AOE报价05.29 valid from 01.Jun' → 正则提取 → 2026-06-01
- 禁止使用 datetime.now()
18b. 频次清洗函数 clean_freq():
- 删除 via 及之后内容(re.sub(r'\bvia\b.*$', '', s))
- 删除所有中文字符
- 删除非频率残留词(转卡车/直达/直飞/服务/窄体等)
- 搜索 \bdaily\b(全字符串)→ 'Daily'
- 🆕 纯中文输入默认 Daily:清理后为空但原始含中文(如"窄体续程")→ 'Daily'
- 触发例:6E BOM~GOX 共15行 col6='窄体续程',col7无频次信息
- 搜索所有 \bD([\d\-\/]+) 模式合并去重 → D{unique_digits}
- D4-5 D5-6 → 合并 → D456
- D3/5 → D35
- D3/4/5/7 VIA HKG → D3457
- parse_freq(val, remark='') 双参数:先试 val,失败则从 remark fallback
- 6E DEL: col6='窄体续程'→空,remark='PVG-CCU DAILY 直达'→Daily
18c. col4 ratio 检测(印度专线子行格式不一致):
- 标准子行:col4(货型)空,col5(MIN)是 ratio → 原逻辑
- 变异:col4 是 ratio(如 1:300),col5 空 → 需额外 is_ratio_str(col4_val) 分支
- 触发例:GI CGO-BLR R71 ['', '', '', '', '1:300', '', '', '21.5', ...]
18d. cargo+ratio 合并更新(O3散货/托盘分类错误):
- col4='散货'(cargo变更)+ col5='1:167'(ratio)→ 需先 current_cargo=col4_val 再处理 ratio
- 触发例:O3 CGO-BLR R78 ['', '', '', '', '散货', '1:167', '', '17.5', ...]
- 之前:用旧的 current_cargo(限1米以下矮托盘)→ 散托类型=托盘 ✗
- 修正:更新后 → 散托类型=散货 ✓
# 子行4路分支处理顺序:
# A) col4是ratio, col5非ratio → ratio在货型列
# B) col4是cargo, col5是ratio → 先更新cargo再ratio
# C) col5是ratio, col4空 → 标准ratio子行
# D) col4是cargo, col5非ratio → 纯cargo变更
坑19:CX CGK 生效日期 + 频次默认Daily(0603修正)
19a. 生效日期:
- R7 'AOE报价6.1' → 正则 (\d{1,2})\.(\d{1,2}) → 2026-06-01
- 禁止使用 datetime.now()
19b. 频次 VIA HKG → Daily:
- R10 routing='VIA HKG' → clean_freq 删除 via 后剩 'HKG' → 非 Daily 非 D-pattern → 空
- parse_freq() 最终 fallback:无识别频率 → 默认 'Daily'
19c. parse_freq() 末尾通用规则(全局生效):
def parse_freq(val, remark=''):
freq = clean_freq(val)
if freq: return freq
if remark:
freq = clean_freq(remark)
if freq: return freq
return 'Daily' # 🆕 无识别频率 → 默认 Daily
- 影响范围:所有 Sheet 中频次列为空/纯中文/VIA 等情况均回退为 Daily
- 例外:EY EHU 单独提取 D1467(不在 routing 列,在 remark),不受影响
坑20:D0 中东 欧洲 生效日期 + ratio减价跳过 + 欧洲段目的地继承(0603修正)
20a. 生效日期:
- R4 col6 'Valid to 04.Jun' → 2026-06-04(统一整个Sheet)
- 禁止使用 datetime.now()
20b. ratio 含 -1/kg → 不展开:
- R6 ratio_text = '1:100 -1/kg 55分;1:300 -1/kg。' → 这些是减价备注,不是真正的比重段
- 检测 ratio_text and '-1/kg' not in ratio_text 跳过,不生成 1:100/1:300 额外行
- BAH/AUH/DWC/SHJ 等中东目的地只有 1:1 主行(各1条)
20c. 欧洲段 R12-R16 目的地解析:
- R12 col1 = 'to LEJ+1/kg ,WAW+3/kg+add on' → 用 re.findall(r'\b([A-Z]{3})\b', col1) 提取 → ['LEJ', 'WAW']
- R13-R16 col1 为空 → 继承 prev_dests(上一行目的地)
- 每个比重(1:80/120/167/250/500)LEJ 和 WAW 各生成一条 → 共 10 条
- 散托类型: col7 含"托盘"→散货,否则→散货/托盘
- 1:80/120/167 col7='托盘+2' → inc=2, 散托=散货
- 1:250/500 col7='' → inc=空, 散托=散货/托盘
20d. 频次从路线头提取:
- R4 col1 = 'PVG-BAH-MES 带电不接 D1234' → re.search(r'D(\d+)', ...) → D1234
- 所有目的港统一使用
20e. 目的地排除列表:
# 只排除纯转运点(MES, BRU),BAH 是目的地不能排除
if len(part) == 3 and part.isalpha() and part not in ('PVG', 'MES', 'BRU'):
坑21:HU 「飞机N区」被跳过(0603修正)
HU Sheet 中转区有两种:卡车N区(公路转运)和 飞机N区(空运转运)。两者结构完全相同,都是 col4 列出目的站列表。
原代码只匹配 is_truck = routing.startswith('卡车'),飞机N区 不匹配任何分支,直接被跳过。
- 影响行:R74-R76(飞机1/2/3区,拉美)、R204-R207(飞机1/2/3/4区,中东)、R213-R215、R219-R220、R225-R226、R255、R265、R287、R303-R304、R306-R307
- 以 PEK-HKT 为例:R287 飞机1区 含 RUH/SHJ/BAH 等 21 个中东目的地,全部丢失
✅ 修复:
# fuben 版(副本AOE 2JUN EVA.xlsx,col3=routing, col4=dest)
is_truck = routing.startswith('卡车') or routing.startswith('飞机')
# 0601 版(AOE 2JUN EVA.xlsx,col3=routing, col4=cargo)
if (routing.startswith('卡车') or routing.startswith('飞机')) and cargo and cargo != 'NA':
坑22:extract_dest *号/括号/非字符污染(0608修正)
通用 extract_dest() 函数缺陷导致 GF/UA 等 Sheet 产出伪目的地:
| 问题 | 输入示例 | 错误产出 | 原因 |
|---|---|---|---|
| 末尾星号 | DEN* / ORD* |
跳过(len=4≠3) | 星号计入长度 |
| 括号备注 | BAH (must go +5/kg) |
KG) 伪码 |
/ 号分割伪造 |
| 非字母码 | Zone 5-6 → 5-6 |
非目的地码 | 无 alpha 检查 |
✅ 修复(三步清洗):
def extract_dest(val):
if not val: return []
val = val.strip().upper()
val = re.sub(r'\*+$', '', val) # ① 去末尾星号
val = re.sub(r'\s*\([^)]*\)', '', val) # ② 去括号备注
# ③ 所有分隔路径统一加 .isalpha()
if ',' in val:
return [d.strip() for d in val.split(',') if len(d.strip())==3 and d.strip().isalpha()]
if '/' in val:
return [d.strip() for d in val.split('/') if len(d.strip())==3 and d.strip().isalpha()]
parts = val.split()
if len(parts) > 1:
return [p.strip() for p in parts if len(p.strip())==3 and p.strip().isalpha()]
return [val] if len(val)==3 and val.isalpha() else []
影响范围:0525/0608 脚本的 GF、UA 提取器(均已修复)。
坑23:南非CXEYVN 整表被跳过(1008修正)
表头结构和印度专线不同:没有「目的港」列名,目的港(JNB)直接写在数据行 C0。模型曾因"表头对不上"误判为参考表整表跳过——这是价表,必须提取。
列映射(表头 R8,数据从 R9 起):
| 列 | 含义 | 说明 |
|---|---|---|
| C0 | 目的港 | R9 值 JNB;R10/R11 该列为空 → 继承上一主行的 JNB |
| C1 | 始发港 | PVG(子行可为空,继承) |
| C2 | 航司 | CX(子行可为空,继承) |
| C3 | 航班频率路径 | D2/3/5/6/7 VIA HKG… → 频次取 D… 串 |
| C4 | MIN/货型混用 | 主行 R9 = 2500(MIN 收费数字,不是货型)→ 该行货型记 1:1;子行 R10=1:300、R11=1:500 才是货型 |
| C5-C9 | 45/100/300/500/1000KG | 主行 R9:45=80、100/300/500/1000=42;子行 1:300:300/500/1000=41(45/100 空);子行 1:500:300=40、500=40、1000=39 |
| C10 | 备注 | 主行有长备注,子行可空 |
跳过行:泡货单询(R12)、DUR/CPT VIA JNB 卡车MIN…(R15/R16,卡车转运行,与目录「卡车行跳过」一致)。
生效日期:R6 AOE10.8 → 走「报价+裸月日」规则(坑24)填当年 10-08。
# 子行空列继承(与印度专线同款 current_* 传播)
if not dest and c0_empty: dest = prev_dest # JNB
if not airline: airline = prev_airline # CX
# C4 是纯数字(MIN收费)→ 货型=1:1;C4 匹配 1[::]\d+ → 货型=该行比率
坑24:表头「报价M.D」裸月日生效日(1008修正,通用)
各 Sheet 表头行常写 AOE报价10.8、AOE10.8、报价6.1——点分裸月日、无年份,就是该批报价的生效日期(年份=提交年)。
- 识别:
(报价|牌价|价目|最新价|调价)\s*[::]?\s*(\d{1,2})[./](\d{1,2})→date(当年, M, D) - 文件名同理:
AOE 8OCT GARY1.xlsx→ 10-08 生效 - ⚠️ 必须是带价关键字前缀的点分数;纯价格小数(
42.0、1.30)绝不是日期 - 禁止因"源文件里没有该日期"就输出 0 行——表头就是生效日,直接填当年
2026-10-08
坑25:起始港禁止写死 PVG(1008修正,⚠️ 高频错误)
印度专线/CX CGK/南非 各 Sheet 的起始港在 C1 列、只在块主行出现,子行留空。同一 Sheet 混着多个起运口岸,绝不能统一填 PVG:
CGO=郑州(GI 鹰航空全部从 CGO 出:DEL/MAA/BLR 三块 17 行)CKG=重庆(JG 块:BOM/NMI 托盘/散货 14 行)NKG=南京(CX 块:BOM/NMI 50/23、1:300=20、1:500=19 共 6 行)- 其余默认
PVG
正确逻辑:
cur_org = "PVG" # 每个 Sheet 起始港默认 PVG
for 行 in 数据行:
if C1 是三字码: cur_org = C1 # 主行显式给港 → 更新
if C0 是目的港: cur_dest = C0 # 主行给港 → 更新
if C2 是航司: cur_air = C2
# 子行 C1 为空 → 沿用 cur_org(继承块主行,不是重置为 PVG)
输出 起始港=cur_org
验收自检(1008 AOE 8OCT GARY1):印度专线成果必须同时出现 PVG/CGO/CKG/NKG 四种起始港;GI 行的起始港只能是 CGO;JG 只能是 CKG;CX 里 45KG=50 那组只能是 NKG。
七、🛡️ 防坑经验总结(0603 批次积累)
以下经验适用于所有价格提取数据源,不限于 AOE。
经验1:频次处理三阶段流水线 ✅
无论哪个 Sheet,频次处理统一走三阶段:
raw_value → clean_freq() → parse_freq(val, remark='') → 最终值
| 阶段 | 函数 | 职责 |
|---|---|---|
| 清理 | clean_freq(val) |
删除 via/VIA + 地名、中文、多余空格;提取 Daily 或 D-pattern |
| 解析 | parse_freq(val, remark='') |
先试 val → 为空则从 remark fallback → 为空则返回 'Daily' |
| 默认 | 最末 fallback | 所有 Sheet 统一:无识别频率 → 'Daily'(不再输出空) |
关键细节:
- D 模式合并去重:D4-5 D5-6 → D456,D3/5 → D35
- 纯中文("窄体续程")→ clean_freq 返回空 → parse_freq 返回 'Daily'
- VIA 路由("VIA HKG")→ clean_freq 过滤后空 → 'Daily'
- EY EHU 例外:频次从 remark 单独提取(D1467),不走通用流水线
经验2:生效日期严禁 datetime.now() ✅
所有 Sheet 的生效日期必须从源文件提取,严禁用 datetime.now()。
提取优先级:
1. 表格中明确标注(Valid to 04.Jun → 2026-06-04)
2. R3/R4/R7 等表头行含日期字符串(AOE报价6.1 → 2026-06-01)
3. 都不行 → 从文件名或用户提供的信息推断,绝不用 datetime.now()
原因:0603 当天是 6 月 3 日,但 D0 报价 valid to 6 月 4 日、印度专线 6 月 2 日调整 —— 系统时间与报价生效日无关。
经验3:子行处理「谁来都一样」分支模式 ✅
子行(无航线名的数据行)格式变化多端,必须按优先级多路检测:
# 子行4路分支(按检测顺序):
# A) col4 是 ratio, col5 不是 → ratio 在货型列(如 GI BLR 1:300)
# B) col4 是 cargo, col5 是 ratio → 先更新 cargo 再处理 ratio(如 O3 BLR 散货 1:167)
# C) col5 是 ratio, col4 空 → 标准 ratio 子行
# D) col4 是 cargo, col5 不是 → 纯 cargo 变更
核心教训:不能假设子行某列一定是什么。即使是「比重列」,也可能被填在货型列;即使是「货型列」,也可能同时含 cargo + 下一列 ratio。
经验4:ratio 含减价/加价标记不展开 ✅
检测 ratio 文本是否含 -1/kg、+1/kg(加减价备注):
if '-1/kg' in ratio_text:
pass # 不展开为比重行
这些只是对主行价格的修正说明,不是真正的比重段。
经验5:中转区/路由区「同类全覆盖」✅
同一 Sheet 中功能相同的路由类型必须全部匹配:
- HU:卡车N区 + 飞机N区 → is_truck = routing.startswith('卡车') or routing.startswith('飞机')
- 教训:写 startswith('卡车') 时必须问自己「有没有其他等价类型?」
经验6:多目的地「空继承」模式 ✅
连续多行的 col1/路由列为空时,继承上一行的目的地:
if not col1_val:
col1_val = prev_dest # 继承上一行
else:
prev_dest = col1_val # 更新继承源
D0 欧洲段 R13-R16 均为空,依赖此机制正确生成 LEJ/WAW 各比重行。
经验7:排除列表 = 纯转运点,不排除目的地 ✅
路线头 PVG-BAH-MES 解析目的地时:
# ❌ 错误:把 BAH 也排除了(BAH 是目的地)
exclude = {'PVG', 'MES', 'BRU', 'BAH'}
# ✅ 正确:只排除纯转运点
exclude = {'PVG', 'MES', 'BRU'}
规则:三字码在路线头中间且不作为独立目的站出现 → 转运点。
经验8:合并单元格增量「跨行传播」✅
合并单元格中的托盘+N 备注要传播到所有合并子行:
- 读取合并单元格 master value
- 建立 merge_map
- 子行无备注时,用 merge_map 查找主行备注
- 从备注提取增量(卡板/托盘+N 优先)
经验9:验证闭环 ✅
每次修正后必须:
1. 跑完整提取脚本
2. 对比修正前后条数
3. 抽查修正行的实际输出
4. 上传 WPS 原地更新
5. 不做假设性验证
八、24 列输出标准
from columns_24 import COL_24, validate_columns
所有提取器统一使用 COL_24 列名,输出前调用 validate_columns()。