2025年7月29日 星期二

台灣銀行 牌告匯率--Python TKInter

台灣銀行 牌告匯率--Python TKInter 




import tkinter as tk

from tkinter import ttk, messagebox

import requests

from bs4 import BeautifulSoup

import datetime


def fetch_exchange_rates():

    """

    從台灣銀行網站抓取即時匯率數據。

    """

    url = "https://rate.bot.com.tw/xrt?Lang=zh-TW"

    headers = {

        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'

    } # 加上 User-Agent 模擬瀏覽器請求,避免被網站阻擋


    try:

        response = requests.get(url, headers=headers)

        response.raise_for_status()  # 檢查 HTTP 請求是否成功

        soup = BeautifulSoup(response.text, 'html.parser')


        # 找到匯率表格

        exchange_table = soup.find('table', class_='table-bordered')


        if not exchange_table:

            messagebox.showerror("解析錯誤", "無法找到匯率表格。網站結構可能已改變。")

            return None


        rates_data = []

        # 遍歷表格的每一行 (從第二行開始,因為第一行是表頭)

        # 注意:使用 tbody 可以確保我們只處理數據行

        for row in exchange_table.find('tbody').find_all('tr'):

            cols = row.find_all('td')

            if len(cols) >= 6: # 確保有足夠的欄位來包含所有匯率類型 (通常是 6 個 td)

                currency_name = cols[0].find('div', class_='print_show').text.strip()

                # 移除幣別名稱中的換行符和多餘空格

                currency_name = currency_name.replace('\n', ' ').strip()

                currency_name = ' '.join(currency_name.split()) # 將多個空格替換為單一空格


                # 現金匯率

                cash_buy_rate = cols[1].text.strip() # 現金買入

                cash_sell_rate = cols[2].text.strip() # 現金賣出


                # 即期匯率

                spot_buy_rate = cols[3].text.strip() # 即期買入

                spot_sell_rate = cols[4].text.strip() # 即期賣出


                # 過濾掉沒有匯率的項目 (例如 "-" )

                if (cash_buy_rate != '-' and cash_sell_rate != '-') or \

                   (spot_buy_rate != '-' and spot_sell_rate != '-'):

                    rates_data.append({

                        'currency': currency_name,

                        'cash_buy': cash_buy_rate,

                        'cash_sell': cash_sell_rate,

                        'spot_buy': spot_buy_rate,

                        'spot_sell': spot_sell_rate

                    })

        return rates_data


    except requests.exceptions.RequestException as e:

        messagebox.showerror("網路錯誤", f"無法連接到台灣銀行網站:{e}")

        return None

    except Exception as e:

        messagebox.showerror("擷取錯誤", f"處理網頁資料時發生錯誤:{e}")

        return None


def update_exchange_display():

    """

    擷取匯率數據並更新 Tkinter 介面。

    """

    current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    status_label.config(text=f"最後更新時間: {current_time} - 擷取中...")

    root.update_idletasks() # 強制更新 GUI 以顯示訊息


    rates = fetch_exchange_rates()

    if rates:

        # 清除舊數據

        for item in tree.get_children():

            tree.delete(item)


        # 插入新數據

        for rate in rates:

            tree.insert('', tk.END, values=(

                rate['currency'],

                rate['cash_buy'],

                rate['cash_sell'],

                rate['spot_buy'],

                rate['spot_sell']

            ))

        status_label.config(text=f"最後更新時間: {current_time} - 數據已更新")

    else:

        # 如果沒有抓到數據,顯示錯誤訊息

        for item in tree.get_children():

            tree.delete(item)

        tree.insert('', tk.END, values=("未能載入匯率資料,請檢查網路或網站結構", "", "", "", ""))

        status_label.config(text=f"最後更新時間: {current_time} - 數據載入失敗")

    

    # 每 5 分鐘自動更新 (300000 毫秒)

    root.after(300000, update_exchange_display)



# --- Tkinter 介面設定 ---

root = tk.Tk()

root.title("台灣銀行即時牌告匯率")

root.geometry("800x750") # 調整視窗大小以容納更多欄位


# 框架用於容納 Treeview 和捲軸

frame = tk.Frame(root, padx=10, pady=10)

frame.pack(expand=True, fill="both")


# 創建 Treeview 來顯示表格數據

columns = ('currency', 'cash_buy', 'cash_sell', 'spot_buy', 'spot_sell')

tree = ttk.Treeview(frame, columns=columns, show='headings')


# 設定欄位標題

tree.heading('currency', text='幣別')

tree.heading('cash_buy', text='現金買入')

tree.heading('cash_sell', text='現金賣出')

tree.heading('spot_buy', text='即期買入')

tree.heading('spot_sell', text='即期賣出')


# 設定欄位寬度

tree.column('currency', width=150, anchor='center')

tree.column('cash_buy', width=120, anchor='center')

tree.column('cash_sell', width=120, anchor='center')

tree.column('spot_buy', width=120, anchor='center')

tree.column('spot_sell', width=120, anchor='center')


# 添加捲軸

scrollbar = ttk.Scrollbar(frame, orient="vertical", command=tree.yview)

tree.configure(yscrollcommand=scrollbar.set)

