新闻详情

新闻详情

首页 / 资讯中心 / 详情

C#与NPOI实现Excel自动分组抽签算法

发布时间:2026/9/12 6:56:21来源:尧图网络
C#与NPOI实现Excel自动分组抽签算法
简介这是一款基于Excel数据驱动的免安装自动抽签分组工具面向赛事组织、活动分组、资源分配等需要按预设规则随机分组的场景。程序支持自定义“大类”约束例如在亚冠抽签中可让同协会球队尽量分散到不同小组避免同组出现过多同类项从而提升公平性与效率。压缩包共43个文件以8个C#源码文件为主体配套4个可直接运行的exe程序、窗体设计文件、资源文件及3个txt说明文档整体仅263KB小巧便携。目前已有216人学习下载适合需要快速落地复杂分组方案的工作人员也适合C#/WinForms初学者研究Excel自动化操作。包内保留了完整的Visual Studio工程结构与核心窗体代码Program.cs、Form1.cs等读者可对照“版本说明.txt”理解抽签算法按需调整分类策略或界面呈现实现个性化二次开发。1. 抽签这事看起来简单一上规则就翻车赛事抽签、活动分组、任务分摊这些都能用 Excel 完成但一旦加上“同一大类的成员不能聚在一个组”这种硬约束手工操作就很容易翻车。比如亚冠抽签时一支协会名下有几支队伍要分散到不同小组简单按 RAND() 排序后切组根本保证不了这一点。Prc.AutoGroup 这个自动分组程序做的事就是读 Excel 名单、按大类约束抽签、把结果直接输出成表。它适合赛事组织者、培训机构以及所有需要“随机但不违规”分组的人。源工程是 C# WinForms文件里包含 Form1.cs、Program.cs、项目文件等属于自己写的桌面工具。它不像在线抽签那样黑盒逻辑都在本地Excel 只负责录入名单分组规则可以自己改。接下来这篇文章按数据读取、分组算法、界面集成、验证排错的顺序拆解这几个环节顺便把容易踩的坑说明白。2. C# 读 Excel先定数据表再选读取方案2.1 表格结构怎么设计在用程序处理之前先把 Excel 工作表规范化。我一般在 Sheet1 里放三列编号、名称、大类。前两列是唯一标识第三列是后续分组时要约束的字段比如亚冠抽签里的“国家/地区”也可以是公司内部活动里的“部门”或“年级”。如果你的场景里还有第二约束比如同一单位但不同赛区的队伍也要回避可以在后面增加二级类别列算法里做同样的处理。程序从A2单元格开始读到最后第一行必须是表头否则第一行数据会被当表头跳过。2.2 用 NPOI 读 xlsx 的代码源工程如果是 C# 写读取 .xlsx 最常见的方式是 NPOI 或 EPPlus。NPOI 开源、免费、不依赖 Office 环境适合部署到普通 Windows 机器上。我一般是这么写的using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; public ListPerson ReadExcel(string filePath) { var list new ListPerson(); using (var fs new FileStream(filePath, FileMode.Open, FileAccess.Read)) { IWorkbook workbook new XSSFWorkbook(fs); // 处理 .xlsx ISheet sheet workbook.GetSheetAt(0); for (int row 1; row sheet.LastRowNum; row) { IRow r sheet.GetRow(row); if (r null) continue; string no r.GetCell(0)?.ToString()?.Trim(); string name r.GetCell(1)?.ToString()?.Trim(); string category r.GetCell(2)?.ToString()?.Trim(); if (string.IsNullOrEmpty(name)) continue; list.Add(new Person { No no, Name name, Category category }); } } return list; }这段代码用XSSFWorkbook是因为它负责解析 .xlsx如果输入是旧版 .xls要换成HSSFWorkbook。GetCell(0)对应第一列单元格可能为 null所以用?.防止空引用。LastRowNum是最后一行的索引从 0 开始所以循环从 1 开始正好跳过表头。要注意NPOI 读出来的单元格类型不全是文本。日期、数字会被ToString()转成奇怪格式比如日期变成时间戳数字。稳妥做法是先判断CellType再用DataFormatter统一处理这个我放到后面排错章节详细说。2.3 用 Excel COM 互操作读取的坑如果你的机器装了 Office也可以用Microsoft.Office.Interop.Excel直接操作 Excel 对象。看压缩包里的工程结构很多人会顺手用 Office 互操作但实际跑起来毛病很多using Excel Microsoft.Office.Interop.Excel; var excelApp new Excel.Application(); Excel.Workbook wb excelApp.Workbooks.Open(filePath); Excel.Worksheet ws wb.Worksheets[1]; int row 2; while (ws.Cells[row, 2].Text ! ) { string name ws.Cells[row, 2].Text; string category ws.Cells[row, 3].Text; // 构造 Person 并添加 row; } wb.Close(false); excelApp.Quit(); System.Runtime.InteropServices.Marshal.ReleaseComObject(excelApp);这种做法的坑在于Marshal.ReleaseComObject只能释放外层对象Excel 经常留下后台进程第二次运行时就会出现“上一次启动失败”之类的问题。服务器上没有安装完整 Office 或许可证没激活new Excel.Application()会直接抛 COM 类工厂异常。所以新项目我基本都用 NPOI。2.4 两种方案的适用场景对比维度NPOIOffice 互操作依赖 Office不依赖必须安装后台进程残留无常见读取速度快较慢格式保真需手动处理由 Excel 引擎保证部署难度低高如果你只是本地跑一跑杀进程麻烦但可控如果要给同事发 exe 用推荐改用 NPOI。这个自动分组场景的数据量通常只有几十到几百行NPOI 完全够用。3. 分组抽签算法先洗牌再按“无同类优先”填充3.1 为什么不能直接随机分配最简单的实现是把人员列表OrderBy(Guid.NewGuid())洗牌后再切 N 段。但这样出现“同一大类队伍挤在同一组”的概率不低。带约束抽签要解决的问题是随机性要保留硬性规则也要满足。算法设计目标有三个每组人数尽量均衡同一大类优先分散到不同组结果不能被提前预测。所以要先洗牌让顺序不可预期再用“无同类优先”的贪心规则去填充组保障约束。3.2 按大类分组洗牌第一步不是把所有人放一起洗而是每个大类各自洗一次牌。这样能保证每个大类内部的抽取顺序随机同时后续填充时每次从每个大类只取一个候选人天然让同类成员分散到不同组。var buckets people .GroupBy(p p.Category) .Select(g g.OrderBy(_ rng.Next()).ToList()) .ToList();这里rng是Random实例。下面在循环里维护一个cursor索引从每个 bucket 的第cursor个位置取人取到这一桶为空为止。所以每个大类每一轮最多贡献一个人避免某个大类一次性把某几个组塞满。3.3 无同类优先的填充逻辑分配每一轮成员时采用贪心策略先找“还没有这个大类成员”的组在这些组里挑人数最少的只有当所有组都已经有这个大类时才允许重复。核心代码public ListGroupResult Draw(ListPerson people, int groupCount, int seed) { var rng new Random(seed); var buckets people.GroupBy(p p.Category) .Select(g g.OrderBy(_ rng.Next()).ToList()) .ToList(); var groups Enumerable.Range(0, groupCount) .Select(i new GroupResult { GroupName 第 (i 1) 组 }) .ToList(); int cursor 0; while (true) { var roundPeople new ListPerson(); foreach (var bucket in buckets) { if (cursor bucket.Count) roundPeople.Add(bucket[cursor]); } if (roundPeople.Count 0) break; // 本轮候选人再随机洗一次避免固定类别顺序带来偏向 for (int i roundPeople.Count - 1; i 0; i--) { int j rng.Next(i 1); var t roundPeople[i]; roundPeople[i] roundPeople[j]; roundPeople[j] t; } foreach (var person in roundPeople) { var safeGroup groups .Where(g !g.Members.Any(m m.Category person.Category)) .OrderBy(g g.Members.Count) .FirstOrDefault(); if (safeGroup ! null) { safeGroup.Members.Add(person); } else { var minCount groups.Min(g g.Members.Count(m m.Category person.Category)); var fallback groups .Where(g g.Members.Count(m m.Category person.Category) minCount) .OrderBy(g g.Members.Count) .First(); fallback.Members.Add(person); } } cursor; } return groups; }safeGroup的筛选条件保证“同一大类绝不在同一组重复”的优先级最高OrderBy(g g.Members.Count)则在安全组里进一步保持人数均衡。fallback分支只有在所有组都已经有该类成员时才会触发比如某大类人数大于组数不可避免出现同组重复时就优先选“该大类在该组出现次数最少”的组做到尽量均匀。roundPeople每轮重新洗牌避免类别 A 永远先挑组。参数说明groupCount是目标组数seed是随机种子传负数可以用时间初始化传固定值可以复现某一次抽签结果。Person类需要包含No、Name、Category三个字段。后续要支持“同单位回避”等二级约束把Category换成元组或者在分配时同时检查两级字段即可。3.4 随机种子与结果复现抽签类工具最怕“结果不能解释”。用固定seed的好处是现场抽签前把种子设成当前时间抽完把种子记录下来需要审计时可以重新生成完全一样的结果。我建议把种子和抽签时间一并写进输出作为本次抽签的凭据。实际工程里可以把seed做成窗体上的输入框留空则用DateTime.Now.Millisecond初始化。参数建议值含义groupCount4~16分组数量根据参与人数动态算seed-1负数表示随机正数表示可复现maxPerGroup00 表示不限制非 0 时按该上限校验maxPerGroup可以在最后检查每组人数是否超限。比如 120 人分 8 组每组上限 15 人这时需要做额外判断而不是只依赖算法自己均衡。4. 把算法装进 WinForms 壳读文件、点按钮、出结果4.1 工程文件里有什么压缩包里能看到Form1.cs、Program.cs、Form1.Designer.cs、Resource1.resx这些文件是一个标准的 C# WinForms 工程。Form1.cs里放按钮事件和业务逻辑Form1.Designer.cs放界面元素声明。这种结构适合做桌面工具不依赖 Web 环境打开就能点。主流程建议控制在三个按钮读入 Excel、开始抽签、导出结果。抽签按钮事件里把前面两个环节串起来private void btnDraw_Click(object sender, EventArgs e) { var people ReadExcel(txtFilePath.Text); int groupCount int.Parse(numGroupCount.Value.ToString()); int seed chkFixedSeed.Checked ? int.Parse(txtSeed.Text) : -1; var result AutoGroup.Draw(people, groupCount, seed); dataGridView1.DataSource result.SelectMany(g g.Members .Select(m new { 组别 g.GroupName, 编号 m.No, 名称 m.Name, 大类 m.Category })).ToList(); }这里用DataGridView直接展示分组结果列头按“组别、编号、名称、大类”铺开。ReadExcel是前文读取方法AutoGroup.Draw是前文算法方法。注意DataGridView数据绑定需要.ToList()才能拿到列结构。4.2 耗时操作不要卡界面如果名单很大读取和分组都在btnDraw_Click里执行界面会短暂“假死”。通常我会把耗时逻辑丢到Task.Run里拿到结果后Invoke回 UI 线程Task.Run(() AutoGroup.Draw(people, groupCount, seed)) .ContinueWith(t { if (t.Exception ! null) { MessageBox.Show(t.Exception.InnerException.Message); return; } dataGridView1.DataSource t.Result.SelectMany(...).ToList(); }, TaskScheduler.FromCurrentSynchronizationContext());TaskScheduler.FromCurrentSynchronizationContext()能保证回调回到 UI 线程避免跨线程访问DataGridView抛InvalidOperationException。4.3 导出分组结果到 Excel分组完成后导出一份带组别的表格。这里用 NPOI 写也很简洁using var fs new FileStream(savePath, FileMode.Create); IWorkbook wb new XSSFWorkbook(); ISheet sheet wb.CreateSheet(分组结果); var header sheet.CreateRow(0); header.CreateCell(0).SetCellValue(组别); header.CreateCell(1).SetCellValue(编号); header.CreateCell(2).SetCellValue(名称); header.CreateCell(3).SetCellValue(大类); int rowIndex 1; foreach (var g in result) { foreach (var m in g.Members) { var row sheet.CreateRow(rowIndex); row.CreateCell(0).SetCellValue(g.GroupName); row.CreateCell(1).SetCellValue(m.No); row.CreateCell(2).SetCellValue(m.Name); row.CreateCell(3).SetCellValue(m.Category); } } wb.Write(fs);保存后的 Excel 可以直接交给裁判组或活动负责人。若要更规范可以再加一张统计表按组别和大类统计人数。导出逻辑和读取逻辑共用Person和GroupResult类不额外引入复杂度。4.4 关于“Excel 无法粘贴数据”这类使用问题程序走的是文件流不碰剪贴板所以不会出现“Excel 无法粘贴数据”的现象。但要提醒使用者读取和导出都别用 Excel 同时打开同一个文件否则FileStream会抛“文件正在被另一个进程使用”的 IOException。如果确实需要预览先关闭 Excel 再点读入。5. 结果验证、排错与扩展方向5.1 校验分组是否真的满足约束抽签结束后要输出一份校验报告不能只看“好像没问题”。校验逻辑按“组别大类”分组统计如果同类数量大于 1说明有重复同时统计各组人数看是否超过上限。var violation result .SelectMany(g g.Members .GroupBy(m new { g.GroupName, m.Category }) .Where(x x.Count() 1) .Select(x ${x.Key.GroupName}中的 {x.Key.Category} 出现 {x.Count()} 次)); if (violation.Any()) MessageBox.Show(string.Join(Environment.NewLine, violation)); else MessageBox.Show(大类已经完全分散);这段代码把“组别大类”合并成键做分组数量大于 1 说明同样的类在同一组出现两次。算法里已经尽力避免这种情况但作为最后一道防线校验逻辑还能兜住后续二次手工修改的问题。5.2 排错读出来全是空行怎么办很多人第一次跑程序Excel 里明明有数据程序却读不到。常见原因有三个LastRowNum比实际行号小前一行有残留格式导致读取空行单元格是数字格式却被当成文本处理。处理办法是在循环里跳过空行同时用DataFormatter统一格式化var formatter new DataFormatter(); string name formatter.FormatCellValue(r.GetCell(1));DataFormatter会把数字、日期都转成用户看到的字符串避免“编号变成科学计数法”这类问题。5.3 扩展二级类别回避如果活动规则是“同一大区且同一主办方的队伍不能同组”可以把Person增加一个SecondaryCategory字段再把safeGroup的筛选条件从单一Category改成(Category, SecondaryCategory)两个字段的组合判断。这样只改一个条件就能支持更复杂的规则不需要重写整个算法。最后一个技巧是抽签前把名单和种子值写进一个 readme 文本随结果一起归档。这样一旦有人质疑结果可以拿着原始名单重新跑一遍seed 固定结果完全一致。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

