☰
从Word成绩登记表到SQLite:数据拆解清洗与存储实战
2026/10/14 11:11:07 网站建设 项目流程

简介:一份面向教师、教务人员及初学者的成绩登记表Word文档,用于规范记录学生平时成绩、期中期末与学期评分等数据。文档设计清晰,表格字段涵盖学号、姓名、语文效果登记、期中效果、期末效果、学期评分等列,既可直接填写打印,也可作为课堂成绩管理和简易数据录入的模板。资源包共1个文件,类型为doc,大小153KB,轻量易用;目前已有100人学习下载。该表在结构上蕴含简洁的数据组织思路,可用于理解字段类型、数据关系以及成绩数据的一致性与冗余控制;结合其中合计、平均分等统计项,能快速完成成绩汇总与学期评定。对需要建立学生成绩台账、开展基础数据分析或总结教学质量评估的读者来说,是一份实用的小型参考模板。

1. 一张“成绩登记表(2).doc”,藏着一门课的数据底子

上学期期中结束第三天,教务老师微信发来一个文件:成绩登记表(2).doc。打开之后是三段几乎一样的 Word 表格,学号、姓名、择要、分数、合计、平均分、期中效果、期末效果、学期评分,一行行排得整整齐齐。班上 40 多人,本来我想把平时分数逐条抄进 Excel 再算总评,结果发现这张表看着朴素,真处理起来全是细节:有合并单元格、有等级不是数字、还有重复了三遍的同一个学生。这篇就围绕这张成绩登记表,把字段结构、转 Excel 的方法、拆库存储的思路和五个最容易翻车的地方一次说清楚。适合要自己汇总成绩的班主任、帮教务做数据整理的信息老师,以及接这类小工具开发的教育数据从业者。

2. 这表每行到底记了什么:字段类型、三块分区与评分口径

拿到表格第一件事不是急着转格式,而是先搞明白每一列是什么、能不能计算、可不可以为空。这张成绩登记表从数据库视角看,就是一个简单表结构,每一行是一条学生成绩记录,每一列是一个字段,学号和姓名共同承担了记录标识的职责。

2.1 从数据库视角看这张 Word 表:字段即列,行即学生

我把表里的字段按类型梳理过一遍,整理成下面这张清单,后续无论是做 Excel 清洗还是建 SQLite 表,都以这份清单为准。

字段类型示例说明
学号String2024001学生唯一标识,转 Excel 时必须按文本处理
姓名String张三与学号一一对应,但也可能重名
择要String作业、听写、课堂表现平时成绩的具体项目名称
分数Integer85与择要对应的单项得分
合计Integer255择要各项分数之和
平均分Decimal85.0合计除以择要项目数得到
期中效果Integer / String87 或 优有时是数字,有时是等级
期末效果Integer / String89 或 良同上,口径不稳定
学期评分Integer88总评成绩,通常由平时、期中、期末加权算出

这里最关键的一点是:学号是 String 类型,不是 Integer 类型。很多 Excel 新手直接导入 Word 文本后,学号就变成了科学计数法,后面想按学号匹配学生,发现全对不上。另外,“择要”字段在不同老师手里含义差别很大,有的老师只写一个总分,有的老师把作业、默写、课堂表现拆成好几行记录。

2.2 “择要”“分数”“合计”“平均分”:平时成绩的三种记法

这张表最容易被忽略的是“择要”和“分数”的关系。从原表结构能看出,平时成绩这一块并不是单一字段,而是一个可重复的项目组:一个学生可能在“择要”里写“作业 1 次、听写 2 次、课堂提问 3 次”,对应“分数”里就是三格数据。每行一个学生,但“择要”列里可能塞了多个项目名称,用换行或空格分隔。

我处理过类似表,常见做法是把“择要”和“分数”拆成多行,一行一个项目。例如“作业 90、听写 85、课堂表现 92”就拆成三条记录。拆完之后,“合计”应等于所有项目分数之和,“平均分”应等于合计除以项目数。实际中有两个坑:一是有的老师不填合计,需要自己算;二是有的老师填了合计,但平均分是四舍五入过的,用它做后续加权计算会有小误差。

2.3 期中效果、期末效果与学期评分:别急着直接算平均分

表格后半部分“期中效果”“期末效果”“学期评分”看着像三个分数,但实际口径五花八门。我见过三类版本:第一类是纯数字,比如期中 87、期末 89;第二类是等级,比如优、良、合格、待及格;第三类是数字和等级混填,期中和期末是等级,学期评分却是数字。