scrollbar.pack(side="right", fill="y")


tree.pack(expand=True, fill="both")


# 狀態標籤,顯示最後更新時間和狀態

status_label = tk.Label(root, text="準備載入匯率...", font=("Arial", 10), anchor="w")

status_label.pack(side="bottom", fill="x", padx=10, pady=5)


# 更新按鈕

update_button = tk.Button(root, text="手動更新匯率", command=update_exchange_display, font=("Arial", 12))

update_button.pack(pady=10)


# 初始化顯示匯率

update_exchange_display()


root.mainloop()


一般天氣預報-今明36小時天氣預報---Python TKInter

一般天氣預報-今明36小時天氣預報---Python TKInter









Python


import tkinter as tk
from tkinter import messagebox
import requests
import json

# 全局變數用於儲存所有縣市的原始天氣資料,避免重複下載
ALL_WEATHER_DATA = None

def fetch_weather_data():
    """從中央氣象署開放資料平台擷取天氣資料"""
    # 替換成你自己的 API 金鑰
    # (此處省略 URL,請確保您的程式碼中有正確的 URL 和 API 金鑰)
    url = "https://opendata.cwa.gov.tw/fileapi/v1/opendataapi/F-C0032-001?Authorization=YOUR_ACTUAL_API_KEY&format=JSON" # 請務必替換為您的金鑰
    try:
        response = requests.get(url)
        response.raise_for_status()  # 如果請求不成功,拋出 HTTPError
        data = response.json()
        return data
    except requests.exceptions.RequestException as e:
        messagebox.showerror("網路錯誤", f"無法連接到天氣資料服務:{e}")
        return None
    except requests.exceptions.JSONDecodeError as e:
        messagebox.showerror("資料解析錯誤", f"無法解析 API 回應為 JSON:{e}\n請檢查 API 金鑰或資料格式是否正確。")
        return None

def parse_and_filter_weather_data(data, target_location_name=None):
    """
    解析天氣資料並提取所需資訊。
    可選參數 target_location_name 用於只顯示特定縣市的資料。
    """
    if not data or "cwaopendata" not in data:
        return "資料格式錯誤或無資料,或頂層無 cwaopendata 鍵。"

    cwa_data = data["cwaopendata"]
    if "dataset" not in cwa_data:
        return "cwaopendata 內部無 dataset 鍵。"

    actual_dataset = cwa_data["dataset"]
    if "location" not in actual_dataset:
        return "dataset 內部無 location 鍵。"

    locations = actual_dataset["location"]
    weather_reports = []

    for loc in locations:
        location_name = loc.get("locationName", "未知地區")

        if target_location_name and location_name != target_location_name:
            continue

        weather_report_for_loc = f"地區:{location_name}\n"

        elements_by_time = {}
        for wp in loc.get("weatherElement", []):
            element_name = wp.get("elementName")
            if element_name:
                if element_name not in elements_by_time:
                    elements_by_time[element_name] = []

                for time_slot in wp.get('time', []):
                    start_time = time_slot.get('startTime', 'N/A')
                    end_time = time_slot.get('endTime', 'N/A')
                    parameter_name = time_slot.get('parameter', {}).get('parameterName', 'N/A')

                    elements_by_time[element_name].append({
                        'startTime': start_time,
                        'endTime': end_time,
                        'parameterName': parameter_name
                    })

        num_time_slots = 0
        if "Wx" in elements_by_time:
            num_time_slots = len(elements_by_time["Wx"])
        elif "PoP" in elements_by_time:
            num_time_slots = len(elements_by_time["PoP"])

        for i in range(num_time_slots):
            weather_report_for_loc += f"\n--- 預報時段 {i+1} ---\n"
            if "Wx" in elements_by_time and len(elements_by_time["Wx"]) > i:
                 start = elements_by_time["Wx"][i]['startTime']
                 end = elements_by_time["Wx"][i]['endTime']
                 weather_report_for_loc += f"時間:{start[5:16]}{end[5:16]}\n"

            if "Wx" in elements_by_time and len(elements_by_time["Wx"]) > i:
                weather_report_for_loc += f"天氣現象:{elements_by_time['Wx'][i]['parameterName']}\n"
            if "PoP" in elements_by_time and len(elements_by_time["PoP"]) > i:
                weather_report_for_loc += f"降雨機率:{elements_by_time['PoP'][i]['parameterName']}%\n"
            if "MinT" in elements_by_time and len(elements_by_time["MinT"]) > i:
                weather_report_for_loc += f"最低溫度:{elements_by_time['MinT'][i]['parameterName']}°C\n"
            if "MaxT" in elements_by_time and len(elements_by_time["MaxT"]) > i:
                weather_report_for_loc += f"最高溫度:{elements_by_time['MaxT'][i]['parameterName']}°C\n"
            if "CI" in elements_by_time and len(elements_by_time["CI"]) > i:
                weather_report_for_loc += f"舒適度:{elements_by_time['CI'][i]['parameterName']}\n"
        
        weather_report_for_loc += "====================\n"
        weather_reports.append(weather_report_for_loc)

    if not weather_reports:
        return f"找不到 {target_location_name} 的天氣資料,或資料處理失敗。"

    return "\n".join(weather_reports)

