「反正每個網站長得不一樣,乾脆一個網站寫一支獨立的爬蟲程式,簡單暴力。」
這個想法短期最快,長期最痛。如果每個來源站各寫一支完全獨立的程式,共同流程(取得頁面、找出內容項目清單、逐項目取得 M3U8 位置)就會被複製貼上好幾份——某個共同流程要修,得記得每一份都要改一次;漏改一份,就是一個沒人發現的 bug。
把處理流程攤開來看:取得頁面 → 解析出內容項目清單 → 逐一取得每個項目的 M3U8 位置 → 產出結果。這個骨架在所有來源站之間是一樣的,會變的只有其中一步:怎麼從頁面裡找出內容項目清單——因為每個站台的 HTML 結構、CSS class 命名都不同。
❌ 反例:每個來源站各寫一支完整流程
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 時,你怎麼確保每一份都改到了?
明天講另一半:知道要用哪個爬蟲實作類別,這個判斷邏輯本身要放在哪裡,才不會變成一堆散落各處的 if-else。