Files
my-daily/02图片生成代码重复问题.md

323 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. **缓存和复用**
* 图片已存在时的跳过逻辑
* 缓存机制与重构后的兼容性
* 增量生成时的正确性