iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
Software Development

下載器不是寫完就能跑:一個 HLS 分段式串流工具的架構與測試修練系列 第 2

Day 02:每個來源站的頁面結構都不一樣,抽象層要切在哪裡

  • 分享至 

  • xImage
  •  

前言:多支援一個來源站,就要多寫一份爬蟲嗎?

「反正每個網站長得不一樣,乾脆一個網站寫一支獨立的爬蟲程式,簡單暴力。」

這個想法短期最快,長期最痛。如果每個來源站各寫一支完全獨立的程式,共同流程(取得頁面、找出內容項目清單、逐項目取得 M3U8 位置)就會被複製貼上好幾份——某個共同流程要修,得記得每一份都要改一次;漏改一份,就是一個沒人發現的 bug。

今日目標

  • 看清楚「每個來源站不一樣」的部分,跟「所有來源站都一樣」的部分,邊界在哪裡
  • 認識 Template Method 這個模式解決的具體問題
  • 看一組「各自獨立 vs 共用骨架」的對照範例

相同的流程骨架,不同的細節填空

把處理流程攤開來看:取得頁面 → 解析出內容項目清單 → 逐一取得每個項目的 M3U8 位置 → 產出結果。這個骨架在所有來源站之間是一樣的,會變的只有其中一步:怎麼從頁面裡找出內容項目清單——因為每個站台的 HTML 結構、CSS class 命名都不同。

❌ vs ✅:獨立實作 vs 共用骨架

❌ 反例:每個來源站各寫一支完整流程

class SiteAScraper:
    async def run(self, url):
        html = await self._get_html(url)
        soup = BeautifulSoup(html, 'html.parser')
        items = soup.select(".site-a-list a")  # 站台 A 專屬
        results = []
        for item in items:
            m3u8 = await self._extract_m3u8(item['href'])
            results.append(m3u8)
        return results

class SiteBScraper:
    async def run(self, url):
        html = await self._get_html(url)  # 跟上面重複
        soup = BeautifulSoup(html, 'html.parser')
        items = soup.select(".site-b-menu li a")  # 站台 B 專屬
        results = []
        for item in items:  # 跟上面重複
            m3u8 = await self._extract_m3u8(item['href'])
            results.append(m3u8)
        return results

✅ 正例:Template Method,共用流程固定在基底類別

class Crawler(ABC):
    async def collect(self, url):
        html = await self._get_html(url)
        soup = BeautifulSoup(html, 'html.parser')
        results = []
        for item in self.get_items(soup, url):  # 唯一變動點
            m3u8 = await self._extract_m3u8(item['href'])
            results.append(m3u8)
        return results

    @abstractmethod
    def get_items(self, soup, url):
        """只需要回傳這個站台的內容項目清單,其餘流程共用"""

class SiteA(Crawler):
    def get_items(self, soup, url):
        return soup.select(".site-a-list a")

class SiteB(Crawler):
    def get_items(self, soup, url):
        return soup.select(".site-b-menu li a")

正例的價值不是程式碼變短,而是共用流程只存在一個地方:要加重試邏輯、要改錯誤處理,只要改 Crawler 基底類別一次,所有站台都受惠;新增一個站台,只要實作 get_items() 這一個方法,不用重新驗證整個流程還跑不跑得動。

抽象層切在哪裡,取決於「什麼會變、什麼不會變」

這是所有抽象設計的共通判斷準則:先找出流程裡真正會因為情境不同而變動的那一小塊,把它獨立成一個方法或介面;不變的部分留在共用骨架裡。切錯的常見症狀是「抽象層洩漏」——例如 get_items() 裡還要傳回一堆站台專屬的旗標,讓呼叫端得知道每個站台的特殊規則,這代表切的位置不夠精準,變動的部分還沒完全收斂進去。

今日思考題

回想你手上處理過「多個外部來源、格式不同」的程式碼:你是每個來源各寫一套,還是也切出過一個共用骨架?如果是前者,共同流程要修 bug 時,你怎麼確保每一份都改到了?

今日重點回顧

  • 「每個來源都不一樣」的部分要獨立出來,「所有來源都一樣」的部分要收斂成共用骨架
  • Template Method 讓共用流程只存在一個地方,加新來源只需要實作那一小塊變動點
  • 抽象層切得好不好,看的是「變動的部分有沒有真的被完全收斂」,不是程式碼行數多寡

明日預告

明天講另一半:知道要用哪個爬蟲實作類別,這個判斷邏輯本身要放在哪裡,才不會變成一堆散落各處的 if-else。


上一篇
Day 01:系列介紹——一個下載工具,為什麼值得認真寫測試
下一篇
Day 03:用 Factory 依網址判斷該用哪個爬蟲,而不是到處寫 if-else
系列文
下載器不是寫完就能跑:一個 HLS 分段式串流工具的架構與測試修練4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言