2025年8月13日 星期三

爬取 Yahoo 股市即時股價 --電機機械 半導體 電子零組件 電子通路 四大類別---Python TKinter

爬取 Yahoo 股市即時股價 --電機機械  半導體  電子零組件  電子通路 四大類別---Python TKinter

https://tw.stock.yahoo.com/class

上市類股


  # 定義要爬取的四個類股及其 URL STOCK_SECTORS = { 

  "電機機械": "https://tw.stock.yahoo.com/class-quote?sectorId=6&exchange=TAI"

  "半導體": "https://tw.stock.yahoo.com/class-quote?sectorId=40&exchange=TAI"

  "電子零組件": "https://tw.stock.yahoo.com/class-quote?sectorId=44&exchange=TAI",

  "電子通路": "https://tw.stock.yahoo.com/class-quote?sectorId=45&exchange=TAI" }



import tkinter as tk

from tkinter import ttk

import requests

from bs4 import BeautifulSoup

import time

import threading


# 定義要爬取的四個類股及其 URL

STOCK_SECTORS = {

    "電機機械": "https://tw.stock.yahoo.com/class-quote?sectorId=6&exchange=TAI",

    "半導體": "https://tw.stock.yahoo.com/class-quote?sectorId=40&exchange=TAI",

    "電子零組件": "https://tw.stock.yahoo.com/class-quote?sectorId=44&exchange=TAI",

    "電子通路": "https://tw.stock.yahoo.com/class-quote?sectorId=45&exchange=TAI"

}


# 爬取資料的函數,現在接受 URL 作為參數

def get_stock_data(url):

    """

    從指定的 URL 爬取股票資訊。

    """

    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'

    }

    

    try:

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

        response.raise_for_status()

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

        

        table_body = soup.find('div', class_='table-body-wrapper')

        if not table_body:

            return []

        

        stock_rows = table_body.find_all('li', class_='List(n)')

        

        data = []

        for row in stock_rows:

            name_and_id_div = row.find('div', class_='Fxs(0)')

            

            # 確保找到所有必要的元素

            name_div = name_and_id_div.find('div', class_='Lh(20px)')

            id_span = name_and_id_div.find('span', class_='Fz(14px)')


            if not name_div or not id_span:

                continue


            name = name_div.text.strip()

            stock_id = id_span.text.strip()

            

            columns = row.find_all('div', class_='Fxg(1)')

            

            if len(columns) >= 4:

                price_span = columns[0].find('span')

                change_span = columns[1].find('span')

                change_percent_span = columns[2].find('span')


                if not price_span or not change_span or not change_percent_span:

                    continue


                price = price_span.text.strip()

                change = change_span.text.strip()

                change_percent = change_percent_span.text.strip()

                

                # 處理漲跌符號,使其更易讀

                if 'C($c-trend-up)' in str(change_span):

                    change = f"▲{change.lstrip('▲')}"

                elif 'C($c-trend-down)' in str(change_span):

                    change = f"▼{change.lstrip('▼')}"

                

                data.append({

                    "name": name,

                    "id": stock_id,

                    "price": price,

                    "change": change,

                    "change_percent": change_percent

                })

        return data

    

    except requests.exceptions.RequestException as e:

        print(f"網路連線錯誤: {e}")

        return None

    except Exception as e:

        print(f"解析資料錯誤: {e}")

        return None


