VS2015 C# 讀取 JSON 格式的方法
今年已是 2022 年,但仍然有單位還在使用 VS2015 做開發,但 VS2015 C# 並不直接支援 JSON 格式,因此撰寫這篇範例,讓有需要的人可以從中獲益。
首先,要讓 VS2015 能夠讀取 JSON 格式,就必須要依靠第三方套件,Newtonsoft.Json,安裝方式請參考圖示順序。
到政府的「氣象資料開放平台」註冊,連結到 https://opendata.cwb.gov.tw/dataset/observation/O-A0001-001 這個網址,下載 JSON 格式的檔案。
連結到 https://jsoneditoronline.org/ ,上傳從氣象開放平台得到的檔案,在 Transform 按下右方箭頭的按鈕,轉換成 Tree 來看。
這讓我們知道在 cwbopendata 底下有一些敘述欄位,而後每一站都在 location 底下, location 底下還有 weather 和 parameter 。數目有多少都可以看得一清二楚,如此一來就可以校驗自己剖析得對不對。
切換到 Visual Studio 2015 ,新增專案,使用 Windows Forms Application 建立專案,在這裡取名為 ReadJSON ,讀者可以自行取名,不影響程式開發。
接著要引用以下的套件:
using Newtonsoft.Json; using System.IO; using Newtonsoft.Json.Linq; using System.Data.SqlClient;
為了讓整個 Form 都能讀到唯一識別碼 identifier ,在這裡建立全域變數,把 氣象資訊的檔頭 內容都建立起來。
public partial class Form1 : Form { private String cwbopendata_xmlns; private String cwbopendata_identifier; private String cwbopendata_sender; private String cwbopendata_sent; private String cwbopendata_status; private String cwbopendata_msgType; private String cwbopendata_dataid; private String cwbopendata_scope; private String cwbopendata_dataset;
接下來先建立資料庫,將讀到的 JSON 資訊儲存在資料庫當中,以便能夠不必每次都要開 JSON 的檔案。
在專案名稱上,按下滑鼠右鍵,出現表單,選擇表單上的 Add ,點選 New Item... 。
選擇 Service-based Database ,設定好資料庫的名字,在這範例是改為 DB1.mdf 。最後按下 Add 即可。
在 Server Explorer 選單中,應該可以看到 db1.mdf ,若無則請自行新增,按上方的圓柱綠色電線鈕,就可以新增。
點開 db1.mdf ,滑鼠右鍵點選 Tables,點選 Add New Table 新增資料表。
以下式資料表 Header、Weather 的設計。我也有附 SQL 檔案,名叫 dbo.Header.sql、dbo.Weather.sql 在 __Document
的目錄內。
此時,我們要連接資料庫就需要連接字串,也是寫在全域變數那邊。
public partial class Form1 : Form { # 這裡是 JSON 檔頭的變數,省略 private string strConn = @"Data Source=(LocalDB)\MSSQLLocalDB;" + "AttachDbFilename=|DataDirectory|DB1.mdf;" + "Integrated Security=True;";
我們讓程式一開始就處理檔頭「ProcessHeader()」,而後處理氣象資訊「ProcessWeather()」。
private void Form1_Load(object sender, EventArgs e) { ProcessHeader(); ProcessWeather(); }
ProcessHeader¶
在 ProcessHeader 有一個預設初始值的參數 ofFile ,它的初始值是 O-A0001-001.json ,這也是我們下載氣象資訊的檔名。因此,要讓程式能夠執行下去,有兩個條件。第一, O-A0001-001.json 這個檔案存在於執行檔的目錄之中。第二,利用開啟其他氣象檔案,載入資訊,開檔之後會提到。
File.Exists(FilePath) 會檢查檔案是否存在,若不存在就直接返回。若存在就執行開檔的動作, FileStream 會建立起與檔案的連結, StreamReader 會讀取檔案的內容。JsonTextReader 會以 JSON 格式剖析文字檔。
JObject jo = (JObject)JToken.ReadFrom(reader); 會建立起 JSON 的物件,讓我們可以方便使用取得 JSON 資訊。
例如:cwbopendata_xmlns = jo["cwopendata"]["@xmlns"].ToString(); ["cwopendata"] 是根節點,["@xmlns"] 是下一層子節點,ToString() 可以把讀到的資料轉換為字串回傳給 cwopendata_xmlns 。
最後記得有開就有關,讀取完資料後,就可以關閉開啟過的物件。
private void ProcessHeader(String ofFile = "O-A0001-001.json") { String FilePath = ofFile; if (File.Exists(FilePath)) { FileStream fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read); StreamReader file = new StreamReader(fs, System.Text.Encoding.UTF8); JsonTextReader reader = new JsonTextReader(file); JObject jo = (JObject)JToken.ReadFrom(reader); cwbopendata_xmlns = jo["cwbopendata"]["@xmlns"].ToString(); cwbopendata_identifier = jo["cwbopendata"]["identifier"].ToString(); cwbopendata_sender = jo["cwbopendata"]["sender"].ToString(); cwbopendata_sent = jo["cwbopendata"]["sent"].ToString(); cwbopendata_status = jo["cwbopendata"]["status"].ToString(); cwbopendata_msgType = jo["cwbopendata"]["msgType"].ToString(); cwbopendata_dataid = jo["cwbopendata"]["dataid"].ToString(); cwbopendata_scope = jo["cwbopendata"]["scope"].ToString(); cwbopendata_dataset = jo["cwbopendata"]["dataset"].ToString(); reader.Close(); file.Close(); fs.Close(); } else { return; }
在這裡使用 using 開啟資料庫連接,是因為只要出了 using 的範圍,就會自動把資料庫關閉,避免忘記將資料庫關閉的問題。
接著設定資料庫連線字串 cn.ConnectionString = strConn; 打開資料庫 cn.Open() ,若資料庫狀態為開啟,cn.State == ConnectionState.Open 就會等於 true 。
設定 SQL 語法,這裡是查詢是否有此識別碼的檔頭資訊。建立 DataTable 好當其他元件的 DataSource,使用 SqlDataAdapter 執行資料庫查詢,將結果填寫到 DataTable dt 內,用 daHeader.Fill(dt); 。
cbbIdentifier 是文字下拉式選單,將 dt 設定為 DataSource , 顯示欄位為 DisplayMember = "identifier" 。
這樣下拉式選單就會有這一筆的唯一識別碼,可供選擇。
using (SqlConnection cn = new SqlConnection()) { cn.ConnectionString = strConn; cn.Open(); if (cn.State == ConnectionState.Open) { String strSQL_Header = "SELECT * FROM Header WHERE identifier = '" + cwbopendata_identifier + "'"; DataTable dt = new DataTable(); SqlDataAdapter daHeader = new SqlDataAdapter(strSQL_Header, cn); daHeader.Fill(dt); cbbIdentifier.DataSource = dt; cbbIdentifier.DisplayMember = "identifier";
如果這一筆的唯一識別碼是從未見過的,那資料筆數就會是零, dt.Rows.Count 等於零,因此我們就來新增這一筆資料的檔頭。
使用 SQL 語法新增資料是 INSERT INTO 表單名稱為 Header ,括號內是欄位名稱, values 關鍵字之後的括號內是要寫入的資訊。
因為要寫入的資訊連接成字串會很長,可讀性也不高。我們採用 SqlCommand 的方式將寫入資訊帶入欄位中。
SqlCommand cmd = new SqlCommand(strSQL, cn); 建立起命令。而帶入資訊的資料都以 @ 符號為起始名稱。
接下來,cmd.Parameters.AddWithValue("@xmlns", cwbopendata_xmlns); 以參數的方式把資料帶入到 @xmlns 裡面去。
最後,執行 cmd.ExecuteNonQuery() 命令,將資料插入到資料庫當中。
執行 daHeader.Fill(dt); 更新資料。
if (dt.Rows.Count == 0) { String strSQL = "INSERT INTO Header(xmlns,identifier,sender,sent,status,msgType,dataid,scope,dataset) " + "values(@xmlns,@identifier,@sender,@sent,@status,@msgType,@dataid,@scope,@dataset)"; SqlCommand cmd = new SqlCommand(strSQL, cn); cmd.Parameters.AddWithValue("@xmlns", cwbopendata_xmlns); cmd.Parameters.AddWithValue("@identifier", cwbopendata_identifier); cmd.Parameters.AddWithValue("@sender", cwbopendata_sender); cmd.Parameters.AddWithValue("@sent", cwbopendata_sent); cmd.Parameters.AddWithValue("@status", cwbopendata_status); cmd.Parameters.AddWithValue("@msgType", cwbopendata_msgType); cmd.Parameters.AddWithValue("@dataid", cwbopendata_dataid); cmd.Parameters.AddWithValue("@scope", cwbopendata_scope); cmd.Parameters.AddWithValue("@dataset", cwbopendata_dataset); cmd.ExecuteNonQuery(); daHeader.Fill(dt); }
這裡是預設第一筆資料的檔頭資訊,填入到表單上的文字框。
textBoxXmlns.Text = dt.Rows[0]["xmlns"].ToString(); //cwbopendata_xmlns; //cbbIdentifier.Text = dt.Rows[0]["identifier"].ToString(); //cwbopendata_identifier; textBoxSender.Text = dt.Rows[0]["sender"].ToString(); //cwbopendata_sender; textBoxSent.Text = dt.Rows[0]["sent"].ToString(); //cwbopendata_sent; textBoxStatus.Text = dt.Rows[0]["status"].ToString(); //cwbopendata_status; textBoxMsgType.Text = dt.Rows[0]["msgType"].ToString(); //cwbopendata_msgType; textBoxDataid.Text = dt.Rows[0]["dataid"].ToString(); //cwbopendata_dataid; textBoxScope.Text = dt.Rows[0]["scope"].ToString(); //cwbopendata_scope; textBoxDataset.Text = dt.Rows[0]["dataset"].ToString(); //cwbopendata_dataset; } }
在這裡是重新連結資料庫,因為檔頭資料表 Header ,不一定只有一筆資料,所以重新查詢,也順便設定唯一識別碼的下拉式選單內容。
using (SqlConnection cn = new SqlConnection()) { cn.ConnectionString = strConn; cn.Open(); if (cn.State == ConnectionState.Open) { String strSQL_Header = "SELECT * FROM Header"; DataTable dt = new DataTable(); SqlDataAdapter daHeader = new SqlDataAdapter(strSQL_Header, cn); daHeader.Fill(dt); cbbIdentifier.DataSource = dt; cbbIdentifier.DisplayMember = "identifier"; } } }
這裡的概念跟 ProcessHeader 一樣,首先參數有個預設檔名 O-A0001-001.json 。
File.Exists(FilePath) 會檢查檔名是否存在,不存在則跳回。若存在就開始建立資料庫連接,由於 ProcessHeader 假設已經幫我們把唯一識別碼填寫在文字框了,因此我們就直接查詢文字框的唯一識別碼是否存在資料。
private void ProcessWeather(String ofFile = "O-A0001-001.json") { String FilePath = ofFile; if (File.Exists(FilePath)) { using (SqlConnection cn = new SqlConnection()) { cn.ConnectionString = strConn; cn.Open(); if (cn.State == ConnectionState.Open) { DataTable dt = new DataTable(); String strWeather = "SELECT * FROM Weather WHERE identifier = '" + cbbIdentifier.Text + "'"; SqlDataAdapter daWeather = new SqlDataAdapter(strWeather, cn); daWeather.Fill(dt);
若查無資料,則 dt.Rows.Count 值為零,那代表我們要將從 JSON 讀到的資訊新增到資料庫當中。
首先我們先設定語系格式,在這裡我們設定為 "en-US" ,代表是要用美式的格式。
接著透過 StreamReader 讀取 JSON 檔案,格式設定為 UTF8 。
StreamReader 透過 ReadToEnd() 方法,一次把 JSON 文字讀到 JSON_Content 內。之後就關閉檔案連結了。
現在用 JObject.Parse(JSON_Content) 剖析文字為 JSON 物件 objJObject。
並且建立氣象站的清單, JToken locationList = objJObject.SelectToken("cwbopendata.location");
為了計數有多少筆資料,我們初始化了一個計數的變數 lCount 。
if (dt.Rows.Count == 0) { System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.CreateSpecificCulture("en-US"); System.IO.StreamReader objStreamReader = new System.IO.StreamReader(FilePath, System.Text.Encoding.UTF8); String JSON_Content = objStreamReader.ReadToEnd(); objStreamReader.Close(); JObject objJObject = JObject.Parse(JSON_Content); JToken locationList = objJObject.SelectToken("cwbopendata.location"); long lCount = 0;
我們用 foreach 巡覽整個氣象站的清單,每個氣象站有氣象站的資訊。除此之外,還有 weatherElement、Parameter 兩個子清單。
先計數氣象站的資料筆數,用 lCount++;
要讀取氣象站的資料,用 location.SelectToken("屬性名稱").Value().ToString();,這樣就讀取了該屬性名稱的內容,並且是字串格式。
如果屬性內還有一個屬性,就像 location.SelectToken("time").Value("obsTime");,這樣就讀到了 obsTime 的內容,原本就是字串格式。
接下來要處理 weatherElement ,我們也要建立 weatherElement 的清單。就像建立 loationList 一樣。
JToken weatherList = location.SelectToken("weatherElement"); 就這樣建立起 weatherElement 的清單。
foreach (JToken location in locationList) { lCount++; String lat = location.SelectToken("lat").Value().ToString(); String lon = location.SelectToken("lon").Value ().ToString(); String lat_wgs84 = location.SelectToken("lat_wgs84").Value ().ToString(); String lon_wgs84 = location.SelectToken("lon_wgs84").Value ().ToString(); String locationName = location.SelectToken("locationName").Value (); String stationId = location.SelectToken("stationId").Value (); String obsTime = location.SelectToken("time").Value ("obsTime"); String strWeatherElement = ""; String strWeatherElementValue = ""; JToken weatherList = location.SelectToken("weatherElement");
這時候我們建立氣象清單中的欄位名稱字串 strWeatherElement ,用巡覽的方式建立。
而後建立數值清單的時候,我們遇到了有些欄位是日期格式,因此先檢查讀到的字串有沒有小於 8 碼,若少於 8 個字則直接加入到字串中,也就是我們的氣象數值字串 strWeatherElementValue 。若大於 8 碼,那就判定為日期字串,我們就在日期前後加上單引號。
foreach (JToken weather in weatherList) { strWeatherElement += weather.SelectToken("elementName").Value() + ","; if (weather.SelectToken("elementValue").Value ("value").ToString().Length < 8) { strWeatherElementValue += weather.SelectToken("elementValue").Value ("value").ToString() + ","; } else { strWeatherElementValue += "'" + weather.SelectToken("elementValue").Value ("value").ToString() + "',"; } }
到了 Parameter ,strParameterElement 是紀錄欄位名稱,strParameterElementValue 是字串陣列,是紀錄欄位內容值。
String strParameterElement = ""; String[] strParameterElementValue = new String[4]; int idx = 0; JToken parameterList = location.SelectToken("parameter"); foreach (JToken parameter in parameterList) { strParameterElement += parameter.SelectToken("parameterName").Value() + ","; strParameterElementValue[idx++] = parameter.SelectToken("parameterValue").Value (); } strParameterElement = strParameterElement.Substring(0, strParameterElement.Length - 1);
在這裡跟 Header 新增資料的方式雷同,strWeatherElement、strParameterElement 這兩個字串增加了氣象資訊和地理位置的欄位,而 strWeatherElementValue 則增加了氣象資訊的數據。
strParameterElementValue[] 字串陣列增加了四個地理位置的資訊。
執行 cmd.ExeuteNonQuery() 就新增一筆資料到 weather 資料表中了。
String strSQL = "INSERT INTO Weather(identifier,lat,lon,lat_wgs84,lon_wgs84,locationName," + "stationId,obsTime," + strWeatherElement + strParameterElement + ") " + "values(@identifier,@lat,@lon,@lat_wgs84,@lon_wgs84,@locationName," + "@stationId,@obsTime," + strWeatherElementValue + "@CITY,@CITY_SN,@TOWN,@TOWN_SN)"; SqlCommand cmd = new SqlCommand(strSQL, cn); cmd.Parameters.AddWithValue("@identifier", cbbIdentifier.Text); cmd.Parameters.AddWithValue("@lat", lat); cmd.Parameters.AddWithValue("@lon", lon); cmd.Parameters.AddWithValue("@lat_wgs84", lat_wgs84); cmd.Parameters.AddWithValue("@lon_wgs84", lon_wgs84); cmd.Parameters.AddWithValue("@locationName", locationName); cmd.Parameters.AddWithValue("@stationId", stationId); cmd.Parameters.AddWithValue("@obsTime", obsTime); cmd.Parameters.AddWithValue("@CITY", strParameterElementValue[0]); cmd.Parameters.AddWithValue("@CITY_SN", strParameterElementValue[1]); cmd.Parameters.AddWithValue("@TOWN", strParameterElementValue[2]); cmd.Parameters.AddWithValue("@TOWN_SN", strParameterElementValue[3]); cmd.ExecuteNonQuery(); }
新增完畢資料後,daWeather.Fill(dt); 更新一下資料,計數文字框也要更新資料 lCount.ToString(),顯示記錄新增成功的訊息。
若一開始就有資料,則不必新增資料,直接更新計數文字框為 dt.Rows.Count.ToString(); 。
程式區塊結束之前,要重新顯示 Header、Weather 的資料,所以會執行 DisplayHeader()、DisplayWeather()。
若連開檔都失敗,則返回 return 。
daWeather.Fill(dt); textBoxRowCount.Text = lCount.ToString(); MessageBox.Show("Record Inserted Successfully"); } else { textBoxRowCount.Text = dt.Rows.Count.ToString(); } } } DisplayHeader(); DisplayWeather(); } else { return; } }
DisplayHeader() 就是連接資料庫重新讀取唯一識別碼相對應的相關資訊。
private void DisplayHeader() { using (SqlConnection cn = new SqlConnection()) { cn.ConnectionString = strConn; cn.Open(); if (cn.State == ConnectionState.Open) { DataTable dt = new DataTable(); String strWeather = "SELECT * FROM Header WHERE identifier = '" + cbbIdentifier.Text + "'"; SqlDataAdapter daWeather = new SqlDataAdapter(strWeather, cn); daWeather.Fill(dt); textBoxXmlns.Text = dt.Rows[0]["xmlns"].ToString(); //cwbopendata_xmlns; //cbbIdentifier.Text = dt.Rows[0]["identifier"].ToString(); //cwbopendata_identifier; textBoxSender.Text = dt.Rows[0]["sender"].ToString(); //cwbopendata_sender; textBoxSent.Text = dt.Rows[0]["sent"].ToString(); //cwbopendata_sent; textBoxStatus.Text = dt.Rows[0]["status"].ToString(); //cwbopendata_status; textBoxMsgType.Text = dt.Rows[0]["msgType"].ToString(); //cwbopendata_msgType; textBoxDataid.Text = dt.Rows[0]["dataid"].ToString(); //cwbopendata_dataid; textBoxScope.Text = dt.Rows[0]["scope"].ToString(); //cwbopendata_scope; textBoxDataset.Text = dt.Rows[0]["dataset"].ToString(); //cwbopendata_dataset; } } }
DisplayWeather() 就是重新連接資料庫,並以唯一識別碼讀取氣象站的資料,以 locationName 升序排序, CITY 降序排序。
其中 DataGridView gdvWeather 有 ReadOnly 的屬性,由 cbxReadOnly.Checked 屬性來切換是否可以編輯,還是唯讀模式。
private void DisplayWeather() { using (SqlConnection cn = new SqlConnection()) { cn.ConnectionString = strConn; cn.Open(); if (cn.State == ConnectionState.Open) { DataTable dt = new DataTable(); String strWeather = "SELECT * FROM Weather WHERE identifier = '" + cbbIdentifier.Text + "' " + " ORDER BY locationName ASC, CITY DESC"; SqlDataAdapter daWeather = new SqlDataAdapter(strWeather, cn); daWeather.Fill(dt); dgvWeather.DataSource = dt; dgvWeather.Columns[0].Visible = false; dgvWeather.Columns[1].Visible = false; dgvWeather.ReadOnly = cbxReadOnly.Checked; textBoxRowCount.Text = dt.Rows.Count.ToString(); } } }
在這裡用 OpenFileDialog ofdWeather.ShowDialog 開啟舊檔,如果回傳 DialogResult.OK ,那就將檔名傳遞給 ProcessHeader、ProcessWeather,等資料都處理好了。就顯示資料,用 DisplayHeader()、DisplayWeather()。
private void btnOpenFile_Click(object sender, EventArgs e) { if (ofdWeather.ShowDialog() == DialogResult.OK) { ProcessHeader(ofdWeather.FileName); ProcessWeather(ofdWeather.FileName); DisplayHeader(); DisplayWeather(); } }
唯一識別碼下拉式選單被切換,唯一識別碼會變更,這時候就要重新顯示新的資料,呼叫 DisplayHeader()、DisplayWeather()。
private void cbbIdentifier_SelectedIndexChanged(object sender, EventArgs e) { DisplayHeader(); DisplayWeather(); }
當核取框有變動的時候,會將值傳遞給 DataGridView dgvWeather.ReadOnly,這樣就可以用核取框控制是否允許唯讀或編輯。
private void cbxReadOnly_CheckedChanged(object sender, EventArgs e) { dgvWeather.ReadOnly = cbxReadOnly.Checked; } } }
Comments
Post a Comment