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 的事件迴圈,讓視窗保持開啟狀態並等待使用者操作,直到視窗關閉。

沒有留言:

張貼留言

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