《赖田捕手:追加篇》第 31 天:初始化 LINE BOT on Heroku

第 31 天:初始化 LINE BOT on Heroku

事实说来可笑:别试着和任何人讨论任何关於 LINE BOT 的任何程序码。原因是,如果你这麽做,那表示你开始想念每一个人了。

~节录自《赖田捕手》第二十六章

延伸自系列文 《从LINE BOT到资料视觉化:赖田捕手》

《赖田捕手:追加篇》:

最近刚换了一份新工作。内容琐碎,压力繁重。每天我都睡眼惺忪地到公司报到。同事见状,都纷纷上前关切:「会不会交给你太多东西了?」实在是一个相当温暖体贴的工作环境。我都摸摸鼻子回说:「不会,不会,我慢慢习惯就好。」其实是不好意思告诉他们,层层叠叠的睡眼圈是家里的草泥马造成的。我在家里豢养了一群草泥马,每天回家最要紧的事就是服侍牠们吃饱喝足,并且为牠们把屎把尿。等到夜深人静,草泥马一只一只地进入梦乡的时候,我会坐在电脑桌前,任萤幕的萤光在我脸上闪烁不定,而我的指节敲打着键盘,写下一行一行建构 LINE BOT 的程序码,这时我才感到身上的疲惫渐渐舒缓,心灵平静充实。没错,我想说的是,当大家对生活感到朴实无华且枯燥的时候,不妨来试着写个 LINE BOT,或许可以为自己的日常增添不少乐趣喔。
《赖田捕手:追加篇》预计会有五天份的文章内容 (不过嘛,预计分五个星期更新),首先会快速回顾如何在 Heroku 上布署 LINE BOT,并建立资料库供 LINE BOT 进行资料存取。接着会追加介绍一些 LINE 官方提供的功能,如 QuickReply 以及 LINE Notify 等等,过程中,我们将慢慢打造出一个能够提供影像处理服务的 LINE BOT。目前设计的段落大纲如下:

第 31 天:初始化 LINE BOT on Heroku
第 32 天:QuickReply 以及 QuickReplyButton 介绍
第 33 天:妥善运用 Heroku APP 暂存空间
第 34 天:妥善运用 LINE Notify 免费推播
第 35 天:制造 Deploy to Heroku 按钮

没有问题的话,就让我们赶快进入今天的内容吧。

布署到 Heroku

这边假定读者已经有 LINE 帐号、Heroku 帐号、Git 帐号,在个人使用的电脑上,也已经准备好 Heroku CLI。没有也不打紧,第 09 天有非常详尽?的介绍,带领各位一步一步完成前置作业。而今天的内容,则主要放在如何将 LINE BOT 布署到 Heroku 上,并添加 Heroku Postgres 资料库,为往後的我们要提供的服务做准备。
为什麽会选择将 LINE BOT 布署到 Heroku 上面呢?其实如今许多公司都注意到这种网路服务的方便性以及商机,因此出现了不少相似的服务,如 AWS Lambda、Google、Azure、Netlify 等等,这些公司所提供的服务,被归类为函式即服务 (Function as a Service,FaaS),各个来头响亮,时髦前卫,功能强大。而 Heroku 所提供的,则属於平台即服务 (Platform as a Service,PaaS),其中的详细差异,各位可以在这里了解。懒得点击连结过去研究也没关系,一句话言简意赅地说,FaaS 比 PaaS 能够较简单的做到更高的资源利用率。而这个差异或许也造就了 Heroku 最为人诟病的缺点:免费 dyno 的 30 分钟闲置入睡设定。
那麽为什麽今天我们还要学着如何将 LINE BOT 放到 Heroku 上面呢?我认为 Heroku 在介面设计上做的很好,简单优雅,一目了然。在 Heroku 上面布署 LINE BOT,你将可以慢慢体会到 LINE 这个平台是如何将我们的 LINE BOT 与使用者连系再一起的,整个资料的流通是如何,整个事件的触发是如何,等等的。反则,若是在 AWS Lambda 上面布署 LINE BOT,对於初学的人来说,或许一开始就会被 AWS 过於庞大的架构给唬住,在各种设定上忙得晕头转向,而没有时间细细品味 LINE 的运作流程了。以上内容纯属个人言论,不代表本台立场?