class StockApp(tk.Tk):

    def __init__(self):

        super().__init__()

        self.title("上市分類行情查詢")

        self.geometry("900x700")

        

        self.current_sector = "電機機械"  # 預設顯示電機機械

        self.create_widgets()

        

        # 啟動首次資料載入

        self.update_data()


    def create_widgets(self):

        """

        建立使用者介面元件,包含下拉選單和表格。

        """

        # 建立控制面板框架

        control_frame = ttk.Frame(self, padding="10")

        control_frame.pack(fill='x')

        

        ttk.Label(control_frame, text="選擇類股:", font=("Helvetica", 12)).pack(side='left', padx=(0, 10))

        

        # 建立下拉式選單

        self.sector_combobox = ttk.Combobox(control_frame, values=list(STOCK_SECTORS.keys()))

        self.sector_combobox.pack(side='left')

        self.sector_combobox.set(self.current_sector) # 設定預設值

        self.sector_combobox.bind("<<ComboboxSelected>>", self.on_sector_change)


        self.info_label = ttk.Label(control_frame, text="正在載入資料...", font=("Helvetica", 12))

        self.info_label.pack(side='right')


        # 建立表格框架

        table_frame = ttk.Frame(self, padding="10")

        table_frame.pack(fill='both', expand=True)


        # 建立表格

        columns = ("股票名稱", "股票代號", "股價", "漲跌", "漲跌幅(%)")

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

        self.tree.pack(fill='both', expand=True)


        for col in columns:

            self.tree.heading(col, text=col, anchor='center')

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


        # 添加滾動條

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

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

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

    

    def on_sector_change(self, event):

        """

        處理下拉式選單的選擇事件,更新當前類股並重新爬取資料。

        """

        self.current_sector = self.sector_combobox.get()

        self.update_data()

        

    def update_data(self):

        """

        更新表格數據,並每60秒自動呼叫一次。

        """

        # 使用多執行緒,避免爬取資料時UI凍結

        def fetch_and_update():

            url = STOCK_SECTORS.get(self.current_sector)

            self.info_label.config(text=f"正在更新【{self.current_sector}】資料...")

            

            if url:

                stock_data = get_stock_data(url)

                

                # 清空舊數據

                for item in self.tree.get_children():

                    self.tree.delete(item)

                

                if stock_data:

                    for stock in stock_data:

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

                            stock['name'],

                            stock['id'],

                            stock['price'],

                            stock['change'],

                            stock['change_percent']

                        ))

                    self.info_label.config(text=f"【{self.current_sector}】資料更新時間: {time.strftime('%Y-%m-%d %H:%M:%S')}")

                else:

                    self.info_label.config(text=f"更新【{self.current_sector}】失敗,請檢查網路連線或稍後再試。")

            

            # 每60秒後再次執行

            self.after(60000, self.update_data)


        # 在一個新的執行緒中執行爬蟲函數

        thread = threading.Thread(target=fetch_and_update)

        thread.daemon = True 

        thread.start()


if __name__ == "__main__":

    app = StockApp()

    app.mainloop()


2025年8月11日 星期一

Python 網路爬蟲 爬取統一發票號碼,自動對獎 --python TKinter

Python 網路爬蟲   爬取統一發票號碼,自動對獎 --python TKinter    


https://invoice.etax.nat.gov.tw/index.html




import tkinter as tk

from tkinter import ttk, messagebox

import requests

from bs4 import BeautifulSoup

import re


# 統一發票開獎號碼網址

URL = 'https://invoice.etax.nat.gov.tw/index.html'


# 全域變數,用於儲存中獎號碼

winning_numbers = None


def fetch_winning_numbers():

    """從網頁爬取最新一期統一發票中獎號碼"""

    try:

        web = requests.get(URL)

        web.encoding = 'utf-8'

        soup = BeautifulSoup(web.text, "html.parser")


        numbers = {}


        # 使用更穩健的方法,透過表格結構來定位號碼

        prize_table = soup.find('table', class_='etw-table-bgbox')

        if not prize_table:

            raise Exception("無法找到中獎號碼表格,網頁結構可能已改變。")


        rows = prize_table.find_all('tr')

        

        for row in rows:

            tds = row.find_all('td')

            if len(tds) < 2:

                continue


            prize_type = tds[0].text.strip()

            

            if prize_type == '特別獎':

                special_num = tds[1].find('span', class_='fw-bold etw-color-red')

                if special_num:

                    numbers['special'] = special_num.text.strip()

            

            elif prize_type == '特獎':

                grand_num = tds[1].find('span', class_='fw-bold etw-color-red')

                if grand_num:

                    numbers['grand'] = grand_num.text.strip()

            

            elif prize_type == '頭獎':

                first_prize_nums = []

                for p_tag in tds[1].find_all('p', class_='etw-tbiggest'):

                    num_parts = p_tag.find_all('span', class_='fw-bold')

                    full_num = "".join([part.text.strip() for part in num_parts])

                    if full_num:

                        first_prize_nums.append(full_num)

                numbers['first'] = first_prize_nums

                

        # 尋找增開六獎

        # 這部分在您提供的 HTML 中沒有,因此先使用安全尋找

        extra_prize_elements = soup.find('div', class_='etw-award-num-box')

        if extra_prize_elements:

             extra_prize_nums = [num.text for num in extra_prize_elements.find_all('span', class_='etw-font-size-3 etw-color-red')]

             numbers['extra'] = extra_prize_nums

        else:

             numbers['extra'] = []



        if not numbers.get('special') or not numbers.get('first'):

            raise Exception("無法從網頁中找到特別獎或頭獎號碼。")


        return numbers


    except requests.exceptions.RequestException as e:

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

        return None

    except Exception as e:

        messagebox.showerror("錯誤", f"解析網頁內容失敗,網頁結構可能已改變。\n錯誤訊息: {e}")

        return None