菜单 任何评分口径都影响后续统计。常见口径是学期评分=平时平均分×40%+期中×30%+期末×30%,也有学校按 3:3:4 或 4:2:4 来配。拿到表先不要默认按某一个公式算,找填表人确认一句“学期评分是怎么算出来的”,比事后返工省太多事。

3. 把 .doc 拆成 Excel 可分析数据:一次转换加三道清洗

前面字段认清了,这一步开始动手。目标是把 Word 里的三张表完整抽取出来,变成一行一个学生的结构化数据。这里需要先解决格式问题,再写脚本抽表,最后做三道清洗。

3.1 格式选型:直接复制不如走一遍“另存为”

成绩登记表(2).doc 是旧版二进制 .doc 格式,python-docx 只能读 .docx,不能直接读 .doc,所以第一步先在 WPS 或 Word 里另存为 .docx。这一步看似多余,实际能省很多麻烦:老 .doc 里可能有批注、修订痕迹、非标准换行符,转换后再读,表格结构会更干净。

如果手上没有 Office 环境,也可以用 LibreOffice 的命令行完成转换,常见做法是:

soffice --headless --convert-to docx "成绩登记表(2).doc" --outdir converted/

转换完成后会生成 converted/成绩登记表(2).docx,再用 python-docx 读取。注意 LibreOffice 转换的排版可能和 Word 原版略有差异,但表格行列数据基本可保留。

3.2 用 python-docx 把表格完整抽出来

接下来写脚本读取 .docx 里的所有表格。先不急着过滤,把每一行每个单元格的原样打印出来,确认表格实际列数,因为 Word 里可能存在合并单元格,直接读取会导致列错位。

from docx import Document doc = Document("converted/成绩登记表(2).docx") for table_idx, table in enumerate(doc.tables): print(f"--- Table {table_idx} ---") for row_idx, row in enumerate(table.rows): cells = [cell.text.strip().replace("\n", "/") for cell in row.cells] print(row_idx, cells)

这段代码的作用是把 docx 文件里所有表格按行遍历,row.cells 返回一行的所有单元格,cell.text 取出文本并清理首尾空格。replace("\n", "/") 把单元格内部的换行转成斜杠,避免数据跨行导致后续解析错位。参数说明:table_idx 是表格序号,这张成绩登记表在文档里出现三段,打印时就能看到三个 Table;row_idx 是行号,第一行一般是表头;cells 是字符串列表,顺序对应 Word 表格从左到右的列。

如果打印出来的行特别长,比如某些单元格把“择要”下面好几项内容叠在一起,说明原表用了合并单元格,python-docx 在 row.cells 里会把合并区域重复返回,这一点后面清洗时要处理。

确认结构无误后,把有效数据行拼成 DataFrame:

import pandas as pd data_rows = [] for table in doc.tables: for row in table.rows: vals = [cell.text.strip() for cell in row.cells] if len(vals) < 9: continue # 只保留真正以学号开头的数据行 if vals[0].isdigit() and len(vals[0]) >= 4: data_rows.append(vals[:9]) columns = ["学号", "姓名", "择要", "分数", "合计", "平均分", "期中效果", "期末效果", "学期评分"] df = pd.DataFrame(data_rows, columns=columns) df.to_excel("成绩登记表_清洗前.xlsx", index=False) print(df.shape)

这段代码先判断每行第一个单元格是否为纯数字且长度不小于 4,这样能把表头和空行过滤掉。vals[:9] 只取前 9 列,防止原表右侧多出空白列。df.shape 打印出行数列数,例如 (128, 9),表示有 128 条记录,符合预期。

这里有一个非常重要的提示:

提示:如果打印后每行只有 3-4 个值,说明 Word 表格结构不是标准表格,而是用空格排版模拟的表格。这种情况不要硬用 python-docx,直接另存为 txt,再用下面的正则清洗。

import re with open("成绩登记表.txt", encoding="gbk") as f: text = f.read() lines = [line.strip() for line in text.splitlines() if line.strip()] for line in lines: if re.match(r"^\d{4,}\s", line): parts = re.split(r"\s{2,}", line) print(parts)

这段代码按两个及以上空格拆分文本行,适配用空格对齐的老式 Word 表格。注意 encoding 参数:中文 .doc 另存的 txt 常见是 GBK 编码,用 UTF-8 读会直接报错,这里我一般会先试 GBK,再试 UTF-8。

