2311061111-lyt/autograde/generate_pdf_report.py

536 lines
19 KiB
Python
Raw Normal View History

2025-12-14 18:19:41 +08:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
generate_pdf_report.py - PDF报告生成脚本
将评分结果和报告内容整合生成PDF文件
"""
import json
import argparse
import sys
import os
from datetime import datetime
# 尝试导入reportlab如果不存在则显示错误
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer,
Table, TableStyle, Image, PageBreak)
from reportlab.lib import colors
from reportlab.lib.units import cm
REPORTLAB_AVAILABLE = True
except ImportError:
REPORTLAB_AVAILABLE = False
def parse_args():
2025-12-14 18:19:41 +08:00
"""解析命令行参数"""
parser = argparse.ArgumentParser(description='PDF Report Generation Script')
parser.add_argument('--score', '--grade', required=True, help='最终成绩文件')
2025-12-14 18:19:41 +08:00
parser.add_argument('--report', required=True, help='REPORT.md文件路径')
parser.add_argument('--frontend', required=True, help='FRONTEND.md文件路径')
parser.add_argument('--out', required=True, help='输出PDF文件路径')
parser.add_argument('--student-id', required=True, help='学生ID')
parser.add_argument('--student-name', required=True, help='学生姓名')
parser.add_argument('--class-id', '--class-name', help='班级ID/班级名称')
parser.add_argument('--assignment-id', default='2311061111', help='作业ID')
parser.add_argument('--images', help='图片目录路径')
parser.add_argument('--commit-sha', help='提交SHA')
2025-12-14 18:19:41 +08:00
return parser.parse_args()
def load_file_content(file_path: str) -> str:
"""加载文件内容"""
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
def load_json_file(file_path: str) -> dict:
"""加载JSON文件内容"""
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def markdown_to_paragraphs(markdown_text: str) -> list:
"""将Markdown文本转换为ReportLab可处理的段落列表"""
# 简单的Markdown处理主要处理标题和段落
paragraphs = []
lines = markdown_text.split('\n')
current_paragraph = ""
for line in lines:
line = line.strip()
if not line:
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
continue
# 处理标题
if line.startswith('# '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('h1', line[2:]))
elif line.startswith('## '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('h2', line[3:]))
elif line.startswith('### '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('h3', line[4:]))
elif line.startswith('#### '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('h4', line[5:]))
# 处理列表
elif line.startswith('- '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('list_item', line[2:]))
elif line.startswith('* '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('list_item', line[2:]))
elif line.startswith('1. '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('list_item', line[3:]))
# 处理代码块
elif line.startswith('```'):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('code_block_start', ''))
elif paragraphs and paragraphs[-1][0] == 'code_block_start':
if line.startswith('```'):
paragraphs.append(('code_block_end', ''))
else:
paragraphs.append(('code_block', line))
# 处理引用
elif line.startswith('> '):
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
current_paragraph = ""
paragraphs.append(('quote', line[2:]))
# 处理粗体和斜体
else:
current_paragraph += ' ' + line if current_paragraph else line
if current_paragraph:
paragraphs.append(('paragraph', current_paragraph))
return paragraphs
def generate_pdf_report(args, score_data, report_content, frontend_content):
"""生成PDF报告"""
if not REPORTLAB_AVAILABLE:
print("❌ ERROR: reportlab库未安装无法生成PDF报告")
print("请运行: pip install reportlab")
return False
# 创建PDF文档
doc = SimpleDocTemplate(args.out, pagesize=A4)
story = []
# 获取样式
styles = getSampleStyleSheet()
# 自定义样式
styles.add(ParagraphStyle(
name='CustomHeading1',
parent=styles['Heading1'],
fontSize=24,
alignment=TA_CENTER,
textColor=colors.HexColor('#2c3e50'),
spaceAfter=30
))
styles.add(ParagraphStyle(
name='CustomHeading2',
parent=styles['Heading2'],
fontSize=18,
textColor=colors.HexColor('#34495e'),
spaceAfter=15,
spaceBefore=15
))
styles.add(ParagraphStyle(
name='CustomHeading3',
parent=styles['Heading3'],
fontSize=14,
textColor=colors.HexColor('#7f8c8d'),
spaceAfter=10
))
styles.add(ParagraphStyle(
name='CustomParagraph',
parent=styles['Normal'],
fontSize=12,
leading=18,
alignment=TA_JUSTIFY,
textColor=colors.HexColor('#000000')
))
styles.add(ParagraphStyle(
name='CustomListItem',
parent=styles['CustomParagraph'],
leftIndent=20,
bulletIndent=10
))
styles.add(ParagraphStyle(
name='CustomCentered',
parent=styles['CustomParagraph'],
alignment=TA_CENTER
))
styles.add(ParagraphStyle(
name='ScoreStyle',
parent=styles['CustomCentered'],
fontSize=36,
textColor=colors.HexColor('#e74c3c'),
fontWeight='bold',
spaceAfter=20
))
# 封面
story.append(Spacer(1, 5*cm))
story.append(Paragraph("作业成绩报告", styles['CustomHeading1']))
story.append(Spacer(1, 2*cm))
# 学生信息
student_info = [
['学生姓名:', args.student_name],
['学生ID:', args.student_id],
['班级ID:', args.class_id],
['作业ID:', args.assignment_id],
['提交日期:', datetime.now().strftime('%Y-%m-%d')]
]
table_style = TableStyle([
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 12),
('BOTTOMPADDING', (0, 0), (-1, -1), 10),
('RIGHTPADDING', (0, 0), (0, -1), 20),
])
info_table = Table(student_info, colWidths=[100, 200])
info_table.setStyle(table_style)
story.append(Spacer(1, 1*cm))
story.append(info_table)
# 最终成绩
story.append(Spacer(1, 3*cm))
story.append(Paragraph("最终成绩", styles['CustomHeading2']))
story.append(Paragraph(f"{score_data['final_score']:.2f}/100", styles['ScoreStyle']))
# 成绩等级
if score_data['final_score'] >= 90:
grade = "优秀"
grade_color = colors.HexColor('#27ae60')
elif score_data['final_score'] >= 80:
grade = "良好"
grade_color = colors.HexColor('#f39c12')
elif score_data['final_score'] >= 70:
grade = "中等"
grade_color = colors.HexColor('#e67e22')
elif score_data['final_score'] >= 60:
grade = "及格"
grade_color = colors.HexColor('#d35400')
else:
grade = "不及格"
grade_color = colors.HexColor('#c0392b')
story.append(Paragraph(f"等级: {grade}",
ParagraphStyle(name='GradeStyle', parent=styles['CustomCentered'], fontSize=20, textColor=grade_color)))
story.append(PageBreak())
# 成绩分布
story.append(Paragraph("成绩分布", styles['CustomHeading2']))
distribution_data = [
['部分', '得分', '权重', '占总分百分比']
]
for part, data in score_data['breakdown'].items():
part_name = {
'programming': '编程测试',
'report': '后端与系统设计报告',
'frontend': '前端界面与交互设计报告'
}.get(part, part)
distribution_data.append([
part_name,
f"{data['score']:.2f}",
f"{data['weight']*100:.0f}%",
f"{data['percentage']:.2f}"
])
distribution_table = Table(distribution_data, colWidths=[150, 60, 60, 100])
distribution_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 12),
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f8f9fa')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
('BOTTOMPADDING', (0, 0), (-1, -1), 10),
('TOPPADDING', (0, 0), (-1, -1), 10),
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6'))
]))
story.append(Spacer(1, 1*cm))
story.append(distribution_table)
story.append(Spacer(1, 2*cm))
# 编程测试详情
story.append(Paragraph("编程测试详情", styles['CustomHeading2']))
programming = score_data['details']['programming']
programming_groups = programming.get('groups', [])
if programming_groups:
prog_data = [['测试组', '通过测试', '总测试数', '得分', '通过率']]
for group in programming_groups:
prog_data.append([
group['name'],
group['passed'],
group['total'],
f"{group['score']:.2f}",
f"{group['passed']/group['total']*100:.1f}%"
])
prog_table = Table(prog_data, colWidths=[100, 70, 70, 60, 70])
prog_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 12),
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f8f9fa')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
('TOPPADDING', (0, 0), (-1, -1), 8),
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6'))
]))
story.append(Spacer(1, 1*cm))
story.append(prog_table)
else:
story.append(Paragraph("暂无编程测试详情", styles['CustomParagraph']))
story.append(Spacer(1, 2*cm))
story.append(PageBreak())
# 后端与系统设计报告
story.append(Paragraph("后端与系统设计报告", styles['CustomHeading2']))
story.append(Spacer(1, 1*cm))
report_paragraphs = markdown_to_paragraphs(report_content)
in_code_block = False
for para_type, content in report_paragraphs:
if para_type == 'h1':
story.append(Paragraph(content, styles['CustomHeading1']))
elif para_type == 'h2':
story.append(Paragraph(content, styles['CustomHeading2']))
elif para_type == 'h3':
story.append(Paragraph(content, styles['CustomHeading3']))
elif para_type == 'paragraph':
story.append(Paragraph(content, styles['CustomParagraph']))
story.append(Spacer(1, 0.5*cm))
elif para_type == 'list_item':
story.append(Paragraph(f'{content}', styles['CustomListItem']))
story.append(Spacer(1, 0.2*cm))
elif para_type == 'quote':
story.append(Paragraph(content,
ParagraphStyle(name='QuoteStyle', parent=styles['CustomParagraph'],
leftIndent=20, rightIndent=20,
borderLeftWidth=3, borderLeftColor=colors.HexColor('#bdc3c7'))))
story.append(Spacer(1, 0.5*cm))
elif para_type == 'code_block_start':
in_code_block = True
story.append(Paragraph('```', styles['CustomParagraph']))
elif para_type == 'code_block_end':
in_code_block = False
story.append(Paragraph('```', styles['CustomParagraph']))
story.append(Spacer(1, 0.5*cm))
elif para_type == 'code_block' and in_code_block:
story.append(Paragraph(content,
ParagraphStyle(name='CodeStyle', parent=styles['CustomParagraph'],
fontName='Courier', fontSize=10,
leftIndent=20, rightIndent=20)))
story.append(PageBreak())
# 前端界面与交互设计报告
story.append(Paragraph("前端界面与交互设计报告", styles['CustomHeading2']))
story.append(Spacer(1, 1*cm))
frontend_paragraphs = markdown_to_paragraphs(frontend_content)
in_code_block = False
for para_type, content in frontend_paragraphs:
if para_type == 'h1':
story.append(Paragraph(content, styles['CustomHeading1']))
elif para_type == 'h2':
story.append(Paragraph(content, styles['CustomHeading2']))
elif para_type == 'h3':
story.append(Paragraph(content, styles['CustomHeading3']))
elif para_type == 'paragraph':
story.append(Paragraph(content, styles['CustomParagraph']))
story.append(Spacer(1, 0.5*cm))
elif para_type == 'list_item':
story.append(Paragraph(f'{content}', styles['CustomListItem']))
story.append(Spacer(1, 0.2*cm))
elif para_type == 'quote':
story.append(Paragraph(content,
ParagraphStyle(name='QuoteStyle', parent=styles['CustomParagraph'],
leftIndent=20, rightIndent=20,
borderLeftWidth=3, borderLeftColor=colors.HexColor('#bdc3c7'))))
story.append(Spacer(1, 0.5*cm))
elif para_type == 'code_block_start':
in_code_block = True
story.append(Paragraph('```', styles['CustomParagraph']))
elif para_type == 'code_block_end':
in_code_block = False
story.append(Paragraph('```', styles['CustomParagraph']))
story.append(Spacer(1, 0.5*cm))
elif para_type == 'code_block' and in_code_block:
story.append(Paragraph(content,
ParagraphStyle(name='CodeStyle', parent=styles['CustomParagraph'],
fontName='Courier', fontSize=10,
leftIndent=20, rightIndent=20)))
# 构建并保存PDF
try:
doc.build(story)
return True
except Exception as e:
print(f"❌ PDF生成失败: {str(e)}")
return False
def generate_text_report(args, score_data, report_content, frontend_content):
"""生成纯文本报告作为备选方案"""
text_report = []
text_report.append("=" * 80)
text_report.append(" 作业成绩报告 ")
text_report.append("=" * 80)
text_report.append("")
# 学生信息
text_report.append(f"学生姓名: {args.student_name}")
text_report.append(f"学生ID: {args.student_id}")
text_report.append(f"班级ID: {args.class_id}")
text_report.append(f"作业ID: {args.assignment_id}")
text_report.append(f"提交日期: {datetime.now().strftime('%Y-%m-%d')}")
text_report.append("")
# 最终成绩
text_report.append("-" * 80)
text_report.append(f"最终成绩: {score_data['final_score']:.2f}/100")
# 成绩等级
if score_data['final_score'] >= 90:
grade = "优秀"
elif score_data['final_score'] >= 80:
grade = "良好"
elif score_data['final_score'] >= 70:
grade = "中等"
elif score_data['final_score'] >= 60:
grade = "及格"
else:
grade = "不及格"
text_report.append(f"等级: {grade}")
text_report.append("")
# 成绩分布
text_report.append("成绩分布:")
text_report.append("| 部分 | 得分 | 权重 | 占总分百分比 |")
text_report.append("|------|------|------|------------|")
for part, data in score_data['breakdown'].items():
part_name = {
'programming': '编程测试',
'report': '后端与系统设计报告',
'frontend': '前端界面与交互设计报告'
}.get(part, part)
text_report.append(f"| {part_name} | {data['score']:.2f} | {data['weight']*100:.0f}% | {data['percentage']:.2f} |")
text_report.append("")
text_report.append("=" * 80)
text_report.append("")
# 保存文本报告
text_report_path = args.out.replace('.pdf', '.txt')
with open(text_report_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(text_report))
print(f"✅ 已生成纯文本报告: {text_report_path}")
return True
def main():
"""主函数"""
args = parse_args()
print("📁 加载评分数据...")
score_data = load_json_file(args.score)
print("📁 加载后端报告内容...")
report_content = load_file_content(args.report)
print("📁 加载前端报告内容...")
frontend_content = load_file_content(args.frontend)
print("📄 生成PDF报告...")
pdf_success = generate_pdf_report(args, score_data, report_content, frontend_content)
# 如果PDF生成失败生成纯文本报告
if not pdf_success:
print("🔄 尝试生成纯文本报告...")
generate_text_report(args, score_data, report_content, frontend_content)
else:
print(f"✅ PDF报告生成成功: {args.out}")
# 生成一个简单的总结文件
summary = {
"student_info": {
"name": args.student_name,
"id": args.student_id,
"class_id": args.class_id,
"assignment_id": args.assignment_id
},
"final_score": score_data["final_score"],
"report_path": args.out,
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"status": "completed"
}
summary_path = args.out.replace('.pdf', '_summary.json')
with open(summary_path, 'w', encoding='utf-8') as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print(f"✅ 总结文件生成成功: {summary_path}")
if __name__ == '__main__':
main()