iT邦幫忙

2023 iThome 鐵人賽

DAY 30
0
Software Development

selenium爬蟲應用至discord bot系列 第 30

[DAY30]Discord bot取得網頁消息 - 4

  • 分享至 

  • xImage
  •  

最後一天我們要將爬蟲加入至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接受的資料形式。

  1. 首先先將內文做適當的換行,再將整個內文頭尾多餘的空白與換行移除。
  2. 接著取出內文中的第一張圖片,其他的圖片網址都移除只留文字。
  3. 使用粗體字串列,找出內文中需標記粗體字的位置,將其替換為**{內文}**的形式,而由於Discord在傳輸文字時如果有連續的粗體字標記為**{內文}****{內文}**這樣會出現不理想的情況,因此需要將四個連續的*字符替換為空字元**{內文}{內文}**
  4. 從內文取得標題與時間戳,方便之後為Embed訊息加上footer
  5. 將顏色進行資料轉換,從rgb(X, X, X)的形式轉換為16進位的形式,且只取黑色與紅色以外的顏色
  6. 最後使用discord模組裡的discord.Embed()方法建立Embed訊息格式並傳入標題、描述訊息、顏色。以及設定圖片還有訊息底部文字(footer)。
  7. 然後將建好的embed訊息格式以及URL回傳

然後到我們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即可運行指令。


上一篇
[DAY29]Discord bot取得網頁消息 - 3
系列文
selenium爬蟲應用至discord bot30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言