def display_selected_weather(location_name):
    """根據選擇的縣市名稱顯示天氣預報"""
    global ALL_WEATHER_DATA
    if ALL_WEATHER_DATA is None:
        messagebox.showwarning("資料未載入", "請稍候,天氣資料正在初始化中...")
        return

    parsed_data = parse_and_filter_weather_data(ALL_WEATHER_DATA, location_name)
    text_area.delete("1.0", tk.END)
    text_area.insert(tk.END, parsed_data)

def initialize_app():
    """初始化應用程式:下載資料並建立縣市按鈕"""
    global ALL_WEATHER_DATA
    text_area.insert(tk.END, "載入中,請稍候...\n") # 給予使用者反饋
    root.update_idletasks() # 強制更新 GUI 以顯示訊息

    ALL_WEATHER_DATA = fetch_weather_data()

    if ALL_WEATHER_DATA:
        try:
            locations = ALL_WEATHER_DATA["cwaopendata"]["dataset"]["location"]
            location_names = sorted([loc.get("locationName", "未知") for loc in locations])

            # 動態生成按鈕 (修改部分在此)
            max_cols = 8 # 每行最多 8 個按鈕
            row = 0
            col = 0
            for name in location_names:
                button = tk.Button(button_frame, text=name, command=lambda n=name: display_selected_weather(n), font=("Arial", 10), width=8, height=2) # 調整寬高
                button.grid(row=row, column=col, padx=3, pady=3) # 調整內邊距
                col += 1
                if col >= max_cols: # 如果達到最大列數,換行
                    col = 0
                    row += 1
            
            # 預設顯示第一個縣市的天氣
            if location_names:
                display_selected_weather(location_names[0])

        except KeyError as e:
            messagebox.showerror("資料結構錯誤", f"無法從 API 資料中找到預期的鍵:{e}\n請檢查 API 資料格式是否已變更。")
            text_area.insert(tk.END, "初始化失敗:資料結構不符預期。")
        except Exception as e:
            messagebox.showerror("初始化錯誤", f"應用程式初始化時發生未知錯誤:{e}")
            text_area.insert(tk.END, "初始化失敗。")
    else:
        text_area.insert(tk.END, "未能載入天氣資料,請檢查網路連線或 API 金鑰。")


# --- Tkinter 介面設定 ---
root = tk.Tk()
root.title("今明36小時天氣預報")
root.geometry("800x700")

# 縣市按鈕框架
button_frame = tk.Frame(root, padx=10, pady=10, bg="#e0e0e0")
button_frame.pack(side="top", fill="x")

# 顯示天氣資訊的文字區域
text_area = tk.Text(root, wrap="word", font=("Arial", 12), padx=10, pady=10)
text_area.pack(expand=True, fill="both")

# 捲軸
scrollbar = tk.Scrollbar(text_area)
scrollbar.pack(side="right", fill="y")
text_area.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=text_area.yview)

# 應用程式啟動時,載入資料並建立按鈕
root.after(100, initialize_app) # 使用 after 延遲呼叫,確保 Tkinter 視窗已初始化

root.mainloop()

2025年7月28日 星期一

空氣品質監測小時值(一般污染物,每日更新) 之 PM2.5 ---Python TKinter

 空氣品質監測小時值(一般污染物,每日更新) 之 PM2.5 ---Python TKinter





import tkinter as tk

from tkinter import ttk

import requests

import json

from datetime import datetime


def get_pm25_specific_data():

    """

    從 API 獲取資料,篩選出 PM2.5 (細懸浮微粒),

    並提取測站代碼、測站中文名稱、監測日期和最新的 PM2.5 測值。

    """

    api_url = "https://data.moenv.gov.tw/api/v2/aqx_p_15?api_key=aa1bc2bf-24c2-4c55-980f-a12075c2caec"

    try:

        response = requests.get(api_url)

        response.raise_for_status()  # 對於錯誤的狀態碼(4xx 或 5xx)引發 HTTPError

        data = response.json()

        

        extracted_data = []

        for record in data.get('records', []):

            # 只處理 itemname 為 "細懸浮微粒" 且 itemengname 為 "PM2.5" 的資料

            if record.get('itemname') == "細懸浮微粒" and record.get('itemengname') == "PM2.5":

                site_id = record.get('siteid', 'N/A') # 新增 siteid

                site_name = record.get('sitename', 'N/A')

                monitor_date = record.get('monitordate', 'N/A')

                

                # 尋找最新的 monitorvalueXX 作為 PM2.5 測值

                latest_pm25_value = 'N/A'

                latest_hour = -1

                

                # 假設最多有 24 個小時的測值,從 00 到 23

                for i in range(24): 

                    hour_key = f'monitorvalue{i:02d}'

                    value = record.get(hour_key)

                    

                    # 檢查值是否存在、非空且不是 'nan' (某些API回傳的無效值)

                    if value is not None and value != '' and value.strip().lower() != 'nan':

                        try:

                            current_hour = int(hour_key[-2:])

                            if current_hour > latest_hour:

                                latest_hour = current_hour

                                latest_pm25_value = value

                        except ValueError:

                            pass 

                

                extracted_data.append((site_id, site_name, monitor_date, latest_pm25_value)) # 修改這裡,包含 site_id

        return extracted_data

    except requests.exceptions.RequestException as e:

        print(f"Error fetching data: {e}")

        return None


