feat: 军事科技每日资讯推送系统 - Docker部署 + 日志系统 + 数据目录重组

This commit is contained in:
poiuy
2026-07-12 20:01:02 +08:00
commit 54ca4b1b6a
267 changed files with 47047 additions and 0 deletions
+208
View File
@@ -0,0 +1,208 @@
"""
图片生成模块:创建军事主题网摘图片(单篇/合并长图)
"""
import re
from datetime import datetime
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
Image, ImageDraw, ImageFont = None, None, None
from .utils import wrap_text
from .logger import get_logger
from .get_chinese_font import get_chinese_font
def _parse_webzine_line(line):
"""解析网摘单行文本,返回 (前缀类型, 前缀原文, 内容文本)"""
if not line.strip():
return ('empty', '', '')
for prefix in ['标题:', '原标题:', '发布日期:', '正文:', '价值点:']:
if line.startswith(prefix):
return (prefix.rstrip(''), prefix, line[len(prefix):])
return ('normal', '', line)
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)
}
def _build_render_commands(webzine_content, fonts, content_width, temp_draw, skip_empty=False):
"""将网摘文本转换为渲染命令列表"""
commands = []
for line in webzine_content.split('\n'):
line_type, prefix, content = _parse_webzine_line(line)
if line_type == 'empty':
if not skip_empty:
commands.append(('empty',))
continue
if line_type == '标题':
full_title = prefix + content
wrapped = wrap_text(full_title, fonts['article_title'], content_width, temp_draw)
for wl in wrapped:
commands.append(('article_title', wl))
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 wl in wrapped:
commands.append(('content_indented', wl))
else:
wrapped = wrap_text(content, fonts['content'], content_width, temp_draw)
for wl in wrapped:
commands.append(('normal', wl))
return commands
def _build_combined_render_commands(webzine_texts, fonts, content_width, temp_draw):
"""将多篇网摘文本合并转换为渲染命令列表(篇间以分隔线分隔)"""
commands = []
for idx, wz in enumerate(webzine_texts, 1):
if idx > 1:
commands.append(('separator',))
commands.extend(_build_render_commands(wz, fonts, content_width, temp_draw, skip_empty=True))
return commands
def _execute_render_commands(draw, commands, fonts, padding, line_height, content_width):
"""执行渲染命令列表,在指定画布上绘制内容,返回最终y坐标"""
y = 135
for cmd in commands:
cmd_type = cmd[0]
if cmd_type == 'separator':
y += 30
draw.line([(padding, y), (content_width + padding, y)], fill=(200, 200, 200), width=1)
y += 30
elif 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
return y
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
line_height = 40
padding = 50
content_width = width - padding * 2
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([c for c in commands if c[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
def create_combined_webzine_image(webzine_texts, output_path, now):
"""将已生成的网摘文本合并渲染为一张3800px长图"""
logger = get_logger()
if Image is None:
logger.warning("PIL 未安装,跳过图片生成")
return None
try:
width = 800
padding = 50
content_width = width - padding * 2
line_height = 45
fonts = _load_webzine_fonts()
temp_img = Image.new('RGB', (100, 100), color=(255, 255, 255))
temp_draw = ImageDraw.Draw(temp_img)
commands = _build_combined_render_commands(webzine_texts, fonts, content_width, temp_draw)
fixed_height = 3800
img = Image.new('RGB', (width, fixed_height), color=(255, 255, 255))
draw = ImageDraw.Draw(img)
draw.rectangle([(0, 0), (width, 90)], fill=(26, 72, 144))
date_str = now.strftime('%Y.%m.%d')
draw.text((padding, 25), f'今日必看TOP3网摘 {date_str}', 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), fixed_height)
return output_path
except Exception as e:
logger.warning("合并长图生成失败: %s", e)
return None