def display_winning_numbers():

    """將中獎號碼顯示在 GUI 上"""

    global winning_numbers

    winning_numbers_label.config(text="載入中...")

    root.update()


    winning_numbers = fetch_winning_numbers()

    

    if winning_numbers:

        # 格式化中獎號碼字串

        text_to_display = (

            f"特別獎 (1000萬): {winning_numbers.get('special', 'N/A')}\n"

            f"特獎 (200萬): {winning_numbers.get('grand', 'N/A')}\n"

            f"頭獎 (20萬): {'、'.join(winning_numbers.get('first', ['N/A']))}\n"

            f"增開六獎 (200元): {'、'.join(winning_numbers.get('extra', ['N/A']))}"

        )

        winning_numbers_label.config(text=text_to_display)

    else:

        winning_numbers_label.config(text="無法取得中獎號碼,請檢查網路。")


def check_prize_logic(num):

    """根據統一發票規則對獎"""

    global winning_numbers

    # 確保輸入是8位數字

    if not num.isdigit() or len(num) != 8:

        return "請輸入正確的8位數發票號碼。"


    # 特別獎

    if num == winning_numbers.get('special'):

        return '恭喜!對中 1000 萬元!'


    # 特獎

    if num == winning_numbers.get('grand'):

        return '恭喜!對中 200 萬元!'


    # 頭獎系列

    for i in winning_numbers.get('first', []):

        if num == i: return '恭喜!對中 20 萬元!'

        if num[-7:] == i[-7:]: return '恭喜!對中 4 萬元!'

        if num[-6:] == i[-6:]: return '恭喜!對中 1 萬元!'

        if num[-5:] == i[-5:]: return '恭喜!對中 4000 元!'

        if num[-4:] == i[-4:]: return '恭喜!對中 1000 元!'

        if num[-3:] == i[-3:]: return '恭喜!對中 200 元!'


    # 增開六獎

    for i in winning_numbers.get('extra', []):

        if num[-3:] == i[-3:]: return '恭喜!對中 200 元!(增開六獎)'

    

    return "很抱歉,您的號碼未中獎。"


def perform_check():

    """執行對獎功能的函數"""

    user_number = entry_number.get()

    

    if not winning_numbers:

        result_label.config(text="中獎號碼尚未載入,請稍後再試。", foreground="red")

        return


    result = check_prize_logic(user_number)

    result_label.config(text=result, foreground="blue")


# 建立 Tkinter 視窗

root = tk.Tk()

root.title("統一發票自動對獎")

root.geometry("450x400")


# --- GUI 元件區 ---


# 標題

label_title = ttk.Label(root, text="統一發票自動對獎", font=("Helvetica", 16, "bold"))

label_title.pack(pady=10)


# 中獎號碼顯示區塊

winning_numbers_frame = ttk.LabelFrame(root, text="本期中獎號碼", padding=(10, 5))

winning_numbers_frame.pack(pady=10, padx=20, fill="x")


winning_numbers_label = ttk.Label(winning_numbers_frame, text="載入中...", font=("Helvetica", 12))

winning_numbers_label.pack(anchor="w")


# 對獎功能區塊

check_frame = ttk.Frame(root, padding=(10, 5))

check_frame.pack(pady=10)


label_instruction = ttk.Label(check_frame, text="請輸入您的8位數發票號碼:")

label_instruction.pack()


entry_number = ttk.Entry(check_frame, width=20, font=("Helvetica", 14))

entry_number.pack(pady=5)


check_button = ttk.Button(check_frame, text="自動對獎", command=perform_check)

