2025年8月10日 星期日

Python 網路爬蟲 空氣品質指標 ( AQI ) --python TKinter

Python 網路爬蟲 空氣品質指標 ( AQI )  --python TKinter

從政府資料開放平臺裡,搜尋「空氣品質指標」,就能開啟空氣品質指標 ( AQI ) 的資料頁面,資料有提供 JSON 格式與 CSV 格式,這個範例中會使用 JSON 格式,點擊對應的按鈕,就可以開啟對應的 JSON API 內容。

Python 教學 - 爬取空氣品質指標 ( AQI ) - 關於空氣品質指標 ( AQI )

檢視資料
資料資源欄位
sitename(測站名稱)、county(縣市)、aqi(空氣品質指標)、pollutant(空氣污染指標物)、status(狀態)、so2(二氧化硫(ppb))、co(一氧化碳(ppm))、o3(臭氧(ppb))、o3_8hr(臭氧8小時移動平均(ppb))、pm10(懸浮微粒(μg/m3))、pm2.5(細懸浮微粒(μg/m3))、no2(二氧化氮(ppb))、nox(氮氧化物(ppb))、no(一氧化氮(ppb))、wind_speed(風速(m/sec))、wind_direc(風向(degrees))、publishtime(資料發布時間)、co_8hr(一氧化碳8小時移動平均(ppm))、pm2.5_avg(細懸浮微粒移動平均值(μg/m3))、pm10_avg(懸浮微粒移動平均值(μg/m3))、so2_avg(二氧化硫移動平均值(ppb))、longitude(經度)、latitude(緯度)、siteid(測站編號)
檔案格式
JSON
編碼格式
UTF-8
資料量
資料資源描述
空氣品質指標(AQI)-JSON