综合能源系统优化调度:光热、ORC与P2G技术解析 2026/9/12 7:38:26

综合能源系统优化调度:光热、ORC与P2G技术解析

1. 项目概述:综合能源系统的优化调度在能源转型的大背景下,如何高效整合多种能源形式成为行业焦点。这个项目聚焦于含光热电站、有机朗肯循环(ORC)和电转气(P2G)技术的综合能源系统优化调度问题。光热电站作为可再生能源的重要代表,其输出具有…

阅读更多 →
Python智慧交通大数据分析系统设计与实现 2026/9/12 7:38:26

Python智慧交通大数据分析系统设计与实现

1. 项目背景与核心价值智慧交通系统正在全球范围内掀起一场革命。作为一名长期从事交通数据分析的工程师,我亲眼见证了从传统统计方法到AI大模型的跨越式发展。这个Python交通大数据分析系统项目,正是这一技术演进的前沿实践。这个系统的核心价值在于&am…

阅读更多 →
STM32内存模型详解:SRAM结构、内存分区与优化实战 2026/9/12 7:38:26

STM32内存模型详解:SRAM结构、内存分区与优化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
大模型应用落地实战指南:从选型到Agent工程化 2026/9/12 7:38:26

大模型应用落地实战指南:从选型到Agent工程化

1. 项目概述:一张动态演进的“大模型应用地图”,不是静态名录,而是实战导航图“国内外知名大模型及应用——模型/应用维度(2026/09/04)”这个标题,乍看像一份简单的榜单更新,但实际它指向一个更…

阅读更多 →
构建高帧率视频处理流水线:帧管理与并发优化实践 2026/9/12 7:38:26

构建高帧率视频处理流水线:帧管理与并发优化实践

1. 整体设计思路:hyperframes 到底在解决什么问题1.1 实时视频处理的老大难先说个背景,搞过视频处理的人应该都有同感:单帧处理并不难,难的是稳定地、持续地、低延迟地把每一帧都处理完。看起来是一句话的事,真做起来一…

阅读更多 →
卷积神经网络核心架构与工业级优化实践 2026/9/12 7:35:26

卷积神经网络核心架构与工业级优化实践

1. 卷积神经网络核心结构解析在上一部分我们讨论了卷积神经网络的基础概念后,现在让我们深入其核心架构。现代CNN通常由多个功能层堆叠而成,每个层都有其独特的数学表达和计算特性。1.1 卷积层的数学本质卷积操作的本质是局部感受野的权重共享。以一个33…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