3.3 三道必做清洗:去科学计数法、拆合并单元格、对齐列名

数据抽出来后,不能直接拿去算,至少要做三道清洗。

第一道是学号去科学计数法。pandas 读入时如果学号列被识别为数字,输出时很容易变成 2.024e+06。解决方法是读取后立刻把学号列转成字符串:

df["学号"] = df["学号"].astype(str).str.replace(r"\.0$", "", regex=True).str.zfill(6)

这段代码先把学号转成字符串,去掉可能存在的“2024001.0”中的“.0”,再用 zfill 补足 6 位,保证学号格式统一。参数说明:str.replace 的正则是匹配结尾的 .0,zfill(6) 的意思是如果学号不足 6 位就在前面补 0。

第二道是拆“择要”和“分数”里的多项目内容。前面提到过,一个学生的作业、听写、课堂可能堆在一个单元格里。常规做法是把多项目和多个分数拆开,一行变成一个项目:

import numpy as np rows = [] for _, row in df.iterrows(): items = re.split(r"[/,,\n]", str(row["择要"])) scores = re.split(r"[/,,\n]", str(row["分数"])) if len(items) == len(scores): for item, score in zip(items, scores): rows.append({ "学号": row["学号"], "姓名": row["姓名"], "择要": item, "分数": int(float(score)), }) else: rows.append(row.to_dict()) df_detail = pd.DataFrame(rows) df_detail.to_excel("平时成绩明细.xlsx", index=False)

这段代码先按斜杠、逗号、中文逗号、换行把择要和分数各拆成列表,如果两边项目数一致就逐配对成多行,如果不一致就把整行原样保留,避免误删数据。参数说明:int(float(score)) 先把字符串转浮点再转整数,兼容“85.0”这类带小数点的写法。

第三道是列名对齐。原表里表头可能换行,比如“期中\n效果”之类,打印出来是乱序的。在 DataFrame 里重新用 columns 覆盖一遍即可,确保传给后续 SQLite 的列名是稳定的英文或中文短名。

4. 从电子表格到 SQLite:拆成两张表再存数据

Excel 适合快速查看,但要做成绩追踪、跨学期对比、按班级统计,还是得进数据库。SQLite 比 MySQL、Oracle 轻量得多,单文件就能跑,适合单个学校或单个班级的数据量,不需要单独服务器。

4.1 为什么要拆:normalization 不只为去重

把表直接照搬进数据库也能用,但路上会遇到两个问题:一是同一个学生的平时成绩、期中、期末都放在一行,后续想查“某学生所有科目”,得同时改表结构;二是“择要”里多个项目时,一行会变成多行,学生姓名和学号被迫重复存多次,更新学号或姓名时要改多行,容易漏。

所以这张成绩登记表我至少拆成两张表:一张存学生基本信息,一张存成绩记录。学生表只关心学号和姓名,成绩表只关心哪个学生的哪次考试得了多少分。这样设计的好处是把学生信息和分数解耦,以后新增科目、新增学期,不用改学生表,只往成绩表里插记录。

建表方式优点缺点
单表全部记录查询简单,一眼看到全貌重复信息多,扩展科目要改表结构
拆学生表 + 成绩表数据冗余低,易扩展查询要写 JOIN,初期稍麻烦

对应到这份文档的抽象描述,就是把“学生管理”和“成绩记录”两个主题分开,避免一张表里塞太多职责。

4.2 建表与写入:student、score 两张表的落地脚本

先把建表语句准备好。student 表用学号做主键,重复写入同一个学生时会自动覆盖,score 表用自增主键,每条成绩记录独立存在。

CREATE TABLE student ( student_id TEXT PRIMARY KEY, name TEXT NOT NULL, class_name TEXT ); CREATE TABLE score ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, exam_type TEXT NOT NULL, score_value REAL, record_date TEXT );

建表完成后,从清洗好的 DataFrame 里读取数据并写入:

import sqlite3 conn = sqlite3.connect("grades.db") cur = conn.cursor() # 建表(已存在时跳过) cur.execute("CREATE TABLE IF NOT EXISTS student (student_id TEXT PRIMARY KEY, name TEXT NOT NULL, class_name TEXT)") cur.execute("CREATE TABLE IF NOT EXISTS score (id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, exam_type TEXT NOT NULL, score_value REAL, record_date TEXT)") # 写入学生表,并用 INSERT OR REPLACE 防止重复学号 for _, row in df.iterrows(): cur.execute( "INSERT OR REPLACE INTO student (student_id, name, class_name) VALUES (?, ?, ?)", (row["学号"], row["姓名"], "三年级二班") ) # 写入成绩表,先把平时成绩明细并进去 for _, row in df_detail.iterrows(): if pd.notna(row["分数"]): cur.execute( "INSERT INTO score (student_id, exam_type, score_value) VALUES (?, ?, ?)", (row["学号"], "平时", float(row["分数"])) ) for _, row in df.iterrows(): if pd.notna(row["期中效果"]): cur.execute( "INSERT INTO score (student_id, exam_type, score_value) VALUES (?, ?, ?)", (row["学号"], "期中", float(row["期中效果"])) ) if pd.notna(row["期末效果"]): cur.execute( "INSERT INTO score (student_id, exam_type, score_value) VALUES (?, ?, ?)", (row["学号"], "期末", float(row["期末效果"])) ) conn.commit() conn.close() print("写入完成")

这段代码把平时明细、期中、期末分别以不同 exam_type 写进 score 表。写入时用的参数化写法(? 占位符)能避免拼 SQL 带来的特殊字符报错。INSERT OR REPLACE 依赖 student 表的主键 student_id,重复写入同一个学号时不会新增第二行,而是覆盖原姓名。这里有一个数据安全意识要养成:学号和姓名属于个人信息,写进 SQLite 后又不要随手传到公开仓库,处理完的副本尽量存本地加密或手动保管。

4.3 查成绩、算均分:两条常用 SQL

拆完表后,日常最常用的查询是按班级拉全量成绩,以及算某个学生的平均分。

SELECT s.name, sc.exam_type, sc.score_value FROM score AS sc JOIN student AS s ON sc.student_id = s.student_id WHERE s.class_name = '三年级二班' ORDER BY sc.exam_type, sc.score_value DESC;

这条 SQL 通过 JOIN 把学生表和成绩表关联起来,student_id 是两张表的连接键。ORDER BY exam_type 让平时、期中、期末分区排列,第二个排序条件按分数降序,方便直接看每类成绩的头尾。

算平均分时要注意,不能直接 AVG 所有分数,否则会把一个学生的三条平时记录和期中期末混在一起,算出来的不是学期均分。正确做法是把同一个学生同一个考试类型的分组后再算:

SELECT sc.student_id, s.name, sc.exam_type, AVG(sc.score_value) AS avg_score FROM score AS sc JOIN student AS s ON sc.student_id = s.student_id WHERE sc.exam_type = '平时' GROUP BY sc.student_id, sc.exam_type;

GROUP BY 分组后,AVG 只对同一考试类型内的分数取均值,这样算出来的才是平时平均分。如果某个学生的平时分数明细多了一条,AVG 的结果也不会被期中期末稀释。

5. 常见坑排查:复制粘贴后最容易翻车的 5 处

这一章不是书本上的“常见问题汇总”,而是我实际拆这张表时踩过的坑,按“现象 → 原因 → 解决”的顺序记录。

5.1 单元格连坐:姓名和分数挤在一个格子里

现象:复制 Word 表格到 Excel 后,学号和姓名在 A 列,姓名后面直接跟了一串分数,用 Excel 分列怎么分都分不干净。

原因:原表里“择要”和“分数”是用空格或换行排版的多项目内容,Word 复制出来时把多个单元格内容拼进了一格,列结构被压扁。

解决:不要手动在 Excel 里一个一个抠,写正则按两个以上空格拆开。我一般在 Python 里用 re.split(r"\s{2,}", cell_text),先看打印结果,确认每一项位置后再转成结构化行。

5.2 学号变 E+08,回头查学生全靠猜

现象:学号导入 Excel 后显示成 2.024E+08,单元格里存的其实是 2024000000,真实学号 2024001 丢了尾号。

原因:Excel 对超过 11 位的数字自动切换科学计数法,学号被当成数值类型处理,后面几位被抹成 0。

解决:在 pandas 读取后用 astype(str) 强制转字符串,并配合正则把“.0”去掉。更稳妥的做法是转换前就把这一列格式设为文本。如果用 SQLite 存,学号字段直接定义成 TEXT,从根源上杜绝数字格式问题。

5.3 合计对不上平时分数:缺项到底按不按 0 算

