11 KiB
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. 提取文本解析函数
将重复的文本前缀识别和内容提取逻辑抽象为独立函数:
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. 提取字体配置函数
将字体加载和配置逻辑集中管理:
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. 提取渲染命令构建函数
将文本换行和渲染命令生成逻辑抽象为独立函数:
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. 提取渲染执行函数
将图片绘制的具体执行逻辑抽象为独立函数:
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. 重构后的主函数
重构后的两个主函数将变得简洁清晰:
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. 主链路
-
完整日报生成流程
-
从RSS抓取到最终图片生成的完整流程
-
验证生成的图片格式、尺寸、内容正确性
-
确认图片文件能正常保存到指定路径
-
-
网摘图片生成
-
单篇网摘图片生成功能
-
TOP3合并长图生成功能
-
验证图片中的文本内容、格式、样式符合预期
-
-
异常情况处理
-
PIL未安装时的降级处理
-
图片生成失败时的错误处理和日志记录
-
确认异常情况下不影响主流程继续执行
-
4.2. 边界情况
-
特殊文本内容
-
包含超长标题的文章
-
包含特殊字符或表情符号的文本
-
空内容或格式异常的网摘文本
-
-
字体和样式
-
不同操作系统的字体兼容性
-
中英文混合内容的正确渲染
-
文本换行和边界情况的处理
-
-
性能和资源
-
生成大量图片时的内存使用情况
-
并发生成图片时的线程安全性
-
大文本内容的处理性能
-
-
缓存和复用
-
图片已存在时的跳过逻辑
-
缓存机制与重构后的兼容性
-
增量生成时的正确性
-