[C#] 取得公開資訊觀測站股票基本資料(上市、上櫃、興櫃、公開發行)
在公開資訊觀測站可以查到股票的基本資料、財報、股東會、重大訊息等資料。
官網連結: 公開資訊觀測站
我這次要講解的是如何利用 C# ASP.Net MVC 取得公開資訊觀測站的股票基本資料。
在公開資訊觀測站可以點選「彙總報表 > 基本資料 > 基本資料查詢彙總表」開啟頁面。
網址為: https://mops.twse.com.tw/mops/web/t51sb01
會看到所有上市、上櫃、興櫃、公開發行公司的「基本資料查詢彙總表」
在不選產業別下直接按「查詢」鈕就會取得所有上市公司的基本資料
查詢結果還提供了「另存CSV」的下載連結。
下載檔案是 CSV 格式,可以利用程式讀取檔案內容
此 CSV 格式的表頭欄位包含這些
1 |
"公司代號","公司名稱","公司簡稱","產業類別","外國企業註冊地國","住址","營利事業統一編號","董事長","總經理","發言人","發言人職稱","代理發言人","總機電話","成立日期","上市日期","普通股每股面額","實收資本額(元)","已發行普通股數或TDR原發行股數","私募普通股(股)","特別股(股)","編製財務報告類型","普通股盈餘分派或虧損撥補頻率","普通股年度(含第4季或後半年度)現金股息及紅利決議層級","股票過戶機構","過戶電話","過戶地址","簽證會計師事務所","簽證會計師1","簽證會計師2","英文簡稱","英文通訊地址","傳真機號碼","電子郵件信箱","公司網址","投資人關係聯絡人","投資人關係聯絡人職稱","投資人關係聯絡電話","投資人關係聯絡電子郵件","公司網站內利害關係人專區網址" |
目前查出來的公司就有 965 筆資料,利用程式讀取大量資料是比較合適的方式。
接下來為講解如何利用 C# 取得所有股票的基本資料 CSV 檔案,並讀取 CSV 檔案內容。
Contents
手動取得「查詢」呼叫網址
在「查詢」鈕按右鍵 > 檢查
在 Elements 頁籤裡面會看到指標停在「查詢」的 HTML 上面,然後往上找最近的 <form> 標籤,看到 <form> 標籤指向的網址為 /mops/web/ajax_t51sb01
在 DevTools 有開啟的情況下,再按一次剛剛的查詢鈕
這時候上面頁籤切換到 「Network」,然後往下找到「ajax_t51sb01」的名稱。
點擊名稱後,右邊頁籤選擇「Header」,可以看到 Request URL 以及下面的 Form Data
點擊 Form Data > view source 可以看到組合參數
網址 Request URL: https://mops.twse.com.tw/mops/web/ajax_t51sb01
參數 Form Data: encodeURIComponent=1&step=1&firstin=1&TYPEK=sii&code=
網址及參數將兩者用 “?” 合併起來為
https://mops.twse.com.tw/mops/web/ajax_t51sb01?encodeURIComponent=1&step=1&firstin=1&TYPEK=sii&code=
就是查詢鈕背後傳送的網址。
單獨執行此網址會回傳 HTML 型式的資料
雖然此頁面已經可以利用 HTML Parser 解析來取得資料,但為了更好解讀資料,還是再取得 CSV 檔案來解讀會比較簡單。
手動取得 CSV 呼叫網址
在「另存 CSV」鈕按右鍵 > 檢查
在頁籤「Elements」裡面會停在「另存CSV」鈕的 HTML 上,然後往上找到最近的 <form> 標籤。
會看到指向目標為 /mops/web/ajax_t51sb01
在 <form> 底下有 3 個參數
<input type="hidden" name="firstin" value="true">
<input type="hidden" name="step" value="10">
<input type="hidden" name="filename" value="t51sb01_20210523_111213410.csv">
這 3 個參數是呼叫 ajax_t51sb01 時傳送欄位
在 DevTools 開啟的情況下,按一次「另存 CSV」鈕,然後頁籤切換到「Network」
點擊名稱「t105sb02」,然後記錄右邊的「Request URL」及「Form Data」 參數
點擊 Form Data > view source 可以看到組合參數
網址 Request URL: https://mops.twse.com.tw/server-java/t105sb02
參數 Form Data: firstin=true&step=10&filename=t51sb01_20210523_111213410.csv
網址及參數將兩者用 “?” 合併起來為
https://mops.twse.com.tw/server-java/t105sb02?firstin=true&step=10&filename=t51sb01_20210523_111213410.csv
就是「另存 CSV」鈕背後傳送的網址。
單獨執行此網址會下載 CSV 檔案資料,此 CSV 檔案是程式要解析的資料。
要分 2 步驟取得 CSV 網址是因為 CSV 網址參數會一直改變,由第 1 步查詢時取得參數,才能再取得 CSV 網址參數。
以上的說明是手動取得網址的方式,重要的是知道背後呼叫的網址,有了網址之後,接下來就會轉換成 C# 語法呼叫網址取得 CSV 檔案後解讀資料。
範例建置環境
後端架構: C# ASP.Net MVC .Net Framework
前端架構: Vue.js, jQuery, Bootstrap
使用 Visual Studio 建立 ASP.Net MVC 專案,我用新專案取得公開資訊觀測站的股票基本資料為範例說明,最下方會提供此範例下載。
C# 取得查詢網址
這是我簡單設計的介面,只提供市場別選擇,查詢結果包含全部產業別。
查詢結果欄位我只顯示前面幾個欄位示範就好,下載 CSV 時會包含所有欄位,實務應用時可以依所需欄位讀取。
可以選擇上市、上櫃、興櫃、公開發行查詢
可以選擇上市、上櫃、興櫃、公開發行查詢,查詢後就會將 CSV 檔案內容呈現在網頁上。
HTML前端 View 語法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
<div id="VuePage"> <div class="panel panel-default"> <div class="panel-heading">查詢條件</div> <div class="panel-body"> <div class="row"> <div class="col-md-2"> 市場別 </div> <div class="col-md-2"> <select class="form-control" v-model="form.Q_MARKET_TYPE"> <option value="sii">上市</option> <option value="otc">上櫃</option> <option value="rotc">興櫃</option> <option value="pub">公開發行</option> </select> </div> </div> </div> <div class="panel-footer"> <button type="button" class="btn btn-default" v-on:click="GetData()">查詢</button> </div> </div> <div class="panel panel-default"> <div class="panel-heading"> 股票基本資料 </div> <div class="panel-body"> <table class="table"> <tr> <th>公司代號</th> <th>公司名稱</th> <th>公司簡稱</th> <th>產業類別</th> <th>外國企業註冊地國</th> <th>住址</th> <th>營利事業統一編號</th> <th>董事長</th> </tr> <tr v-for="(item, index) in gridList.datas"> <td>{{item.CompanyCode}}</td> <td>{{item.CompanyName}}</td> <td>{{item.CompanyAbbreviation}}</td> <td>{{item.IndustryCategory}}</td> <td>{{item.ForeignCompanyRegistrationCountry}}</td> <td>{{item.Address}}</td> <td>{{item.UniformNumberProfitBusiness}}</td> <td>{{item.Chairman}}</td> </tr> </table> </div> </div> </div> |
Javascript 前端 View 語法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
<script> var VuePage = new Vue({ el: '#VuePage' , data: { form: { Q_MARKET_TYPE:"" } , gridList: { datas: [] } } , methods: { GetData: function () { var self = this; var postData = {}; postData["Q_MARKET_TYPE"] = self.form.Q_MARKET_TYPE; // 開啟鎖定 $.blockUI({ message: '處理中...' }); $.ajax({ url:'@Url.Content("~/Home/GetData")', method:'POST', dataType:'json', data: { inModel: postData }, success: function (datas) { if (datas.ErrMsg) { alert(datas.ErrMsg); // 停止鎖定 $.unblockUI(); return; } // 顯示列表資料 self.gridList.datas = []; for (var i in datas.gridList) { var gridData = {}; for (var key in datas.gridList[i]) { gridData[key] = datas.gridList[i][key]; } self.gridList.datas.push(gridData); } // 停止鎖定 $.unblockUI(); }, error: function (err) { alert(err.responseText); }, }); } } }) </script> |
C# 後端 Controller 語法
後端語法有用到一個新元件 HtmlAgilityPack 此套件主要是解析 HTML 標籤,取得 HtmlAgilityPack 方法在 NuGet 上搜尋名稱「HtmlAgilityPack」,我安裝時的版本為 1.11.33。
安裝之後,在引用時就可以加入
using HtmlAgilityPack;
GetData() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
/// <summary> /// 查到股票基本資料 /// </summary> /// <param name="inModel"></param> /// <returns></returns> public ActionResult GetData(GetDataIn inModel) { GetDataOut outModel = new GetDataOut(); if (string.IsNullOrEmpty(inModel.Q_MARKET_TYPE)) { outModel.ErrMsg = "請選擇市場別"; return Json(outModel); } outModel.gridList = new List<StockRow>(); WebClient webClient = new WebClient(); HtmlAgilityPack.HtmlDocument doc = new HtmlAgilityPack.HtmlDocument(); // 市場別網址 string QueryUrl = "https://mops.twse.com.tw/mops/web/ajax_t51sb01?encodeURIComponent=1&step=1&firstin=1&TYPEK={0}&code="; QueryUrl = string.Format(QueryUrl, inModel.Q_MARKET_TYPE); // 取得查詢回傳 MemoryStream ms = new MemoryStream(webClient.DownloadData(QueryUrl)); doc.Load(ms, Encoding.Default); // 取得另存 CSV form HtmlNodeCollection formNode = doc.DocumentNode.SelectNodes("//form[@name='fm']"); if (formNode != null) { // 取得欄位 HtmlNode filenameNode = doc.DocumentNode.SelectSingleNode("//form[@name='fm']/input[@name='filename']"); string filenameValue = filenameNode.GetAttributeValue("value", ""); // CSV 網址 string csvUrl = "https://mops.twse.com.tw/server-java/t105sb02?firstin=true&step=10&filename={0}"; csvUrl = string.Format(csvUrl, filenameValue); // 呼叫 CSV 網址 string csvData = webClient.DownloadString(csvUrl); if (csvData.Trim().Length > 0) { DataTable dt = new DataTable(); string[] lineStrs = csvData.Split('\n'); for (int i = 0; i < lineStrs.Length; i++) { string strline = lineStrs[i]; // 解析資料 ArrayList csvLine = new ArrayList(); this.ParseCSVData(csvLine, strline); if (i == 0) { for (int c = 0; c < csvLine.Count; c++) { dt.Columns.Add(csvLine[c].ToString()); } } else { // 寫入 Datatable DataRow dr = dt.NewRow(); for (int c = 0; c < csvLine.Count; c++) { dr[c] = csvLine[c].ToString(); } dt.Rows.Add(dr); } } // 輸出資料 foreach (DataRow dr in dt.Rows) { // 只示範前面幾個欄位 StockRow row = new StockRow(); row.CompanyCode = dr["公司代號"].ToString(); row.CompanyName = dr["公司名稱"].ToString(); row.CompanyAbbreviation = dr["公司簡稱"].ToString(); row.IndustryCategory = dr["產業類別"].ToString(); row.ForeignCompanyRegistrationCountry = dr["外國企業註冊地國"].ToString(); row.Address = dr["住址"].ToString(); row.UniformNumberProfitBusiness = dr["營利事業統一編號"].ToString(); row.Chairman = dr["董事長"].ToString(); outModel.gridList.Add(row); } } } // 輸出json return Json(outModel); } /// <summary> /// 解析 CSV /// </summary> /// <param name="result"></param> /// <param name="data"></param> private void ParseCSVData(ArrayList result, string data) { int position = -1; while (position < data.Length) result.Add(ParseCSVField(ref data, ref position)); } /// <summary> /// 解析 CSV /// </summary> /// <param name="data"></param> /// <param name="StartSeperatorPos"></param> /// <returns></returns> private string ParseCSVField(ref string data, ref int StartSeperatorPos) { if (StartSeperatorPos == data.Length - 1) { StartSeperatorPos++; return ""; } int fromPos = StartSeperatorPos + 1; if (data[fromPos] == '"') { int nextSingleQuote = GetSingleQuote(data, fromPos + 1); StartSeperatorPos = nextSingleQuote + 1; string tempString = data.Substring(fromPos + 1, nextSingleQuote - fromPos - 1); tempString = tempString.Replace("'", "''"); return tempString.Replace("\"\"", "\""); } int nextComma = data.IndexOf(',', fromPos); if (nextComma == -1) { StartSeperatorPos = data.Length; return data.Substring(fromPos); } else { StartSeperatorPos = nextComma; return data.Substring(fromPos, nextComma - fromPos); } } /// <summary> /// 解析 CSV /// </summary> /// <param name="data"></param> /// <param name="SFrom"></param> /// <returns></returns> private int GetSingleQuote(string data, int SFrom) { int i = SFrom - 1; while (++i < data.Length) if (data[i] == '"') { if (i < data.Length - 1 && data[i + 1] == '"') { i++; continue; } else return i; } return -1; } |
C# 後端 Model 語法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class GetDataIn { public string Q_MARKET_TYPE { get; set; } } public class GetDataOut { public string ErrMsg { get; set; } public List<StockRow> gridList { get; set; } } public class StockRow { // 只示範前面幾個欄位 public string CompanyCode { get; set; } public string CompanyName { get; set; } public string CompanyAbbreviation { get; set; } public string IndustryCategory { get; set; } public string ForeignCompanyRegistrationCountry { get; set; } public string Address { get; set; } public string UniformNumberProfitBusiness { get; set; } public string Chairman { get; set; } public string GeneralManage { get; set; } } |
例外處理
以上語法在 Controller 執行 webClient.DownloadData()
語法時會出現錯誤
System.Net.WebException: ‘伺服器認可通訊協定違規. Section=ResponseStatusLine’
此錯誤問題是 WebClient 是對 HttpWebrequest 進行了封裝,
The server committed a protocol violation. Section=ResponseHeader Detail=CR must be followed by LF 微軟沒有容忍不符合 RFC 822 中的 httpHeader 必須以 CRLF 結束的規定服務器響應。
解決方法是 Web.config 增加一段設定
1 2 3 4 5 |
<system.net> <settings> <httpWebRequest useUnsafeHeaderParsing="true" /> </settings> </system.net> |
測試注意事項
此範例的資料來源是公開資訊觀測站,而公開資訊觀測站會避免使用者密集頻繁的讀取資料,若發現此情況,則會立即封鎖 IP,導致使用者無法再使用網站,這是為了安全著想,避免網站因 DoS (denial-of-service attack) 被攻擊。
若太密集頻繁查詢而被封鎖 IP,我測試時就發生了一次,實際封鎖時間不知道,但隔一天後就可以正常使用了,所以提醒大家不要太密集頻繁的向公開資訊觀測站查詢資料,包含證交所也是,太密集頻繁查詢也是會被封鎖 IP 的。
重點整理
- 利用程式模擬網頁下載 CSV 資料
- 使用 WebClient 取得網頁原始碼
- 使用 HtmlAgilityPack 解析原始碼
- 解析 CSV 取出有用資料
範例下載
實際網頁專案開發範例
此連結是我實際應用在網頁上呈現的範例: Winvest 雲投資
相關學習文章
開發應用網站
如果你在學習上有不懂的地方,需要諮詢服務,可以參考站長服務,我想辨法解決你的問題
如果文章內容有過時、不適用或錯誤的地方,幫我在下方留言通知我一下,謝謝
請問這樣算爬蟲嗎?
這樣算是啊