NANMEI_001 价格提取 Skill
提取 NANMEI(usd.xlsx)南美空运运价表 → 24列标准输出 → 上传 WPS。
⚠️ 铁律:仅提取用户最新提交文件,禁止复制历史数据
- 禁止查看/引用/复制任何历史脚本中的 price/data 数组
- 禁止从历史输出文件直接读取数据
- 禁止硬编码历史文件路径作为源文件
- 每次提取必须从用户当前提交文件全新读取
- 有就是有,没有就是没有:不得用历史数据填补缺失值
一、核心参数
| 参数 | 值 |
|---|---|
| 源文件 | 用户提交(通常为 usd.xlsx) |
| Sheet | Hoja1 |
| 起始港 | PVG(从路由提取,默认 PVG) |
| 价格编号 | nanmei_001 |
| 来源 | nanmei |
| 生效日期 | 动态 datetime.now().strftime()(0601改为动态,不再硬编码) |
| 45KG | 始终为空(南美文件无 45KG 列) |
| 引擎 | openpyxl(.xlsx) |
全局规则
- 频次为空时默认填"Daily":频次列如果为空则填入 Daily(0727全局规则)
- 分泡列不提取:分泡列始终留空,不提取任何分泡数据(0720全局规则)
二、强制流程
1. 确认用户提交的源文件路径
2. openpyxl 加载 Hoja1
3. 第一遍扫描:收集原始数据(行级状态追踪)
4. 第二遍处理:按数据块链式比重推导
5. 生成 DataFrame → 24列输出
6. 保存 → 自动上传 WPS → 删除本地临时文件
三、Excel 列映射(openpyxl col 索引)
| col | 字段 | 作用 |
|---|---|---|
| 1 | AOD(目的地) | 目的地三字码,空时从路由提取 |
| 2 | Carrier / Ratio | 航司代码 OR 比重货型(KGS/CBM>XXX) |
| 3 | 100KG | 价格 |
| 4 | 300KG | 价格 |
| 5 | 500KG | 价格 |
| 6 | 1000KG | 价格 |
| 7 | Frequency | 航班频次 |
| 8 | Routing | 路由信息(PVG-XXX-YYY-ZZZ) |
| 9 | Flight details | 航班详情 |
| 10 | Remark | 备注 |
| 11 | - | 附加信息 |
四、两遍处理架构
4.1 第一遍:收集原始数据
逐行扫描,维护行级状态:
| 状态变量 | 更新时机 |
|---|---|
current_dest |
col1 有值 OR col8 路由提取 |
current_carrier |
col2 被识别为航司代码 |
current_routing |
col8 有值 |
current_origin |
从路由第一部分提取 |
current_transfer |
从路由中间段提取 |
current_freq |
col7 有值 |
current_remark |
col10 有值 |
block_key |
(目的港, 航司, 路由) 三元组 |
数据行分类:
- col2 是航司代码 → 「航司行」(货型=1:1,价格从 col3-6)
- col2 是比重格式 KGS/CBM>XXX → 「比重行」(价格从 col3-6)
- col2 为空或非以上两者 → 跳过
# 只有两种情况才入 raw_data:
if is_carrier:
raw_data.append({...ratio_type: None, raw_ratio: '1:1', ...})
elif ratio_type and (w100 or w300 or w500 or w1000):
raw_data.append({...ratio_type: 'gt/lt', ...})
4.2 第二遍:链式比重推导
按 block_key 分组,每组内调用 chain_ratio_derivation():
GT档:按阈值升序,start=阈值,end=下档start,末档=9999
LT档:按阈值升序,start=上档end(首档=0),end=阈值
1:1档:start=0,end=首LT档阈值 或 首GT档阈值 或 9999
五、航司识别(is_carrier_code)
通过条件
- 2-4位字母数字(AA, DL, CK, 5Y, K4, D0)
- 多航司
/分隔(AF/KL, CA/MU, UX/AD → 每个段 2-4 位)
排除条件
| 条件 | 示例 | 原因 |
|---|---|---|
含 KGS / CBM |
KGS/CBM>300 |
货型格式 |
含 LOOSE / PALLET |
2C loose |
货型标识 |
| 空格分隔 | AA UC |
非有效格式 |
| 段长 >4 或 <2 | XXXXX |
超出航司代码长度 |
航司原样保留
# AF/KL → 保留 "AF/KL"(不拆分)
parse_carrier_codes(val) → [val] # 含/直接整体返回
六、比重类型解析(parse_ratio_type)
KGS/CBM>300 → ratio_type='gt', threshold=300, cargo_type='>300'
KGS/CBM<140 → ratio_type='lt', threshold=140, cargo_type='<140'
其他 → None, None, None
七、路由提取(extract_routing_info)
输入: "PVG-NRT-LAX-MEX-GUA"
输出: origin="PVG", transfers=["NRT","LAX","MEX"], dest="GUA"
目的港优先级
- col1(AOD)有值且不含
/→ 直接使用 - col1 为空或含
/→ 从 col8 路由提取最终段
多目的地路由
"PEK-AMS/CDG-PTY"
→ origin="PEK", transfers=["AMS/CDG"], dest="PTY"
→ 取 / 后部分作为目的地
跳转点输出
"跳转点": "/".join(transfers) # "NRT/LAX/MEX"
八、散托类型判断(parse_remark_for_san)
从备注(col10)判断,优先级:
| 备注关键词 | 散托类型 | 说明 |
|---|---|---|
LOOSE CARTON ONLY |
散货 | 明确散货 |
LOOSE + PALLET 同时出现 |
散货/托盘 | 混合 |
PALLET 或 SKID |
托盘 | 纯托盘 |
LOOSE(无 PALLET) |
散货 | 纯散货 |
| 无以上关键词 | 散货/托盘 | 默认 |
散托增量
# 从备注提取托盘加价
USD X.XX/KG → usd_increment = X.XX
九、价格解析(get_price)
#N/A/-/N/A/nan/ 空 → None- float ≤ 0 → None
- 其他 → float(保留小数)
十、完整数据结构
{
"起始港": origin, # PVG(默认)
"目的港": dest, # 三字码
"航司": carrier, # 原样(含/)
"开始比重": start_ratio, # 链式推导结果
"结束比重": end_ratio, # 链式推导结果
"货型": raw_ratio, # 1:1 / >300 / <140
"45KG": "", # ⚠️ 始终为空
"100KG": w100,
"300KG": w300,
"500KG": w500,
"1000KG": w1000,
"散托类型": san_type, # 散货/托盘/散货/托盘
"开始生效日期": EFF_DATE,
"分泡": "",
"频次": freq,
"Remark": "",
"跳转点": "/".join(transfers), # ⚠️ 跳转点放这里
"转运点": "",
"转运费增量值(RMB/KG)": "",
"转运费最低收费(/票)": "",
"备注": remark,
"来源": "nanmei",
"价格编号": "nanmei_001",
"散托增量": usd_increment,
}
十一、常见坑和修复记录
坑1:链式比重 GT/LT 混排
正确做法:先收集 GT 和 LT,分别排序推导,1:1 档取交汇点。
坑2:多航司 AF/KL 误拆分
应原样保留 AF/KL,不拆成两行。
坑3:col1 为空时目的港丢失
必须从 col8 路由提取最终段作为目的地。
坑4:KGS/CBM 格式误识别为航司
必须在 is_carrier_code 中排除含 KGS/CBM 的内容。
坑5:2C loose/pallet 特殊处理(⚠️ 0601修正)
2C loose/2C pallet 含 LOOSE/PALLET 会被 is_carrier_code 排除,但它是有效价格行(carrier=2C)。
✅ 正确做法:新增 parse_carrier_2c() 专门匹配 2C\s+(loose|pallet) 格式:
- 2C loose → carrier=2C, san_type=散货
- 2C pallet → carrier=2C, san_type=托盘
坑6:AFKL 未归一化为 AF/KL
源文件中偶有 AFKL(无斜杠)写法,需在 carrier 识别后归一化为 AF/KL:
if col2_str == 'AFKL':
col2_str = 'AF/KL'
坑7:无价格航司行不应提取
部分航司行(MU/CM/BA/CV/Y8/5Y/AD)col3-6 全空,但 is_carrier_code 仍返回 True。
✅ 正确做法:航司行入 raw_data 前检查 if not (w100 or w300 or w500 or w1000): continue
坑8:MEX/NLU 双目的地 col1
col1 为 MEX/NLU 时需要拆分为 MEX, NLU(英文逗号+空格):
def clean_dest(dest_str):
if '/' in dest_str:
return ', '.join([p.strip() for p in dest_str.split('/')])
return dest_str
坑9:第二个表头段
0601 文件 Row 272 有重复表头 AOD|Carrier|100|300|...,下方 MAD/BCN 价格需正常提取。
✅ 现有逻辑跳过 col1=="AOD" and col2=="Carrier" 已覆盖。
坑10:生效日期硬编码
Skill 写死 2026-04-20 00:00:00,0601 改为动态:
from datetime import datetime
EFF_DATE = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
十二、脚本模板
脚本模板:/Users/hantao/WorkBuddy/oone/extract_nanmei.py
源文件:用户提交
输出:NANMEI---标准化价格表.xlsx(临时,上传后删除)
新批次时需更新:
1. file_path 指向新文件
2. EFF_DATE 为动态日期(不再硬编码)
3. 运行 → 验证 → 上传 WPS
十三、版本更新记录
- 0601: 新增
parse_carrier_2c()处理 2C loose/pallet;跳过无价格航司行;AFKL→AF/KL 归一化;clean_dest()MEX/NLU;动态日期;新增 MAD/BCN/MVD/ASU/GYE/UIO/VVI 目的地