Przeglądaj źródła

上传文件至 '换座位'

一个完整的座位表脚本
nonvioce 6 miesięcy temu
rodzic
commit
7e61e9a633
1 zmienionych plików z 400 dodań i 0 usunięć
  1. 400 0
      换座位/generate_seating.py

+ 400 - 0
换座位/generate_seating.py

@@ -0,0 +1,400 @@
+#!/usr/bin/env python3
+"""
+Generate seating charts from Excel class data.
+
+Required input columns: 班级, 姓名, 性别, 总分
+"""
+
+import argparse
+import random
+import sys
+from pathlib import Path
+
+try:
+    from openpyxl import Workbook
+    from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
+    from openpyxl.utils import get_column_letter
+except ImportError:
+    print("Error: openpyxl is required. Install with: pip install openpyxl")
+    sys.exit(1)
+
+
+def parse_args():
+    parser = argparse.ArgumentParser(
+        description="根据Excel学生数据生成座位表"
+    )
+    parser.add_argument(
+        "excel_file", type=str,
+        help="输入Excel文件路径 (.xlsx)"
+    )
+    parser.add_argument(
+        "-c", "--class", dest="class_name", type=str, required=True,
+        help="要生成座位表的班级名称"
+    )
+    parser.add_argument(
+        "-s", "--style", type=str, required=True,
+        help="座位表样式,如 '2222'(4组2人桌)、'3332'(3组3人桌+1组2人桌)"
+    )
+    parser.add_argument(
+        "-m", "--allow-mixed", action="store_true",
+        help="允许男女生同桌(默认不允许)"
+    )
+    parser.add_argument(
+        "-t", "--strategy", type=str, default="good-with-good",
+        choices=["good-with-good", "good-with-bad", "random"],
+        help="成绩配对策略:good-with-good(好+好)、good-with-bad(好+差)、random(无关)"
+    )
+    parser.add_argument(
+        "-o", "--output", type=str, default=None,
+        help="输出Excel文件路径(默认:<输入文件>_<班级>_座位表.xlsx)"
+    )
+    parser.add_argument(
+        "-n", "--count", type=int, default=1,
+        help="生成座位表的个数(默认1个)"
+    )
+    parser.add_argument(
+        "--seed", type=int, default=None,
+        help="随机种子(用于 random 策略的可重复性)"
+    )
+    parser.add_argument(
+        "--usage", action="help",
+        help="显示帮助信息"
+    )
+    return parser.parse_args()
+
+
+def read_students(filepath, class_name):
+    """从Excel读取学生数据,按班级筛选。自动查找表头行。"""
+    from openpyxl import load_workbook
+
+    wb = load_workbook(filepath, data_only=True)
+    ws = wb.active
+
+    required = ["班级", "姓名", "性别", "总分"]
+    indices = {}
+    header_row = None
+
+    # 自动查找包含所有必需列的表头行(最多扫描前10行)
+    for r in range(1, min(ws.max_row + 1, 11)):
+        row_vals = []
+        for col in range(1, ws.max_column + 1):
+            val = ws.cell(row=r, column=col).value
+            row_vals.append(str(val).strip() if val else "")
+        if all(col_name in row_vals for col_name in required):
+            for col_name in required:
+                indices[col_name] = row_vals.index(col_name)
+            header_row = r
+            break
+
+    if header_row is None:
+        # 输出前几行帮助诊断
+        print("错误:Excel文件中未找到 '班级'、'姓名'、'性别'、'总分' 列。")
+        for r in range(1, min(ws.max_row + 1, 6)):
+            preview = [str(ws.cell(row=r, column=c).value or "") for c in range(1, ws.max_column + 1)]
+            print(f"  第{r}行:{preview}")
+        sys.exit(1)
+
+    # 规范化班级名:去除"班"后缀以便灵活匹配
+    class_name_clean = class_name.rstrip("班")
+    data_start = header_row + 1
+
+    students = []
+    for row in range(data_start, ws.max_row + 1):
+        class_val = str(ws.cell(row=row, column=indices["班级"] + 1).value or "").strip()
+        class_val_clean = class_val.rstrip("班")
+        if class_val_clean != class_name_clean:
+            continue
+
+        name = str(ws.cell(row=row, column=indices["姓名"] + 1).value or "").strip()
+        gender = str(ws.cell(row=row, column=indices["性别"] + 1).value or "").strip()
+        score_str = str(ws.cell(row=row, column=indices["总分"] + 1).value or "").strip()
+
+        if not name:
+            continue
+
+        try:
+            score = float(score_str)
+        except ValueError:
+            score = 0.0
+
+        students.append({"name": name, "gender": gender, "score": score})
+
+    wb.close()
+    return students
+
+
+def interleave_high_low(items):
+    """将按分数降序排列的列表重新排列为 高-低-高-低 交错顺序。"""
+    result = []
+    left, right = 0, len(items) - 1
+    while left <= right:
+        result.append(items[left])
+        left += 1
+        if left <= right:
+            result.append(items[right])
+            right -= 1
+    return result
+
+
+def build_desk_groups(style_str, num_students):
+    """根据样式字符串构建桌组大小列表。"""
+    sizes = [int(c) for c in style_str]
+    for s in sizes:
+        if s < 1:
+            print(f"错误:样式中的数字必须大于0,发现 '{s}'")
+            sys.exit(1)
+
+    row_capacity = sum(sizes)
+    num_rows = max(1, (num_students + row_capacity - 1) // row_capacity)
+
+    desk_groups = []
+    for _ in range(num_rows):
+        for size in sizes:
+            desk_groups.append(size)
+
+    return desk_groups
+
+
+def is_male(gender):
+    return gender in ("男", "M", "m", "male", "Male")
+
+
+def is_female(gender):
+    return gender in ("女", "F", "f", "female", "Female")
+
+
+def allocate_students(students, desk_sizes, strategy, allow_mixed, seed):
+    """将学生分配到桌组。"""
+    if seed is not None:
+        random.seed(seed)
+
+    males = [s for s in students if is_male(s["gender"])]
+    females = [s for s in students if is_female(s["gender"])]
+    others = [s for s in students if not is_male(s["gender"]) and not is_female(s["gender"])]
+
+    # 无法识别性别的归入人数多的一方
+    if others:
+        if len(males) >= len(females):
+            males.extend(others)
+        else:
+            females.extend(others)
+
+    # --- 允许混坐:所有学生一起排 ---
+    if allow_mixed:
+        all_students = males + females
+        if strategy == "good-with-good":
+            all_students.sort(key=lambda s: s["score"], reverse=True)
+        elif strategy == "good-with-bad":
+            all_students.sort(key=lambda s: s["score"], reverse=True)
+            all_students = interleave_high_low(all_students)
+        else:
+            random.shuffle(all_students)
+
+        groups = []
+        idx = 0
+        for size in desk_sizes:
+            group = all_students[idx:idx + size]
+            if group:
+                groups.append(group)
+            idx += size
+            if idx >= len(all_students):
+                break
+        return groups
+
+    # --- 性别分开排 ---
+    if strategy == "good-with-good":
+        males.sort(key=lambda s: s["score"], reverse=True)
+        females.sort(key=lambda s: s["score"], reverse=True)
+    elif strategy == "good-with-bad":
+        males.sort(key=lambda s: s["score"], reverse=True)
+        males = interleave_high_low(males)
+        females.sort(key=lambda s: s["score"], reverse=True)
+        females = interleave_high_low(females)
+    else:
+        random.shuffle(males)
+        random.shuffle(females)
+
+    groups = []
+    m_idx, f_idx = 0, 0
+
+    for size in desk_sizes:
+        m_rem = len(males) - m_idx
+        f_rem = len(females) - f_idx
+
+        if m_rem >= f_rem:
+            take = min(size, m_rem)
+            group = males[m_idx:m_idx + take]
+            m_idx += take
+        else:
+            take = min(size, f_rem)
+            group = females[f_idx:f_idx + take]
+            f_idx += take
+
+        if group:
+            groups.append(group)
+
+    return groups
+
+
+def generate_excel(groups, style_str, output_path, class_name):
+    """生成座位表Excel文件。"""
+    wb = Workbook()
+    ws = wb.active
+    ws.title = f"{class_name}座位表"
+
+    sizes = [int(c) for c in style_str]
+    num_groups_per_row = len(sizes)
+    num_rows = (len(groups) + num_groups_per_row - 1) // num_groups_per_row
+
+    total_cols = sum(sizes) + (num_groups_per_row - 1)
+
+    thin_border = Border(
+        left=Side(style="thin"), right=Side(style="thin"),
+        top=Side(style="thin"), bottom=Side(style="thin"),
+    )
+    center_align = Alignment(horizontal="center", vertical="center", wrap_text=True)
+    male_fill = PatternFill(start_color="D6EAF8", end_color="D6EAF8", fill_type="solid")
+    female_fill = PatternFill(start_color="FADBD8", end_color="FADBD8", fill_type="solid")
+    podium_fill = PatternFill(start_color="D5D8DC", end_color="D5D8DC", fill_type="solid")
+    podium_font = Font(bold=True, size=14)
+    name_font = Font(size=14)
+    # score_font kept for reference but not used separately
+
+    # --- 讲台行 ---
+    ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=total_cols)
+    podium_cell = ws.cell(row=1, column=1, value="讲  台")
+    podium_cell.font = podium_font
+    podium_cell.alignment = center_align
+    podium_cell.fill = podium_fill
+    for c in range(1, total_cols + 1):
+        ws.cell(row=1, column=c).fill = podium_fill
+        ws.cell(row=1, column=c).border = thin_border
+    ws.row_dimensions[1].height = 35
+
+    # --- 空行 ---
+    # (直接将学生放在第3行及以下)
+
+    # --- 学生座位 ---
+    group_idx = 0
+    for row_num in range(num_rows):
+        excel_row = row_num + 3  # row 1 = 讲台, row 2 = blank
+        col_offset = 1
+
+        for g in range(num_groups_per_row):
+            size = sizes[g]
+
+            if group_idx < len(groups):
+                group = groups[group_idx]
+                group_idx += 1
+
+                for i in range(size):
+                    col = col_offset + i
+                    cell = ws.cell(row=excel_row, column=col)
+
+                    if i < len(group):
+                        student = group[i]
+                        cell.value = student['name']
+                        cell.font = name_font
+                        cell.alignment = center_align
+                        if is_male(student["gender"]):
+                            cell.fill = male_fill
+                        elif is_female(student["gender"]):
+                            cell.fill = female_fill
+                    else:
+                        cell.value = ""
+
+                    cell.border = thin_border
+                    ws.column_dimensions[get_column_letter(col)].width = 13
+            else:
+                for i in range(size):
+                    col = col_offset + i
+                    cell = ws.cell(row=excel_row, column=col)
+                    cell.border = thin_border
+                    ws.column_dimensions[get_column_letter(col)].width = 13
+
+            col_offset += size
+
+            # 过道
+            if g < num_groups_per_row - 1:
+                aisle_col = col_offset
+                ws.column_dimensions[get_column_letter(aisle_col)].width = 4
+                col_offset += 1
+
+        ws.row_dimensions[excel_row].height = 36
+
+    wb.save(output_path)
+    print(f"座位表已保存至:{output_path}")
+
+
+def main():
+    args = parse_args()
+
+    # 验证样式
+    if not args.style.isdigit() or len(args.style) < 2:
+        print("错误:样式必须为纯数字字符串,至少2位,如 '2222' 或 '3332'")
+        sys.exit(1)
+
+    input_path = Path(args.excel_file)
+    if not input_path.exists():
+        print(f"错误:文件不存在:{args.excel_file}")
+        sys.exit(1)
+
+    if args.count < 1:
+        print("错误:生成个数必须大于0")
+        sys.exit(1)
+
+    # 确定输出路径模板
+    if args.output:
+        output_base = args.output
+    else:
+        output_base = f"{input_path.stem}_{args.class_name}_座位表.xlsx"
+
+    # 读取学生(只读一次)
+    print(f"正在读取 {args.excel_file} ...")
+    students = read_students(args.excel_file, args.class_name)
+
+    if not students:
+        print(f"警告:班级 '{args.class_name}' 中没有找到任何学生,将生成空座位表。")
+    else:
+        m_count = sum(1 for s in students if is_male(s["gender"]))
+        f_count = sum(1 for s in students if is_female(s["gender"]))
+        print(f"班级 '{args.class_name}' 共有 {len(students)} 名学生(男:{m_count},女:{f_count})")
+
+    # 构建桌组(只算一次)
+    desk_sizes = build_desk_groups(args.style, len(students))
+
+    # 确定基础种子
+    base_seed = args.seed if args.seed is not None else random.randint(0, 999999)
+
+    strategy_names = {
+        "good-with-good": "好+好",
+        "good-with-bad": "好+差",
+        "random": "与成绩无关",
+    }
+
+    # 生成多个座位表
+    for i in range(args.count):
+        seed = base_seed + i
+
+        # 输出文件名:多个时加序号后缀
+        if args.count > 1:
+            p = Path(output_base)
+            output_path = str(p.parent / f"{p.stem}_{i + 1}{p.suffix}")
+        else:
+            output_path = output_base
+
+        groups = allocate_students(students, desk_sizes, args.strategy, args.allow_mixed, seed)
+
+        generate_excel(groups, args.style, output_path, args.class_name)
+
+    # 摘要
+    print(f"\n座位表配置:")
+    print(f"  样式:{args.style}({len(args.style)}组,每组{'-'.join(args.style)}人)")
+    print(f"  策略:{strategy_names[args.strategy]}")
+    print(f"  男女生同桌:{'允许' if args.allow_mixed else '不允许'}")
+    print(f"  生成个数:{args.count}")
+    print(f"  总桌组数:{len(groups)},总排数:{(len(groups) + len(args.style) - 1) // len(args.style)}")
+
+
+if __name__ == "__main__":
+    main()