check_button.pack(pady=10)


result_label = ttk.Label(root, text="", font=("Helvetica", 14, "bold"), foreground="red")

result_label.pack(pady=10)


# 程式啟動時,自動爬取並顯示中獎號碼

root.after(100, display_winning_numbers)


# 啟動 Tkinter 事件迴圈

root.mainloop()


程式碼逐行說明

1. 導入函式庫

Python
import tkinter as tk
from tkinter import ttk, messagebox
import requests
from bs4 import BeautifulSoup
import re
  • import tkinter as tk:導入 Python 內建的 GUI 函式庫 Tkinter,並給它一個簡寫 tk

  • from tkinter import ttk, messagebox:從 Tkinter 導入 ttk 模組,它提供了更現代化的 GUI 元件;以及 messagebox 模組,用於顯示彈出式訊息框。

  • import requests:導入 requests 函式庫,用於發送 HTTP 請求,從網頁抓取資料。

  • from bs4 import BeautifulSoup:從 beautifulsoup4 函式庫導入 BeautifulSoup,用於解析 HTML 或 XML 文件。

  • import re:導入 re 模組,提供正則表達式操作,用於處理字串,例如檢查號碼是否為數字。

2. 全域變數與常數

Python
# 統一發票開獎號碼網址
URL = 'https://invoice.etax.nat.gov.tw/index.html'

# 全域變數,用於儲存中獎號碼
winning_numbers = None
  • URL = '...':定義一個常數 URL,儲存財政部統一發票開獎號碼的網址。

  • winning_numbers = None:定義一個全域變數 winning_numbers,初始值為 None。這個變數將用於儲存從網頁爬取下來的各獎項號碼,以便在程式的不同函數中共享資料。

3. 爬蟲函式:fetch_winning_numbers()

Python
def fetch_winning_numbers():
    """從網頁爬取最新一期統一發票中獎號碼"""
    try:
        web = requests.get(URL)
        web.encoding = 'utf-8'
        soup = BeautifulSoup(web.text, "html.parser")
        numbers = {}

        prize_table = soup.find('table', class_='etw-table-bgbox')
        if not prize_table:
            raise Exception("無法找到中獎號碼表格,網頁結構可能已改變。")

        rows = prize_table.find_all('tr')
        
        for row in rows:
            tds = row.find_all('td')
            if len(tds) < 2:
                continue

            prize_type = tds[0].text.strip()
            
            if prize_type == '特別獎':
                special_num = tds[1].find('span', class_='fw-bold etw-color-red')
                if special_num:
                    numbers['special'] = special_num.text.strip()
            
            elif prize_type == '特獎':
                grand_num = tds[1].find('span', class_='fw-bold etw-color-red')
                if grand_num:
                    numbers['grand'] = grand_num.text.strip()
            
            elif prize_type == '頭獎':
                first_prize_nums = []
                for p_tag in tds[1].find_all('p', class_='etw-tbiggest'):
                    num_parts = p_tag.find_all('span', class_='fw-bold')
                    full_num = "".join([part.text.strip() for part in num_parts])
                    if full_num:
                        first_prize_nums.append(full_num)
                numbers['first'] = first_prize_nums
                
        # 尋找增開六獎
        extra_prize_elements = soup.find('div', class_='etw-award-num-box')
        if extra_prize_elements:
             extra_prize_nums = [num.text for num in extra_prize_elements.find_all('span', class_='etw-font-size-3 etw-color-red')]
             numbers['extra'] = extra_prize_nums
        else:
             numbers['extra'] = []

        if not numbers.get('special') or not numbers.get('first'):
            raise Exception("無法從網頁中找到特別獎或頭獎號碼。")

        return numbers

    except requests.exceptions.RequestException as e:
        messagebox.showerror("錯誤", f"無法連線到網站,請檢查網路連線或網頁狀態。\n錯誤訊息: {e}")
        return None
    except Exception as e:
        messagebox.showerror("錯誤", f"解析網頁內容失敗,網頁結構可能已改變。\n錯誤訊息: {e}")
        return None
  • def fetch_winning_numbers()::定義一個用來爬取網頁資料的函數。

  • try...except:這是一個錯誤處理區塊。try 裡面的程式碼會被執行,如果發生任何錯誤,程式會跳到 except 區塊,避免程式崩潰。

  • web = requests.get(URL):發送 GET 請求到指定的 URL,取得網頁內容。

  • web.encoding = 'utf-8':設定網頁內容的編碼為 utf-8,以確保中文字元顯示正常。

  • soup = BeautifulSoup(web.text, "html.parser"):使用 BeautifulSoup 解析網頁內容。

  • prize_table = soup.find('table', class_='etw-table-bgbox'):尋找 HTML 文件中 class 屬性為 etw-table-bgbox<table> 標籤。

  • rows = prize_table.find_all('tr'):找到表格中所有 <tr>(行)標籤。

  • for row in rows::遍歷每一行。

  • tds = row.find_all('td'):在每一行中找到所有 <td>(儲存格)標籤。

  • prize_type = tds[0].text.strip():取出第一個儲存格的文字內容,並用 .strip() 移除前後空白。

  • if prize_type == '特別獎': ...:根據儲存格文字判斷獎別,然後在第二個儲存格 (tds[1]) 中尋找對應的號碼標籤,並將其文字內容存入 numbers 字典。

  • 頭獎的特殊處理for p_tag in ... 迴圈用於處理頭獎號碼被分割成多個 <span> 的情況,"".join(...) 將這些部分拼接成一個完整的號碼。

  • 增開六獎的處理:程式碼會尋找一個特定的 div 區塊,如果找不到,則將增開六獎設為空列表。

  • if not numbers.get('special') ...:檢查是否成功抓取到重要的獎項號碼,如果沒有,就拋出一個錯誤。

  • return numbers:函數成功執行後,返回包含中獎號碼的字典。