现象:某学生平时明细只有两条记录,合计却等于三条分数的总和,导致明细相加小于合计。

原因:这张成绩登记表的部分行“择要”里只填了部分项目,另外的项目分数没填,但合计里照算。这是填表人的习惯问题,他可能认为缺项就是 0 分。

解决:这一步不能自作主张补 0。我一般会把“合计”和“明细求和”的差值单独拉一列,差值不为 0 的行标注出来,拿回去和填表人确认,而不是直接改数据。数据录入界的血泪经验就是:不要替原始数据做决定,除非书面确认过。

5.4 同一行数据在表里出现三遍

现象:拆完表后,统计学生总数发现是 128,但班级实际只有 48 人。

原因:原 .doc 是三段式排版,同一张表在文档里复制了三次。python-docx 按 doc.tables 遍历时会把三段都当成独立表格读出来,同一批学生就被重复记录了。

解决:用学号去重。pandas 里取 df.drop_duplicates(subset="学号"),或 SQLite 里把 student_id 设为主键,配合 INSERT OR REPLACE 自动覆盖。做去重时保留第一条,并核对姓名是否一致,避免学号相同但姓名不同的脏数据被静默覆盖。

5.5 期中期末是等级不是数字:求平均之前先做映射

现象:期中效果列填的是“优、良、合格”,代码里直接 float() 报错,本来想求平均,结果整个脚本停在那里。

原因:填表老师按等级评定给期中和期末打分,这种等级字段本质是分类数据,不是连续数值,不能直接做算术运算。

解决:先建立等级映射表,再转数字。但注意映射关系要保守,不能自己想当然。常见映射是优=90、良=80、合格=70、待及格=60,两个学校之间可能差很多。我习惯把原始等级单独留一列,映射后的数值放到新列,这样将来检查时还能追溯原文。

level_map = {"优": 90, "良": 80, "合格": 70, "待及格": 60} df["期中效果_数值"] = df["期中效果"].map(level_map)

参数说明:df["期中效果"].map 遇到映射表里没有的值会返回 NaN,写完后立刻检查 df["期中效果_数值"].isna().sum(),看有没有漏网的等级写法。

6. 进阶用法:倒推学期评分的权重,顺手核对整张表

6.1 先确认评分口径,再动手核对

如果这张表的学期评分不是直接录入,而是由平时平均分、期中、期末加权计算出来的,那你可以用数据反推权重,反向验证填表人用的到底是 4:3:3 还是 3:3:4。我一般先拿三个学生的数据手工试算,假设学期评分=平时均分×0.4+期中×0.3+期末×0.3,算出来和表上数字对比,如果误差在正负 2 分以内,口径基本就锁定了。

6.2 用最小二乘反推权重,找出对不上的学生

手工试算只能验证假设,真正稳妥的办法是用回归反推三个系数。这里用 numpy 的 lstsq 做一次最小二乘,能一次性算出平时、期中、期末对应的权重。

import numpy as np x1 = df["平时均分"].values x2 = df["期中效果_数值"].values x3 = df["期末效果_数值"].values y = df["学期评分"].values A = np.vstack([x1, x2, x3, np.ones(len(x1))]).T w, residuals, rank, sv = np.linalg.lstsq(A, y, rcond=None) print("权重:平时=%.3f 期中=%.3f 期末=%.3f 常数=%.3f" % tuple(w))

这段代码把三个自变量的列堆叠成矩阵 A,追加一列 1 是常数项,然后调用 lstsq 拟合 y。输出结果里四个数字就是最符合当前数据的三项权重和常数。常数项正常应该在 0 附近,如果常数项特别大,说明这张表的学期评分里还有其他加分项,比如课堂表现额外加分、考勤扣分等。

再用拟合结果做全表核对:

y_pred = A @ w df["预测评分"] = y_pred df["差异"] = y - y_pred bad = df[np.abs(df["差异"]) > 2] print(bad[["学号", "姓名", "学期评分", "预测评分", "差异"]])

把差异绝对值大于 2 的学生单独拉出来看,这些行大概率是录入错误或口径不一致,比如某个学生的平时均分里漏了一条记录,或者学期评分被手工改过。每次处理完这种登记表,从那以后我拿到任意一张成绩登记表,都要求自己先问四句话:合计是公式还是手填?缺项算不算 0?期中和期末是等级还是数字?学期评分有没有加分项?这套流程走完,再动手写转换脚本,整个拆表过程基本不再是玄学。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询