def update_pm25_display():

    """更新 PM2.5 顯示,包含測站代碼、測站中文名稱、監測日期和 PM2.5 測值。"""

    pm25_filtered_data = get_pm25_specific_data()

    

    if pm25_filtered_data:

        # 清除舊的內容

        for widget in tree.get_children():

            tree.delete(widget)


        # 插入新的資料

        for site_id, site_name, monitor_date, latest_pm25_value in pm25_filtered_data: # 修改這裡,包含 site_id

            tree.insert("", tk.END, values=(site_id, site_name, monitor_date, latest_pm25_value))

    else:

        # 如果沒有資料,顯示錯誤訊息

        for widget in tree.get_children():

            tree.delete(widget)

        tree.insert("", tk.END, values=("無法獲取 PM2.5 資料", "", "", "")) # 調整錯誤訊息欄位數

    

    # 每 5 分鐘更新一次(300000 毫秒)

    root.after(300000, update_pm25_display)


# 設定 Tkinter 視窗

root = tk.Tk()

root.title("PM2.5 即時監測 (細懸浮微粒)")


# 建立 Treeview 顯示資料

columns = ("測站代碼", "測站中文名稱", "監測日期", "PM2.5 測值") # 新增 "測站代碼"

tree = ttk.Treeview(root, columns=columns, show="headings")


for col in columns:

    tree.heading(col, text=col)

    tree.column(col, anchor=tk.CENTER, width=100) # 調整寬度


# 特別調整測站中文名稱的寬度

tree.column("測站中文名稱", width=150)

tree.column("PM2.5 測值", width=100)



tree.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)


# 執行首次更新

update_pm25_display()


# 啟動 Tkinter 事件迴圈

root.mainloop()


2025年7月27日 星期日

臺中市政府環境保護局即時空氣品質監測網 ---Python TKinter

臺中市政府環境保護局即時空氣品質監測網 ---Python TKinter


空氣品質指標(AQI)發布時間:2025年07月27日 21時





import tkinter as tk

from tkinter import ttk, messagebox

import requests

from bs4 import BeautifulSoup

import pandas as pd

import io # 確保導入 io 模組


