顯示具有 Python 標籤的文章。 顯示所有文章
顯示具有 Python 標籤的文章。 顯示所有文章

2026年8月21日 星期五

Python一下:從風土資料到智慧生活的程式設計

智慧生活科技專業社群|Python學習導航

Python一下:從風土資料到智慧生活的程式設計

從一隻會畫圖的海龜出發,走進百香果分級、中文檔案、小遊戲、網路資料與智慧物聯網。這不是文章清單,而是一條可以真正走完的Python學習路徑。

學Python不只是記住語法,而是學會把地方問題說清楚、把資料整理好,再讓程式替生活創造新的可能。

先看地圖:我現在應該學什麼?

第一次學習請由左至右;已有基礎者,可以直接從需要的主題開始。

STEP 1看見程式
輸出、Turtle、指令順序
STEP 2控制程式
變數、判斷、迴圈
STEP 3整理資料
List、Tuple、Set、Dict
STEP 4完成作品
函式、檔案、遊戲、IoT
學習方式:每篇文章都建議完成「先預測結果 → 親手輸入 → 修改一處 → 解釋差異」四個動作。不要只複製貼上,也不要只看AI的答案。

第一站|認識Python,讓程式動起來

打好Python程式基礎

入門Turtle函式

從畫線、長方形、正方形到星星,理解指令順序、迴圈與函式。

挑戰:改變顏色、角度與重複次數,創作自己的風土圖騰。

第二站|變數、判斷與迴圈

Python迴圈的美

基礎whileforif

以Turtle重複畫圓,比較逐行重複、whilefor及遞迴的不同寫法。

關鍵問題:同一個結果,為什麼可以有多種程式寫法?

Python雙迴圈基礎教材

基礎進階巢狀迴圈

從列與欄理解雙迴圈,建立圖形排列、座位表與二維資料的基礎。

挑戰:輸出實心方塊、空心方塊、三角形及棋盤圖案。

夠Python,九九乘法表

進階生成式join

將傳統雙迴圈改寫成串列生成式,體會精簡但仍須兼顧可讀性的Pythonic風格。

第三站|用風土資料學四大資料結構

當資料愈來愈多,重點不只是「存進去」,而是選擇最適合的資料結構。以下系列以百香果分級為共同情境。

資料結構生活比喻適用情形文章主題
List 串列可以調整順序的清單有順序、可重複、可修改從百香果分級看List串列
Tuple 元組公告後固定的標準有順序,但不希望被更改以Tuple表示百香果分級標準
Set 集合不重複的品項名單去除重複、交集與差集用Set進行百香果分級判斷
Dict 字典用等級查詢規格以鍵快速找到對應的值用Dict查詢百香果分級標準

前往「Python創新教材」彙整頁閱讀四篇文章 →

風土任務:將「百香果分級」改成雲林文旦、花生、烏魚子或水井村農漁產資料,並說明為什麼選擇這種資料結構。

第四站|函式:把解法變成可以重複使用的工具

函式不是為了把程式變難,而是替一段有意義的工作命名。建議回到「打好Python程式基礎」的繪圖程式,依序完成:

  1. 先寫出可以執行的重複指令。
  2. def將繪圖步驟包成函式。
  3. 加入長度、顏色、角度等參數。
  4. 讓函式回傳計算結果或執行狀態。
  5. 組合多個小函式,完成一個可測試的作品。
AI Pair任務:請AI提出重構建議,但學生必須逐項說明「修改了什麼、為什麼更好、測試結果為何」。

第五站|檔案與中文資料處理

檔案的簡易操作

基礎openUTF-8with

練習寫入、讀取、換行、中文編碼及with open()的安全寫法。

第六站|用小遊戲整合基本語法

用集合特性開發新版XAXB

程式重構Set生成式

比較2018與2019版本,觀察集合及生成式如何縮短程式。

Challenge AI:比較兩版的正確性、可讀性、效率及輸入例外。

第七站|從基本語法走向智慧生活

Python網路爬文

網路資料

讓Python從本機走向網路,取得可分析的公開資料。

micro:bit OLED初接觸

MicroPython硬體

