# 程式碼實作 **2 hr** [ToC] ## 1. Django 設定 1. 安裝套件 ``` $ pip install Django ``` ``` $ pip install line-bot-sdk ``` ``` $ pip install pymysql ``` 2. 建立專案 ``` $ django-admin startproject '專案名稱' ``` ``` cd '專案名稱' ``` 3. 建立 APP:可以進行多個 Line Bot 開發 ``` $ python manage.py startapp 'APP名稱' ``` 4. 新建兩個資料夾 ``` $ md static $ md templates ``` static 用於靜態資料如圖片、檔案 templates 用於放寫好的 html ## 2. 在 `settings.py` 進行 1. 在最上面 ```python import pymysql pymysql.install_as_MySQLdb() ``` 2. 設定 Channel Access Token & Channel Secret 第 22 行左右的地方 ```python= CHANNEL_ACCESS_TOKEN = 'Channel_access_token' CHANNEL_SECRET = 'Channel_secret' ``` 3. 於 INSTALLED_APPS 中設定建立的 APP 名稱 ```python= INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', '【app】' #新增 app 名稱 ] ``` 4. 於 TEMPLATES 中設定 templates 的資料夾路徑 ```python 'DIRS'[os.path.join(BASE_DIR,'templates')] ``` 5. staitc ```python STATIC_URL = '/static' STATICILES_DIRS = [os.path.join(BASE_DIR,'static')] ``` 6. 語言及時區設定 ## 利用 Django 連接資料庫 (共 7 個步驟) 1. `settings.py` ```python= DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': '【your DB name】', 'USER': '【your user name】', 'PASSWORD': '【your password】', 'HOST': '', 'PORT': '3306', } } ``` 2. debug > 如果MySQL 服務器配置為使用 caching_sha2_password 或 sha256_password 作為身份驗證插件時,PyMySQL(這是 Django 用來與 MySQL 通信的一個 Python 庫)需要 cryptography 套件來加密密碼。 ``` $ pip install cryptography ``` 3. 同步資料庫 ``` $ python manage.py inspectdb > 【your_app_name】/models.py ``` 4. 檢查 `model.py` 有沒有你的 database 5. `admin.py` (後台資料) 5-1 from `models.py` import the database table ```python= from django.contrib import admin # Register your models here. from 【your_app_name】.models import * #【below is optional,想要做 5-2 再做就可以】 class user_message_admin(admin.ModelAdmin): list_display = ('uid', 'name', 'message', 'time') admin.site.register(user_message, user_message_admin) ``` 5-2 optional 若我們想從 Linebot 中記錄用戶 ID 以及訊息內容 ```python= #from django.db import models # Create your models here. class user_message(models.Model): uid = models.CharField(max_length=50, null=False, primary_key=True) # user id name = models.CharField(max_length=50, blank=True, null=False) # LINE 名字 message = models.CharField(max_length=600, blank=True, null=False) # 文字訊息紀錄 time = models.DateTimeField(auto_now=True) # 日期時間 def __str__(self): return self.name # print the user name in terminal ``` 7. 更新資料庫 之後在 mysql workbench 有異動都可以直接輸這個更新 ``` $ python manage.py makemigrations ``` ``` $ python manage.py migrate ``` 7-1. debug 有可能出現 >SyntaxError: source code string cannot contain null bytes - 意思是有 null bytes 出現,而非一般的空格(ex. a + b,就屬於一般空格) - 解決方式: (1) 下載 hex editor (發行者 :microsoft) (2) 右鍵點選檔案>開啟方式> 16 進位編輯器 > crtl+F 查找`00` $\quad$在 16 進位中,一般空格應顯示`20` $\quad$範例圖示如下 STEP1 ![image](https://hackmd.io/_uploads/S16fIhqA6.png) STEP2 ![image](https://hackmd.io/_uploads/BkZr8nc0T.png) STEP 3 注意要選擇「以二進位模式搜尋」(會自動變成 16 進位不用擔心) ![image](https://hackmd.io/_uploads/BkTKI290a.png) (3) TIP $\quad$因為 `model.py` 的 table 都是從 mysql 導入,比較有可能出錯,所以可以先從這邊下手 (4) $\quad$ 4-1 找到有錯的檔案之後,用文字編輯器打開,複製所有程式。 $\quad$$\quad$$\quad$重新建立一個`.py`檔案,貼上 `models.py` 的內容。 $\quad$ 4-2 貼上後一樣用 16 進位的方式確認沒有出現 null bytes (`00`) $\quad$ 4-3 用新檔案取代舊的 `models.py` (一樣的路徑、一樣的名字) (5) 重新輸入 `$ python manage.py makemigrations`,確認沒有問題 - 這樣就 debug 結束了~ ## 資料表操作 1. `views.py` 2. 將` models.py` 資料表匯入 `views.py` ```python from 【your_app_name】.models import * ``` 3. 修改 `views.py` ```python= from django.shortcuts import render # Create your views here. from app.models import * import re import logging from django.conf import settings from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden from django.views.decorators.csrf import csrf_exempt from linebot import LineBotApi, WebhookParser from linebot.exceptions import InvalidSignatureError, LineBotApiError from linebot.models import MessageEvent, TextSendMessage from .models import SnakeSpecies, Product, CareRequirements line_bot_api = LineBotApi(settings.CHANNEL_ACCESS_TOKEN) parser = WebhookParser(settings.CHANNEL_SECRET) # 設置日誌 logger = logging.getLogger(__name__) @csrf_exempt def callback(request): if request.method != 'POST': return HttpResponseBadRequest() signature = request.META.get('HTTP_X_LINE_SIGNATURE', '') body = request.body.decode('utf-8') # 日誌輸出 logger.info(f"Request body: {body}") logger.info(f"Signature: {signature}") try: events = parser.parse(body, signature) except InvalidSignatureError: logger.error("Invalid signature error") return HttpResponseForbidden() except LineBotApiError as e: logger.error(f"LineBotApiError: {str(e)}") return HttpResponseBadRequest() for event in events: if isinstance(event, MessageEvent): handle_message(event) return HttpResponse('OK') def handle_message(event): message_text = event.message.text.strip() if "蛇的學名" in message_text: scientific_name = extract_scientific_name(message_text) species_exists = SnakeSpecies.objects.filter(scientific_name=scientific_name).exists() if species_exists: reply_text = f"蛇類 {scientific_name} 已經存在於資料庫中。" else: reply_text = f"您所輸入的蛇類不存在,請您建立新的檔案。若要建立,請回覆 OK" line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_text)) elif "new" in message_text: # 调用 handle_snake_species 并传递 message_text,这个函数负责发送回复 handle_snake_species(event, message_text) elif "【學名】" in message_text: handle_read(event, message_text) elif "刪除產品編號" in message_text: handle_delete(event, message_text) elif "更新" in message_text: handle_update(event, message_text) def extract_scientific_name(text): # 假设用户会按照【學名】snake的格式输入 pattern = r'蛇的學名[::](.+)' match = re.search(pattern, text) return match.group(1) if match else None def extract_scientific_name2(text): # 假设用户会按照【學名】snake的格式输入 pattern = r'【學名】(\w+\s?\w*)' match = re.search(pattern, text) return match.group(1) if match else None def handle_snake_species(event, message_text): data = parse_data(message_text) if data: species, created = SnakeSpecies.objects.get_or_create( scientific_name=data['scientific_name'], chinese_name=data['chinese_name'], defaults={'amount': data['amount']} ) if created: #reply_text = f"儲存成功,蛇類編號為 {species.id}" //這個有錯,好像讀不出來整個掰掰 reply_text = "儲存成功" else: reply_text = "儲存失敗" else: reply_text = "儲存失敗,請確認訊息格式是否有誤" line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_text)) def parse_data(text): pattern = r'.*\n【學名】(\w+)\n【中文名稱】(\S+)\n【數量】(\d+)' match = re.search(pattern, text, re.MULTILINE | re.DOTALL) if match: return { 'scientific_name': match.group(1), 'chinese_name': match.group(2), 'amount': int(match.group(3)) } return None def handle_read(event, message_text): # 提取学名 scientific_name = extract_scientific_name2(message_text) if not scientific_name: line_bot_api.reply_message(event.reply_token, TextSendMessage(text="格式错误,请重新输入。")) return # 查询数据库,获取所有匹配的 SnakeSpecies 对象 matching_species = SnakeSpecies.objects.filter(scientific_name=scientific_name) if matching_species: # 构建产品名称列表 product_names = [f"{species.chinese_name} (數量: {species.amount})" for species in matching_species] product_names_str = "\n".join(product_names) reply_text = f"以下是與學名 '{scientific_name}' 匹配的產品:\n\n{product_names_str}" line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_text)) return # 如果没有匹配的对象 line_bot_api.reply_message(event.reply_token, TextSendMessage(text="没有找到对应的蛇类。")) return def handle_update(event, message_text): pattern = r'【產品編號:(\d+)】(\d+)' match = re.search(pattern, message_text) if match: product_id, new_price = match.groups() try: # 尝试获取产品 product = Product.objects.get(product_id=int(product_id)) # 更新价格 product.price = float(new_price) product.save() reply_text = f"產品編號 {product_id} 的價格已更新為 {new_price}" except Product.DoesNotExist: reply_text = f"没有找到產品編號 {product_id} 的產品。" except Exception as e: reply_text = f"錯誤: {str(e)}" else: reply_text = "格式錯誤" # 发送回复消息 line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_text)) def handle_delete(event, message_text): # 假设用户输入格式为:“【刪除產品編號】12345” pattern = r'【刪除產品編號】(\d+)' match = re.search(pattern, message_text) if not match: line_bot_api.reply_message(event.reply_token, TextSendMessage(text="格式錯誤")) return product_id = int(match.group(1)) try: product = Product.objects.get(product_id=product_id) product.delete() reply_text = f"產品編號 {product_id} 已成功删除。" except Product.DoesNotExist: reply_text = f"沒有找到 {product_id} " except Exception as e: reply_text = f"刪除過程中出現錯誤 {str(e)}" line_bot_api.reply_message(event.reply_token, TextSendMessage(text=reply_text)) ```