2009年9月9日 星期三

[C#]上櫃股票(OTC)的CSV Parser

最近在寫一個可以讀入股票資料的Parser,先用WebClient將每天的收盤資料下載後
再用Parser將資料寫到資料庫去

從OTC網站下載下來的股票資料是Excel的CSV檔案格式,所以重點就落在如何讀取CSV檔案

用文書編輯軟體打開下載下來的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位數字:
\d{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類的正則表示式簡寫成:
([+-])?\s*(\w+[.]?)+
所以最後的正則表示式為:
(?<=["])(\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)的處理方法也差不多,稍加修改就可以讀入囉~~

1 意見:

匿名,  2012年4月1日 凌晨1:28  

哇~ 講得好詳細~~
下載上市股票資料可以參考這篇歐~
http://empathic-resource.blogspot.com/2012/03/c.html

張貼留言