class TaichungAQIApp:

    def __init__(self, root):

        self.root = root

        self.root.title("臺中市 AQI 資料一覽表")

        self.root.geometry("1400x750") # 調整視窗大小以容納更多欄位


        # 關鍵修正:目標 URL 改為實際包含 AQI 表格數據的頁面

        self.url = "https://taqm.epb.taichung.gov.tw/TQAMNEWAQITABLE.ASPX" 


        # 用於顯示發布時間的標籤

        self.publish_time_label = ttk.Label(root, text="資料發布時間: 載入中...", font=('Arial', 12, 'bold'))

        self.publish_time_label.pack(pady=(10, 0))


        self.create_widgets()

        self.fetch_and_display_data()


    def create_widgets(self):

        # 控制按鈕區塊

        control_frame = tk.Frame(self.root)

        control_frame.pack(pady=5)


        refresh_button = tk.Button(control_frame, text="重新整理資料", command=self.fetch_and_display_data, font=('Arial', 10))

        refresh_button.pack(side=tk.LEFT, padx=5)


        # Treeview 顯示資料

        self.tree = ttk.Treeview(self.root, show="headings")

        self.tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)


        # 設定捲軸

        vsb = ttk.Scrollbar(self.tree, orient="vertical", command=self.tree.yview)

        vsb.pack(side='right', fill='y')

        self.tree.configure(yscrollcommand=vsb.set)


        hsb = ttk.Scrollbar(self.tree, orient="horizontal", command=self.tree.xview)

        hsb.pack(side='bottom', fill='x')

        self.tree.configure(xscrollcommand=hsb.set)


    def fetch_and_display_data(self):

        self.clear_treeview() # 先清除舊資料


        try:

            # 獲取網頁內容

            response = requests.get(self.url)

            response.raise_for_status() # 檢查 HTTP 請求是否成功


            # 使用 BeautifulSoup 解析 HTML

            soup = BeautifulSoup(response.text, 'html.parser')


            # --- 擷取發布時間 ---

            # 根據提供的 HTML,發布時間在 id="Label2" 的 span 標籤內

            publish_time_tag = soup.find('span', {'id': 'Label2'}) 

            if publish_time_tag:

                time_text = publish_time_tag.get_text(strip=True)

                self.publish_time_label.config(text=time_text)

            else:

                self.publish_time_label.config(text="資料發布時間: 未找到")

            # --- 擷取發布時間結束 ---


            # 尋找包含 AQI 資料的表格,使用其明確的 ID

            table = soup.find('table', {'id': 'GridView1'}) 

            

            if not table:

                messagebox.showerror("錯誤", "未能找到 ID 為 'GridView1' 的 AQI 資料表格。請檢查網頁結構是否已更改。")

                return


            # 使用 pandas 來方便地讀取 HTML 表格

            # str(table) 將 BeautifulSoup 物件轉換為字串,然後 io.StringIO 包裹

            dfs = pd.read_html(io.StringIO(str(table)))

            

            if not dfs:

                messagebox.showerror("錯誤", "未能從網頁表格中解析出任何數據。")

                return


            # AQI 資料表格是第一個 (或唯一一個) 被找到的表格

            df = dfs[0]


            # 清洗欄位名稱,處理多層表頭和特殊字符

            # pandas.read_html 會自動處理多層表頭,生成 MultiIndex

            if isinstance(df.columns, pd.MultiIndex):

                new_columns = []

                for col_tuple in df.columns:

                    # 組合多層表頭,例如:('二氧化硫(SO2)', '小時濃度(ppb)')

                    # 考慮到子標題可能是空或重複的特殊情況

                    main_col = col_tuple[0].strip().replace('\n', '')

                    sub_col = col_tuple[1].strip().replace('\n', '')


                    if not sub_col or main_col == sub_col:

                        new_columns.append(main_col)

                    else:

                        # 對特定子標題進行簡化

                        if '小時濃度(ppb)' in sub_col:

                            sub_col = '(ppb)'

                        elif '8小時移動平均濃度(ppm)' in sub_col:

                            sub_col = '(ppm)'

                        elif '移動平均濃度(μg/m3)' in sub_col:

                            sub_col = '(μg/m3)'

                        elif '小時濃度(ppm)' in sub_col: # 確保處理所有可能的ppm

                            sub_col = '(ppm)'

                        elif '8小時移動平均濃度(ppb)' in sub_col: # 確保處理所有可能的ppb

                             sub_col = '(ppb)'


                        new_columns.append(f"{main_col}{sub_col}")

                df.columns = new_columns

            else:

                 # 如果不是多層表頭,也進行一般的清理

                df.columns = [col.strip().replace('\n', '') for col in df.columns]


            # 最終的欄位名稱映射,確保顯示名稱的一致性

            # 這裡定義了我們期望的最終顯示名稱和它們在DataFrame中可能出現的名稱

            # 這是為了處理 read_html 可能產生的各種組合名稱

            column_rename_map = {

                '測站名稱': '測站名稱',

                '對健康影響等級': '對健康影響等級',

                'AQI指標值': 'AQI指標值',

                '指標污染物': '指標污染物',

                '二氧化硫(SO2)(ppb)': '二氧化硫(SO2)',

                '一氧化碳(CO)(ppm)': '一氧化碳(CO)',

                '臭氧(O3)(ppb)': '臭氧(O3)',

                '臭氧(O3)8小時移動平均濃度(ppb)': '臭氧(O3)8小時移動平均',

                '二氧化氮(NO2)(ppb)': '二氧化氮(NO2)',

                '懸浮微粒(PM10)(μg/m3)': '懸浮微粒(PM10)',

                '細懸浮微粒(PM2.5)(μg/m3)': '細懸浮微粒(PM2.5)',

            }

            

            # 反轉映射,方便檢查 DataFrame 中是否存在這些最終名稱

            # 並處理原始 DataFrame 可能包含的額外空白或換行

            actual_cols_to_target_names = {key.replace(' ', '').replace('\n', '').lower(): value for key, value in column_rename_map.items()}

            

            # 對 DataFrame 的欄位進行標準化和重新命名

            current_df_cols = {}

            for col in df.columns:

                cleaned_col = col.replace(' ', '').replace('\n', '').lower()

                if cleaned_col in actual_cols_to_target_names:

                    current_df_cols[col] = actual_cols_to_target_names[cleaned_col]


            df.rename(columns=current_df_cols, inplace=True)

            

            # 確保欄位名稱與您提供的列表完全一致,並保持順序

            # 這裡列出您期望看到的欄位名稱及其順序

            final_display_order = [

                '測站名稱', '對健康影響等級', 'AQI指標值', '指標污染物',

                '二氧化硫(SO2)', '一氧化碳(CO)', '臭氧(O3)', '臭氧(O3)8小時移動平均',

                '二氧化氮(NO2)', '懸浮微粒(PM10)', '細懸浮微粒(PM2.5)'

            ]


            # 篩選並重新排序 DataFrame 的欄位

            # 只保留 final_display_order 中存在的欄位

            df_display = df[[col for col in final_display_order if col in df.columns]]

            

            # 設定 Treeview 的欄位

            self.tree["columns"] = list(df_display.columns)

            for col in df_display.columns:

                self.tree.heading(col, text=col, command=lambda c=col: self.sort_treeview(self.tree, c, False))

                # 調整部分欄位寬度以更好顯示內容

                if col == '測站名稱':

                    self.tree.column(col, width=150, anchor='center')

                elif col == '對健康影響等級':

                    self.tree.column(col, width=100, anchor='center')

                elif col == '指標污染物':

                    self.tree.column(col, width=100, anchor='center')

                else:

                    self.tree.column(col, width=80, anchor='center') # 預設欄寬


            # 插入資料

            for index, row in df_display.iterrows():

                # 將 NaN, '--', ' ', '無監測' 轉換為空字串或更友好的顯示

                row_values = []

                for val in row:

                    s_val = str(val).strip()

                    # 檢查各種空值或無效數據的表示

                    if pd.isna(val) or s_val == '--' or s_val == ' ' or s_val == '無監測' or s_val == '*':

                        row_values.append("") # 顯示為空字串

                    else:

                        # 移除 <br> 標籤和 <sup> 標籤

                        s_val = s_val.replace('<br>', '').replace('<sup>', '').replace('</sup>', '')

                        row_values.append(s_val)

                self.tree.insert("", "end", values=row_values)


        except requests.exceptions.RequestException as e:

            messagebox.showerror("網路錯誤", f"無法連接到網站或獲取資料: {e}")

            self.publish_time_label.config(text="資料發布時間: 載入失敗")

        except Exception as e:

            messagebox.showerror("資料解析錯誤", f"處理網頁資料時發生錯誤: {e}\n\n請檢查網頁結構或更新程式。")

            self.publish_time_label.config(text="資料發布時間: 載入失敗")


    def clear_treeview(self):

        for item in self.tree.get_children():

            self.tree.delete(item)

        self.tree["columns"] = () # 清除所有欄位

        self.tree.heading("#0", text="") # 清除預設標題


    def sort_treeview(self, tree, col, reverse):

        try:

            col_index = list(tree["columns"]).index(col)

        except ValueError:

            return 

        

        l = [(tree.set(k, col), k) for k in tree.get_children('')]


        def try_numeric_sort(value):

            clean_value = str(value).strip()

            # 移除所有非數字和小數點的字符

            numeric_chars = ''.join(filter(lambda x: x.isdigit() or x == '.' or x == '-' , clean_value)) # 考慮負號

            if numeric_chars and numeric_chars.count('.') <= 1:

                try:

                    return float(numeric_chars)

                except ValueError:

                    pass

            return value


        l.sort(key=lambda t: try_numeric_sort(t[0]), reverse=reverse)


        for index, (val, k) in enumerate(l):

            tree.move(k, '', index)

        

        tree.heading(col, command=lambda: self.sort_treeview(tree, col, not reverse))



