| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770 |
- #!/usr/bin/env python3
- """
- Generate seating charts from Excel class data.
- Required input columns: 班级, 姓名, 性别, 总分
- """
- import argparse
- import itertools
- 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", "good-with-good-advance"],
- help="成绩配对策略:good-with-good(好+好)、good-with-bad(好+差)、"
- "random(无关)、good-with-good-advance(好+好进阶:好生集中,"
- "差生正前方或正后方必有优生,同桌同性)"
- )
- parser.add_argument(
- "--score-column", type=str, default="赋分成绩",
- help="排序用的成绩列(默认:赋分成绩,即「总分」组下的赋分子列;找不到则用总分列)"
- )
- parser.add_argument(
- "--good-ratio", type=float, default=1 / 3,
- help="优生比例(仅 good-with-good-advance 策略,默认 1/3)"
- )
- parser.add_argument(
- "--bad-ratio", type=float, default=1 / 3,
- help="差生比例(仅 good-with-good-advance 策略,默认 1/3)"
- )
- 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(
- "-S", "--sheet", type=str, default=None,
- help="指定要读取的工作表名称(默认:第一个工作表)"
- )
- parser.add_argument(
- "--usage", action="help",
- help="显示帮助信息"
- )
- return parser.parse_args()
- def find_score_column(ws, header_row, total_col, score_column):
- """在「总分」分组表头下方找到子表头为 score_column 的那一列(1-based)。
- 排行榜表格第2行是分组表头(总分/语数外/…),第3行才是子表头
- (原始成绩/赋分成绩/班级排名)。直接取「总分」列拿到的是原始成绩,
- 这里按子表头选列。返回 (列号, 是否按子表头命中);找不到就用「总分」列。
- """
- sub_row = header_row + 1
- if sub_row <= ws.max_row:
- # 只在「总分」组内往后找,避免串到后面的科目分组
- for c in range(total_col, min(total_col + 4, ws.max_column + 1)):
- val = ws.cell(row=sub_row, column=c).value
- if val and str(val).strip() == score_column:
- return c, True
- return total_col, False
- def read_students(filepath, class_name, sheet_name=None, score_column="赋分成绩"):
- """从Excel读取学生数据,按班级筛选。自动查找表头行。"""
- from openpyxl import load_workbook
- wb = load_workbook(filepath, data_only=True)
- if sheet_name:
- if sheet_name not in wb.sheetnames:
- print(f"错误:工作表 '{sheet_name}' 不存在。可用工作表:{wb.sheetnames}")
- sys.exit(1)
- ws = wb[sheet_name]
- else:
- ws = wb[wb.sheetnames[0]]
- 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
- total_col = indices["总分"] + 1
- score_col, hit = find_score_column(ws, header_row, total_col, score_column)
- if hit:
- print(f"排序成绩列:第 {score_col} 列({score_column})")
- else:
- print(f"排序成绩列:第 {total_col} 列(未找到「{score_column}」子列,改用表头「总分」列)")
- 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=score_col).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
- # ------------------------------------------------------- 分层排布(进阶策略)
- TIER_GOOD = "优"
- TIER_MID = "中"
- TIER_BAD = "差"
- TIER_CYCLE = (TIER_GOOD, TIER_BAD, TIER_MID) # 排层次循环:优生排紧跟差生排
- def gender_key(student):
- """把性别归一成「女」/「男」两池(无法识别的归入男生池)。"""
- return "女" if is_female(student["gender"]) else "男"
- def assign_tiers(students, good_ratio, bad_ratio):
- """按成绩排名划分层次:前 good_ratio 为优生,后 bad_ratio 为差生,其余为中。"""
- ordered = sorted(students, key=lambda s: (-s["score"], s["name"]))
- n = len(ordered)
- n_good = min(n, max(0, int(round(n * good_ratio))))
- n_bad = min(n - n_good, max(0, int(round(n * bad_ratio))))
- for i, stu in enumerate(ordered):
- if i < n_good:
- stu["tier"] = TIER_GOOD
- elif i >= n - n_bad:
- stu["tier"] = TIER_BAD
- else:
- stu["tier"] = TIER_MID
- return ordered
- def _subset_sums(widths):
- """一组桌宽能凑出的所有座位数。"""
- sums = {0}
- for w in widths:
- sums |= {s + w for s in sums}
- return sorted(sums)
- def _choose_counts(rows_bands, total, rng):
- """给每一排定一个女座数,使合计等于 total,并尽量让女生分散到各排。"""
- options = [_subset_sums([w for _, w in rb]) for rb in rows_bands]
- caps = [sum(w for _, w in rb) for rb in rows_bands]
- total_cap = sum(caps) or 1
- best = None
- def walk(i, remain, acc):
- nonlocal best
- if i == len(rows_bands):
- if remain != 0:
- return
- score = sum(abs(acc[j] - total * caps[j] / total_cap) for j in range(len(caps)))
- if best is None or score < best[0] - 1e-9 or (
- abs(score - best[0]) < 1e-9 and rng.random() < 0.5):
- best = (score, list(acc))
- return
- for v in options[i]:
- if v <= remain:
- walk(i + 1, remain - v, acc + [v])
- walk(0, total, [])
- return best[1] if best else None
- def _choose_bands(bands, target, rng):
- """在一排里挑出若干桌组坐女生,座位数之和等于 target,尽量打散到更多组。"""
- for size in range(len(bands), -1, -1):
- hits = [combo for combo in itertools.combinations(range(len(bands)), size)
- if sum(bands[i][1] for i in combo) == target]
- if hits:
- return {bands[i][0] for i in rng.choice(hits)}
- return None
- def _tail_positions(sizes, count):
- """从最右侧座位往左数 count 个,返回 [(组, 组内位置), ...]。"""
- out = []
- for b in range(len(sizes) - 1, -1, -1):
- for p in range(sizes[b] - 1, -1, -1):
- out.append((b, p))
- if len(out) >= count:
- return out
- return out
- def plan_tiered_layout(students, sizes, good_ratio, bad_ratio, seed):
- """分层排布:优生集中,差生正前方或正后方必有优生。
- 1. 排层次按「优-差-中」循环,所以每排差生的正前方恰好是优生排;
- 每排各桌组同层次、同性别(同桌必定同性)。
- 2. 各层次座位容量减去人数得到空位数。优生排的空位必须和它正后方
- 差生排的「同组、同位置」一起挖空,否则会出现差生正前方变成空位、
- 前后都没有优生的情况。
- 3. 按层次把女生的座位数分配到各排,再决定排内哪几个组坐女生。
- 4. 每个 (性别, 层次) 池内按成绩降序,从前往后、从左往右填座,
- 所以优生里分数最高的坐在最前排。
- """
- rng = random.Random(seed)
- ordered = assign_tiers(students, good_ratio, bad_ratio)
- n = len(ordered)
- row_capacity = sum(sizes)
- unknown = [s["name"] for s in ordered if not is_male(s["gender"]) and not is_female(s["gender"])]
- if unknown:
- print(f"警告:{len(unknown)} 名学生性别无法识别({'、'.join(unknown[:5])}"
- f"{' 等' if len(unknown) > 5 else ''}),已按男生编排。")
- if n == 0:
- return {
- "groups": [[] for _ in range(len(sizes))],
- "grid": {}, "voids": set(), "row_roles": [TIER_CYCLE[0]],
- "num_rows": 1, "ordered": [],
- }
- num_rows = max(1, (n + row_capacity - 1) // row_capacity)
- row_roles = [TIER_CYCLE[r % len(TIER_CYCLE)] for r in range(num_rows)]
- capacity = {t: 0 for t in TIER_CYCLE}
- for t in row_roles:
- capacity[t] += row_capacity
- need = {t: 0 for t in TIER_CYCLE}
- for stu in ordered:
- need[stu["tier"]] += 1
- deficit = {t: capacity[t] - need[t] for t in TIER_CYCLE}
- for t in TIER_CYCLE:
- if deficit[t] < 0:
- print(f"错误:{num_rows} 排座位装不下 {need[t]} 名「{t}」等生(该层次只有 {capacity[t]} 座)。")
- print(" 可调整 --good-ratio / --bad-ratio,或换一个座位样式。")
- sys.exit(1)
- if deficit[TIER_GOOD] != deficit[TIER_BAD]:
- print(f"错误:优生空位({deficit[TIER_GOOD]})与差生空位({deficit[TIER_BAD]})不相等,")
- print(" 无法保证每个差生的正前或正后方都是优生。请让优生、差生比例保持一致。")
- sys.exit(1)
- # --- 空位:优生排与其正后方的差生排按同组同位置配对挖空 ---
- voids = set()
- pair = min(deficit[TIER_GOOD], row_capacity)
- if pair:
- anchor = None
- for r in range(num_rows - 1, -1, -1):
- if row_roles[r] == TIER_GOOD and r + 1 < num_rows and row_roles[r + 1] == TIER_BAD:
- anchor = r
- break
- if anchor is None:
- print("错误:排数不足以形成「优生排 + 差生排」的配对空位。")
- sys.exit(1)
- for b, p in _tail_positions(sizes, pair):
- voids.add((anchor, b, p))
- voids.add((anchor + 1, b, p))
- # --- 空位:中等生排从后往前挖 ---
- left = deficit[TIER_MID]
- for r in range(num_rows - 1, -1, -1):
- if left <= 0:
- break
- if row_roles[r] != TIER_MID:
- continue
- free = [(b, p) for b in range(len(sizes)) for p in range(sizes[b])
- if (r, b, p) not in voids]
- for b, p in reversed(free):
- if left <= 0:
- break
- voids.add((r, b, p))
- left -= 1
- if left > 0:
- print(f"错误:还有 {left} 个「{TIER_MID}」等生空位没地方安排。")
- sys.exit(1)
- # --- 每排实际可用的桌组与座位数 ---
- row_bands = {}
- for r in range(num_rows):
- bands = [(b, sum(1 for p in range(sizes[b]) if (r, b, p) not in voids))
- for b in range(len(sizes))]
- row_bands[r] = [item for item in bands if item[1] > 0]
- # --- 男女分配:先定每排女座数,再定排内哪几个组坐女生 ---
- female_need = {t: sum(1 for s in ordered if s["tier"] == t and gender_key(s) == "女")
- for t in TIER_CYCLE}
- band_gender = {}
- for t in TIER_CYCLE:
- rows = [r for r in range(num_rows) if row_roles[r] == t]
- if not rows:
- continue
- counts = _choose_counts([row_bands[r] for r in rows], female_need[t], rng)
- if counts is None:
- print(f"错误:「{t}」等生的男女人数排不进对应的桌组,请调整比例或样式。")
- sys.exit(1)
- for r, want in zip(rows, counts):
- female_bands = _choose_bands(row_bands[r], want, rng)
- if female_bands is None:
- print(f"错误:第 {r + 1} 排无法排出 {want} 个女生座位。")
- sys.exit(1)
- for b, _ in row_bands[r]:
- band_gender[(r, b)] = "女" if b in female_bands else "男"
- # --- 填座 ---
- pools = {(g, t): [s for s in ordered if gender_key(s) == g and s["tier"] == t]
- for g in ("女", "男") for t in TIER_CYCLE}
- idx = {key: 0 for key in pools}
- grid = {}
- for t in TIER_CYCLE:
- for r in range(num_rows):
- if row_roles[r] != t:
- continue
- for b, _ in row_bands[r]:
- g = band_gender[(r, b)]
- pool = pools[(g, t)]
- for p in range(sizes[b]):
- if (r, b, p) in voids or idx[(g, t)] >= len(pool):
- continue
- grid[(r, b, p)] = pool[idx[(g, t)]]
- idx[(g, t)] += 1
- unseen = {key: len(pools[key]) - idx[key] for key in pools if len(pools[key]) != idx[key]}
- if unseen:
- print(f"错误:有学生没排下:{unseen}")
- sys.exit(1)
- groups = []
- for r in range(num_rows):
- for b in range(len(sizes)):
- groups.append([grid[(r, b, p)] for p in range(sizes[b]) if (r, b, p) in grid])
- return {
- "groups": groups,
- "grid": grid,
- "voids": voids,
- "row_roles": row_roles,
- "num_rows": num_rows,
- "ordered": ordered,
- }
- def verify_tiered_layout(layout, sizes):
- """校验硬性约束:同桌同性、差生正前或正后方同列有优生、落座人数。"""
- grid = layout["grid"]
- num_rows = layout["num_rows"]
- problems = []
- mixed = 0
- for r in range(num_rows):
- for b in range(len(sizes)):
- genders = {gender_key(grid[(r, b, p)]) for p in range(sizes[b]) if (r, b, p) in grid}
- if len(genders) > 1:
- mixed += 1
- problems.append(f"第 {r + 1} 排第 {b + 1} 组同桌男女混坐")
- tiers = {grid[(r, b, p)]["tier"] for p in range(sizes[b]) if (r, b, p) in grid}
- if len(tiers) > 1:
- problems.append(f"第 {r + 1} 排第 {b + 1} 组层次不统一")
- roster = sorted(s["name"] for s in layout["ordered"])
- seated = sorted(stu["name"] for stu in grid.values())
- if seated != roster:
- problems.append(f"落座名单与班级名单不符(落座 {len(seated)} 人,班级 {len(roster)} 人)")
- bad_total = bad_ok = 0
- for (r, b, p), stu in sorted(grid.items()):
- if stu["tier"] != TIER_BAD:
- continue
- bad_total += 1
- if any(grid.get((r + dr, b, p)) and grid[(r + dr, b, p)]["tier"] == TIER_GOOD
- for dr in (-1, 1)):
- bad_ok += 1
- else:
- problems.append(f"第 {r + 1} 排第 {b + 1} 组「{stu['name']}」正前/正后方没有优生")
- return {
- "mixed": mixed,
- "bad_total": bad_total,
- "bad_ok": bad_ok,
- "placed": len(grid),
- "empty": num_rows * sum(sizes) - len(grid),
- "problems": problems,
- }
- def print_tiered_report(layout, stats, sizes):
- """打印分层排布的每排构成与约束校验结论。"""
- grid = layout["grid"]
- num_rows = layout["num_rows"]
- print("\n分层排布结果(good-with-good-advance):")
- for r in range(num_rows):
- counts = {}
- for b in range(len(sizes)):
- for p in range(sizes[b]):
- stu = grid.get((r, b, p))
- if stu:
- key = (stu["tier"], gender_key(stu))
- counts[key] = counts.get(key, 0) + 1
- detail = ",".join(f"{t}生{k} {v}人" for (t, k), v in sorted(counts.items()))
- empty = sum(1 for b in range(len(sizes)) for p in range(sizes[b])
- if (r, b, p) not in grid)
- tail = f",空 {empty} 座" if empty else ""
- print(f" 第 {r + 1:2d} 排({layout['row_roles'][r]}生排){detail}{tail}")
- ok_gender = "通过" if stats["mixed"] == 0 else "不通过"
- ok_frontback = "通过" if stats["bad_ok"] == stats["bad_total"] else "不通过"
- print("\n约束校验:")
- print(f" 同桌同性:{ok_gender}(混坐 {stats['mixed']} 组)")
- print(f" 差生正前或正后方有优生:{ok_frontback}({stats['bad_ok']}/{stats['bad_total']} 人)")
- print(f" 落座 {stats['placed']} 人,空 {stats['empty']} 座")
- if stats["problems"]:
- print(" 未通过明细(最多列 20 条):")
- for msg in stats["problems"][:20]:
- print(f" - {msg}")
- 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, args.sheet, args.score_column)
- 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": "与成绩无关",
- "good-with-good-advance": "好+好进阶(分层排布,差生前后排必有优生)",
- }
- if args.strategy == "good-with-good-advance" and args.allow_mixed:
- print("提示:good-with-good-advance 固定「同桌同性」,-m/--allow-mixed 对它无效。")
- # 生成多个座位表
- 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
- if args.strategy == "good-with-good-advance":
- sizes = [int(c) for c in args.style]
- layout = plan_tiered_layout(
- students, sizes, args.good_ratio, args.bad_ratio, seed
- )
- groups = layout["groups"]
- generate_excel(groups, args.style, output_path, args.class_name)
- stats = verify_tiered_layout(layout, sizes)
- print_tiered_report(layout, stats, sizes)
- else:
- 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()
|