1. 问题
构建之法(现代软件工程)东北师大站[http://www.cnblogs.com/younggift/]的每周学生作业成绩,执行教学团队[https://home.cnblogs.com/u/xinz]要求,发布在 cnblogs 上。作业中包括每位同学在作业单项中取得的分数、累加、按比例分配、线性映射等数据,本学期约17次作业成绩或排序统计,产生约70个表格。成绩公布后学生申诉教师修改成绩时,这些表格需要重新计算,再次发布。每次作业申诉数量大约5次左右。
Emacs是我平时的主要工作环境,所以优选熟悉的工具。上学期第一次成绩发布使用了 org-mode 中的表格,发布为html源代码,粘贴到 cnblogs上。成绩累加、变更后的重新计算非常麻烦,org-mode主要是大纲写作工具,不是电子表格。使用的感觉类似于在word的表格中计算。
Excel适合记录、计算、数据变更后再计算,cnblogs使用纯文本、html 或markdown格式。其中 markdown 格式语法简洁,支持大纲式写作和表格,所以适合成绩发布和变更后再次发布。因此,上学期除最初一两次,以及本学期大部分作业成绩发布,先在excel下记录和计算,然后转换为 markdown 格式。
本文回顾我使用的三种方法,由 excel 文件转为 markdown,及收获。
2. 方案1,FFL 的 exceltk.exe
推荐使用此方案。在本学期大多发布中,我都使用了这一工具。没有使用的几次是在等待升级,采用了临时方案。
exceltk 最初是小牛同学拷给我的,说这个非常方便。后来 FLL 老师做过升级,其中对公式的支持、支持移动设备上查看cnblogs上的表头不变形、小数点保留位数这几次升级都很有帮助。
FFL 老师对 exceltk.exe 的介绍在
[http://www.cnblogs.com/math/p/exceltk.html]。
源码:https://github.com/fanfeilong/exceltk
下载:http://files.cnblogs.com/files/math/exceltk0.0.9.7
我的使用方法类似
exceltk.exe -t md -p 2 -xls 构建之法作业成绩debug.xls
把excel中的每个 sheet 导出成 markdown,小数点保留两位精度。
3. 方案2,sed
exceltk有一段时间不支持 excel 公式计算结果,我换用了临时方案,等待exceltk升级后切换回来。
3.1 为什么需要公式
我的excel中使用了 vlookup, match 等函数,以方便学生申诉以后的成绩变更。
比如个人作业单项变更,需要因此变更的字段有 个人作业总和、个人作业映射到占本周总成绩20%、本周成绩总和、数周累积、数周累积排序、数周累积去除负分同学排序、数周累积映射到[50,100];再如团队成绩单项变更,需要因此变更的字段有 该团队总分、该团队总分映射到本周总成绩的30%、该团队所有成员的团队成绩、该团队所有成员的本周总成绩、该团队所有成员的数周累积以及排序和映射到[50,100]。诸如此类。由每周作业的单项数目不同,所以公式不宜用固定列的序号,比如"=SUM(B4:L4)",而采用了vlookup & match 函数对字段寻址。
vlookup & match 函数类似这样:
=VLOOKUP(F4,小组!$A:$W,MATCH("合计",小组!$1:$1,0)+1,FALSE)
含义是
(1) F4单元格所在列是"所属小组",每行一人,随行变化。此例中的值为
"=VLOOKUP(A4,组员归属!A:B,2,FALSE)",求值结果 "飞天小女警"。
(2) 取"飞天小女警"的"合计。"取 名为"小组"的工作表 中,表头 (第一行)写
作"合计"的那列的数据,要求 A列的值为 F4的那行,即"飞天小女警"。
(3) 总结,姓名 -> 组员归属 -> 小组成绩.
这样,当小组成绩变更以后,该团队所有成员的小组成绩、本周总成绩、数据累积等都会自动变化。我只要修改变更的单项,然后再把excel导出成 markdown发布就行了。不使用公式,每次变更需要顺序修改、复制粘贴若干次,时间长工作量大,每个单项都要消耗30分钟左右,还担心出错。使用公式后成绩变更一次几分钟。
3.2 excel -> csv -> markdown
sed 是 perl 的灵感来源之一,另一个是 awk。它们专门辅助 shell 脚本,awk做计算,sed做文本替换。
我用的临时方案脚本,在这里[https://coding.net/u/younggift/p/xls2md/]。
3.2.1 excel -> csv, vba
我把 excel 导出为 csv 格式,这样完成了公式的计算,也成为了文本格式,sed才能处理。
使用了 stackoverflow 上的 vbs 脚本,稍作修改,按数据表名导出。
----脚本开始
if WScript.Arguments.Count < 2 Then
WScript.Echo "Please specify the source and the destination files. Usage: ExcelToCsv <xls/xlsx source file> "
Wscript.Quit
End If
csv_format = 6
Set objFSO = CreateObject("Scripting.FileSystemObject")
src_file = objFSO.GetAbsolutePathName(Wscript.Arguments.Item(0))
dest_file = objFSO.GetAbsolutePathName(WScript.Arguments.Item(1))
rem msgbox(dest_file)
Dim oExcel
Set oExcel = CreateObject("Excel.Application")
Dim oBook
Set oBook = oExcel.Workbooks.Open(src_file)
oBook.Worksheets(1).ActivateoBook.SaveAs dest_file+"\个人", csv_format
oBook.Worksheets(2).ActivateoBook.SaveAs dest_file+"\结对", csv_format
oBook.Worksheets(3).ActivateoBook.SaveAs dest_file+"\小组", csv_format
oBook.Worksheets(5).ActivateoBook.SaveAs dest_file+"\本周", csv_format
oBook.Worksheets(8).ActivateoBook.SaveAs dest_file+"\数周排序-去除负分", csv_format
oBook.Worksheets(9).ActivateoBook.SaveAs dest_file+"\数周累积负分", csv_format
oBook.Close False
oExcel.Quit
----脚本结束
调用的时候,在bat中,如下。
----bat片段开始
chcp 936
set filename=构建之法作业成绩beta-review.xls
xls2csv.vbs %filename% .
----bat片段结束
3.2.2 cvs -> mark down, sed
根据不同数据表的格式不同,我写了不同的 sed 脚本。"应该"把某些 sed 脚本抽象合并到同一个文件中,不过考虑到复用次数不多、可预见的复用增长不大、以及懒,所以就复制粘贴,然后分别修改了。
所以 shell 脚本看起来这样,里面的 c1_head 与 c1, c2_head 与 c2 长得很像,抽象优化强迫症患者可能感觉不好。
----shell脚本片段开始
sed -f c1_head.sed 本周.csv | sed -f c2_head.sed > 本周.md
sed -f c1.sed 数周排序-去除负分.csv | sed -f c2.sed > 数周排序-去除负分.md
----shell脚本片段开始
每行分成两个sed执行,用管道连接,重定向到指定名称的md即markdown文件中。分成两个sed执行是必要的,因为 sed 不支持对刚刚粘贴来的行通过引用行号编辑。或者是因为我没有做出足够好的正则表达式 (@典同学,@marverick@柳园bbs) ,考虑sed/正则表达式的处理能力,此处应该不涉及类似括号匹配的上下文无关文法。
3.3 sed解读
3.3.1 测试用例
(1) cvs的前几行
列之间用","分隔。在我的临时sed脚本中,没有处理转义","的情况,解决的方案是在xls中避免使用半角逗号。
,20160901,20160908,20160922,20160929,20161013,20161020,20161027,20161103,20161110,累积,映射至[100,60],映射至[100,50]
,,,pre-α,α-1,α-2,α-review,β-1,β-2,β-review,,,
[黄兴](http://www.cnblogs.com/huangxman),72.00 ,80.00 ,68.60 ,5.15 ,41.06 ,63.93 ,60.60 ,69.60 ,78.03 ,538.97 ,100.00 ,100.00
[李俞寰](http://www.cnblogs.com/li-yuhuan/),85.00 ,86.00 ,69.53 ,35.75 ,44.35 ,64.53 ,41.20 ,31.20 ,78.63 ,536.19 ,99.79 ,99.73
[张金生](https://www.cnblogs.com/jx8zjs/),93.00 ,94.00 ,72.47 ,-1.00 ,66.06 ,-1.53 ,39.60 ,63.88 ,88.41 ,514.89 ,98.14 ,97.67
[程媛媛](https://www.cnblogs.com/yuanyuancheng/),61.00 ,76.00 ,-7.60 ,13.27 ,41.23 ,94.67 ,69.40 ,75.20 ,86.51 ,509.68 ,97.73 ,97.17
(2) markdown的前几行
形如"|:--|"的文字,用于分隔出表头。
||20160901|20160908|20160922|20160929|20161013|20161020|20161027|20161103|20161110|累积|映射至[100,60]|映射至[100,50]|
|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
|[黄兴](http://www.cnblogs.com/huangxman)|72.00 |80.00 |68.60 |5.15 |41.06 |63.93 |60.60 |69.60 |78.03 |538.97 |100.00 |100.00 |
|[李俞寰](http://www.cnblogs.com/li-yuhuan/)|85.00 |86.00 |69.53 |35.75 |44.35 |64.53 |41.20 |31.20 |78.63 |536.19 |99.79 |99.73 |
|[张金生](https://www.cnblogs.com/jx8zjs/)|93.00 |94.00 |72.47 |-1.00 |66.06 |-1.53 |39.60 |63.88 |88.41 |514.89 |98.14 |97.67 |
|[程媛媛](https://www.cnblogs.com/yuanyuancheng/)|61.00 |76.00 |-7.60 |13.27 |41.23 |94.67 |69.40 |75.20 |86.51 |509.68 |97.73 |97.17 |
|[张政](https://www.cnblogs.com/regretless/)|90.00 |98.00 |71.27 |5.07 |65.48 |-2.73 |20.00 |68.38 |92.91 |508.38 |97.63 |97.04 |
3.3.2 把 , 转成 |
# , => |
s/,/|/g;
s/^/|/g;
s/$/|/g;
(1) s是substitute.
(2) s / 原来的文字 / 替换成的文字 / 全局
(3) ^表示行首,$表示行尾。
总的效果是,把所有逗号换成竖线,行首行尾各加一条竖线。
3.3.3 表头 |:--|
数据流是这样的 (cvs) -> c1_head -> c2_head -> (md),其中括号里的是产物,没括号的是加工。
在 c1_head.sed 中:
# table head, copy & paste
1h
1G
在 c2_head.sed 中:
2s/[^|]//g
2s/|/|:--/g
2s/|:--$/|/g
(1) 1h 复制第1行,1G粘贴在当前位置。得到
||20160901|20160908|20160922|20160929|20161013|20161020|20161027|20161103|20161110|累积|映射至[100,60]|映射至[100,50]|
||20160901|20160908|20160922|20160929|20161013|20161020|20161027|20161103|20161110|累积|映射至[100,60]|映射至[100,50]|
(2) c2_head.sed中几行的作用,是对只转换第2行,不是对全局影响。
(3) 2s/[^|]//g,除了竖线以外,去除所有字符。
||20160901|20160908|20160922|20160929|20161013|20161020|20161027|20161103|20161110|累积|映射至[100,60]|映射至[100,50]|
||||||||||||||
(4) 2s/|/|:--/g,把第2行的所有竖线,转换为 |:--
||20160901|20160908|20160922|20160929|20161013|20161020|20161027|20161103|20161110|累积|映射至[100,60]|映射至[100,50]|
|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--
(5) 2s/|:--$/|/g,把第2行行尾前的 :-- 转换为 竖线。
||20160901|20160908|20160922|20160929|20161013|20161020|20161027|20161103|20161110|累积|映射至[100,60]|映射至[100,50]|
|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
之所以采用复制粘贴-替换的方法,是因为sed不会计数。
3.3.4 空行 ||||||||||||||
原始 cvs 形如:
---cvs片段开始
姓名,继续迭代,PSP,进度条,代码堆积图,博客字数堆积图,beta发布评论,加分事项,加分分值,合计,占比20%
满分分值,,5,5,5,5,5,,,25,20.00
[程媛媛](https://www.cnblogs.com/yuanyuancheng/),,5,5,5,5,5,,,25,20.00
[杜桥](http://www.cnblogs.com/duq11/),,5,5,5,5,5,,,25,20.00
---cvs片段结束
期待修改为形如下面的样子。"||||||||||||||"一行,用于建立空行,目的是造成两行表头的效果。
||个人作业|占比20%|结对|占比20%|所属小组|小组成绩|占比30%|贡献系数(4人分配4*20)|占比30%|特别加分事由|特别加分数值|本周得分|
|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
|满分分值|25.00 |20.00 |10|20||35.00 |30.00 |5*N|30.00 |||100.00 |
||||||||||||||
|[程媛媛](https://www.cnblogs.com/yuanyuancheng/)|25.00 |20.00 |0|0|飞天小女警|37.00 |31.71 |5.80 |34.80 |||86.51 |
|[杜桥](http://www.cnblogs.com/duq11/)|25.00 |20.00 |0|0|奋斗吧兄弟|32.00 |27.43 |5.00 |30.00 |||77.43 |
|[杜月](http://www.cnblogs.com/qianhuihui/)|24.00 |19.20 |0|0|金州勇士|48.00 |41.14 |5.12 |30.72 |||91.06 |
|[宫成荣](http://www.cnblogs.com/gongcr/)|25.00 |20.00 |0|0|新蜂|19.00 |16.29 |6.00 |36.00 |||72.29 |
在 c1_head.sed 中:
# table head, copy & paste
1h
1G
# blank line, copy & paste
3G
在 c2_head.sed 中:
4d
5s/[^|]//g
(1) 在 c1_head中 复制第1行,另粘贴到第3行一份。此时文本文件仍维持原有
的行号,新粘贴的文字不能使用行号引用,因此不能进一步编辑。
||个人作业|占比20%|结对|占比20%|所属小组|小组成绩|占比30%|贡献系数(4人分配4*20)|占比30%|特别加分事由|特别加分数值|本周得分|
|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
|满分分值|25.00 |20.00 |10|20||35.00 |30.00 |5*N|30.00 |||100.00 |
||个人作业|占比20%|结对|占比20%|所属小组|小组成绩|占比30%|贡献系数(4人分配4*20)|占比30%|特别加分事由|特别加分数值|本周得分|
[程媛媛](https://www.cnblogs.com/yuanyuancheng/),,5,5,5,5,5,,,25,20.00
[杜桥](http://www.cnblogs.com/duq11/),,5,5,5,5,5,,,25,20.00
(2) c2_head.sed中的4d,删除第4行空白行 (在c1_head中的第3行) 。
||个人作业|占比20%|结对|占比20%|所属小组|小组成绩|占比30%|贡献系数(4人分配4*20)|占比30%|特别加分事由|特别加分数值|本周得分|
|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
|满分分值|25.00 |20.00 |10|20||35.00 |30.00 |5*N|30.00 |||100.00 |
||个人作业|占比20%|结对|占比20%|所属小组|小组成绩|占比30%|贡献系数(4人分配4*20)|占比30%|特别加分事由|特别加分数值|本周得分|
[程媛媛](https://www.cnblogs.com/yuanyuancheng/),,5,5,5,5,5,,,25,20.00
[杜桥](http://www.cnblogs.com/duq11/),,5,5,5,5,5,,,25,20.00
(3) 5s/[^|]//g,把原第5行 (删除第4行后显示为第4行,仍计数第5行)改为 ||||||||||||||
||个人作业|占比20%|结对|占比20%|所属小组|小组成绩|占比30%|贡献系数(4人分配4*20)|占比30%|特别加分事由|特别加分数值|本周得分|
|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
|满分分值|25.00 |20.00 |10|20||35.00 |30.00 |5*N|30.00 |||100.00 |
||||||||||||||
|[程媛媛](https://www.cnblogs.com/yuanyuancheng/)|25.00 |20.00 |0|0|飞天小女警|37.00 |31.71 |5.80 |34.80 |||86.51 |
|[杜桥](http://www.cnblogs.com/duq11/)|25.00 |20.00 |0|0|奋斗吧兄弟|32.00 |27.43 |5.00 |30.00 |||77.43 |
|[杜月](http://www.cnblogs.com/qianhuihui/)|24.00 |19.20 |0|0|金州勇士|48.00 |41.14 |5.12 |30.72 |||91.06 |
|[宫成荣](http://www.cnblogs.com/gongcr/)|25.00 |20.00 |0|0|新蜂|19.00 |16.29 |6.00 |36.00 |||72.29 |
4. 方案3,emacs elisp
Emacs是我平时使用的工具,所以本学期最初的转换,当需要公式,因此由 cvs转成 markdow 还没有被 ffl 支持时,自然地想到用 elisp 作为临时方案。
elisp是上下文无关文法 (或者更强?)的语言,因此可以计数,得以避免使用复制粘贴-修改这样的手段生成表头行。col-count用于存储列的数量。
(defun cvs2md-table ()
"replace cvs format to markdown talbe."
(interactive)
; , -> |
(goto-char (point-min))
(replace-string "," "|")
(goto-char (point-min))
(replace-regexp "^" "|")
(goto-char (point-min))
(replace-regexp "$" "|")
; table head
(setq col-count 0)
(goto-char (point-min))
(setq col-count (count-matches "|" (line-beginning-position) (line-end-position)))
(goto-line 2)
(setq head-count 0)
(while (< head-count col-count)
(insert "|:--")
(setq head-count (1+ head-count))
)
(insert "|")
(open-line 1)
; delete "||" in the last line
(goto-char (point-max))
(beginning-of-line)
(kill-line)
)
5. 收获
一个技术方案是否能被别人采用,因此具有更帮助更多的人而不仅是自己,取决于多个方面。比如,emacs这类相对小众之下开发的代码,sed这种需要运行环境和只能命令行操作的脚本,对于很多人不算友好。ffl的工具可以复制粘贴,也支持命令行,基于.net运行环境在当今不再是个问题,友好得多。
语言的能力越强,越接近于图灵机,实现通用功能,比如计数、插入某个特定数量的字符,就越容易。所以在DSL中要小心配置文件容易迅速成长为上下文无关文法,然后图灵等价,成为新的语言,专用特性的优势就消失了。能力弱一些的语言,也不见得不能实现,比如把插入特定数量的字符等价为 (事实上这才是原始的需求)复制粘贴再修改某一行。还是要确定自己的问题到底是什么,计算模型和数学模型的选择,然后才是代码。
要清楚--尽可能第一时间发现--语言或工具的限制,比如sed不为刚插入和文本编排行号,因此不能基于行编辑。
------------------------------------------------------------
博客会手工同步到以下地址:
[http://zhuanlan.zhihu.com/younggift]
[https://younggift.net/]
[http://blog.csdn.net/younggift]
[http://giftdotyoung.blogspot.com]