4. GUI 顯示與更新函式:display_winning_numbers()

Python
def display_winning_numbers():
    """將中獎號碼顯示在 GUI 上"""
    global winning_numbers
    winning_numbers_label.config(text="載入中...")
    root.update()

    winning_numbers = fetch_winning_numbers()
    
    if winning_numbers:
        text_to_display = (
            f"特別獎 (1000萬): {winning_numbers.get('special', 'N/A')}\n"
            f"特獎 (200萬): {winning_numbers.get('grand', 'N/A')}\n"
            f"頭獎 (20萬): {'、'.join(winning_numbers.get('first', ['N/A']))}\n"
            f"增開六獎 (200元): {'、'.join(winning_numbers.get('extra', ['N/A']))}"
        )
        winning_numbers_label.config(text=text_to_display)
    else:
        winning_numbers_label.config(text="無法取得中獎號碼,請檢查網路。")
  • global winning_numbers:宣告要使用全域變數 winning_numbers

  • winning_numbers_label.config(...):更新 Tkinter 標籤 (Label) 的文字內容。

  • root.update():強制 Tkinter 視窗立即更新,這樣使用者會先看到「載入中...」的提示。

  • winning_numbers = fetch_winning_numbers():調用爬蟲函數,將結果賦值給全域變數。

  • if winning_numbers::檢查是否成功抓取到號碼。如果成功,就格式化一個包含所有中獎號碼的字串,並更新標籤的文字。f-string.get('key', 'N/A') 的用法讓格式化更簡潔安全。

5. 對獎邏輯函式:check_prize_logic()

Python
def check_prize_logic(num):
    """根據統一發票規則對獎"""
    global winning_numbers
    if not num.isdigit() or len(num) != 8:
        return "請輸入正確的8位數發票號碼。"

    if num == winning_numbers.get('special'):
        return '恭喜!對中 1000 萬元!'
    # ... 其他獎項的對獎邏輯 ...
    return "很抱歉,您的號碼未中獎。"
  • global winning_numbers:使用全域變數。

  • if not num.isdigit() ...:驗證使用者輸入的號碼是否為 8 位數字。

  • if num == winning_numbers.get('special'): ...:這是一個條件判斷,如果輸入號碼等於特別獎號碼,就返回對應的訊息。

  • for i in winning_numbers.get('first', [])::遍歷頭獎號碼列表,並從後三碼、後四碼... 一路比對到後八碼。如果對中任何一組,就立即返回結果。

  • return "很抱歉,您的號碼未中獎。":如果所有獎項都沒對中,則返回此訊息。

