用文書編輯軟體打開下載下來的CSV檔可以發現,主要的資料大概就是如下這幾種形式
1: 98年05月05日 股票行情(不含盤後定價)
2: 代號,名稱,收盤 ,漲跌,開盤 ,最高 ,最低,成交股數 , 成交金額(元), 成交筆數 ,最後買價,最後賣價,發行股數 ,次日漲停價 ,次日跌停價
3: 1107,建台,1.28,+ 0.00 ,1.36,1.36,1.27,"151,000","193,090","13","1.27","1.29","314,035,300",1.36,1.20
4: 2396,精碟,0.89,- 0.06 ,0.90,0.90,0.89,"2,429,000","2,162,690","105","0.00","0.89","1,276,356,078",0.95,0.83
5: 4801,碼斯特,0.00,--- ,0.00,0.00,0.00,"0","0","0","0.00","0.00","29,004,449",10.35,9.03
以列來分類主要的資料形態只有第1,2,3種,第1種記錄交易日期,第2種記錄欄位代號,第3種記錄股票資料
第3,4,5種其實都是股票資料,只是內容值不太一樣(有正浮點數,負浮點數,---等)
對我而言,最主要的當然是股票資料啦,那如何識別那一列是交易日期?那一列是股票資料呢?
稍微觀察一下可以看出股票資料的第一個欄位是股票代號,而股票代號一定都超過4個數字
只要用下列的正則表示式(Regular Expressions)就可以判斷出是否為4位數字:
所以就可以用下列的程式來處理股票資料:
1: Regex stockDataRegex = new Regex(@"\d{4}", RegexOptions.IgnorePatternWhitespace);
2: if(stockDataRegex.IsMatch(line.Substring(0, 4))) //如果是股票資料
3: {
4: //處理股票資料
5: }
CSV檔案的前2行是檔頭的說明,比較重要的資訊只有日期而已,可以直接用String的substring()方法將日期抓出來
DateTime date = new DateTime(1911+Int32.Parse(line.Substring(0,2)), Int32.Parse(line.Substring(3,2)), Int32.Parse(line.Substring(6,2)));
也可以用下面的正則表示式抓:
\d+(?=年)|\d+(?=月)|\d+(?=日)
使用正則表示式的好處是以後若OTC的檔案格式若有修改,可能不必改Code,例如改成"民國98年05月05日",程式碼就不需要修改
但也只是可能而已,改成其它形式程式碼依然要修改,而且使用正則表示式要多好幾行程式碼,所以這裡採用第一種方式
最麻煩的地方在於股票資料的部份,可以再細分成10類:
1: "13" //帶雙引號整數
2: "151,000" //帶雙引號及1個逗號的整數
3: "314,035,300" //帶雙引號及2個逗號的整數
4: "1,276,356,078" //帶雙引號及3個逗號的整數
5: + 0.00 //帶空格正浮點數
6: - 0.06 //帶空格負浮點數
7: 建台 //純中文
8: 1107 //純整數
9: 1.28 //浮點數
10: --- //帶空格連續負號
第1,2,3,4類只有逗號的數目不一樣,也可以說是同一類,不過有些處理方法會因逗號的數目而有影響,所以寫成3類強調逗號的數目
股票資料的各欄位使用逗點分隔,最簡單的方式就是用String的Split()方法,把每個欄位切開存到字串陣列裡
假設先讀一行股票資料到字串變數line裡,可以用下面程式依逗號分格將各欄位依序存到lineArray陣列中:
string[] lineArray = line.Split(',');
不過直接這樣做會有點小問題,因為第2,3,4類的數字中也有帶逗號,可能會造成誤判
以第3類的例子的"314,035,300"而言,分割出來不會是一個整數,而會是如下結果:
1: lineArray[0] = "314
2: lineArray[1] = 035
3: lineArray[2] = 300"
要解決這問題主要也是2種方法:
1: 先將第2,3,4類字串中的逗號全部移除
2: 使用正則表示式
第一種方式還蠻直覺的,程式實作起來也不難,多寫一個processLine()的方法,每次呼叫Split()之前先用processLine()將字串處理過即可 processLine()的主要邏輯就是依序將line字串中的每個字元加到newLine字串中,若是雙引號內的逗號則不加到newLine中,最後回傳newLine
1: public string processLine(string line)
2: {
3:
4: string newLine = "";
5: bool inDoubleQuotes = false;
6: for (int i = 0; i < line.Length; i++)
7: {
8: if (line[i] == '"')
9: {
10: inDoubleQuotes = inDoubleQuotes ? false : true;
11: }
12: else
13: {
14: if (!inDoubleQuotes || (inDoubleQuotes && line[i] != ','))
15: newLine += line[i];
16: }
17: }
18: return newLine;
19: }
以下列資料為例:
2396,精碟,0.89,- 0.06 ,0.90,0.90,0.89,"2,429,000","2,162,690","105","0.00","0.89","1,276,356,078",0.95,0.83
處理完會變成:
2396,精碟,0.89,- 0.06 ,0.90,0.90,0.89,2429000,2162690,105,0.00,0.89,1276356078,0.95,0.83
使用||運算子(Or)時,左運算元(!inDoubleQuotes)條件不成立才會判斷右運算元(inDoubleQuotes && line[i] != ','),所以第14行的程式碼可以簡寫成:
if (!inDoubleQuotes || (line[i] != ','))
不過這樣寫日後可能會猜不太出來原始用意,所以我一般不會這樣簡寫
使用正則表示式不需要多寫一個processLine()方法,所以可以使程式碼簡單一點
(?<=["])(\w+[,]?[.]?)+|([+-])\s*(\w+[.]?)+|(\w+[.]?)+|[-]+
這段正則表示式把之前的10類資列,再整理成4大類:
1: (?<=["])(\w+[,]?[.]?)+ #"13","151,000","314,035,300","1,276,356,078"
2: ([+-])\s*(\w+[.]?)+ #+ 0.00, - 0.06
3: (\w+[.]?)+ #建台,1107,1.28
4: [-]+ #---
第一類處理有帶雙引號的資料,第二類處理帶正負號的資料,第三類處理中文,整數,浮點數,第四類處理連續負號
看了一下,第2,3類蠻類似的,可以再把2,3類的正則表示式簡寫成:
所以最後的正則表示式為:
(?<=["])(\w+[,]?[.]?)+|([+-])?\s*(\w+[.]?)+|[-]+
最後整個method的程式碼就出爐啦:
1: public void otcParser(string fileName)
2: {
3: List<Stock> stockList = new List<Stock>();
4: FileStream fileStream = new FileStream(fileName, FileMode.Open);
5: StreamReader streamReader = new StreamReader(fileStream, System.Text.Encoding.Default);
6:
7: Regex stockDataRegex = new Regex(@"\d{4}", RegexOptions.IgnorePatternWhitespace);
8: Regex cvsRegex = new Regex( @"(?<=[""])(\w+[,]?[.]?)+|([+]|[-])?\s*(\w+[.]?)+|[-]+", RegexOptions.IgnorePatternWhitespace);
9:
10: string line = streamReader.ReadLine();
11: DateTime date = new DateTime(1911+Int32.Parse(line.Substring(0,2)), Int32.Parse(line.Substring(3,2)), Int32.Parse(line.Substring(6,2)));
12: while( (line = streamReader.ReadLine()) != null)
13: {
14: if(stockDataRegex.IsMatch(line.Substring(0, 4))) //判斷是否為股票資料
15: {
16: MatchCollection maches = cvsRegex.Matches(line);
17: stockList.Add(new Stock(date, maches[0].Value, maches[1].Value, maches[2].Value, maches[4].Value, maches[5].Value, maches[6].Value, maches[7].Value, maches[8].Value, maches[10].Value, maches[11].Value));
18: }
19: }
20: streamReader.Close();
21: }
對於上市股票(TWSE)的處理方法也差不多,稍加修改就可以讀入囉~~