iT邦幫忙

2

從韌體跨足軟體領域的學習歷程-1

  • 分享至 

  • xImage
  •  

大學期間大部分都專攻韌體領域的技術與知識,就連大四也都是在實習韌體工程師,對於軟體領域的點點滴滴基本上可以說是一竅不通,偏偏研究所的選擇到純軟體的實驗室進行學習,為了彌補軟體領域基本常識不足的問題,我設定了一些題目來練習,希望可以藉由「做中學」快速認識這個領域!

由於大學課堂上有接觸過HTML、CSS與JS的皮毛,對於前端有非常粗淺的認識,而後端則是完全沒有接觸過,因此決定先學習後端資料處理,在網路上看過許多案例之後想先學習透過Python做資料擷取,再使用SQL將資料保存下來方便後續應用或分析,廢話不多說,馬上進入正題~


本次目標:取得全台測速照相執法設置點

  • 資料來源:政府資料開放平台
    https://ithelp.ithome.com.tw/upload/images/20240223/20165452IjkO2UEFO7.png

  • 觀察資料後找到下載網址
    https://ithelp.ithome.com.tw/upload/images/20240223/20165452ChWAMupcU6.png

  • 檢視資料的內容,由圖中可以看到總共有2189筆資料,每一筆資料都有包含「設置縣市」、「設置市區鄉鎮」、「設置地址」、「管轄警局」、「管轄分局」、「經度」、「緯度」、「拍攝方向」以及「速限」
    https://ithelp.ithome.com.tw/upload/images/20240301/201654526TQ0bq3BWZ.png

  • 找到資料來源同時確認資料符合需求後,接著就要思考資料如何儲存。在網路上看了一些教學後我感覺SQL Server比較好入門,因此我事先安裝了SQL Server並選擇Azure Data Studio做為開發環境。關於安裝SQL Server的流程與注意事項可以參考這篇文章

  • 根據使用需求創建資料表以存放測速執法設置點的資料

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Speedlimit](
	[SID] [bigint] NOT NULL,
	[x] [decimal](9, 6) NOT NULL,
	[y] [decimal](9, 6) NOT NULL,
	[direction] [nvarchar](max) NOT NULL,
	[speedlimit] [int] NOT NULL,
	[lastmodified] [datetime2](0) NOT NULL,
	[insertDT] [datetime2](0) NOT NULL,
	[status] [bit] NOT NULL,
 CONSTRAINT [PK_Speedlimit] PRIMARY KEY CLUSTERED 
(
	[SID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
ALTER TABLE [dbo].[Speedlimit] ADD  CONSTRAINT [DEFAULT_Speedlimit_status]  DEFAULT (0x01) FOR [status]
GO
  • 為了後續插入資料方便,順道學習一下Store Procedure的用途與寫法
  • 雖然政府資料開放平台中測速執法設置點標記為每日更新,但我猜想資料實際變動應該不會這麼頻繁,因此於procedure中我會去比對資料,假如此次取得的資料與資料庫中的資料相同的話,我只會將該筆資料的「lastmodified」修改為系統時間
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE procedure [dbo].[sp_InsertSpeedlimit](
    @sid bigint = null,
    @x decimal(9, 6) = null,
    @y decimal(9, 6) = null,
    @direction nvarchar(MAX) = null,
    @speedlimit int = null
)
as
begin
    declare @now datetime2(0) = GETDATE();

    if not exists(select 1 from Speedlimit where x = @x and y = @y and direction = @direction and speedlimit = @speedlimit)
    begin
        insert into Speedlimit(sid, x, y, direction, speedlimit, lastmodified, insertDT)
        values(@sid, @x, @y, @direction, @speedlimit, @now, @now)
    end

    else
    begin
        update Speedlimit set lastmodified = @now, status = 0x01 
        where sid = (select sid from Speedlimit where x = @x and y = @y and direction = @direction and speedlimit = @speedlimit);
    end
end
GO
  • 在檔案總管中新增資料夾並創建「Main.py」
  • 透過VS Code開發Python,關於使用VS Code開發Python可以參考這篇文章
  • 在「Main.py」中程式碼主要有一個主程式掌控所有動作,主程式中會列印開始執行的時間,之後呼叫API取得資料並加以處理,最終透過procedure將資料存進資料庫中;呼叫API時若出現錯誤會寄信通知
from datetime import datetime
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import requests
import time
import json
import pyodbc

# 出現錯誤時要發送信件時呼叫
def send_email(subject, body):
    sender_email = "chu891106@gmail.com"
    receiver_email = "chu891106@gmail.com"
    app_password = "gwlv osuk kdjz zwuw"

    message = MIMEMultipart()
    message["From"] = sender_email
    message["To"] = receiver_email
    message["Subject"] = subject
    body_text = MIMEText(body, "plain")
    message.attach(body_text)

    try:
        with smtplib.SMTP("smtp.gmail.com", 587) as server:
            server.starttls()
            server.login(sender_email, app_password)
            server.sendmail(sender_email, receiver_email, message.as_string())
        print("電子郵件通知發送成功。")
    except Exception as e:
        print(f"發送電子郵件通知時出錯:{str(e)}")


# 呼叫API
def call_api(open_api):
    try:
        packet = None
        while packet is None:
            try:
                response = requests.get(open_api)
                if response.status_code == 200:
                    data = response.json()
                    packet = json.loads(json.dumps(data, ensure_ascii=False))
                    return packet
                else:
                    print(f"請求發生錯誤,HTTP狀態碼:{response.status_code}")
                    time.sleep(3)
            except requests.exceptions.RequestException as e:
                print(f"請求發生錯誤:{str(e)}")
                time.sleep(3)
    except Exception as e:
        error_subject = "Python腳本中的錯誤"
        error_message = f"腳本中發生錯誤:\n\n{str(e)}"
        send_email(error_subject, error_message)

# 處理資料
def proc_points(data_list):
    packet = []
    if data_list["success"]:
        data_list = data_list["result"]["records"][1:]
        if data_list:
            for index, point in enumerate(data_list):
                packet.append(
                    {
                        "sid": datetime.now().strftime("%Y%m%d") + str(index + 1),
                        "y": point["Latitude"],
                        "x": point["Longitude"],
                        "direction": point["direct"],
                        "speedlimit": point["limit"],
                    }
                )
            return packet

# 透過procedure儲存資料
def save_to_NewsHandle(data_list, server_config_path="server.json"):
    def handle_error(function_name, error):
        error_subject = f"Error in {function_name} function"
        error_message = (
            f"An error occurred in the {function_name} function:\n\n{str(error)}"
        )
        send_email(error_subject, error_message)
        print(f"An error occurred in {function_name} function: {str(error)}")

    try:
        with open(server_config_path, "r") as config_file:
            server_config = json.load(config_file)

        connection_string = (
            f"DRIVER={{{server_config['drv']}}};"
            f"SERVER={server_config['srv']};"
            f"DATABASE={server_config['db']};"
            f"UID={server_config['uid']};"
            f"PWD={server_config['pwd']}"
        )
        connection = pyodbc.connect(connection_string)
        cursor = connection.cursor()

        query = """
            exec NewsHandle.dbo.sp_InsertSpeedlimit
                @sid = ?,
                @x =?,
                @y =?,
                @direction =?,
                @speedlimit =?
        """
        new_order = ["sid", "x", "y", "direction", "speedlimit"]
        transformed_data_list = [
            tuple(data[field] for field in new_order) for data in data_list
        ]
        cursor.executemany(query, transformed_data_list)
        connection.commit()
    except Exception as e:
        handle_error("exec_procedure", e)
    finally:
        if cursor:
            cursor.close()
        if connection:
            connection.close()

# 主程式
if __name__ == "__main__":
    print(f'開始更新測速照相 -> {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')

    speedlimit = call_api(
        "https://od.moi.gov.tw/api/v1/rest/datastore/A01010000C-000674-011"
    )
    speedlimit = proc_points(speedlimit)
    save_to_NewsHandle(speedlimit)

    print(f'測速照相更新完成 -> {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
    print("-" * 50)
    print()

成果
https://ithelp.ithome.com.tw/upload/images/20240302/20165452rWq7PeCgIJ.png

心得
此次的練習內容可能看起來沒什麼,但作為一個新上路的菜鳥在過程中可以說是相當崎嶇,光是安裝作業環境就遇到許多問題,幸好網路上有許多先進前輩的文章可以參考,特別感謝一些大神與大老不厭其煩看我敘述問題並予以解答!另外在開發過程印象最深刻的是「Python行結尾不能打;」,這一點對我來說有點苛刻,以前在用寫C++韌體時已經養成結果打「;」的習慣了,這一點需要花一些時間來調整。我自知自己能力與認知淺薄,因此決定將學習過程分享出來當作一個紀錄,若有寫得不好、做得不好的地方也歡迎各位指教,這樣我才能更快提升。


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言