6. GUI 事件處理函式:perform_check()

Python
def perform_check():
    """執行對獎功能的函數"""
    user_number = entry_number.get()
    
    if not winning_numbers:
        result_label.config(text="中獎號碼尚未載入,請稍後再試。", foreground="red")
        return

    result = check_prize_logic(user_number)
    result_label.config(text=result, foreground="blue")
  • user_number = entry_number.get():獲取使用者在輸入框中輸入的文字。

  • if not winning_numbers::檢查中獎號碼是否已經成功載入。

  • result = check_prize_logic(user_number):將使用者輸入的號碼傳給對獎邏輯函數,並將回傳的結果儲存起來。

  • result_label.config(...):將對獎結果顯示在 GUI 介面的 result_label 標籤上,並設定文字顏色。

7. 建立 GUI 介面

Python
# 建立 Tkinter 視窗
root = tk.Tk()
root.title("統一發票自動對獎")
root.geometry("450x400")

# --- GUI 元件區 ---
# ... 這裡創建了所有標籤(Label)、輸入框(Entry)、按鈕(Button)等元件 ...

# 程式啟動時,自動爬取並顯示中獎號碼
root.after(100, display_winning_numbers)

# 啟動 Tkinter 事件迴圈
root.mainloop()

  • root = tk.Tk():創建主視窗。

  • root.title(...):設定視窗的標題。

  • root.geometry(...):設定視窗的初始大小。

  • label_title = ttk.Label(...):創建並設定一個顯示標題的標籤。

  • winning_numbers_frame = ttk.LabelFrame(...):創建一個帶有邊框和標題的框架,用來組織元件。

  • winning_numbers_label = ttk.Label(...):創建一個用來顯示中獎號碼的標籤。

  • entry_number = ttk.Entry(...):創建一個供使用者輸入號碼的輸入框。

  • check_button = ttk.Button(..., command=perform_check):創建一個按鈕,並將 perform_check 函數綁定到其 command 屬性,這樣點擊按鈕時就會調用該函數。

  • root.after(100, display_winning_numbers):這是關鍵的一行。它告訴 Tkinter 在 100 毫秒後調用 display_winning_numbers 函數。這樣做可以讓視窗先顯示出來,再執行網路請求,避免程式在啟動時因為網路延遲而卡住。

  • root.mainloop():啟動 Tkinter 的事件迴圈,讓視窗保持開啟狀態並等待使用者操作,直到視窗關閉。

Python 網路爬蟲 爬取現在天氣--python TKinter

 Python 網路爬蟲  爬取現在天氣--python TKinter


https://data.gov.tw/dataset/9176


資料資源欄位
StationName、StationId、DateTime、CoordinateName、CoordinateFormat、StationLatitude、StationLongitude、StationAltitude、CountyName、TownName、CountyCode、TownCode、Weather、Precipitation、WindDirection、WindSpeed、AirTemperature、RelativeHumidity、AirPressure、PeakGustSpeed
檔案格式
JSON
編碼格式
UTF-8
資料量
0
資料資源描述
無人自動站氣象資料


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

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

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


# --- 變數定義 ---


# API 網址,此為中央氣象署提供的範例授權碼。實際應用需替換成您自己的授權碼。

URL = "https://opendata.cwa.gov.tw/fileapi/v1/opendataapi/O-A0001-001?Authorization=rdec-key-123-45678-011121314&format=JSON"


# --- 函數定義 ---


