Skip to main content

VS2015 C# 讀取 JSON 格式的方法

VS2015 C# 讀取 JSON 格式的方法

VS2015 C# 讀取 JSON 格式的方法

賴岱佑

今年已是 2022 年,但仍然有單位還在使用 VS2015 做開發,但 VS2015 C# 並不直接支援 JSON 格式,因此撰寫這篇範例,讓有需要的人可以從中獲益。

首先,要讓 VS2015 能夠讀取 JSON 格式,就必須要依靠第三方套件,Newtonsoft.Json,安裝方式請參考圖示順序。

image.png

到政府的「氣象資料開放平台」註冊,連結到 https://opendata.cwb.gov.tw/dataset/observation/O-A0001-001 這個網址,下載 JSON 格式的檔案。

image.png

連結到 https://jsoneditoronline.org/ ,上傳從氣象開放平台得到的檔案,在 Transform 按下右方箭頭的按鈕,轉換成 Tree 來看。

image.png

這讓我們知道在 cwbopendata 底下有一些敘述欄位,而後每一站都在 location 底下, location 底下還有 weather 和 parameter 。數目有多少都可以看得一清二楚,如此一來就可以校驗自己剖析得對不對。

切換到 Visual Studio 2015 ,新增專案,使用 Windows Forms Application 建立專案,在這裡取名為 ReadJSON ,讀者可以自行取名,不影響程式開發。

image.png

接著要引用以下的套件:

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... 。

image.png

選擇 Service-based Database ,設定好資料庫的名字,在這範例是改為 DB1.mdf 。最後按下 Add 即可。

image.png

在 Server Explorer 選單中,應該可以看到 db1.mdf ,若無則請自行新增,按上方的圓柱綠色電線鈕,就可以新增。
點開 db1.mdf ,滑鼠右鍵點選 Tables,點選 Add New Table 新增資料表。

image.png

以下式資料表 Header、Weather 的設計。我也有附 SQL 檔案,名叫 dbo.Header.sql、dbo.Weather.sql 在 __Document 的目錄內。

image.png

image.png

此時,我們要連接資料庫就需要連接字串,也是寫在全域變數那邊。

    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