{
    "fields": [
        {
            "id": "sitename",
            "type": "text",
            "info": {
                "label": "測站名稱"
            }
        },
        {
            "id": "county",
            "type": "text",
            "info": {
                "label": "縣市"
            }
        },
        {
            "id": "aqi",
            "type": "text",
            "info": {
                "label": "空氣品質指標"
            }
        },
        {
            "id": "pollutant",
            "type": "text",
            "info": {
                "label": "空氣污染指標物"
            }
        },
        {
            "id": "status",
            "type": "text",
            "info": {
                "label": "狀態"
            }
        },
        {
            "id": "so2",
            "type": "text",
            "info": {
                "label": "二氧化硫(ppb)"
            }
        },
        {
            "id": "co",
            "type": "text",
            "info": {
                "label": "一氧化碳(ppm)"
            }
        },
        {
            "id": "o3",
            "type": "text",
            "info": {
                "label": "臭氧(ppb)"
            }
        },
        {
            "id": "o3_8hr",
            "type": "text",
            "info": {
                "label": "臭氧8小時移動平均(ppb)"
            }
        },
        {
            "id": "pm10",
            "type": "text",
            "info": {
                "label": "懸浮微粒(μg\/m3)"
            }
        },
        {
            "id": "pm2.5",
            "type": "text",
            "info": {
                "label": "細懸浮微粒(μg\/m3)"
            }
        },
        {
            "id": "no2",
            "type": "text",
            "info": {
                "label": "二氧化氮(ppb)"
            }
        },
        {
            "id": "nox",
            "type": "text",
            "info": {
                "label": "氮氧化物(ppb)"
            }
        },
        {
            "id": "no",
            "type": "text",
            "info": {
                "label": "一氧化氮(ppb)"
            }
        },
        {
            "id": "wind_speed",
            "type": "text",
            "info": {
                "label": "風速(m\/sec)"
            }
        },
        {
            "id": "wind_direc",
            "type": "text",
            "info": {
                "label": "風向(degrees)"
            }
        },
        {
            "id": "publishtime",
            "type": "text",
            "info": {
                "label": "資料發布時間"
            }
        },
        {
            "id": "co_8hr",
            "type": "text",
            "info": {
                "label": "一氧化碳8小時移動平均(ppm)"
            }
        },
        {
            "id": "pm2.5_avg",
            "type": "text",
            "info": {
                "label": "細懸浮微粒移動平均值(μg\/m3)"
            }
        },
        {
            "id": "pm10_avg",
            "type": "text",
            "info": {
                "label": "懸浮微粒移動平均值(μg\/m3)"
            }
        },
        {
            "id": "so2_avg",
            "type": "text",
            "info": {
                "label": "二氧化硫移動平均值(ppb)"
            }
        },
        {
            "id": "longitude",
            "type": "text",
            "info": {
                "label": "經度"
            }
        },
        {
            "id": "latitude",
            "type": "text",
            "info": {
                "label": "緯度"
            }
        },
        {
            "id": "siteid",
            "type": "text",
            "info": {
                "label": "測站編號"
            }
        }
    ],
    "resource_id": "8d2f907f-bbb4-4fdf-8f08-8eabae15da45",
    "__extras": {
        "api_key": "9b651a1b-0732-418e-b4e9-e784417cadef"
    },
    "include_total": true,
    "total": "86",
    "resource_format": "object",
    "limit": "1000",
    "offset": "0",
    "_links": {
        "start": "\/api\/v2\/aqx_p_432?api_key=9b651a1b-0732-418e-b4e9-e784417cadef&limit=1000&format=JSON&sort=ImportDate desc",
        "next": "\/api\/v2\/aqx_p_432?api_key=9b651a1b-0732-418e-b4e9-e784417cadef&limit=1000&format=JSON&sort=ImportDate desc&offset=1000"
    },
    "records": [
        {
            "sitename": "基隆",
            "county": "基隆市",
            "aqi": "22",
            "pollutant": "",
            "status": "良好",
            "so2": "0.7",
            "co": "0.1",
            "o3": "23",
            "o3_8hr": "20",
            "pm10": "8",
            "pm2.5": "0",
            "no2": "1",
            "nox": "2.5",
            "no": "1.1",
            "wind_speed": "1.5",
            "wind_direc": "78",
            "publishtime": "2025\/08\/11 13:00:00",
            "co_8hr": "0.2",
            "pm2.5_avg": "3.2",
            "pm10_avg": "13",
            "so2_avg": "0",
            "longitude": "121.760056",
            "latitude": "25.129167",
            "siteid": "1"
        },
        {
            "sitename": "汐止",
            "county": "新北市",
            "aqi": "33",
            "pollutant": "",
            "status": "良好",
            "so2": "0.9",
            "co": "0.17",
            "o3": "45",
            "o3_8hr": "24",
            "pm10": "16",
            "pm2.5": "9",
            "no2": "8",
            "nox": "9",
            "no": "0.7",
            "wind_speed": "1.9",
            "wind_direc": "43",
            "publishtime": "2025\/08\/11 13:00:00",
            "co_8hr": "0.2",
            "pm2.5_avg": "8.2",
            "pm10_avg": "17",
            "so2_avg": "1",
            "longitude": "121.64081",
            "latitude": "25.06624",
            "siteid": "2"
        },
        {
            "sitename": "新店",
            "county": "新北市",
            "aqi": "28",
            "pollutant": "",
            "status": "良好",
            "so2": "0.8",
            "co": "0.24",
            "o3": "69",
            "o3_8hr": "30",
            "pm10": "22",
            "pm2.5": "11",
            "no2": "9",
            "nox": "9.8",
            "no": "0.7",
            "wind_speed": "1.1",
            "wind_direc": "250",
            "publishtime": "2025\/08\/11 13:00:00",
            "co_8hr": "0.2",
            "pm2.5_avg": "6",
            "pm10_avg": "17",
            "so2_avg": "1",
            "longitude": "121.537778",
            "latitude": "24.977222",
            "siteid": "4"
        },

<<Python TKinter>>

import tkinter as tk  # 導入 tkinter 庫,用於建立 GUI 介面

from tkinter import ttk, messagebox, filedialog  # 從 tkinter 導入 ttk(主題套件)、messagebox(訊息視窗)和 filedialog(檔案對話框)

import requests  # 導入 requests 庫,用於發送 HTTP 請求

import csv  # 導入 csv 庫,用於處理 CSV 檔案

import pandas as pd # 導入 pandas 庫,此程式未使用,但可用於數據處理


# --- 函數定義 ---


def fetch_data():

    """從環保署 API 擷取空氣品質資料"""

    # 定義環保署空氣品質 API 的 URL

    url = "https://data.moenv.gov.tw/api/v2/aqx_p_432?api_key=9b651a1b-0732-418e-b4e9-e784417cadef&limit=1000&sort=ImportDate desc&format=JSON"

    try:

        response = requests.get(url)  # 向 API 網址發送 GET 請求

        response.raise_for_status()  # 如果請求失敗(例如 404 或 500 錯誤),則引發 HTTPError

        data = response.json().get('records', [])  # 將 JSON 格式的回應轉換成 Python 字典,並取出 'records' 列表;如果 'records' 不存在,則回傳空列表

        

        # 過濾出需要的欄位

        filtered_data = [

            # 遍歷 data 列表中的每個字典(record),並取出 'county', 'sitename', 'aqi', 'status' 的值

            # 如果某個鍵不存在,則使用 'N/A' 作為預設值

            (record.get('county', 'N/A'), record.get('sitename', 'N/A'), record.get('aqi', 'N/A'), record.get('status', 'N/A'))

            for record in data

        ]

        return filtered_data  # 返回過濾後的資料列表

    except requests.exceptions.RequestException as e:

        # 如果請求過程中發生任何錯誤(如網路連線問題),則顯示錯誤訊息視窗

        messagebox.showerror("錯誤", f"無法連線到 API,請檢查網路連線或 API 狀態。\n錯誤訊息:{e}")

        return []  # 回傳空列表


def create_treeview():

    """建立並設定 Treeview 元件"""

    tree_frame = ttk.Frame(root)  # 建立一個 ttk.Frame 作為 Treeview 的容器

    tree_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)  # 將容器打包到主視窗中,設定邊距,並使其填滿可用空間


    columns = ('county', 'sitename', 'AQI', 'status')  # 定義 Treeview 的欄位名稱

    tree = ttk.Treeview(tree_frame, columns=columns, show='headings')  # 建立 Treeview 元件,並指定欄位和只顯示標題


    # 為每個欄位設定標題文字

    tree.heading('county', text='縣市')

    tree.heading('sitename', text='測站名稱')

    tree.heading('AQI', text='AQI')

    tree.heading('status', text='空氣品質狀態')


    # 設定每個欄位的寬度

    tree.column('county', width=100)

    tree.column('sitename', width=150)

    tree.column('AQI', width=80)

    tree.column('status', width=150)


    # 添加滾動條

    scrollbar = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=tree.yview)  # 建立垂直滾動條

    tree.configure(yscrollcommand=scrollbar.set)  # 將 Treeview 的 yscollcommand 與滾動條綁定

    scrollbar.pack(side=tk.RIGHT, fill=tk.Y)  # 將滾動條打包到右側,並填滿垂直空間

    tree.pack(fill=tk.BOTH, expand=True)  # 將 Treeview 打包,使其填滿並擴展


    return tree  # 回傳建立好的 Treeview 元件


