From 5fd05fcd3c91fdfb0c858ac42a5a257814ff590d Mon Sep 17 00:00:00 2001 From: "qichi.liang" Date: Sat, 3 Jan 2026 22:37:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=BD=AC=E5=A0=86?= =?UTF-8?q?=E4=BD=9C=E4=B8=9A=E8=A7=A3=E6=9E=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 _parse_relocation() 方法解析转堆作业 - 新增 _parse_log_entry() 方法复用解析逻辑 - 转堆作业使用 '转堆作业' 作为船名标识 --- src/confluence/log_parser.py | 222 +++++++++++++++++++++++++---------- 1 file changed, 160 insertions(+), 62 deletions(-) diff --git a/src/confluence/log_parser.py b/src/confluence/log_parser.py index ed249c2..cc772fc 100644 --- a/src/confluence/log_parser.py +++ b/src/confluence/log_parser.py @@ -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]: """ 从文件解析日志