把變數、迴圈及字串顯示在真實硬體上,感受程式與環境互動。

給學生的建議修課順序

階段學習內容代表文章學習成果
W1–W2動機、輸出、Turtle初遇Python、打好Python基礎完成風土圖騰
W3–W5變數、型別、判斷、迴圈變數是標籤、迴圈的美完成互動判斷程式
W6–W8雙迴圈、期中整合雙迴圈教材、九九乘法表完成圖形排列與解釋
W10–W12四大資料結構百香果List/Tuple/Set/Dict建立風土分級資料
W13–W14函式、檔案、例外檔案簡易操作、中文字數完成資料讀寫工具
W15–W17遊戲或智慧生活PBL猜數字、XAXB、UDP、micro:bit完成可展示專題
三層學習法:No-AI先獨立理解與手寫;AI Pair利用AI解釋、除錯及比較;Challenge AI則閱讀AI產生的程式,找錯、修改、設計測資並提出證據。

最後提醒:會執行,不等於會程式設計

真正的學習證據不是「畫面跑出來了」,而是能回答以下五個問題:

  1. 這段程式要解決什麼問題?
  2. 資料如何輸入、處理與輸出?
  3. 為什麼選擇這種流程或資料結構?
  4. 如果輸入錯誤或遇到邊界值,程式會怎樣?
  5. 你如何證明修改後的程式是正確的?

從風土資料出發,讓程式理解地方;從智慧生活出發,讓技術回到人的需要。

2026年7月24日 星期五

如何設計自動分析顧客購買行為的智慧銷售平台?

以 PythonAnywhere、Python/Django、Vibe Coding 與 Vibe Design 完成可部署的教學範例

主題:智慧銷售平台|顧客行為分析|Django ORM|資料視覺化

本文以「水井村特色商品智慧銷售平台」為情境,示範如何將顧客、商品、訂單與訂單明細資料轉換成可自動更新的行銷洞察。每當新增一筆已完成訂單,系統便重新計算顧客總消費、購買次數、平均客單價、最近購買日期、回購狀態、商品偏好及顧客分群,並在 Dashboard 顯示行銷建議。


圖 1 智慧銷售平台教學文章與分析儀表板整體示意

圖片說明:圖片呈現完整教學文章的視覺架構,包含地方特色商品情境、顧客與訂單資料表、分析結果、Django ORM 程式區塊,以及銷售趨勢、熱銷商品、顧客分群與行銷建議等 Dashboard 元件。可作為文章首圖或社群分享圖。

一、先定義「自動分析」要回答的問題

智慧銷售平台不應只是一個商品展示網站,而要能協助經營者回答具體的行銷問題:

  1. 哪些顧客的累積消費金額最高?
  2. 哪些顧客曾再次購買,整體回購率是多少?
  3. 哪些商品最熱銷,哪些商品分類最受偏好?
  4. 哪些顧客已長時間未消費,需要啟動喚回活動?
  5. 應該對不同顧客提供什麼優惠或推薦?
交易資料Django ORM 彙整 行為指標顧客分群 Dashboard行銷建議

二、範例情境與資料

平台可銷售水井姻緣花、玉米籜金魚、在地農產禮盒、水井村導覽體驗券與智慧農業體驗課程。課堂實作建議至少準備 10 位顧客、8 項商品、30 筆訂單與 50 筆訂單明細。

顧客資料

顧客編號姓名加入日期
C001王小明2026-01-10
C002李小華2026-02-15
C003陳美玲2026-03-20

商品資料

商品編號商品名稱分類單價
P001水井姻緣花文創商品350 元
P002玉米籜金魚永續工藝250 元
P003農產禮盒在地農產600 元
P004水井村導覽券體驗活動300 元

三、Django 資料模型

最小可行版本使用四個模型:CustomerProductOrderOrderItem。顧客與商品的購買關係,透過訂單與訂單明細串接。

程式碼 1 核心資料模型(models.py)
from decimal import Decimal
from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=100)
    category = models.CharField(max_length=50)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    is_active = models.BooleanField(default=True)

    def __str__(self):
        return self.name