LINE 运作方式

好的,讲了老半天,那到底 LINE BOT 是怎麽运作的呢?大家可以先看一下图一

https://ithelp.ithome.com.tw/upload/images/20201220/20120178hCriNlvjN2.png
图一、LINE BOT 运作方式

我们将 LINE BOT 布署到 Heroku 上面,而使用者透过 LINE 所提供的平台来和我们的 LINE BOT 进行互动。当使用者对 LINE BOT 发送讯息时,讯息会先经过 LINE 平台。而 LINE 平台像一个讯息转运处,会将讯息转传至 Webhook URL 指定的地方,在我们的例子里,就是 Heroku。
要让我们布署在 Heroku 上面的 LINE BOT 能够成功回覆讯息,我们要准备好 LINE BOT 的运作逻辑,以及用来核对身分的 LINE BOT 个人资料。当使用者的讯息透过 LINE 平台送到 Heroku 上面来时,我们的 LINE BOT 必须要亮出身分证,也就是CHANNEL_ACCESS_TOKEN以及CHANNEL_SECRET,核对成功,才能够将回覆讯息传回到 LINE 平台。同样的,LINE 平台作为中继站,会再将讯息传到使用者端。如此,就完成整个讯息传递的流程。

准备设定档

因此,要让我们的 LINE BOT 在 Heroku 上面运作,首先要提供几个最基本的设定档,包括runtime.txtrequirements.txt,以及Procfile。要注意的是,这几个档案的档名不能有错,否则 Heroku 会认不出来,造成布署失败。那麽就来看看这些档案到底是做了什麽设定。

  • runtime.txt
python-3.8.6

runtime.txt这个档案是要告诉 Heroku 该用什麽语言以及版本来执行我们的程序,也就是 LINE BOT。以上面内容为例,我希望 Heroku 用 Python 3.xx 版来执行接下来要布署的 LINE BOT。关於 Heroku 能够提供的 Python 执行版本,大家可以在这边找到。另外一点比较特别的是,如果不想指定 Python 版本的话,就要忽略runtime.txt这个档案,也就是不要准备runtime.txt

  • requirements.txt
Flask==1.1.2
gunicorn==20.0.4
line-bot-sdk==1.17.0
psycopg2==2.8.6
numpy==1.19.4
Pillow==8.0.1

requirements.txt这个档案是要请 Heroku 帮我们安装几个需要用到的资源库。Flask是要帮我们处理网路框架的资源库,gunicorn是有关服务器运行的资源库,而line-bot-sdk当然是有关 LINE BOT 的资源库。psycopg2是 Pyhon 用来操作 SQL 资料的资源库,numpy是 Python 用来做数据处理非常好用的资源库,而Pillow则是 Python 用来做简易影像处理的资源库。若大家将来在实作 LINE BOT 时,有需要用到其他资源库,那就可以把它们摆到requirements.txt这个档案里面,请 Heroku 帮我们安装好。据说 Heroku 没有特别限制可用的安装空间,不过官方建议不要超过 600 MB (可参考这里)。

  • Procfile
web: gunicorn 你 Python 执行档案的名字:app --preload

Procfile则是告诉 Heroku 该执行什麽程序。以我而言,等等我希望能够执行的 Python 档案档名是Alma.py,所以Procfile就写下:

web: gunicorn Alma:app --preload

准备 LINE BOT 样板程序码

好了,该准备的设定档都有了,现在我们可以来撰写真正让 LINE BOT 运作起来的程序码了:

  • Alma.py
# 初始化LINT BOT
import os

from flask import Flask
from linebot import LineBotApi, WebhookHandler

app = Flask(__name__)

line_bot_api = LineBotApi(os.environ['CHANNEL_ACCESS_TOKEN'])
handler = WebhookHandler(os.environ['CHANNEL_SECRET'])

# 利用 handler 处理 LINE 触发事件
from linebot.models import MessageEvent, TextMessage, TextSendMessage

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(
        event.reply_token, TextSendMessage(text=f"Hello {line_bot_api.get_profile(event.source.user_id).display_name}!")
    )

# 利用 route 处理路由
from flask import request, abort
from linebot.exceptions import InvalidSignatureError

@app.route("/callback", methods=['POST'])
def callback():
    signature = request.headers['X-Line-Signature']

    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'OK'

