1. 程式人生 > >由 excel 轉換為 markdown,及收穫

由 excel 轉換為 markdown,及收穫


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> <csv destination 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).Activate
oBook.SaveAs dest_file+"\個人", csv_format

oBook.Worksheets(2).Activate
oBook.SaveAs dest_file+"\結對", csv_format

oBook.Worksheets(3).Activate
oBook.SaveAs dest_file+"\小組", csv_format

oBook.Worksheets(5).Activate
oBook.SaveAs dest_file+"\本週", csv_format

oBook.Worksheets(8).Activate
oBook.SaveAs dest_file+"\數週排序-去除負分", csv_format

oBook.Worksheets(9).Activate
oBook.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 不支援對剛剛貼上來的行通過引用行號編輯。或者是因為我沒有做出足夠好的正則表示式 (@典同學,@[email protected]柳園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]