def fetch_weather_data():

    """從中央氣象署 API 取得即時天氣觀測資料"""

    try:

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

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

        data = response.json()  # 將 JSON 格式的回應轉換成 Python 字典

        

        weather_data = []  # 建立一個空列表,用於存放處理後的觀測資料


        # 根據提供的 JSON 結構,安全地修正資料解析路徑

        cwa_data = data.get('cwaopendata', {})  # 嘗試取得 'cwaopendata' 鍵,若無則回傳空字典

        dataset = cwa_data.get('dataset', {})  # 嘗試取得 'dataset' 鍵,若無則回傳空字典

        locations = dataset.get('Station', [])  # 嘗試取得 'Station' 鍵,若無則回傳空列表


        if not locations:  # 檢查 locations 列表是否為空

            messagebox.showwarning("警告", "API 回應中未找到天氣觀測資料。")  # 彈出警告視窗

            return []  # 回傳空列表

        

        for loc in locations:  # 遍歷 locations 列表中的每個觀測站字典

            station_name = loc.get('StationName', 'N/A')  # 取得測站名稱,若無則為 'N/A'

            

            geo_info = loc.get('GeoInfo', {})  # 取得地理資訊字典

            county_name = geo_info.get('CountyName', 'N/A')  # 取得縣市名稱

            town_name = geo_info.get('TownName', 'N/A')  # 取得行政區名稱

            

            weather_elements = loc.get('WeatherElement', {})  # 取得氣象元素字典

            air_temp = weather_elements.get('AirTemperature', 'N/A')  # 取得氣溫

            relative_humidity_raw = weather_elements.get('RelativeHumidity', 'N/A')  # 取得相對濕度(原始值)

            weather = weather_elements.get('Weather', 'N/A')  # 取得天氣現象

            wind_speed = weather_elements.get('WindSpeed', 'N/A')  # 取得風速

            air_pressure = weather_elements.get('AirPressure', 'N/A')  # 取得氣壓


            # 轉換並處理無效值

            if relative_humidity_raw not in ('N/A', '-99'):

                # 原始值已是百分比,直接格式化為字串

                relative_humidity = f"{float(relative_humidity_raw):.2f}%"

            else:

                relative_humidity = 'N/A'


            air_temp = f"{air_temp}°C" if air_temp not in ('N/A', '-99') else 'N/A'  # 格式化氣溫

            air_pressure = f"{air_pressure} hPa" if air_pressure not in ('N/A', '-99') else 'N/A'  # 格式化氣壓

            wind_speed = f"{wind_speed} m/s" if wind_speed not in ('N/A', '-99') else 'N/A'  # 格式化風速

            

            # 將處理後的資料組合成一個元組

            weather_data.append((

                station_name, county_name, town_name, air_temp, relative_humidity, weather, wind_speed, air_pressure

            ))

            

        return weather_data  # 回傳包含所有觀測站資料的列表

    except requests.exceptions.RequestException as e:

        # 如果請求過程中發生任何錯誤,顯示錯誤訊息

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

        return []

    except Exception as e:

        # 捕捉所有其他可能發生的錯誤(例如 JSON 解析錯誤),並顯示錯誤訊息

        messagebox.showerror("錯誤", f"解析 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 = ('StationName', 'CountyName', 'TownName', 'AirTemperature', 'RelativeHumidity', 'Weather', 'WindSpeed', 'AirPressure')

    global tree  # 宣告 tree 為全域變數,以便在其他函數中使用

    tree = ttk.Treeview(tree_frame, columns=columns, show='headings')  # 建立 Treeview


    # 設定表格標題

    tree.heading('StationName', text='測站地點')

    tree.heading('CountyName', text='城市')

    tree.heading('TownName', text='行政區')

    tree.heading('AirTemperature', text='氣溫')

    tree.heading('RelativeHumidity', text='相對濕度')

    tree.heading('Weather', text='天氣現象')

    tree.heading('WindSpeed', text='風速')

    tree.heading('AirPressure', text='氣壓')


    # 設定表格欄位寬度

    tree.column('StationName', width=120)

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

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

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

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

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

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

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

    

    # 新增捲軸

    scrollbar = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=tree.yview)

    tree.configure(yscrollcommand=scrollbar.set)

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

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

    

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


def update_treeview():

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

    for item in tree.get_children():

        tree.delete(item)  # 清空舊資料

    

    data = fetch_weather_data()  # 呼叫 fetch_weather_data 獲取新資料

    if data:  # 檢查是否有資料

        for row in data:

            tree.insert('', 'end', values=row)  # 將每一行新資料插入表格

    

    if not data:

        messagebox.showwarning("警告", "目前無可顯示的觀測資料。")

            

# --- 程式主體 ---


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

root.title("即時天氣觀測")  # 設定視窗標題

root.geometry("1000x600")  # 設定視窗大小


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

button_frame.pack(pady=5)  # 打包框架


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

update_button.pack(padx=5)  # 打包按鈕


tree = create_treeview()  # 建立 Treeview 來顯示資料


update_treeview()  # 程式啟動時自動載入資料


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


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