以上是一个最简单的 LINE BOT 样板程序码。我们会先利用这个样板程序码来确认 LINE BOT 能不能顺利运作,之後再从这个样板程序码出发,为我们的 LINE BOT 加油添醋,装备各种功能。样板程序码看起来长,但也不难理解,大略可分成三个部分,初始化 LINE BOT、handler处理 LINE 的触发事件、以及route处理路由。
初始化 LINE BOT 是利用

app = Flask(__name__)

建构出网路框架,并利用

line_bot_api = LineBotApi(os.environ['CHANNEL_ACCESS_TOKEN'])
handler = WebhookHandler(os.environ['CHANNEL_SECRET'])

来确认 LINE BOT 的身分,以便跟我们在 LINE 上面申请的聊天机器人帐号做连接。

而用handler来处理 LINE 触发事件的方式也相当直觉:

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):

白话翻译就是,当有MessageEvent而且是TextMessage传过来到 LINE BOT 这边时,请执行下面定义的这个函式,也就是handle_message(event)

最後一部分,用route来处理路由,这边的程序码看起来七零八落,长长一串不好理解,不过大意就是,我们要建立一个路由,专门接收 LINE 送过来的事件。这边我们用来接收 LINE 事件的路由是"/callback"。这只是一个名称,你要取什麽都可以,但习惯上大家会用"/callback"

推向 Heroku

好啦,该有的档案都准备好了以後,我们就可以将这些档案传送到 Heroku 上面,请 Heroku 帮我们执行,让 LINE BOT 动起来。最後检查一次我们现在有哪些档案,以我为例,我想把appendix这个资料夹传送到 Heroku 上面,因此资料夹里面的资料结构应该要如这边所示:

D:\appendix>tree /f
Folder PATH listing
Volume serial number is 9C33-6XXD
D:.
runtime.txt
requirements.txt
Procfile
Alma.py

No subfolders exist

确认没问题之後,登入 Heroku,申请一个 APP 空间:

D:\appendix>heroku login –i

申请一个 APP 空间,名称可以自己取:

D:\appendix>heroku create –a 你-APP-的名字

Heroku 会先检查是否有重复的 APP 名称,若无,则会开始准备空间并初始化该 APP。完成了以後,我们就可以用版本控制系统 Git 来将我们指定的资料夹送到 Heroku了。

首先初始化 Git:

D:\appendix>git init

将资料夹与 APP 做连接:

D:\appendix>heroku git:remote –a 你-APP-的名字

将所有档案放到版本控制系统当中:

D:\appendix>git add .

提交版本:

D:\appendix>git commit –m "init"

推向 Heroku:

D:\appendix>git push heroku master

如此一来,我们就算是准备好 LINE BOT 的运作逻辑了,如图二

https://ithelp.ithome.com.tw/upload/images/20201220/20120178UMjAVl6ytQ.png
图二、LINE BOT 运作逻辑准备完成

环境变数

利用版本控制系统 Git,我们已经将 LINE BOT 的运作逻辑放到 Heroku 上面了。接下来,我们可以再用 Heroku 环境变数的方式,把 LINE BOT 的个人相关资料放在 Heroku 上面,也就是CHANNEL_ACCESS_TOKENCHANNEL_SECRET。要设定 Heroku 环境变数,可以透过两种方式,第一种是 Heroku CLI,第二种则是藉由 Heroku 控制面板。两种方式我们都来看一看:

  • Heroku CLI
    打开命令提示字元,登入 Heroku 并指定 APP 之後,输入heroku config就可以看到我们 APP 目前所有的环境变数了:
D:\appendix>heroku config –a 你-APP-的名字
=== 你-APP-的名字 Config Vars

空空如也,对吧?那是因为我们还没添加任何环境变数的设定。要设定环境变数也不困难:

D:\appendix>heroku config:set CHANNEL_ACCESS_TOKEN=your channel access token

意思是,在当前 APP 里面,新增一个环境变数,变数名称 (key) 是CHANNEL_ACCESS_TOKEN,变数数值 (value) 是your channel access token。当然这边要放入的变数数值是代表 LINE BOT 的 channel access token。那麽,变数名称一定要叫做CHANNEL_ACCESS_TOKEN吗?不用的,不过要跟我们先前的程序码对上。还记得 LINE BOT 样板程序码里面,我们是用什麽来验证 LINE BOT 的身分吗?

