# 1. 问题 图片生成模块中存在大量重复代码,两个核心函数 `create_webzine_image` 和 `create_combined_webzine_image` 包含相似的文本解析、字体处理和图片渲染逻辑,导致代码维护困难且容易出错。 ## 1.1. **代码重复严重** `scripts/modules/image_generator.py` 文件中的两个函数 `create_webzine_image`(第13-131行)和 `create_combined_webzine_image`(第134-239行)存在大量重复代码: * 都包含相同的文本解析逻辑,识别"标题:"、"正文:"、"价值点:"等前缀 * 都使用相同的字体加载和配置代码 * 都包含相似的文本换行和宽度计算逻辑 * 都有重复的图片绘制命令处理 这种重复导致: * 代码行数冗余,两个函数共约230行,其中至少60%是重复逻辑 * 修改渲染逻辑时需要在两个地方同步修改,容易遗漏 * 增加了代码理解和维护的成本 ## 1.2. **缺乏抽象层次** 当前实现没有将共同的渲染逻辑抽象为独立的函数,导致: * 文本解析逻辑散落在两个函数中,形成"面条代码" * 字体配置和图片尺寸参数硬编码在多个位置 * 渲染命令的构建逻辑重复,没有统一的处理接口 ## 1.3. **错误处理不一致** 两个函数在异常处理上存在细微差异: * `create_webzine_image` 在PIL未安装时返回None并记录警告 * `create_combined_webzine_image` 有类似的处理但日志信息略有不同 * 缺乏统一的错误处理策略,难以保证行为一致性 # 2. 收益 通过重构图片生成模块,提取公共逻辑并建立清晰的抽象层次,可以显著提升代码质量和开发效率。 ## 2.1. **减少代码重复** 重构后可以将重复代码从约140行减少到约30行,代码总量减少约40%。通过提取公共函数,两个核心函数的代码行数都将显著减少,提升代码的简洁性和可读性。 ## 2.2. **提升可维护性** 统一的渲染逻辑意味着: * 修改文本解析规则时只需修改一处 * 调整字体或样式时可以集中配置 * 添加新的渲染特性时可以复用现有抽象 * 降低因修改不同步导致的bug风险 ## 2.3. **增强可测试性** 提取后的公共函数可以独立进行单元测试,不再需要依赖完整的图片生成流程。这样可以更容易地验证文本解析、换行逻辑等核心功能的正确性。 ## 2.4. **改善代码可读性** 通过合理的函数命名和职责分离,代码的自解释性将显著提升。新的开发者可以更快理解图片生成的流程,降低学习成本。 # 3. 方案 系统性地重构图片生成模块,通过提取公共函数、建立渲染抽象层次,消除代码重复并提升代码质量。 ## 3.1. **提取文本解析函数** 将重复的文本前缀识别和内容提取逻辑抽象为独立函数: ```python def _parse_webzine_line(line): """解析网摘单行文本,返回 (前缀类型, 前缀文本, 内容文本)""" if not line.strip(): return ('empty', '', '') prefixes = ['标题:', '原标题:', '发布日期:', '正文:', '价值点:'] for prefix in prefixes: if line.startswith(prefix): return (prefix.rstrip(':'), prefix, line[len(prefix):]) return ('normal', '', line) ``` 这个函数统一处理文本解析逻辑,消除了两个函数中的重复代码。 ## 3.2. **提取字体配置函数** 将字体加载和配置逻辑集中管理: ```python def _load_webzine_fonts(): """加载网摘图片所需的所有字体""" return { 'banner': get_chinese_font(size=40, bold=True), 'article_title': get_chinese_font(size=36, bold=True), 'bold': get_chinese_font(size=32, bold=True), 'content': get_chinese_font(size=32, bold=False) } ``` 这样可以确保字体配置的一致性,并便于集中调整样式。 ## 3.3. **提取渲染命令构建函数** 将文本换行和渲染命令生成逻辑抽象为独立函数: ```python def _build_render_commands(webzine_content, fonts, content_width, temp_draw): """将网摘文本转换为渲染命令列表""" commands = [] for line in webzine_content.split('\n'): line_type, prefix, content = _parse_webzine_line(line) if line_type == 'empty': commands.append(('empty',)) elif line_type == '标题': full_title = prefix + content wrapped = wrap_text(full_title, fonts['article_title'], content_width, temp_draw) for wrapped_line in wrapped: commands.append(('article_title', wrapped_line)) elif line_type in ['原标题', '发布日期', '正文', '价值点']: test_line = prefix + content test_bbox = temp_draw.textbbox((0, 0), test_line, font=fonts['content']) test_width = test_bbox[2] - test_bbox[0] if test_width <= content_width: commands.append(('prefix_sameline', prefix, content)) else: commands.append(('prefix_only', prefix)) wrapped = wrap_text(content, fonts['content'], content_width, temp_draw) for wrapped_line in wrapped: commands.append(('content_indented', wrapped_line)) else: wrapped = wrap_text(content, fonts['content'], content_width, temp_draw) for wrapped_line in wrapped: commands.append(('normal', wrapped_line)) return commands ``` 这个函数将复杂的文本处理逻辑封装起来,使主函数更加清晰。 ## 3.4. **提取渲染执行函数** 将图片绘制的具体执行逻辑抽象为独立函数: ```python def _execute_render_commands(draw, commands, fonts, padding, line_height, content_width): """执行渲染命令列表,在指定画布上绘制内容""" y = 135 for cmd in commands: cmd_type = cmd[0] if cmd_type == 'empty': y += line_height elif cmd_type == 'normal': draw.text((padding, y), cmd[1], fill=(0, 0, 0), font=fonts['content']) y += line_height elif cmd_type == 'article_title': draw.text((padding, y), cmd[1], fill=(0, 0, 0), font=fonts['article_title']) y += line_height elif cmd_type == 'prefix_sameline': label_bbox = draw.textbbox((0, 0), cmd[1], font=fonts['bold']) label_width = label_bbox[2] - label_bbox[0] draw.text((padding, y), cmd[1], fill=(0, 0, 0), font=fonts['bold']) draw.text((padding + label_width, y), cmd[2], fill=(0, 0, 0), font=fonts['content']) y += line_height elif cmd_type == 'prefix_only': draw.text((padding, y), cmd[1], fill=(0, 0, 0), font=fonts['bold']) y += line_height elif cmd_type == 'content_indented': draw.text((padding, y), cmd[1], fill=(0, 0, 0), font=fonts['content']) y += line_height elif cmd_type == 'separator': y += 30 draw.line([(padding, y), (content_width + padding, y)], fill=(200, 200, 200), width=1) y += 30 return y ``` 这样可以将绘制逻辑与业务逻辑分离,提升代码的模块化程度。 ## 3.5. **重构后的主函数** 重构后的两个主函数将变得简洁清晰: ```python def create_webzine_image(webzine_content, output_path, title_text): """将网摘文本内容渲染为PNG图片(800px宽,自适应高度)""" logger = get_logger() if Image is None: logger.warning("PIL 未安装,跳过图片生成") return None try: width = 800 padding = 50 content_width = width - padding * 2 line_height = 40 # 解析日期 date_match = re.search(r'\d{4}年\d{2}月\d{2}日', webzine_content) if date_match: date_str = date_match.group(0).replace('年', '.').replace('月', '.').replace('日', '') else: date_str = datetime.now().strftime('%Y.%m.%d') banner_text = f'网摘 {date_str}' # 加载字体 fonts = _load_webzine_fonts() # 构建渲染命令 temp_img = Image.new('RGB', (100, 100), color=(255, 255, 255)) temp_draw = ImageDraw.Draw(temp_img) commands = _build_render_commands(webzine_content, fonts, content_width, temp_draw) # 计算图片高度 actual_lines_count = len([cmd for cmd in commands if cmd[0] != 'separator']) img_height = 140 + actual_lines_count * line_height + padding * 2 img_height = int(img_height * 1.15) # 创建图片并绘制 img = Image.new('RGB', (width, img_height), color=(255, 255, 255)) draw = ImageDraw.Draw(img) # 绘制标题栏 draw.rectangle([(0, 0), (width, 90)], fill=(26, 72, 144)) draw.text((padding, 25), banner_text, fill=(255, 255, 255), font=fonts['banner']) draw.line([(padding, 100), (width - padding, 100)], fill=(180, 180, 180), width=2) # 执行渲染命令 _execute_render_commands(draw, commands, fonts, padding, line_height, content_width) img.save(output_path, 'PNG', quality=95) logger.info("网摘图片已保存: %s (%d条命令, %dpx)", output_path, len(commands), img_height) return output_path except Exception as e: logger.warning("图片生成失败: %s", e) return None ``` 重构后的函数结构清晰,职责单一,易于理解和维护。 # 4. 回归范围 本次重构主要影响图片生成功能,需要重点测试网摘图片生成的正确性和稳定性。 ## 4.1. 主链路 1. **完整日报生成流程** * 从RSS抓取到最终图片生成的完整流程 * 验证生成的图片格式、尺寸、内容正确性 * 确认图片文件能正常保存到指定路径 2. **网摘图片生成** * 单篇网摘图片生成功能 * TOP3合并长图生成功能 * 验证图片中的文本内容、格式、样式符合预期 3. **异常情况处理** * PIL未安装时的降级处理 * 图片生成失败时的错误处理和日志记录 * 确认异常情况下不影响主流程继续执行 ## 4.2. 边界情况 1. **特殊文本内容** * 包含超长标题的文章 * 包含特殊字符或表情符号的文本 * 空内容或格式异常的网摘文本 2. **字体和样式** * 不同操作系统的字体兼容性 * 中英文混合内容的正确渲染 * 文本换行和边界情况的处理 3. **性能和资源** * 生成大量图片时的内存使用情况 * 并发生成图片时的线程安全性 * 大文本内容的处理性能 4. **缓存和复用** * 图片已存在时的跳过逻辑 * 缓存机制与重构后的兼容性 * 增量生成时的正确性