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

Popular posts from this blog

Python 日期與時間的處理

Visual Basic 6.0 (VB6) 程式語言案例學習 (10. 條碼列印程式)

寫作:波蘭文學習之旅:1-1. 波蘭文字母與發音(注音版)

Python 日期與時間的處理

Image

Visual Basic 6.0 (VB6) 程式語言案例學習 (10. 條碼列印程式)

Image

寫作:波蘭文學習之旅:1-1. 波蘭文字母與發音(注音版)

Image

數位影像處理:最佳化處理策略之快速消除扭曲演算法

Image

Visual Basic .Net (VB.Net) 程式語言案例學習 (06. 題庫測驗系統)

Image

用10種程式語言做影像二值化(Image binarization)

Visual Basic 6.0 (VB6) 程式語言案例學習 (04. 人事考勤管理系統)

Image

修復損毀的 SQLite DB 資料庫

Image

Visual Basic 6.0 (VB6) 程式語言案例學習 (07. 收據列印程式)

Image

Visual Basic .Net (VB.Net) 程式語言案例學習 (03. 場地預約系統)

Image