line_bot_api = LineBotApi(os.environ['CHANNEL_ACCESS_TOKEN'])
handler = WebhookHandler(os.environ['CHANNEL_SECRET'])

没错,就是这两行。而os.environ['CHANNEL_ACCESS_TOKEN']则可以找到 Heroku 环境变数当中变数名称为CHANNEL_ACCESS_TOKEN的变数数值。因此,如果大家想要换掉这边的变数名称,记得 LINE BOT 样板程序码也要跟着修改。
没问题的话,就把CHANNEL_SECRET也加上吧:

D:\appendix>heroku config:set CHANNEL_SECRET=your channel secret

好罗,这时候再回头看看:

D:\appendix>heroku config –a 你-APP-的名字
=== 你-APP-的名字 Config Vars
CHANNEL_ACCESS_TOKEN: your channel access token
CHANNEL_SECRET: your channel secret

是不是都有了呢!

  • Heroku 控制面板
    其实用 Heroku 控制面板来设定环境变数也是相当方便的一个做法。首先来到 Heroku 官网,登入帐号,选择欲做设定的 APP。来到 Settings 这个分页,应该可以看到 Config Vars 这个栏位,如图三。Real Config Vars 那个按钮就给他大力按下去,这样就可以开始设定我们的环境变数了。

https://ithelp.ithome.com.tw/upload/images/20201220/20120178GCMhL1TvCk.png
图三、Real Config Vars 大力按下去,开始设定环境变数。

是不是很简单呢?

最後记得到 LINE BOT 帐号设定面板里,把 Webhook URL 填上 Heroku 帮我们创造出来的路由:https://你-APP-的名字.herokuapp.com/callback

这样,我们就可以开心的和 LINE BOT 聊天了喔!

https://ithelp.ithome.com.tw/upload/images/20201220/20120178sBFyXsBipr.png
图四、你好 Alma

整理资料夹

现在 LINE BOT 已经可以成功运作了。但为了往後的扩充以及维护,或说增加程序码的可读性,我想在这个地方对Alma.py这个代表 LINE BOT 运作逻辑的档案稍做调整,将各种功能清楚的分门别类。还记得Alma.py帮我们处理了三件事,分别为

  • 初始化 LINE BOT
  • 利用handler处理触发事件
  • 利用router处理路由

现在我要把这三件事拆成三个档案。怎麽做呢?首先把Alma.py改成这样:

  • Alma.py
from app import app

if __name__ == "__main__":
    app.run()

接着在目前资料夹appendix底下开一个资料夹app,并新增一个档案__init__.py用来初始化 LINE BOT:

  • app/__init__.py
import os

from flask import Flask
from linebot import LineBotApi, WebhookHandler

app = Flask(__name__)

line_bot_api = LineBotApi(os.environ['CHANNEL_ACCESS_TOKEN'])
handler = WebhookHandler(os.environ['CHANNEL_SECRET'])

from app import routes, models_for_line

在资料夹app里面再新增一个档案models_for_line.py用来处理触发事件:

  • app/models_for_line.py
from app import handler, line_bot_api

from linebot.models import MessageEvent, TextMessage, TextSendMessage

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(
        event.reply_token, TextSendMessage(text=f"Hello {line_bot_api.get_profile(event.source.user_id).display_name}!")
    )

在资料夹app里面再新增一个档案routes.py用来处理路由:

  • app/routes.py
from app import app, handler

from flask import request, abort
from linebot.exceptions import InvalidSignatureError

# 接收 LINE 的资讯
@app.route("/callback", methods=['POST'])
def callback():
    signature = request.headers['X-Line-Signature']

    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'OK'

这样我们整个资料夹的架构就变成:

D:\appendix>tree /f
Folder PATH listing
Volume serial number is 9C33-6XXD
D:.
│   runtime.txt
│   requirements.txt
│   Procfile
│   Alma.py
│
└───app
        __init__.py
        models_for_line.py
        routes.py 

看起来是不是很简洁,而且分工有条理呢?

将整理好的资料夹推向 Heroku,并试试看 LINE BOT 是不是仍然运作如常。

