2025年12月1日 星期一

parse QR code for Taiwanese Electronic Invoices

Install the libraries:

$ pip install pyzbar pillow
or
$ uv init
$ uv add pyzbar pillow

$ sudo apt-get install libzbar0

parse code from jpg file
import os
import sys
import csv
import shutil
from PIL import Image, ImageOps
from pyzbar.pyzbar import decode

# Output filename
CSV_FILENAME = "invoices.csv"

def parse_invoice_string(record):
    """
    Parses the raw QR string into a dictionary.
    """
    record = record.strip()
    record = record.split(":")[0]

    if len(record) < 53:
        return None

    try:
        invoice_code = record[0:10]              
        date_minguo = record[10:17]              
        price_hex = record[29:37]                
        seller_buy_combined = record[37:53]
        buyer_id  = seller_buy_combined[0:8]     
        seller_id = seller_buy_combined[8:16]     

        year = int(date_minguo[0:3]) + 1911
        month = date_minguo[3:5]
        day = date_minguo[5:7]
        date_str = f'{year}/{month}/{day}'

        price = int(price_hex, 16)

        return {
            "invoice_code": invoice_code,
            "seller_id": seller_id,
            "buyer_id": buyer_id,
            "date": date_str,
            "price": price
        }
    except Exception as e:
        return None

def save_to_csv(data):
    """
    Appends a single invoice record to the CSV file.
    """
    file_exists = os.path.isfile(CSV_FILENAME)

    with open(CSV_FILENAME, mode='a', newline='', encoding='utf-8') as csvfile:
        writer = csv.writer(csvfile)
        if not file_exists:
            writer.writerow(["統一發票碼", "賣方統編", "日期", "價錢"])
        
        writer.writerow([
            data['invoice_code'], 
            data['seller_id'], 
            data['date'], 
            data['price']
        ])

def scan_single_image(image_path):
    """
    處理單張圖片,回傳是否成功找到並解析出至少一張發票。
    """
    try:
        img = Image.open(image_path)
        img = ImageOps.exif_transpose(img)
        decoded_objects = decode(img)

        qr_objects = [obj for obj in decoded_objects if obj.type == 'QRCODE']
        if not qr_objects:
            return False

        qr_objects.sort(key=lambda obj: obj.rect.left)

        valid_count = 0
        for obj in qr_objects:
            raw_data = obj.data.decode('utf-8')
            result = parse_invoice_string(raw_data)

            if result:
                print(f"  ✅ 成功解析: {result['invoice_code']} | ${result['price']}")
                save_to_csv(result)
                valid_count += 1
        
        return valid_count > 0
    except Exception as e:
        print(f"  ❌ 處理發生錯誤: {e}")
        return False

def process_directory(target_dir):
    """
    掃描資料夾內的所有圖片。若失敗則拷貝至 failed 資料夾。
    """
    if not os.path.isdir(target_dir):
        print(f"❌ 找不到目錄: {target_dir}")
        return

    failed_dir = os.path.join(target_dir, "failed")
    # 建立失敗檔案的存放目錄 (若已存在則忽略)
    os.makedirs(failed_dir, exist_ok=True)

    valid_extensions = {".jpg", ".jpeg", ".png", ".bmp"}
    
    # 列出目標資料夾下所有的檔案 (排除子資料夾)
    files = [f for f in os.listdir(target_dir) if os.path.isfile(os.path.join(target_dir, f))]
    
    processed_count = 0
    failed_count = 0

    for filename in files:
        ext = os.path.splitext(filename)[1].lower()
        if ext not in valid_extensions:
            continue

        image_path = os.path.join(target_dir, filename)
        print(f"🔍 Scanning: {filename}...")
        
        success = scan_single_image(image_path)
        processed_count += 1
        
        if not success:
            print(f"  ⚠️ 無法辨識,已拷貝至失敗資料夾。")
            shutil.copy2(image_path, os.path.join(failed_dir, filename))
            failed_count += 1

    print(f"\n📊 掃描完成!共處理 {processed_count} 張圖片,其中 {failed_count} 張無法辨識。")

# --- Main Execution ---
if __name__ == "__main__":
    if len(sys.argv) > 1:
        target_path = sys.argv[1]
    else:
        print("請提供要掃描的檔案或資料夾路徑。")
        sys.exit(1)

    # 根據傳入的參數是資料夾還是檔案,決定處理邏輯
    if os.path.isdir(target_path):
        process_directory(target_path)
    elif os.path.isfile(target_path):
        print(f"🔍 Scanning single file: {target_path}...")
        success = scan_single_image(target_path)
        if not success:
            print("⚠️ 無法辨識。")
    else:
        print(f"❌ 找不到指定的路徑: {target_path}")



=================================================================


$ tree 01/
01/
├── drive-download-20260129T092115Z-3-001.zip
├── Notes_123.txt
├── Notes_456.txt
...
...

$ cat 01/Notes_123.txt
YD999900001150109209600000064000000691231132055556666s4oOFzB5IMqpbir5l6eEIQ==:*****

parse_invoice.py

import os
import sys
import csv

def parse_invoice_folder(folder_path, output_file="invoices.csv"):
    if not os.path.isdir(folder_path):
        print(f"資料夾不存在: {folder_path}")
        return

    txt_files = [f for f in os.listdir(folder_path) if f.endswith(".txt")]
    if not txt_files:
        print(f"資料夾內沒有 txt 檔: {folder_path}")
        return

    with open(output_file, mode='w', newline='', encoding='utf-8') as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(["統一發票碼", "賣方統編", "日期", "價錢"])

        for txt_file in txt_files:
            file_path = os.path.join(folder_path, txt_file)
            with open(file_path, "r", encoding="utf-8") as f:
                for line_num, line in enumerate(f, 1):
                    # 去除 BOM + 前後空格 + 換行
                    line = line.encode('utf-8').decode('utf-8-sig').strip()
                    if not line:
                        continue

                    # 取冒號前部分
                    record = line.split(":")[0]

                    # 檢查長度
                    if len(record) $lt; 53:
                        print(f"[跳過] 檔案 {txt_file} 行 {line_num} 字串太短: {record}")
                        continue

                    # ✅ 偏移解析
                    invoice_code = record[0:10]              # 統一發票碼
                    date_minguo = record[10:17]              # 發票日期
                    #price_hex = record[29:37]                # 價錢 HEX
                    price_hex = record[29:37]                # 價錢 HEX
                    seller_buy_combined = record[37:53]      # 往後16碼
                    seller_id = seller_buy_combined[0:8]     # 賣方統編
                    buyer_id = seller_buy_combined[8:16]   # 如果需要買方統編

                    # 日期換算民國→西元
                    try:
                        year = int(date_minguo[0:3]) + 1911
                        month = date_minguo[3:5]
                        day = date_minguo[5:7]
                        date_str = f'{year}/{month}/{day}'
                    except Exception as e:
                        print(f"[錯誤] 日期解析失敗: {date_minguo} 行 {line_num}")
                        continue

                    # 價錢 HEX → 10進位
                    try:
                        price = int(price_hex, 16)
                    except Exception as e:
                        print(f"[錯誤] 價錢解析失敗: {price_hex} 行 {line_num}")
                        continue

                    writer.writerow([invoice_code, buyer_id, f'{date_str}', price])

    print(f"CSV 檔案已生成: {output_file}")

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("使用方法: python3 parse_invoice.py $lt;資料夾路徑>")
        sys.exit(1)

    folder_path = sys.argv[1]
    parse_invoice_folder(folder_path)

沒有留言:

張貼留言

/*nice*/