if __name__ == "__main__":

    root = tk.Tk()

    app = TaichungAQIApp(root)

    root.mainloop()




view-source:https://taqm.epb.taichung.gov.tw/TQAMNEWAQITABLE.ASPX
<script async src='https://www.googletagmanager.com/gtag/js?id=UA-137158817-2'></script><script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'UA-137158817-2');</script>

<!DOCTYPE html>
<html lang="zh-Hant" xmlns="http://www.w3.org/1999/xhtml">
<head><meta charset="utf-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>
AQI TABLE LIST
</title>
<style type="text/css">
.auto-style1 {
width: 100%;
}
a, a:link, a:visited {
color: #904E0E;
text-decoration: none;
}
a:hover {
color: #F4AB25;
background-color: #FFECD9;
text-decoration: none;
}
.breadcrumb {
list-style: none;
display: flex;
flex-wrap: wrap;
padding: 0;
margin: 0;
background-color: #f8f9fa;
}

.breadcrumb-item + .breadcrumb-item::before {
content: ">";
padding: 0 8px;
color: #6c757d;
}

.breadcrumb-item a {
text-decoration: none;
color: #007bff;
}

.breadcrumb-item.active {
color: #6c757d;
}

.breadcrumb-item a:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
h1 {
font-size: 1.5em; /* 調整字體大小為1倍的基準字體大小 */
}
.inline-container {
display: flex; /* 使用 flexbox 讓元素在一行顯示 */
align-items: center; /* 垂直對齊中心 */
}

.brick, .breadcrumb {
margin-right: 10px; /* 可以調整兩者之間的距離 */
}
</style>
</head>
<body>
<noscript>
<div style="color: red; font-weight: bold;">
您的瀏覽器不支援JavaScript功能,若網頁功能無法正常使用時,請開啟瀏覽器的JavaScript狀態。
</div>
</noscript>
<div class="inline-container">
<a href="#Accesskey_B" class="brick" id="Accesskey_B" accesskey="B" title="主功能區塊,由選單點選後於此呈現相關資訊">:::</a>
<nav aria-label="麵包屑導航">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/TQAMNEWAQI.ASPX">首頁</a></li>
<li class="breadcrumb-item"><a href="/TQAMNEWAQItable.ASPX">AQI資料一覽表</a></li>
</ol>
</nav>
</div>
<form method="post" action="./TQAMNEWAQITABLE.ASPX" id="form1">
<div class="aspNetHidden">
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="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" />
</div>

<div class="aspNetHidden">