挂载 Heroku Postgres

替 LINE BOT 加上资料库是一种相当常见的设计。有了资料库,使用者跟 LINE BOT 互动时,就可以让 LINE BOT 记下使用者传送的资料、使用者当前的状态、或是使用者的设定等等的,这样在回覆讯息时,就可以做到客制化、根据不同内容回覆不同条件的讯息。
而我们接下来要实现的影像处理服务,就需要一个资料库来记下使用者的种种设定。Heroku 官方提供了一个好用的关联式资料库:Heroku Postgres,让我们简单的透过添加扩充元件 (Add-ons),就可以让 Heroku APP 拥有一个资料库。现在就来快速地看一看怎麽做吧。
举一反三一下,这边作法也有两种,透过 Heroku CLI 跟 Heroku 控制面板。

  • Heroku CLI
    我们可以用heroku addons –a 你-APP-的名字先来检查一下目前使用的 APP 有没有任何扩充元件
D:\appendix>heroku addons –a
No add-ons for app 你-APP-的名字.

如果没有的话,可以用heroku addons:create heroku-postgresql:hobby-dev来增加一个采用 hobby-dev 方案的 Heroku Postgres 资料库 (怕放错扩充元件位置的话,可以用-a 你-APP-的名字来指定 APP):

D:\appendix>heroku addons:create heroku-postgresql:hobby-dev -a 你-APP-的名字

成功了以後,可以试着在输入一次heroku addons –a 你-APP-的名字

D:\appendix>heroku addons -a 你-APP-的名字
Add-on                          Plan       Price  State
──────────────────────────────  ─────────  ─────  ───────
heroku-postgresql (postgresql)  hobby-dev  free   created
 └─ as DATABASE

出现了,我们的 Heroku Postgres!

  • Heroku 控制面板
    在 Overview 这个页面上,找到 Installed add-ons 这个栏位,右边有一个 Configure Add-ons,大力点下去,如图五

https://ithelp.ithome.com.tw/upload/images/20201220/20120178NkX37FQV5M.png
图五、Configure Add-ons

搜寻 Heroku Postgres,在搜寻列中输入 H…e…r…o…k…u…P…o…s… 啊,有了!

https://ithelp.ithome.com.tw/upload/images/20201220/20120178MuoqaptjYb.png
图六、搜寻 Heroku Postgres

我们最爱当免费仔,所以就选 hobby-dev 这个版本。

https://ithelp.ithome.com.tw/upload/images/20201220/20120178fnNQIQC8xI.png
图七、hobby-dev,免费的最贵

完成了!是不是很简单呢?

初始化表格

注:表格栏位以及资料型态已於 2020.12.27 更新

我们在 第 15 天 里知道该怎麽从自己的电脑 (本机端) 发送指令到 Heroku Postgres,并在资料库当中创造表格。不过呢。如果每次要布署 LINE BOT 都要从本机端多执行这个动作,是不是很麻烦呢?因此,今天我们来试试看可不可以把这些操作包在 LINE BOT 的逻辑程序码里面,布署出去就自动创造表格。

先来写一段万用公式,每一次连接资料库时的起手式:

def access_database():
    DATABASE_URL = os.environ['DATABASE_URL']
    conn = psycopg2.connect(DATABASE_URL, sslmode='require')
    cursor = conn.cursor()
    return conn, cursor
  • 第二行:DATABASE_URL = os.environ['DATABASE_URL']
    只要我们为 APP 添加了 Heroku Postgres 扩充元件,Heroku 的就会多了一个变数名称是DATABASE_URL的环境变数,利用这一行程序码就可以拿到该变数的数值。

  • 第三行:conn = psycopg2.connect(DATABASE_URL, sslmode='require')
    利用psycopg2连接资料库。

  • 第四行:cursor = conn.cursor()
    制造一个可以操作资料库的游标。

先用 SQL 语言来介绍我们想要创造的表格,当中的栏位跟栏位限制:

注:表格栏位以及资料型态已於 2020.12.27 更新

CREATE TABLE user_dualtone_settings (
    user_id VARCHAR ( 50 ) PRIMARY KEY,
    message_id VARCHAR ( 50 ) NOT NULL,
    mode VARCHAR ( 20 ) NOT NULL,
    gradient_factor VARCHAR ( 20 ) NOT NULL,
    first_tone VARCHAR ( 20 ) NOT NULL,
    second_tone VARCHAR ( 20 ) NOT NULL
);