def update_treeview():

    """獲取資料並更新 Treeview"""

    for item in tree.get_children():  # 遍歷 Treeview 中所有現有的項目

        tree.delete(item)  # 刪除每個項目,清空舊資料

    

    data = fetch_data()  # 呼叫 fetch_data 函數獲取最新資料

    if data:  # 檢查是否成功獲取到資料

        for row in data:  # 遍歷資料列表中的每一行

            tree.insert('', 'end', values=row)  # 將每一行資料插入到 Treeview 的末尾


def export_to_csv():

    """將 Treeview 中的資料匯出為 CSV 檔案"""

    # 獲取 Treeview 中所有可見的資料,以列表形式儲存

    data = [tree.item(item)['values'] for item in tree.get_children()]

    if not data:  # 如果沒有資料,則顯示警告視窗

        messagebox.showwarning("警告", "沒有資料可供匯出。")

        return

    

    # 打開檔案儲存對話框,讓使用者選擇儲存路徑和檔名

    file_path = filedialog.asksaveasfilename(

        defaultextension=".csv",  # 設定預設副檔名為 .csv

        filetypes=[("CSV files", "*.csv")],  # 設定檔案類型篩選

        title="儲存 CSV 檔案"  # 設定對話框標題

    )

    

    if file_path:  # 如果使用者選擇了路徑並按下儲存

        try:

            # 以寫入模式 'w'、newline=''(防止寫入多餘空行)、並以 'utf-8-sig' 編碼(支援中文並防止亂碼)開啟檔案

            with open(file_path, 'w', newline='', encoding='utf-8-sig') as f:

                writer = csv.writer(f)  # 建立 csv 寫入器

                writer.writerow(['縣市', '測站名稱', 'AQI', '空氣品質狀態'])  # 寫入 CSV 檔案的標題列

                writer.writerows(data)  # 將所有資料行寫入檔案

            messagebox.showinfo("成功", f"資料已成功儲存至 {file_path}")  # 顯示成功訊息

        except Exception as e:

            messagebox.showerror("錯誤", f"匯出檔案時發生錯誤:{e}")  # 如果寫入檔案時發生錯誤,顯示錯誤訊息


# --- 程式主體 ---


# 初始化主視窗

root = tk.Tk()  # 建立主視窗物件

root.title("台灣 AQI 空氣品質查詢")  # 設定視窗標題

root.geometry("600x500")  # 設定視窗的初始大小


# 建立按鈕框架

button_frame = ttk.Frame(root)  # 建立一個框架來放置按鈕

button_frame.pack(pady=5)  # 將框架打包,並在上方留有 5 像素的垂直邊距


# 建立更新按鈕

update_button = ttk.Button(button_frame, text="獲取資料並更新", command=update_treeview)  # 建立一個按鈕,點擊時執行 update_treeview 函數

update_button.pack(side=tk.LEFT, padx=5)  # 將按鈕打包到框架左側,並設定水平邊距


# 建立匯出按鈕

export_button = ttk.Button(button_frame, text="匯出為 CSV", command=export_to_csv)  # 建立一個按鈕,點擊時執行 export_to_csv 函數

export_button.pack(side=tk.LEFT, padx=5)  # 將按鈕打包到框架左側,並設定水平邊距


# 建立 Treeview 來顯示資料

tree = create_treeview()  # 呼叫 create_treeview 函數,並將返回的 Treeview 元件賦值給 tree


# 程式啟動時自動載入資料

update_treeview()  # 在程式啟動時自動呼叫一次,以便第一次打開時就顯示資料


# 啟動主迴圈

root.mainloop()  # 進入 Tkinter 的事件主迴圈,保持視窗運行並響應使用者操作


沒有留言:

張貼留言

8-QAM Signal 4 Phases 2 Amplitudes + 8PSK

 8-QAM Signal 4 Phases 2 Amplitudes + 8PSK import tkinter as tk from tkinter import messagebox import math import cmath # --- 8-QAM 參數設定 ---...