feat: 添加转堆作业解析功能

- 新增 _parse_relocation() 方法解析转堆作业
- 新增 _parse_log_entry() 方法复用解析逻辑
- 转堆作业使用 '转堆作业' 作为船名标识
This commit is contained in:
2026-01-03 22:37:30 +08:00
parent f04478fd8f
commit 5fd05fcd3c

View File

@@ -241,6 +241,12 @@ class HandoverLogParser:
def _parse_ships(self, content: str, date: str, shift: str, logs: List[ShipLog]) -> None:
"""解析船次"""
try:
# 首先解析转堆作业(无船名)
relocation_content = self._parse_relocation(content, date, shift)
if relocation_content:
logs.append(relocation_content)
# 然后解析实船作业(按 "实船作业:" 分割)
parts = content.split('实船作业:')
for part in parts:
@@ -258,72 +264,164 @@ class HandoverLogParser:
ship_name = ship_match.group(2)
# 移除二次靠泊等标注
ship_name = re.sub(r'(二次靠泊)|(再次靠泊)|\(二次靠泊\)|\(再次靠泊\)', '', ship_name).strip()
vehicles_match = re.search(r'上场车辆数:(\d+)', cleaned)
teu_eff_match = re.search(
r'作业量/效率:(\d+)TEU[,\s]*', cleaned
)
# 解析TEU
teu = None
if teu_eff_match:
try:
teu = int(teu_eff_match.group(1))
except ValueError as e:
logger.warning(f"TEU解析失败: {teu_eff_match.group(1)}, 错误: {e}")
# 解析车辆数
vehicles = None
if vehicles_match:
try:
vehicles = int(vehicles_match.group(1))
except ValueError as e:
logger.warning(f"车辆数解析失败: {vehicles_match.group(1)}, 错误: {e}")
# 解析尺寸箱量
twenty_feet = None
forty_feet = None
# 查找作业量/效率后面的括号内的尺寸信息
# 匹配模式TEU数字后跟括号括号内包含尺寸信息
size_pattern = re.search(r'TEU[,\s]*([^]+)', cleaned)
if not size_pattern:
# 也尝试匹配没有逗号的情况
size_pattern = re.search(r'TEU\s*([^]+)', cleaned)
if size_pattern:
size_text = size_pattern.group(1)
# 匹配20尺*数字
twenty_match = re.search(r'20尺\*(\d+)', size_text)
if twenty_match:
try:
twenty_feet = int(twenty_match.group(1))
except ValueError as e:
logger.warning(f"20尺箱量解析失败: {twenty_match.group(1)}, 错误: {e}")
# 匹配40尺*数字
forty_match = re.search(r'40尺\*(\d+)', size_text)
if forty_match:
try:
forty_feet = int(forty_match.group(1))
except ValueError as e:
logger.warning(f"40尺箱量解析失败: {forty_match.group(1)}, 错误: {e}")
log = ShipLog(
date=date,
shift=shift,
ship_name=ship_name,
teu=teu,
efficiency=None, # 目前日志中没有效率数据
vehicles=vehicles,
twenty_feet=twenty_feet,
forty_feet=forty_feet
)
logs.append(log)
# 解析车辆数、TEU、尺寸箱量
self._parse_log_entry(cleaned, date, shift, ship_name, logs)
except Exception as e:
logger.warning(f"解析船次失败: {date} {shift}, 错误: {e}")
def _parse_relocation(self, content: str, date: str, shift: str) -> Optional[ShipLog]:
"""
解析转堆作业(无船名的作业类型)
参数:
content: 班次内容
date: 日期
shift: 班次
返回:
ShipLog 对象,如果未找到转堆作业则返回 None
"""
try:
# 检查是否包含转堆作业
if '转堆作业:' not in content:
return None
# 提取转堆作业部分(从 "转堆作业:" 到下一个空行或下一个实船作业)
relocation_start = content.find('转堆作业:') + len('转堆作业:')
# 查找下一个 "实船作业:" 作为结束标记
next_ship = content.find('实船作业:', relocation_start)
if next_ship == -1:
relocation_content = content[relocation_start:]
else:
relocation_content = content[relocation_start:next_ship]
cleaned = relocation_content.replace('\xa0', ' ').strip()
# 解析车辆数、TEU、尺寸箱量
vehicles_match = re.search(r'上场车辆数:(\d+)', cleaned)
teu_eff_match = re.search(r'作业量/效率:(\d+)TEU', cleaned)
teu = None
if teu_eff_match:
try:
teu = int(teu_eff_match.group(1))
except ValueError as e:
logger.warning(f"转堆作业 TEU 解析失败: {teu_eff_match.group(1)}, 错误: {e}")
vehicles = None
if vehicles_match:
try:
vehicles = int(vehicles_match.group(1))
except ValueError as e:
logger.warning(f"转堆作业车辆数解析失败: {vehicles_match.group(1)}, 错误: {e}")
# 解析尺寸箱量
twenty_feet = None
forty_feet = None
size_pattern = re.search(r'TEU[,\s]*([^]+)', cleaned)
if not size_pattern:
size_pattern = re.search(r'TEU\s*([^]+)', cleaned)
if size_pattern:
size_text = size_pattern.group(1)
twenty_match = re.search(r'20尺\*(\d+)', size_text)
if twenty_match:
try:
twenty_feet = int(twenty_match.group(1))
except ValueError as e:
logger.warning(f"转堆作业 20尺箱量解析失败: {twenty_match.group(1)}, 错误: {e}")
forty_match = re.search(r'40尺\*(\d+)', size_text)
if forty_match:
try:
forty_feet = int(forty_match.group(1))
except ValueError as e:
logger.warning(f"转堆作业 40尺箱量解析失败: {forty_match.group(1)}, 错误: {e}")
# 使用 "转堆作业" 作为船名标识
log = ShipLog(
date=date,
shift=shift,
ship_name='转堆作业',
teu=teu,
efficiency=None,
vehicles=vehicles,
twenty_feet=twenty_feet,
forty_feet=forty_feet
)
logger.info(f"解析转堆作业: {date} {shift} {log.teu}TEU")
return log
except Exception as e:
logger.warning(f"解析转堆作业失败: {date} {shift}, 错误: {e}")
return None
def _parse_log_entry(self, cleaned: str, date: str, shift: str, ship_name: str, logs: List[ShipLog]) -> None:
"""解析单条日志记录车辆数、TEU、尺寸箱量"""
try:
vehicles_match = re.search(r'上场车辆数:(\d+)', cleaned)
teu_eff_match = re.search(r'作业量/效率:(\d+)TEU[,\s]*', cleaned)
# 解析TEU
teu = None
if teu_eff_match:
try:
teu = int(teu_eff_match.group(1))
except ValueError as e:
logger.warning(f"TEU解析失败: {teu_eff_match.group(1)}, 错误: {e}")
# 解析车辆数
vehicles = None
if vehicles_match:
try:
vehicles = int(vehicles_match.group(1))
except ValueError as e:
logger.warning(f"车辆数解析失败: {vehicles_match.group(1)}, 错误: {e}")
# 解析尺寸箱量
twenty_feet = None
forty_feet = None
# 查找作业量/效率后面的括号内的尺寸信息
size_pattern = re.search(r'TEU[,\s]*([^]+)', cleaned)
if not size_pattern:
size_pattern = re.search(r'TEU\s*([^]+)', cleaned)
if size_pattern:
size_text = size_pattern.group(1)
twenty_match = re.search(r'20尺\*(\d+)', size_text)
if twenty_match:
try:
twenty_feet = int(twenty_match.group(1))
except ValueError as e:
logger.warning(f"20尺箱量解析失败: {twenty_match.group(1)}, 错误: {e}")
forty_match = re.search(r'40尺\*(\d+)', size_text)
if forty_match:
try:
forty_feet = int(forty_match.group(1))
except ValueError as e:
logger.warning(f"40尺箱量解析失败: {forty_match.group(1)}, 错误: {e}")
log = ShipLog(
date=date,
shift=shift,
ship_name=ship_name,
teu=teu,
efficiency=None,
vehicles=vehicles,
twenty_feet=twenty_feet,
forty_feet=forty_feet
)
logs.append(log)
except Exception as e:
logger.warning(f"解析日志记录失败: {date} {shift} {ship_name}, 错误: {e}")
def parse_from_file(self, filepath: str) -> List[ShipLog]:
"""
从文件解析日志