看懂了吗?我们想要创造一个表格,表格名称是user_dualtone_settings,用user_id作为PRIMARY KEY,这个栏位必定有值,而且同一个表格内不能有两个相同的user_id。其他栏位包括message_idmodegradient_factorfirst_tonesecond_tone。这些栏位也都必须填入资料,而填入的资料型态必须是字串VARCHAR,只是有不同的长度限制 (( 50 )或是( 20 ))。

没问题的话,就来写一个初始化表格的函式吧:

注:表格栏位以及资料型态已於 2020.12.27 更新

def init_table():
    conn, cursor = access_database()
    postgres_table_query = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'"
    cursor.execute(postgres_table_query)
    table_records = cursor.fetchall()
    table_records = [i[0] for i in table_records]

    if 'user_dualtone_settings' not in table_records:
        create_table_query = """CREATE TABLE user_dualtone_settings (
            user_id VARCHAR ( 50 ) PRIMARY KEY,
            message_id VARCHAR ( 50 ) NOT NULL,
            mode VARCHAR ( 20 ) NOT NULL,
            gradient_factor VARCHAR ( 20 ) NOT NULL,
            first_tone VARCHAR ( 20 ) NOT NULL,
            second_tone VARCHAR ( 20 ) NOT NULL
        );"""

        cursor.execute(create_table_query)
        conn.commit()

    return True
  • 第三行:SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'
    我们可以用
SELECT * FROM pg_catalog.pg_tables

来查询 Heroku Postgres (事实上所有的 PostgreSQL 都可以这麽做) 资料库里面所有的表格。同时因为我们只关注表格名称,也就是tablename,所以可以再仔细一点:

SELECT tablename FROM pg_catalog.pg_tables

同时因为pg_catalog.pg_tables会查到一些用来描述该资料库状态和设定的表格,而我们并不在意那些表格,只自私的想知道我们创造出来的表格,所以可以再仔细一点:

SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'

没错,就变成第三行的程序码了。
所以init_table()这段程序码可以帮我们做到的事,就是先检查资料库里是否存在user_dualton_settings这个表格,如果没有,就按照我们的需求,替我们建立这个表格。剩下的,就是当我们需要用到表格时,呼叫这个函式了。

今天快速的复习了一下如何在 Heroku 上初始化 LINE BOT。其中包含了许多主题,包括 LINE BOT 运作原理,Git 基本操作,Heroku CLI 基本操作,LINE BOT 身分资料,Heroku Postgres 扩充元件,SQL 语法,在 Python 中以psycopg2操作 SQL 资料库,若想了解更详细的,可以参考:

第 09 天:LINE BOT SDK:注册!注册!注册!
第 10 天:LINE BOT SDK:初始化聊天机器人
第 15 天:LINE BOT SDK:Heroku Postgres 资料库

感谢大家花时间看到这里,下一篇文章预计 12/27 更新。

那麽,今天最重要的一件事就是,感谢 iT邦帮忙 和 博硕文化,LINE Bot by Python 全攻略 集结成书了,欢迎大家有兴趣的可以参考呢!


<<:  【资料结构】DFS与BFS的追踪

>>:  Day 21 - 天眼CNN 的耳朵和嘴巴 - RNN(2) -LSTM

Day 16: AWS Config简介

What is AWS Config? AWS Config是一项能让你随时监控你组态(Config...

[DAY 01] 线上测验的难点与可能降低作弊的方法

因为疫情的关系,学校从5月中旬就开始了远(ㄕㄨˇ)距(ㄐㄧㄚˋ) 各级学校除了最开始的设备、网路问题...

EP 04 - gem 起手式之环境设定

Youtube 频道:https://www.youtube.com/c/kaochenlong ...

[Day05]程序菜鸟自学C++资料结构演算法 – 阵列Array List实作之二

前言:昨天介绍了如何建立专案、建立空阵列、读取存放资料及修改储存空间,今天要继续实作阵列的其他功能。...

JavaScript Day15 - event(2)

event 查目前网页的 event,开启 Chrome 的开发者工具,点选 Elements,之後...