class Customer(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(blank=True)
    joined_at = models.DateField(auto_now_add=True)

    def __str__(self):
        return self.name

class Order(models.Model):
    STATUS_CHOICES = [
        ("pending", "待處理"),
        ("completed", "已完成"),
        ("cancelled", "已取消"),
    ]
    customer = models.ForeignKey(
        Customer, on_delete=models.CASCADE, related_name="orders"
    )
    ordered_at = models.DateTimeField(auto_now_add=True)
    status = models.CharField(
        max_length=20, choices=STATUS_CHOICES, default="completed"
    )

    @property
    def total_amount(self):
        return sum(
            (item.subtotal for item in self.items.all()),
            Decimal("0.00")
        )

class OrderItem(models.Model):
    order = models.ForeignKey(
        Order, on_delete=models.CASCADE, related_name="items"
    )
    product = models.ForeignKey(
        Product, on_delete=models.PROTECT, related_name="order_items"
    )
    quantity = models.PositiveIntegerField(default=1)
    unit_price = models.DecimalField(max_digits=10, decimal_places=2)

    @property
    def subtotal(self):
        return self.unit_price * self.quantity
程式碼說明:訂單價格應保存當下的 unit_price,避免商品日後調價時改變歷史交易。分析時只納入狀態為 completed 的有效訂單。

四、建立顧客購買行為指標

指標計算方式行銷用途
總消費金額有效訂單明細小計加總辨識高價值顧客
購買次數有效訂單數判斷回購與忠誠度
平均客單價總消費金額 ÷ 購買次數規劃滿額優惠與組合銷售
最近購買日期有效訂單日期最大值辨識沉睡或流失風險
回購率購買兩次以上顧客數 ÷ 購買顧客總數評估會員經營成效
程式碼 2 使用 Django ORM 彙整顧客行為(services.py)
from decimal import Decimal
from django.db.models import (
    Count, DecimalField, ExpressionWrapper,
    F, Max, Q, Sum, Value
)
from django.db.models.functions import Coalesce
from .models import Customer

def get_customer_analysis():
    completed = Q(orders__status="completed")

    item_total = ExpressionWrapper(
        F("orders__items__quantity")
        * F("orders__items__unit_price"),
        output_field=DecimalField(
            max_digits=12, decimal_places=2
        ),
    )

    return Customer.objects.annotate(
        total_spending=Coalesce(
            Sum(item_total, filter=completed),
            Value(
                Decimal("0.00"),
                output_field=DecimalField(
                    max_digits=12, decimal_places=2
                ),
            ),
        ),
        purchase_count=Count(
            "orders", filter=completed, distinct=True
        ),
        last_purchase=Max(
            "orders__ordered_at", filter=completed
        ),
    ).order_by("-total_spending")
程式碼說明:annotate() 會將每位顧客的分析結果附加到查詢物件上;Coalesce() 可把沒有消費紀錄時的 NULL 轉為 0;distinct=True 可避免訂單因多筆明細而被重複計數。

五、顧客自動分群

初學課程可先使用規則式分群,不必立即導入機器學習。判斷順序要明確,避免同一位顧客同時落入多個主要類型。

顧客類型範例條件建議策略
沉睡顧客90 天以上未購買發送限期喚回優惠
高價值顧客總消費達 1,000 元以上VIP 服務、新品優先通知
忠誠顧客購買 3 次以上會員集點、專屬活動
一般顧客購買 2 次推薦關聯商品
新顧客購買 1 次第二次購買優惠
潛在顧客尚無購買紀錄首次購買優惠
程式碼 3 顧客分類函式(services.py)
from decimal import Decimal
from django.utils import timezone

def classify_customer(customer):
    inactive_days = None

    if customer.last_purchase:
        inactive_days = (
            timezone.now() - customer.last_purchase
        ).days

    if inactive_days is not None and inactive_days >= 90:
        return "沉睡顧客"

    if customer.total_spending >= Decimal("1000"):
        return "高價值顧客"

    if customer.purchase_count >= 3:
        return "忠誠顧客"

    if customer.purchase_count >= 2:
        return "一般顧客"

    if customer.purchase_count == 1:
        return "新顧客"

    return "潛在顧客"

六、自動產生行銷建議

程式碼 4 顧客類型對應行銷建議
def get_marketing_suggestion(customer_type):
    suggestions = {
        "新顧客": "提供第二次購買優惠,鼓勵完成首次回購。",
        "一般顧客": "推薦相關商品組合,提高購買頻率。",
        "忠誠顧客": "提供會員集點、專屬活動及新品通知。",
        "高價值顧客": "提供 VIP 服務與新品優先購買。",
        "沉睡顧客": "發送限期回購優惠,搭配過去偏好商品。",
        "潛在顧客": "推薦熱門商品與首次購買優惠。",
    }
    return suggestions.get(
        customer_type,
        "持續觀察顧客購買行為。"
    )

七、將分析結果傳給 Dashboard

程式碼 5 View 整合顧客分析結果(views.py)
from django.shortcuts import render
from .services import (
    classify_customer,
    get_customer_analysis,
    get_marketing_suggestion,
)

def customer_analysis_dashboard(request):
    customers = get_customer_analysis()
    rows = []

    for customer in customers:
        customer_type = classify_customer(customer)
        average_order_value = (
            customer.total_spending / customer.purchase_count
            if customer.purchase_count else 0
        )

        rows.append({
            "customer": customer,
            "total_spending": customer.total_spending,
            "purchase_count": customer.purchase_count,
            "average_order_value": average_order_value,
            "last_purchase": customer.last_purchase,
            "customer_type": customer_type,
            "suggestion": get_marketing_suggestion(customer_type),
        })

    return render(
        request,
        "sales/customer_analysis.html",
        {"analysis_rows": rows},
    )
程式碼 6 顧客分析結果表格(customer_analysis.html)
<table class="table table-striped">
  <thead>
    <tr>
      <th>顧客</th>
      <th>總消費</th>
      <th>購買次數</th>
      <th>平均客單價</th>
      <th>顧客類型</th>
      <th>行銷建議</th>
    </tr>
  </thead>
  <tbody>
  {% for row in analysis_rows %}
    <tr>
      <td>{{ row.customer.name }}</td>
      <td>{{ row.total_spending }}</td>
      <td>{{ row.purchase_count }}</td>
      <td>{{ row.average_order_value|floatformat:0 }}</td>
      <td>{{ row.customer_type }}</td>
      <td>{{ row.suggestion }}</td>
    </tr>
  {% endfor %}
  </tbody>
</table>




圖 2 Django 智慧銷售平台自動分析教學資訊圖

圖片說明:圖片由左至右呈現範例情境、顧客與訂單資料、Customer/Order/OrderItem/Product 關聯、前端 KPI 與圖表,以及 ORM 查詢、顧客分群程式與行銷應用。適合放在文章中段,協助讀者快速掌握「資料建模—分析—視覺化—決策」的完整流程。

八、Dashboard 應呈現哪些資訊?

建議至少包含四張 KPI 卡、三張圖表與三類顧客名單:

  • KPI:總銷售額、訂單數、平均客單價、回購率。
  • 圖表:每月銷售趨勢、熱銷商品排行、顧客分群比例。
  • 名單:高價值顧客、忠誠顧客、沉睡顧客。
程式碼 7 Chart.js 熱銷商品長條圖
<canvas id="topProductsChart"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ctx = document.getElementById("topProductsChart");

new Chart(ctx, {
  type: "bar",
  data: {
    labels: {{ product_labels|safe }},
    datasets: [{
      label: "銷售數量",
      data: {{ product_values|safe }}
    }]
  },
  options: {
    responsive: true,
    indexAxis: "y",
    plugins: {
      legend: { display: false }
    }
  }
});
</script>

九、Vibe Coding 的課堂使用方式

學生可先用自然語言描述功能,再請生成式 AI 產生程式骨架,但必須自行驗證欄位、關聯、計算公式與錯誤處理。

提示詞範例:
請扮演 Django 開發工程師。我已有 Customer、Order 與 OrderItem 模型,請使用 Django ORM 計算每位顧客的總消費金額、有效訂單數、平均客單價及最近購買日期。分析邏輯放在 services.py,需排除 cancelled 訂單,並提供測試資料與預期結果。

十、課堂最低完成規格

  1. 商品、顧客、訂單與訂單明細管理。
  2. 總消費、購買次數、平均客單價、最近購買日期與回購率。
  3. 至少四種顧客類型的規則式分群。
  4. 至少三張分析圖表與一份顧客清單。
  5. 每類顧客至少一項自動行銷建議。
  6. 部署至 PythonAnywhere,新增訂單後分析結果能自動更新。

結語

這個範例的核心不是建立複雜的人工智慧模型,而是讓學生先理解:只要資料模型正確、指標定義清楚、查詢經過驗證,Django ORM 就能把日常交易資料轉換成具行銷價值的顧客洞察。完成規則式分析後,還可再延伸至 RFM、商品關聯分析、個人化推薦或生成式 AI 行銷文案。

行銷管理與品牌經營|智慧銷售平台教學範例

AI協作說明: 本文由作者主導內容規劃,並使用生成式AI協助文字整理、 程式碼初稿、版面設計與圖片生成;所有內容均經作者審核、 修訂與測試。

2026年6月1日 星期一

把近日的漁產品交易行情儲存到Excel檔案中

上一篇文章:把近日的漁產品交易行情儲存到Excel檔案中

執行結果:

  File "C:/cmlin/虎尾科技大學/112-2/電腦軟體應用/範例檔案/ch14/test2.py", line 7, in <module>

    r = requests.get('https://data.moa.gov.tw/Service/OpenData/FromM/AquaticTransData.aspx')

  File "C:\Users\cheng-min\AppData\Local\Programs\Python\Python314\Lib\site-packages\requests\api.py", line 73, in get

    return request("get", url, params=params, **kwargs)

  File "C:\Users\cheng-min\AppData\Local\Programs\Python\Python314\Lib\site-packages\requests\api.py", line 59, in request

    return session.request(method=method, url=url, **kwargs)

  File "C:\Users\cheng-min\AppData\Local\Programs\Python\Python314\Lib\site-packages\requests\sessions.py", line 589, in request

    resp = self.send(prep, **send_kwargs)

  File "C:\Users\cheng-min\AppData\Local\Programs\Python\Python314\Lib\site-packages\requests\sessions.py", line 703, in send

    r = adapter.send(request, **kwargs)

  File "C:\Users\cheng-min\AppData\Local\Programs\Python\Python314\Lib\site-packages\requests\adapters.py", line 675, in send

    raise SSLError(e, request=request)

requests.exceptions.SSLError: HTTPSConnectionPool(host='data.moa.gov.tw', port=443): Max retries exceeded with url: /Service/OpenData/FromM/AquaticTransData.aspx (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Missing Subject Key Identifier (_ssl.c:1081)')))

範例一、修正後的程式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import requests
import json
from openpyxl import Workbook


# 從URL取得JSON資料
r = requests.get('https://data.moa.gov.tw/Service/OpenData/FromM/AquaticTransData.aspx', verify=False)
json_data = json.loads(r.text)

# 建立一個新的Workbook
wb = Workbook()
ws = wb.active
ws.title = "漁產品交易行情"

# 寫入標題行
headers = ['交易日期', '品種代碼', '魚貨名稱', '市場名稱', '上價', '中價', '下價', '交易量', '平均價']
ws.append(headers)

# 寫入資料
for item in json_data:
    row = [
        item.get('交易日期'),
        item.get('品種代碼'),
        item.get('魚貨名稱'),
        item.get('市場名稱'),
        item.get('上價'),
        item.get('中價'),
        item.get('下價'),
        item.get('交易量'),
        item.get('平均價')
    ]
    ws.append(row)

# 儲存到Excel檔案
wb.save('AquaticTransData.xlsx')

print("資料已儲存到 AquaticTransData.xlsx")

執行結果:
Warning (from warnings module):
  File "C:\Users\cheng-min\AppData\Local\Programs\Python\Python314\Lib\site-packages\urllib3\connectionpool.py", line 1097
    warnings.warn(
InsecureRequestWarning: Unverified HTTPS request is being made to host 'data.moa.gov.tw'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
資料已儲存到 AquaticTransData.xlsx

範例二、再修訂把警告訊息也拿掉

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import requests
import json
from openpyxl import Workbook
import urllib3

# 關閉 HTTPS 警告
urllib3.disable_warnings()

# 從URL取得JSON資料
r = requests.get('https://data.moa.gov.tw/Service/OpenData/FromM/AquaticTransData.aspx', verify=False)
json_data = json.loads(r.text)

# 建立一個新的Workbook
wb = Workbook()
ws = wb.active
ws.title = "漁產品交易行情"

# 寫入標題行
headers = ['交易日期', '品種代碼', '魚貨名稱', '市場名稱', '上價', '中價', '下價', '交易量', '平均價']
ws.append(headers)

# 寫入資料
for item in json_data:
    row = [
        item.get('交易日期'),
        item.get('品種代碼'),
        item.get('魚貨名稱'),
        item.get('市場名稱'),
        item.get('上價'),
        item.get('中價'),
        item.get('下價'),
        item.get('交易量'),
        item.get('平均價')
    ]
    ws.append(row)

# 儲存到Excel檔案
wb.save('AquaticTransData.xlsx')

print("資料已儲存到 AquaticTransData.xlsx")

執行結果:
資料已儲存到 AquaticTransData.xlsx

AquaticTransData.xlsx的部份內容:



2026年5月19日 星期二

[水井USR] 自動更新水井USR網站上即時魚溫資料

 


Python測試程式(由ChatGPT產生):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import requests

url = "https://shuijingusr.pythonanywhere.com/api/pond/sensor-reading/"

payload = {
    "token": "abc123",
    "farm_name": "蛤之美生態養殖場",
    "pond_code": "1",
    "water_temperature": 28.9,
    "salinity": 18.4,
    "ph": 7.6,
    "dissolved_oxygen": 6.8,
    "water_source": "直流變頻水車運轉中",
    "recorded_at": "2026-05-19T10:30:00+08:00"
}

response = requests.post(url, json=payload)

print("狀態碼:", response.status_code)

try:
    print("回傳資料:", response.json())
except Exception:
    print("伺服器回傳不是 JSON:")
    print(response.text)

執行結果:
狀態碼: 201
回傳資料: {'ok': True, 'message': '魚塭感測資料已成功寫入', 'data': {'id': 7, 'farm': '蛤之美生態養殖場', 'pond_id': 1, 'pond_code': '1', 'pond_name': '文蛤', 'water_temperature': 28.9, 'salinity': 18.4, 'ph': 7.6, 'dissolved_oxygen': 6.8, 'water_source': '直流變頻水車運轉中', 'status': 'normal', 'status_display': '正常', 'alert_message': '目前水質穩定', 'recorded_at': '2026-05-19T10:30:00+08:00'}}

2026年5月13日 星期三

[水井USR] 自動更新水井USR網站上即時生態資料



 
水井USR網站:水井村USR|生態感測

範例一:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import requests

url = "https://shuijingusr.pythonanywhere.com/api/ecology/sensor-reading/"

data = {
    "token": "abc123",
    "water_quality": 7.1,
    "humidity": 70.0,
    "temperature": 29.5,
    "light": 42.0,
    "recorded_at": "2025-05-13T10:30:00+08:00"
}

try:
    response = requests.post(url, json=data, timeout=10)

    print("狀態碼:", response.status_code)

    try:
        print("回傳資料:")
        print(response.json())
    except ValueError:
        print("伺服器回傳不是 JSON:")
        print(response.text)

except requests.exceptions.RequestException as e:
    print("連線失敗:", e)


執行結果:
狀態碼: 201
回傳資料:
{'ok': True, 'message': '生態感測資料已成功寫入', 'data': {'id': 2, 'water_quality': 7.1, 'humidity': 70.0, 'temperature': 29.5, 'light': 42.0, 'status': 'normal', 'recorded_at': '2025-05-13T10:30:00+08:00'}}

範例二:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import time
import random
import requests
from datetime import datetime, timezone, timedelta

url = "https://shuijingusr.pythonanywhere.com/api/ecology/sensor-reading/"
taiwan_tz = timezone(timedelta(hours=8))

while True:
    now = datetime.now(taiwan_tz).isoformat()

    data = {
        "token": "abc123",
        "water_quality": round(random.uniform(7.0, 7.8), 1),
        "humidity": round(random.uniform(60.0, 75.0), 1),
        "temperature": round(random.uniform(28.0, 31.5), 1),
        "light": round(random.uniform(35.0, 55.0), 1),
        "recorded_at": now
    }

    try:
        response = requests.post(url, json=data, timeout=10)
        print("送出資料:", data)
        print("狀態碼:", response.status_code)
        print("回傳:", response.json())
        print("-" * 40)

    except requests.exceptions.RequestException as e:
        print("上傳失敗:", e)

    time.sleep(10)

執行結果:
送出資料: {'token': 'abc123', 'water_quality': 7.2, 'humidity': 60.9, 'temperature': 30.8, 'light': 37.8, 'recorded_at': '2026-05-13T21:33:30.885065+08:00'}
狀態碼: 201
回傳: {'ok': True, 'message': '生態感測資料已成功寫入', 'data': {'id': 12, 'water_quality': 7.2, 'humidity': 60.9, 'temperature': 30.8, 'light': 37.8, 'status': 'normal', 'recorded_at': '2026-05-13T21:33:30.885065+08:00'}}
----------------------------------------
送出資料: {'token': 'abc123', 'water_quality': 7.1, 'humidity': 66.7, 'temperature': 28.2, 'light': 42.6, 'recorded_at': '2026-05-13T21:33:42.168665+08:00'}
狀態碼: 201
回傳: {'ok': True, 'message': '生態感測資料已成功寫入', 'data': {'id': 13, 'water_quality': 7.1, 'humidity': 66.7, 'temperature': 28.2, 'light': 42.6, 'status': 'normal', 'recorded_at': '2026-05-13T21:33:42.168665+08:00'}}

2026年4月11日 星期六

[水井USR] 玉米籜和蓪草紙創作網站上架到Pythonanywhere

網址:https://fiberheart.pythonanywhere.com/





前一篇介紹如何用Vibe Desige設計工具:Google Stitch初體驗-設計玉米籜和蓪草紙創作網站

另外利用Grok、Gemini、ChatGPT把Stitch滙出的程式轉成Django程式,並佈署在Pythonanywhere,影片和圖片則使用Google Flow工具來協助產生。

2026年4月4日 星期六

[水井USR] 在Pythonanaywhere上用Django實作玉米籜作品網站

 



1.seniorartlab/gallery/models.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# gallery/models.py
from django.db import models

class Artwork(models.Model):
    title = models.CharField(max_length=100)
    description = models.TextField(blank=True)

    # 圖片改成可選,這樣可以做純影片作品
    image = models.ImageField(upload_to='artworks/', blank=True, null=True)

    # 新增 mp4 影片欄位
    video = models.FileField(upload_to='videos/', blank=True, null=True)

    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

2.seniorartlab/gallery/admin.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# gallery/admin.py
from django.contrib import admin
from .models import Artwork

@admin.register(Artwork)
class ArtworkAdmin(admin.ModelAdmin):
    list_display = ('title', 'created_at', 'has_image', 'has_video')

    def has_image(self, obj):
        return bool(obj.image)
    has_image.short_description = '有圖片'

    def has_video(self, obj):
        return bool(obj.video)
    has_video.short_description = '有影片'

3.seniorartlab/gallery/views.py

1
2
3
4
5
6
7
# gallery/views.py
from django.shortcuts import render
from .models import Artwork

def home(request):
    artworks = Artwork.objects.all().order_by('-created_at')
    return render(request, 'gallery/home.html', {'artworks': artworks})

4.seniorartlab/gallery/urls.py

1
2
3
4
5
6
7
# gallery/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='home'),
]

5.seniorartlab/seniorartlab/urls.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# seniorartlab/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('gallery.urls')),  # 前台首頁與作品
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

6.seniorartlab/seniorartlab/settings.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
"""
Django settings for seniorartlab project.

Generated by 'django-admin startproject' using Django 5.1.3.

For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-&6icjhr7uh0dw!5i^5*w0=vn1*4%va_lr14@12b3spl%i=t=vn"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['seniorartlab.pythonanywhere.com']


# Application definition

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    'gallery',
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "seniorartlab.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / 'templates'],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "seniorartlab.wsgi.application"


# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}


# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]


# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/

STATIC_URL = "static/"

# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# default static files settings for PythonAnywhere.
# see https://help.pythonanywhere.com/pages/DjangoStaticFiles for more info
MEDIA_ROOT = '/home/seniorartlab/seniorartlab/media'
MEDIA_URL = '/media/'
STATIC_ROOT = '/home/seniorartlab/seniorartlab/static'
STATIC_URL = '/static/'
# 媒體檔案設定
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

STATIC_URL = '/static/'

7.seniorartlab/templates/gallery/home.html


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
<!-- gallery/templates/gallery/home.html -->
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SeniorArtLab 作品展示</title>
    <style>
        * {
            box-sizing: border-box;
        }

        body {
            margin: 0;
            font-family: "Microsoft JhengHei", sans-serif;
            background: #f5f7fb;
            color: #333;
        }

        .page-container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 40px 20px;
        }

        .page-title {
            text-align: center;
            font-size: 32px;
            margin-bottom: 10px;
            color: #222;
        }

        .page-subtitle {
            text-align: center;
            color: #666;
            margin-bottom: 36px;
            font-size: 16px;
        }

        .artwork-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
            gap: 24px;
            align-items: start;
        }

        .artwork-card {
            background: #fff;
            border-radius: 18px;
            overflow: hidden;
            box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
            transition: transform 0.2s ease, box-shadow 0.2s ease;
        }

        .artwork-card:hover {
            transform: translateY(-4px);
            box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
        }

        .artwork-media {
            width: 100%;
            height: auto;
            display: block;
            background: #e9ecef;
        }

        .artwork-content {
            padding: 18px;
        }

        .artwork-title {
            font-size: 20px;
            margin: 0 0 10px;
            color: #1f2937;
            line-height: 1.4;
        }

        .artwork-description {
            font-size: 14px;
            line-height: 1.7;
            color: #555;
            margin-bottom: 14px;
            white-space: pre-line;
        }

        .artwork-meta {
            font-size: 12px;
            color: #888;
            border-top: 1px solid #eee;
            padding-top: 10px;
        }

        .empty-box {
            text-align: center;
            background: #fff;
            padding: 50px 20px;
            border-radius: 16px;
            box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06);
            color: #777;
        }

        @media (max-width: 576px) {
            .page-title {
                font-size: 26px;
            }

            .artwork-grid {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
    <div class="page-container">
        <h1 class="page-title">SeniorArtLab 作品展示</h1>
        <p class="page-subtitle">圖片與影片作品展示牆</p>

        {% if artworks %}
            <div class="artwork-grid">
                {% for artwork in artworks %}
                    <div class="artwork-card">

                        {% if artwork.video %}
                            <video class="artwork-media" controls preload="metadata">
                                <source src="{{ artwork.video.url }}" type="video/mp4">
                                您的瀏覽器不支援影片播放。
                            </video>
                        {% elif artwork.image %}
                            <img src="{{ artwork.image.url }}" alt="{{ artwork.title }}" class="artwork-media">
                        {% endif %}

                        <div class="artwork-content">
                            <h2 class="artwork-title">{{ artwork.title }}</h2>

                            <div class="artwork-description">
                                {% if artwork.description %}
                                    {{ artwork.description }}
                                {% else %}
                                    暫無作品說明
                                {% endif %}
                            </div>

                            <div class="artwork-meta">
                                上傳時間:{{ artwork.created_at|date:"Y-m-d H:i" }}
                            </div>
                        </div>
                    </div>
                {% endfor %}
            </div>
        {% else %}
            <div class="empty-box">
                <p>目前沒有作品</p>
            </div>
        {% endif %}
    </div>
</body>
</html>