<input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="FB0C4454" />
</div>
<div>
<h1>AQI測值一覽表</h1>
</div>
<table class="auto-style1">
<tr>
<td>
<br />
<span id="Label2" style="font-weight:bold;">空氣品質指標(AQI)發布時間:2025年07月27日 21時</span>
</td>
</tr>
<tr>
<td>
</td>
</tr>
<tr>
<td>
<div>
<table cellspacing="0" cellpadding="4" id="GridView1" style="border-collapse:collapse;">
<tr style="color:White;background-color:#1C5E55;font-weight:bold;">
<th ROWspan="2" style="width:130px;">測站名稱</th><th ROWspan="2" style="width:70px;">對健康影響等級</th><th ROWspan="2" style="width:49px;">AQI指標值</th><th ROWspan="2" style="width:100px;">指標污染物</th><th COLspan="2" style="width:1px;">二氧化硫(SO<sub>2</sub>)</th><th COLspan="2" style="width:74px;">一氧化碳(CO)</th><th COLspan="2" style="width:1px;">臭氧(O<sub>3</sub>)</th><th COLspan="2" style="width:74px;">二氧化氮(NO<sub>2</sub>)</th><th COLspan="2" style="width:1px;">懸浮微粒(PM<sub>10</sub>)</th><th COLspan="2" style="width:74px;">細懸浮微粒(PM<sub>2.5</sub>)</th></tr><tr></th><th style="width:1px;"></th><th style="width:74px;">小時濃度(ppb)</th><th style="width:1px;"></th><th style="width:84px;">8小時移動平均濃度(ppm)</th><th style="width:1px;">小時濃度(ppb)</th><th style="width:1px;">8小時移動平均濃度(ppb)</th><th></th><th>小時濃度(ppb)</th><th></th><th>移動平均濃度(μg/m<sup>3</sup>)</th><th></th><th>移動平均濃度(μg/m<sup>3</sup>)</th>
</tr><tr align="center" style="color:#4FB133;background-color:#EFF3FB;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=1">西屯測站(環境部)</a></td><td style="width:70px;">良好</td><td style="width:49px;">42</td><td style="width:100px;">懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.7</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.10</td><td style="width:1px;">28.0</td><td style="width:74px;">33</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">3.0</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">25</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">10</td>
</tr><tr align="center" style="color:#4FB133;background-color:White;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=2">忠明測站(環境部)</a></td><td style="width:70px;">良好</td><td style="width:49px;">42</td><td style="width:100px;">細懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.9</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.10</td><td style="width:1px;">26.0</td><td style="width:74px;">32</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">4.0</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">20</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">10</td>
</tr><tr align="center" style="color:#E69D00;background-color:#EFF3FB;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=3">大里測站(環境部)</a></td><td style="width:70px;">普通</td><td style="width:49px;">52</td><td style="width:100px;">細懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.7</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.10</td><td style="width:1px;">27.0</td><td style="width:74px;">32</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">5.0</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">21</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">13</td>
</tr><tr align="center" style="color:#4FB133;background-color:White;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=4">沙鹿測站(環境部)</a></td><td style="width:70px;">良好</td><td style="width:49px;">42</td><td style="width:100px;">懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.2</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.10</td><td style="width:1px;">30.0</td><td style="width:74px;">33</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">3.0</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">25</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">9</td>
</tr><tr align="center" style="color:#4FB133;background-color:#EFF3FB;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=5">豐原測站(環境部)</a></td><td style="width:70px;">良好</td><td style="width:49px;">35</td><td style="width:100px;">細懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">1.2</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.10</td><td style="width:1px;">31.0</td><td style="width:74px;">31</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">3.0</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">19</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">9</td>
</tr><tr align="center" style="color:#4FB133;background-color:White;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=6">文山測站</a></td><td style="width:70px;">良好</td><td style="width:49px;">47</td><td style="width:100px;">懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.7</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.47</td><td style="width:1px;">28.3</td><td style="width:74px;">32</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">2.6</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">28</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">8</td>
</tr><tr align="center" style="color:#4FB133;background-color:#EFF3FB;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=7">大甲測站</a></td><td style="width:70px;">良好</td><td style="width:49px;">40</td><td style="width:100px;">細懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">1.2</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.49</td><td style="width:1px;">32.5</td><td style="width:74px;">33</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">6.4</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">24</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">10</td>
</tr><tr align="center" style="color:#4FB133;background-color:White;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=8">太平測站</a></td><td style="width:70px;">良好</td><td style="width:49px;">47</td><td style="width:100px;">細懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">1.1</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.53</td><td style="width:1px;">29.9</td><td style="width:74px;">29</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">5.8</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">18</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">12</td>
</tr><tr align="center" style="color:#4FB133;background-color:#EFF3FB;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=9">霧峰測站</a></td><td style="width:70px;">良好</td><td style="width:49px;">42</td><td style="width:100px;">懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">1.6</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.41</td><td style="width:1px;">29.6</td><td style="width:74px;">33</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">2.9</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">25</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">9</td>
</tr><tr align="center" style="color:#4FB133;background-color:White;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=10">烏日測站</a></td><td style="width:70px;">良好</td><td style="width:49px;">31</td><td style="width:100px;">臭氧</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.8</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.34</td><td style="width:1px;">29.0</td><td style="width:74px;">34</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">4.2</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">17</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">7</td>
</tr><tr align="center" style="color:#4FB133;background-color:#EFF3FB;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=11">后里測站</a></td><td style="width:70px;">良好</td><td style="width:49px;">41</td><td style="width:100px;">細懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">1.5</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.49</td><td style="width:1px;">28.1</td><td style="width:74px;">29</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">1.4</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">19</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">10</td>
</tr><tr align="center" style="color:#E69D00;background-color:White;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=12">梧棲站(台電)</a></td><td style="width:70px;">普通</td><td style="width:49px;">51</td><td style="width:100px;">懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">1.3</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">無監測</td><td style="width:1px;">30.5</td><td style="width:74px;">34</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">2.2</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">31</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">7</td>
</tr><tr align="center" style="color:#4FB133;background-color:#EFF3FB;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=13">大肚站(台電)</a></td><td style="width:70px;">良好</td><td style="width:49px;">47</td><td style="width:100px;">懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">1.5</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">無監測</td><td style="width:1px;">29.1</td><td style="width:74px;">34</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">3.6</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">28</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">8</td>
</tr><tr align="center" style="color:#4FB133;background-color:White;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=14">東大站(台電)</a></td><td style="width:70px;">良好</td><td style="width:49px;">38</td><td style="width:100px;">細懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">1.8</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">無監測</td><td style="width:1px;">25.6</td><td style="width:74px;">31</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">1.6</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">19</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">10</td>
</tr><tr align="center" style="color:#4FB133;background-color:#EFF3FB;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=15">清水站(台電)</a></td><td style="width:70px;">良好</td><td style="width:49px;">43</td><td style="width:100px;">懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">1.5</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">無監測</td><td style="width:1px;">32.4</td><td style="width:74px;">33</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">4.0</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">26</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">9</td>
</tr><tr align="center" style="color:#4FB133;background-color:White;">
<td style="width:130px;"><a href="aqi/aqiNEW.ASPX?name=16">龍井站(台電)</a></td><td style="width:70px;">良好</td><td style="width:49px;">47</td><td style="width:100px;">懸浮微粒</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">1.3</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">0.16</td><td style="width:1px;">32.4</td><td style="width:74px;">37</td><td style="width:1px;">&nbsp;</td><td style="width:74px;">2.2</td><td style="width:1px;">&nbsp;</td><td style="width:84px;">28</td><td style="width:1px;">&nbsp;</td><td style="width:1px;">10</td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</form>
<h2>資料說明</h2>
<table>
<tr>
<td>
<p>1. 各污染物以環境部公布即時空氣品質指標(AQI)值計算方法換算,<a href="https://airtw.epa.gov.tw/CHT/Information/Standard/AirQualityIndicator.aspx#tg4" target="_blank">參考網址</a></p>
<p>2. 測值說明:</p>
<p> O<sub>3</sub>:取最近連續8小時移動平均值</p>
<p> PM<sub>2.5</sub>:0.5×前12小時平均+0.5×前4小時平均(前4小時2筆有效,前12小時6筆有效)</p>
<p> PM<sub>10</sub>:0.5×前12小時平均+0.5×前4小時平均(前4小時2筆有效,前12小時6筆有效)</p>
<p> CO:取最近連續8小時移動平均值</p>
<p> SO<sub>2</sub>:取即時濃度值</p>
<p> NO<sub>2</sub>:取即時濃度值</p>
<p>"—"為無效數據(監測資料不足),副指標部分"*"表該濃度無副指標對應值</p>
</td>
</tr>
</table>
</body>
</html>
  • 最終確認目標 URL:

    • 我將 self.url 精確地設定為 https://taqm.epb.taichung.gov.tw/TQAMNEWAQITABLE.ASPX。這是最關鍵的修正,因為這個頁面確實包含了完整的 AQI 數據表格。

  • 精確定位發布時間:

    • 根據 HTML,發布時間現在是位於 <span id="Label2"> 標籤中,程式碼已更新為 soup.find('span', {'id': 'Label2'})

  • 使用表格 ID 定位數據:

    • 最可靠地找到數據表格的方式是使用其唯一的 ID:id="GridView1"。程式碼現在使用 soup.find('table', {'id': 'GridView1'}) 來確保找到正確的表格。

  • 更全面的欄位名稱清洗與標準化:

    • pandas.read_html 處理複雜的 HTML 表頭時,可能會生成一些包含換行符、空格或奇怪組合的 MultiIndex。我對欄位清洗邏輯進行了加強,使其能夠:

      • 將多層表頭組合為更簡潔、易讀的單層名稱(例如 二氧化硫(SO2)小時濃度(ppb) 簡化為 二氧化硫(SO2)(ppb))。

      • 提供一個 column_rename_map,用於將這些讀取到的實際欄位名稱進一步標準化為您期望的簡潔顯示名稱(例如 二氧化硫(SO2)(ppb) 最終顯示為 二氧化硫(SO2))。

      • 確保最終顯示的欄位只包含 final_display_order 中定義的欄位,並且按照該順序排列

  • 增強數據值處理:

    • 在插入數據到 Treeview 時,除了 NaN--無監測,現在也處理 &nbsp; (HTML 中的不換行空格) 和 * (副指標無對應值) 這些可能代表空值的符號,將它們統一顯示為空字串,使表格更整潔。

    • 對於包含 <br><sup> 等 HTML 標籤的數據,在顯示前將其移除,只保留純文本。

  • 優化欄位寬度:

    • 對某些欄位(如「測站名稱」、「對健康影響等級」、「指標污染物」)設置了更合適的初始寬度,以改善視覺呈現。

  • ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite

     ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite  ESP32 VS Code 程式 ; PlatformIO Project Configuration File ; ;   Build op...