爬蟲擷取YT影片資訊和影片字幕
本偏要說的是如何利用爬蟲爬取特定 YouTube 頻道的影片資訊以及影片字幕。
使用了幾個 Python 外掛程式庫來完成不同的任務,如控制瀏覽器進行網頁操作 (Selenium) 和抓取 YouTube 字幕 (YouTubeTranscriptApi)。
Note: 安裝 Chrome 瀏覽器,確保你的系統上安裝了 Chrome 瀏覽器,並且版本是最新的。
下載 ChromeDriver: 到 ChromeDriver 官方網站 下載對應於你 Chrome 瀏覽器版本的 chromedriver。
確保將下載的 chromedriver 添加到系統的環境變數(PATH)
ChromeDriver 的版本必須與你的 Chrome 瀏覽器版本匹配
如果環境設定有問題直接在code寫下面這行就好
driver = webdriver.Chrome(executable_path="/path/to/chromedriver")
重點解說:
主要功能
1. 取指定頻道的影片資訊
程式使用 Selenium 自動化瀏覽器,模擬用戶訪問 YouTube 頻道頁面。
它會自動滾動頁面以載入所有影片資訊。
2. 儲存影片標題與連結
抓取影片標題和連結後,將其儲存為一個字典列表。
3. 抓取影片字幕
對於每個影片,程式會透過 YouTubeTranscriptApi 嘗試抓取對應的字幕。
預設嘗試使用繁體中文字幕(zh-TW),但如果抓取失敗,則會記錄為空。
4. 將結果儲存為 JSON 檔案
抓取的影片資訊與字幕會被存為 JSON 檔案,方便後續分析或使用。
程式碼架構
整份解說的完整程式碼我會放在最後面,想直接用可以拉到最下面copy就好
1. 輸入參數和初始化瀏覽器
channel-name,用於指定要爬取的 YouTube 頻道名稱。
使用 Selenium 的 ChromeDriver 開啟 YouTube 頻道影片頁面。
自動滾動頁面,直到所有影片被載入。
2. 影片資訊的爬取
使用 CSS 選擇器 (div#details 和 a#video-title-link) 定位影片標題與連結。
將每個影片的標題與連結儲存為字典。
3. 字幕爬取和資料儲存
對於每個影片連結,提取其影片 ID,並使用 YouTubeTranscriptApi 抓取字幕。如果字幕抓取成功,將其儲存到對應的字典中。影片資訊與字幕結果被保存為 JSON 檔案,檔案名稱基於影片 ID 命名。
範例YT channel
我們以大家說英語雜誌 Official Channel的YT來進行擷取影片字幕,需要複製YT帳號 LTStudioclassroomCom。
本來要用泱泱的YT,但我找了一下一些知名的YT都沒有提供字幕檔案。
1. 輸入參數和初始化瀏覽器
channel-name,用於指定要爬取的 YouTube 頻道名稱。
channel_name透過selenium內的webdriver.Chrome()來開啟網頁。
import json
import time
import os
# import mysql.connector
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from youtube_transcript_api import YouTubeTranscriptApi
channel_name = 'LTStudioclassroomCom'
print('channel_name: {}'.format(channel_name))
url = f"https://www.youtube.com/@{channel_name}"
print('url: {}'.format(url))
# init driver
driver = webdriver.Chrome()
driver.get(url + "/videos") # 打開網頁
# driver.quit() # 關閉瀏覽器
上面程式是打開chrome瀏覽器然後開啟YT影片連結,通常打開網頁的時候只會讀部分影片,我們一般人工在操作都是要滾動畫面才會繼續將之前的影片讀進來。
所以如果要自動化全部影片都讀進來瀏覽器,需要自動滾動畫面,滾動到網頁底部,並確保所有內容已加載完成。
現代網頁經常採用延遲加載(Lazy Loading)技術,特別是像 YouTube 這樣的網站。這種技術會在頁面滾動到一定位置時,才向伺服器請求並載入更多內容。如果你不模擬滾動,僅停留在頁面初始狀態,可能只能抓到一小部分影片資訊。
- 這邊寫一個Selenium WebDriver的滾動函式 (```scroll_to_bottom```)
scroll_to_bottom 的主要目的是讓網頁中的所有內容(特別是動態加載的內容,例如影片清單)被完全加載到頁面上。
三個參數:
1. driver: Selenium WebDriver 物件。
2. max_scrolls: 限制滾動次數,避免遇到無法加載完全的特殊情況。
3. wait_time: 每次滾動後的等待時間,避免網頁來不及加載新內容。
在滾動時持續檢查網頁高度是否發生變化,如果沒有變化則停止滾動。
def scroll_to_bottom(driver, max_scrolls=20, wait_time=2):
"""
滾動到網頁底部,並確保所有內容已加載完成。
Args:
driver: Selenium WebDriver 物件
max_scrolls: 最大滾動次數(防止無窮迴圈)
wait_time: 每次滾動後的等待時間(秒)
"""
print("Starting to scroll the page...")
scrolls = 0 # 記錄滾動次數
while scrolls < max_scrolls:
# 獲取當前網頁高度
prev_ht = driver.execute_script("return document.documentElement.scrollHeight;")
# 滾動到頁面底部
driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);")
# 等待加載
time.sleep(wait_time)
# 再次獲取新高度
ht = driver.execute_script("return document.documentElement.scrollHeight;")
# 如果高度未變化,則停止滾動
if prev_ht == ht:
print("Reached the bottom of the page.")
break
scrolls += 1
print(f"Scroll {scrolls}/{max_scrolls} complete.")
# 如果達到最大滾動次數
if scrolls == max_scrolls:
print("Reached the maximum scroll limit.")
# 滾動到頁面底部
scroll_to_bottom(driver, max_scrolls=20, wait_time=2)
理論上前面執行成功會跳出一個新的視窗如上,會有「Chrome目前受到自動測試軟體控制」,然後螢幕畫面會自己再往下滑。
2. 影片資訊的爬取
## 抓取影片資料
使用 Selenium 的 find_element 和 get_attribute 抓取每個影片的標題 (title) 和連結 (href)。
這邊也一個函數```get_video_details```
我這邊會先宣告一個video的字典dict(```inin_video```)
Keys為
1. title
2. href
3. video_id
4. transcript
5. channel_name
然後將抓所有影片的標題 (title) 和連結 (href)寫到這個字典內
def get_video_details(driver, channel_name):
"""
抓取影片標題和連結。
Args:
driver: Selenium WebDriver 物件
Returns:
videos: 包含影片 title 和 href 的清單
"""
videos = []
def inin_video():
video={}
video['video_id'] = ''
video['transcript'] = ''
video['title'] = ''
video['href'] = ''
return video
try:
# 等待影片元素加載完成
elements = WebDriverWait(driver, 20).until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div#details"))
)
for element in elements:
title = element.find_element(By.CSS_SELECTOR, "a#video-title-link").get_attribute(
"title"
)
href = element.find_element(By.CSS_SELECTOR, "a#video-title-link").get_attribute(
"href"
)
video = inin_video()
video['title'] = title
video['href'] = href
video_id = href.split("=")[-1]
video['video_id'] = video_id
video['channel_name'] = channel_name
videos.append(video)
except Exception as e:
print(f"Error while fetching video details: {e}")
return videos
# 抓取影片資料
videos = get_video_details(driver, channel_name)
# 輸出抓到影片資訊的結果
for i, video in enumerate(videos, 1):
print(f"{i}. {video['title']}: {video['href']}")
這邊就是將網頁資訊抓到videos的list裡面,裡面每個video都是我宣告的data structure,如下(但此函數我寫到get_video_details內)
def inin_video():
video={}
video['video_id'] = ''
video['transcript'] = ''
video['title'] = ''
video['href'] = ''
return video
所以get_video_details是將每個影片的video_id、title、href和channel_name寫入,transcript字幕部分還沒。
3. 字幕爬取和資料儲存
對於每個影片連結,提取其影片 ID,並使用 YouTubeTranscriptApi 抓取字幕。如果字幕抓取成功,將其儲存到對應的字典中。
利用函數```YouTubeTranscriptApi.get_transcript```開始從剛剛爬到的影片資訊來擷取字幕,並且將字幕寫到json檔案內。
def get_all_transcript_tojson(videos, output_path):
# videos: output from function: ```get_video_details````
try:
os.mkdir(output_path) #創建儲存檔案夾
print('{} is not exit, create folder'.format(output_path) )
except:
pass
for video_i in videos:
video_id = video_i['video_id']
save_fname = os.path.join(output_path, f"{video_id}.json")
# check if file exist
if os.path.isfile(entity_fname):
continue
try:
transcript_i = YouTubeTranscriptApi.get_transcript(
video_id, languages=["en", "zh-TW"]
) # get transcripts
video_i['transcript'] = transcript_i
except Exception as e:
e
video_i['transcript'] = []
finally:
# save obj as json to local
with open(save_fname, "w") as f:
json.dump(video_i, f)
get_all_transcript_tojson(videos, output_path='./transcript_' + channel_name)
執行上面的程式就會將所有影片的字幕抓下來,languages=[“en”, “zh-TW”] 這邊是因為有可能有中文字幕也有英文,所以兩個都去抓,如果字幕抓不到就直接給字幕的變數(transcript)空的list。
因為我們video已經是dict,所以可以直接寫到output_path的資料夾內,檔案名稱就是影片的ID,也就是YT給予影片的編碼(你看網址後面那串)。
Example: https://www.youtube.com/watch?v=ji3Qoy1vIFE
ID就是: ji3Qoy1vIFE
以我的寫法是output_path=’./transcript_’ + channel_name
所以大家說英文的字幕會存在我執行檔案夾下開一個新的檔案夾(transcript_LTStudioclassroomCom)
直接去點開json是亂碼看不懂,我們可以用python在環境來打開json
# 讀取 JSON 檔案
file_path = "transcript_LTStudioclassroomCom/_9WVNgjV_4c.json" # 替換成你的檔案路徑
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 輸出解析後的 JSON 內容
print("JSON 資料:")
print(f"標題: {data['title']}")
print(f"連結: {data['href']}")
for tmp in data['transcript']:
print(f"{tmp}")
完整程式碼
import json
import time
import os
# import mysql.connector
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from youtube_transcript_api import YouTubeTranscriptApi
def scroll_to_bottom(driver, max_scrolls=20, wait_time=2):
"""
滾動到網頁底部,並確保所有內容已加載完成。
Args:
driver: Selenium WebDriver 物件
max_scrolls: 最大滾動次數(防止無窮迴圈)
wait_time: 每次滾動後的等待時間(秒)
"""
print("Starting to scroll the page...")
scrolls = 0 # 記錄滾動次數
while scrolls < max_scrolls:
# 獲取當前網頁高度
prev_ht = driver.execute_script("return document.documentElement.scrollHeight;")
# 滾動到頁面底部
driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);")
# 等待加載
time.sleep(wait_time)
# 再次獲取新高度
ht = driver.execute_script("return document.documentElement.scrollHeight;")
# 如果高度未變化,則停止滾動
if prev_ht == ht:
print("Reached the bottom of the page.")
break
scrolls += 1
print(f"Scroll {scrolls}/{max_scrolls} complete.")
# 如果達到最大滾動次數
if scrolls == max_scrolls:
print("Reached the maximum scroll limit.")
def get_video_details(driver, channel_name):
"""
抓取影片標題和連結。
Args:
driver: Selenium WebDriver 物件
Returns:
videos: 包含影片 title 和 href 的清單
"""
videos = []
def inin_video():
video={}
video['video_id'] = ''
video['transcript'] = ''
video['title'] = ''
video['href'] = ''
return video
try:
# 等待影片元素加載完成
elements = WebDriverWait(driver, 20).until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div#details"))
)
for element in elements:
# title = element.find_element(By.CSS_SELECTOR, "a#video-title").get_attribute("title")
# href = element.find_element(By.CSS_SELECTOR, "a#video-title").get_attribute("href")
title = element.find_element(By.CSS_SELECTOR, "a#video-title-link").get_attribute(
"title"
)
href = element.find_element(By.CSS_SELECTOR, "a#video-title-link").get_attribute(
"href"
)
video = inin_video()
video['title'] = title
video['href'] = href
video_id = href.split("=")[-1]
video['video_id'] = video_id
video['channel_name'] = channel_name
videos.append(video)
except Exception as e:
print(f"Error while fetching video details: {e}")
return videos
def get_all_transcript_tojson(videos, output_path):
# videos: output from function: ```get_video_details````
try:
os.mkdir(output_path)
print('{} is not exit, create folder'.format(output_path) )
except:
pass
for video_i in videos:
video_id = video_i['video_id']
entity_fname = os.path.join(output_path, f"{video_id}.json")
# check if file exist
if os.path.isfile(entity_fname):
continue
try:
transcript_i = YouTubeTranscriptApi.get_transcript(
video_id, languages=["en", "zh-TW"]
) # get transcripts
video_i['transcript'] = transcript_i
except Exception as e:
e
video_i['transcript'] = []
finally:
# save obj as json to local
with open(entity_fname, "w") as f:
json.dump(video_i, f)
channel_name = 'LTStudioclassroomCom'
print('channel_name: {}'.format(channel_name))
url = f"https://www.youtube.com/@{channel_name}"
print('url: {}'.format(url))
# init driver
driver = webdriver.Chrome()
driver.get(url + "/videos") # 打開網頁
# 滾動到頁面底部
scroll_to_bottom(driver, max_scrolls=20, wait_time=2)
# 抓取影片資料
videos = get_video_details(driver, channel_name)
# 輸出抓到影片資訊的結果
for i, video in enumerate(videos, 1):
print(f"{i}. {video['title']}: {video['href']}")
get_all_transcript_tojson(videos, output_path='./transcript_' + channel_name)
driver.quit() # 關閉瀏覽器
備註: 這份資料文字和文字內容有利用ChatGPT來修改和生成。