最後一天我們要將爬蟲加入至Discord bot裡。昨天我們已將Discord bot的基本建構都設定完了
├─ cmds #Cog檔
│ ├─ event.py #bot的event事件✅
│ ├─ main.py #bot的主要指令✅
│ ├─ react.py #bot的指令響應訊息✅
│ └─ task.py #bot的指令任務✅
├─ core
│ ├─ bot_event.py #這裡放一些bot主要的event事件✅
│ └─ classes.py #這裡放一些需要重複使用class設定✅
│
├─ bot.py #bot本體✅
└─ setting.json #bot的設定檔✅
今天要加之前寫的爬蟲加入進來,首先我們在專案底下再建一個資料夾取名為blue_archive,把之前寫的爬蟲程式都放進來。並新增一個message.py
來處理需要回傳給Discord的Embed訊息格式。
├─ blue_archive
│ ├─ color_fetch.js #獲取文字顏色✅
│ ├─ fetch.js #抓取網頁內容✅
│ ├─ fetch.py #抓取網頁內容✅
│ ├─ message.py #新增檔案,回傳Discord Embed訊息格式
│ └─ split.py #從網頁內容中分出粗體文字的部分✅
├─ cmds #Cog檔
├─ core
├─ bot.py #bot本體✅
└─ setting.json #bot的設定檔✅
接著我們先來小小修改一下先前的檔案。以下用-
表示刪除+
表示新增
fetch.py
+ import blue_archive.split as split
...
# 讀取 JavaScript 檔案來抓取內容
- if os.path.exists('color_fetch.js'):
+ if os.path.exists('./blue_archive/color_fetch.js'):
- with open('color_fetch.js', 'r', encoding="utf-8") as f:
+ with open('./blue_archive/color_fetch.js', 'r', encoding="utf-8") as f:
- if os.path.exists('fetch.js'):
+ if os.path.exists('./blue_archive/fetch.js'):
- with open('fetch.js', 'r', encoding="utf-8") as f:
+ with open('./blue_archive/fetch.js', 'r', encoding="utf-8") as f:
...
# 讀取並分段文字
- return content, browser.current_url, colors
+ return *(split.read(category)), browser.current_url, colors
# 測試呼叫函式
- print(fetch())
這裡我們引入了先前寫的split.py
裡的read
函式來將內文的部分,分成原內文(沒有粗體字標記的)部分和粗體字的部分,然後將原本讀取檔案的位址改了一下。
並且回傳形式改為: ( 原內文 粗體字串列 當前網址 字體顏色 ),
呼叫split.read()
函式後會回傳( 原內文 粗體字串列 ),因此這裡加上一個*
字符,*(split.read())
將回傳的資料解包。
然後要開始處理要傳給Discord bot的Embed訊息格式。
message.py
import discord
import blue_archive.fetch as fetch
def Embed():
# 取得網頁內容
content, strong_list, url, colors = fetch.fetch()
# 將內容組合為單行文字
content = '\n'.join(content)
# 移除頭尾空白與換行
content = content.strip(' \n')
# 找出第一張圖片網址並將網址儲存
img_start = content.find('https://')
img_end = content.find('.png') + 4
img = content[img_start:img_end]
# 移除內文中的所有圖片網址
while True:
if 'https://' in content:
img_start = content.find('https://')
img_end = content.find('.png') + 4
content = content.replace(content[img_start:img_end] + '\n', '', 1)
else:
break
# 將強調文字加上粗體標記
end = 0
for strong_text in strong_list:
content = content[:end] + content[end:].replace(strong_text, f'**{strong_text}**', 1)
end = end + content[end:].find(f'**{strong_text}**') + len(strong_text) + 4
content = content.replace('****', '')
# 分割文字取得各字段
content_list = content.split('\n')
title = content_list[0]
time_stamp = content_list[1][:-3]
footer = "ブルアカ" + '・' + time_stamp
content = '\n'.join(content_list[2:])
# 從顏色列表中刪除黑色
colors = [i for i in colors if i != 'rgb(0, 0, 0)']
# 如果顏色列表不為空
if colors:
# 選擇第一種顏色
rgb = colors[0]
# 從顏色列表中刪除紅色
colors = [i for i in colors if i != 'rgb(255, 0, 0)']
# 如果顏色列表不為空
if colors:
# 選擇第一種顏色
rgb = colors[0]
# 將RGB字符串轉換為整數值
r, g, b = map(int, rgb[4:-1].split(","))
hex_int = r << 16 | g << 8 | b
# 設置顏色值
color = hex_int
else:
# 如果顏色列表為空,則設置默認顏色為青色
color = 0x00FFFF
# 建立並回傳 Embed 物件
embed = discord.Embed(title=title, description=content,
color=color, url=url)
embed.set_image(url=img)
embed.set_footer(text=footer,
icon_url="https://pbs.twimg.com/profile_images/1509908445054443527/ObBk7aEE_400x400.png")
return embed, url
以上程式先用先前寫好的fetch()
函式取得相關的網頁內容後,分別針對內文、圖片、粗體字、字體顏色進行處理,轉換為Discord接受的資料形式。
**{內文}**
的形式,而由於Discord在傳輸文字時如果有連續的粗體字標記為**{內文}****{內文}**
這樣會出現不理想的情況,因此需要將四個連續的*
字符替換為空字元**{內文}{內文}**
。rgb(X, X, X)
的形式轉換為16進位的形式,且只取黑色與紅色以外的顏色discord.Embed()
方法建立Embed訊息格式並傳入標題、描述訊息、顏色。以及設定圖片還有訊息底部文字(footer)。然後到我們Cog資料夾下的task.py
檔案裡加上以下程式
task.py
...
import blue_archive.message as message
from concurrent.futures import ThreadPoolExecutor
# 創建一個線程池
executor = ThreadPoolExecutor(max_workers=3)
...
class Task(Cog_Extension):
...
# 創建混和指令(斜槓),暫時取名為test
@commands.hybrid_command(name="test",with_app_command=True, description="ブルアカニュース")
async def test(self, ctx):
# 延遲響應
await ctx.defer()
# 提交任務
future = executor.submit(message.Embed)
# 任務完成時呼叫
future.add_done_callback(lambda x:
self.bot.loop.create_task(ctx.send(embed=x.result()[0])))
...
這裡使用了Python的concurrent.futures模塊中的ThreadPoolExecutor類來創建一個線程池。
線程池可以用來管理多個線程,並且可以控制同時運行的線程數量。
在這個例子中,創建了一個最大工作線程數量為3的線程池,意味著在任何時候,該線程池中最多可以有3個線程同時運行。
然後在我們定義的指令裡面加上了ctx.defer()
來延遲響應,這主要是針對斜槓指令的,其原因是斜槓指令預設有一定的響應時間,如果超過這響應時間會視同失敗,所以這裡需要加上這行來延遲響應。
接著使用executor.submit(message.Embed)
提交一個(線程)任務來創建一個嵌入消息。
這主要是因為selenium在等待網頁響應時,如果不使用不同步函式來執行的話會造成Discord bot鎖死無法去處理其他指令訊息。
因此這裡做為範例使用多線程來運行爬蟲,以防止鎖死問題。
最後使用future.add_done_callback()
方法添加一個回調函數,該函式會在任務完成時調用傳入的函式。
在這個例子中,回調函數會調用我們定義的匿名函式lambda x:self.bot.loop.create_task(ctx.send(embed=x.result()[0]))
來創建一個新的任務,該任務將嵌入消息發送到上下文(ctx)的頻道中。
而bot.loop.create_task()
這個方法是用於在bot的事件循環中創建一個新的任務。這允許你在bot運行時異步執行一些操作,而不會阻塞主事件循環。
以上程式若不調用這個方法會導致bot會為了等待傳送這個嵌入式訊息而造成鎖死問題,因此才要調用這個函式來防止鎖死。
之後我們只需在discord中輸入^^test
或是/test
即可運行指令。