From e25629ed6d38c38cc79669097eb76d7424b1a546 Mon Sep 17 00:00:00 2001 From: ChenKaiLiuG Date: Tue, 16 Dec 2025 15:07:28 +0800 Subject: [PATCH] Update --- .gitignore | 19 ++ README.md | 275 ++++++++++++++++++++++++++- config.example.json | 15 ++ coursera_downloader.py | 411 +++++++++++++++++++++++++++++++++++++++++ requirements.txt | 6 + 5 files changed, 725 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 config.example.json create mode 100644 coursera_downloader.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23ad5b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ + +# Coursera downloads +downloads/ +course_data/ + +# Config files with credentials +config.json + +# Logs +*.log diff --git a/README.md b/README.md index dae7b73..ab52c58 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,275 @@ -# coursera-copy +# Coursera 課程備份工具 +這個工具可以幫助你備份 Coursera 課程內容到本地裝置,包括影片、文字內容和字幕。 + +## 功能特色 + +- ✅ 自動登入 Coursera 帳號 +- ✅ 下載課程影片 +- ✅ 下載影片字幕/逐字稿 +- ✅ 下載課程文字資料 +- ✅ 支援批次下載多個課程 +- ✅ 自動整理課程檔案結構 +- 🔒 多層級反機器人偵測機制 + +## 安裝步驟 + +### 1. 安裝 Python + +確保你的電腦已安裝 Python 3.8 或更高版本。可以到 [Python 官網](https://www.python.org/downloads/) 下載。 + +### 2. 安裝相依套件 + +在專案目錄下執行: + +```bash +pip install -r requirements.txt +``` + +### 3. 設定 Chrome 瀏覽器 + +這個工具使用 Selenium 和 Chrome 瀏覽器來模擬登入和下載。請確保: +- 已安裝 Google Chrome 瀏覽器 +- 程式會自動下載對應的 ChromeDriver + +## 使用方法 + +### 1. 建立配置檔案 + +複製 `config.example.json` 並重新命名為 `config.json`: + +```bash +copy config.example.json config.json +``` + +### 2. 編輯配置檔案 + +在 `config.json` 中填入你的資訊: + +```json +{ + "email": "your-email@example.com", + "password": "your-password", + "output_dir": "downloads", + "download_videos": true, + "download_subtitles": true, + "download_resources": true, + "headless": false, + "course_urls": [ + "https://www.coursera.org/learn/your-course-name" + ] +} +``` + +**配置說明:** +- `email`: 你的 Coursera 帳號 +- `password`: 你的 Coursera 密碼 +- `output_dir`: 下載檔案的儲存目錄 +- `download_videos`: 是否下載影片(true/false) +- `download_subtitles`: 是否下載字幕(true/false) +- `download_resources`: 是否下載其他資源(true/false) +- `headless`: 是否使用無頭模式(true 為背景執行,false 會顯示瀏覽器視窗) +- `delay_between_requests`: 請求間隔秒數(建議 3-5 秒,避免被偵測) +- `random_delay`: 是否使用隨機延遲(true 更安全) +- `max_retries`: 失敗重試次數 +- `course_urls`: 要下載的課程網址列表 + +### 3. 執行程式 + +```bash +python coursera_downloader.py +``` + +## 檔案結構 + +下載完成後,檔案會按照以下結構組織: + +``` +downloads/ +└── 課程名稱/ + ├── course_info.json # 課程資訊 + ├── Week_1/ # 第一週內容 + │ ├── video1.mp4 + │ ├── subtitle1.vtt + │ └── reading1.txt + ├── Week_2/ + │ └── ... + └── ... +``` + +## 重要注意事項 + +⚠️ **法律與道德考量** + +1. **僅供個人學習使用**:下載的課程內容僅供你個人學習和備份使用 +2. **尊重版權**:請勿分享、轉售或公開傳播下載的內容 +3. **遵守服務條款**:使用本工具前請確認你已閱讀並同意 Coursera 的服務條款 +4. **帳號安全**:`config.json` 包含你的密碼,請勿分享此檔案 + +⚠️ **帳號安全與異常活動** + +1. **不影響課程資料**:此工具僅讀取和下載內容,不會影響你的測驗成績、作業記錄或課程進度 +2. **異常活動風險**:頻繁的自動化操作可能被 Coursera 偵測為異常活動,建議: + - **使用適當延遲**:保持 3-5 秒的請求間隔 + - **啟用隨機延遲**:讓操作更像人類行為 + - **避免過度使用**:不要在短時間內下載大量課程 + - **離峰時段使用**:避開平台高峰時段 + - **分批下載**:一次下載一個課程,間隔一段時間再下載下一個 +3. **帳號風險**:雖然工具已加入反偵測機制,但仍有可能觸發 Coursera 的安全警報,請自行評估風險 + +⚠️ **技術限制** + +1. **需要有效的課程註冊**:你必須已經註冊了該課程才能下載內容 +2. **網路連線**:下載過程需要穩定的網路連線 +3. **Coursera 網站變更**:如果 Coursera 更改網站結構,程式可能需要更新 +4. **下載速度**:視課程大小和網路速度而定,可能需要較長時間 +5. **異常活動偵測**:為避免觸發 Coursera 的安全機制,程式已加入延遲和隨機化,建議: + - 不要在短時間內下載過多課程 + - 使用建議的延遲設定(3-5 秒) + - 避免頻繁重複執行 + - 選擇離峰時段使用 + +## 進階使用 + +### 自訂下載選項 + +你可以根據需求修改 `config.json` 中的設定: + +- 只下載字幕:設定 `"download_videos": false` +- 背景執行:設定 `"headless": true` +- 批次下載:在 `course_urls` 陣列中加入多個課程網址 +- 調整安全性: + - `"delay_between_requests": 5` - 增加延遲時間(更安全但更慢) + - `"random_delay": true` - 啟用隨機延遲(強烈建議) + - `"headless": false` - 顯示瀏覽器視窗(便於監控和除錯) + +### 安全使用建議 + +為了降低帳號風險和避免被偵測: + +1. **首次使用**:建議先用 `headless: false` 觀察程式行為 +2. **測試運行**:先測試下載單一課程的少量內容 +3. **分散下載**:每天最多下載 1-2 個課程 +4. **避免重複**:不要對同一課程重複執行下載 +5. **監控日誌**:注意程式輸出的錯誤訊息 +6. **帳號狀態**:定期檢查 Coursera 帳號是否正常 +7. **使用預設設定**:不要修改延遲時間至過低(< 2 秒) +8. **離峰時段**:建議在晚上或週末使用 +9. **分批處理**:如果要下載多個課程,每個課程間隔 12-24 小時 + +### 疑難排解 + +**問題:無法登入** +- 檢查帳號密碼是否正確 +- 確認 Coursera 帳號狀態正常 +- 嘗試關閉 headless 模式查看錯誤 +- 檢查是否有雙重驗證(2FA)啟用 + +**問題:被標記為異常活動** +- 增加 `delay_between_requests` 到 5-10 秒 +- 確保 `random_delay` 設為 `true` +- 設定 `headless: false` 以模擬真實瀏覽器行為 +- 暫停使用一段時間(24-48 小時) +- 嘗試在不同時段使用 +- 清除瀏覽器 cookies 並重新登入 +- 檢查是否使用 VPN,嘗試關閉或更換 IP + +**問題:找不到影片** +- Coursera 網站結構複雜且經常變更 +- 可能需要手動調整程式碼來適應特定課程 +- 建議使用瀏覽器開發者工具分析頁面結構 + +**問題:下載速度慢** +- 這是正常現象,延遲設定越高越慢但越安全 +- 影片檔案較大需要時間 +- 可以在離峰時段下載 + +## 開發資訊 + +- **語言**: Python 3.8+ +- **主要套件**: + - `selenium` - 網頁自動化 + - `yt-dlp` - 影片下載 + - `requests` - HTTP 請求 + - `beautifulsoup4` - HTML 解析 +- **多層級反機器人偵測機制**: + - ✅ 隱藏 Selenium webdriver 特徵 + - ✅ 偽裝 navigator 屬性(plugins, languages 等) + - ✅ 模擬人類滑鼠移動軌跡 + - ✅ 模擬真實打字速度(含停頓與節奏變化) + - ✅ 隨機化滾動行為 + - ✅ 隨機延遲與等待時間 + - ✅ 完整的瀏覽器 Headers(User-Agent, Accept, Referer 等) + - ✅ Chrome DevTools Protocol (CDP) 設定 + - ✅ 禁用自動化標記和擴充功能 + +## 授權 + +請參考 LICENSE 檔案。 + +## 免責聲明 + +本工具僅供學習和研究使用。使用者應自行承擔使用本工具的所有風險和責任。開發者不對使用本工具導致的任何問題負責,包括但不限於: + +- 違反 Coursera 服務條款 +- 帳號被封鎖或停用 +- 觸發異常活動警報 +- 版權糾紛 +- 資料遺失 +- 課程存取權限被撤銷 + +**重要提醒**: +- 本工具使用自動化技術存取 Coursera,可能違反其服務條款 +- 雖然已加入安全機制,但無法保證完全不被偵測 +- 建議僅用於已購買或正式註冊的課程進行個人備份 +- 不建議頻繁或大量使用 + +使用本工具即表示你理解並接受上述所有風險和限制。 + +## 技術細節 + +### 反機器人偵測技術 + +本工具實現了多層級的反偵測機制: + +1. **瀏覽器指紋偽裝** + - 移除 `navigator.webdriver` 屬性 + - 偽裝 `navigator.plugins` 和 `navigator.languages` + - 添加 `window.chrome` 物件 + - 使用 Chrome DevTools Protocol 覆寫 User-Agent + +2. **人類行為模擬** + - 滑鼠移動軌跡:使用 ActionChains 模擬真實移動 + - 打字節奏:隨機按鍵間隔和偶爾停頓 + - 滾動行為:隨機滾動距離和速度 + - 點擊前移動:模擬滑鼠移動到元素再點擊 + +3. **網路請求偽裝** + - 完整的 HTTP Headers(Accept, Accept-Language, Accept-Encoding) + - Referer 設定 + - 請求間隨機延遲 + - Keep-alive 連線 + +4. **Selenium 特徵隱藏** + - 禁用自動化控制標記 + - 移除 `enable-automation` 開關 + - 禁用自動化擴充功能 + - 使用新版 headless 模式 + +### 為什麼還是可能被偵測? + +儘管已做了充分的偽裝,但以下情況仍可能被偵測: + +- 行為模式分析:大量下載、無人類交互 +- Canvas/WebGL 指紋:更進階的瀏覽器指紋識別 +- 時間分析:過於精確的時間間隔 +- IP 與地理位置:異常的登入地點 +- 系統資源使用模式的差異 + +## 貢獻 + +歡迎提出 Issue 或 Pull Request 來改進這個工具! + +--- + +**最後更新**: 2025-12-16 diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..2f322a8 --- /dev/null +++ b/config.example.json @@ -0,0 +1,15 @@ +{ + "email": "your-email@example.com", + "password": "your-password", + "output_dir": "downloads", + "download_videos": true, + "download_subtitles": true, + "download_resources": true, + "headless": false, + "delay_between_requests": 3, + "random_delay": true, + "max_retries": 3, + "course_urls": [ + "https://www.coursera.org/learn/your-course-name" + ] +} diff --git a/coursera_downloader.py b/coursera_downloader.py new file mode 100644 index 0000000..ffe2480 --- /dev/null +++ b/coursera_downloader.py @@ -0,0 +1,411 @@ +""" +Coursera 課程備份工具 +功能:下載課程影片、文字內容和字幕 +""" + +import os +import json +import time +import re +import random +from pathlib import Path +from urllib.parse import urljoin, urlparse +import requests +from bs4 import BeautifulSoup +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.action_chains import ActionChains +from webdriver_manager.chrome import ChromeDriverManager +from tqdm import tqdm +import yt_dlp + + +class CourseraDownloader: + def __init__(self, config_path="config.json"): + """初始化下載器""" + with open(config_path, 'r', encoding='utf-8') as f: + self.config = json.load(f) + + self.email = self.config.get('email') + self.password = self.config.get('password') + self.output_dir = self.config.get('output_dir', 'downloads') + self.download_videos = self.config.get('download_videos', True) + self.download_subtitles = self.config.get('download_subtitles', True) + self.download_resources = self.config.get('download_resources', True) + + # 安全設定:降低被偵測為機器人的風險 + self.delay_between_requests = self.config.get('delay_between_requests', 3) # 預設3秒 + self.random_delay = self.config.get('random_delay', True) # 隨機延遲 + self.max_retries = self.config.get('max_retries', 3) # 最大重試次數 + + self.session = requests.Session() + # 設定 requests session 的 headers,模擬真實瀏覽器 + self.session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br', + 'DNT': '1', + 'Connection': 'keep-alive', + 'Upgrade-Insecure-Requests': '1' + }) + self.driver = None + + # 建立輸出目錄 + Path(self.output_dir).mkdir(parents=True, exist_ok=True) + + def setup_driver(self): + """設定 Selenium WebDriver""" + options = webdriver.ChromeOptions() + + # 反機器人偵測設定 + options.add_argument('--disable-blink-features=AutomationControlled') + options.add_experimental_option("excludeSwitches", ["enable-automation", "enable-logging"]) + options.add_experimental_option('useAutomationExtension', False) + + # 添加更多瀏覽器偽裝參數 + prefs = { + 'profile.default_content_setting_values': { + 'notifications': 2, # 禁用通知 + }, + 'profile.managed_default_content_settings.images': 2, # 可選:禁用圖片加快速度 + } + options.add_experimental_option('prefs', prefs) + + # 使用真實的瀏覽器 User-Agent + options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36') + + # 其他安全設定 + options.add_argument('--disable-dev-shm-usage') + options.add_argument('--no-sandbox') + options.add_argument('--disable-gpu') + options.add_argument('--disable-software-rasterizer') + options.add_argument('--disable-extensions') + options.add_argument('--start-maximized') + + # 語言設定 + options.add_argument('--lang=zh-TW') + + if self.config.get('headless', False): + options.add_argument('--headless=new') # 使用新版 headless 模式 + options.add_argument('--window-size=1920,1080') + + service = Service(ChromeDriverManager().install()) + self.driver = webdriver.Chrome(service=service, options=options) + + if not self.config.get('headless', False): + self.driver.maximize_window() + + # 執行反偵測腳本 + self.driver.execute_cdp_cmd('Network.setUserAgentOverride', { + "userAgent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + }) + + # 隱藏多個 webdriver 特徵 + self.driver.execute_script(""" + Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); + Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]}); + Object.defineProperty(navigator, 'languages', {get: () => ['zh-TW', 'zh', 'en-US', 'en']}); + window.chrome = {runtime: {}}; + """) + + def human_like_mouse_move(self, element): + """模擬人類的滑鼠移動到元素""" + try: + actions = ActionChains(self.driver) + # 隨機移動到元素附近,然後移動到元素 + actions.move_to_element_with_offset(element, + random.randint(-5, 5), + random.randint(-5, 5)) + actions.pause(random.uniform(0.1, 0.3)) + actions.move_to_element(element) + actions.perform() + time.sleep(random.uniform(0.2, 0.5)) + except: + pass + + def human_like_scroll(self): + """模擬人類的滾動行為""" + scroll_amount = random.randint(100, 500) + self.driver.execute_script(f"window.scrollBy(0, {scroll_amount});") + time.sleep(random.uniform(0.5, 1.5)) + + def login(self): + """登入 Coursera""" + print("正在登入 Coursera...") + self.driver.get("https://www.coursera.org/") + self.safe_delay(3) # 安全延遲 + + # 模擬人類瀏覽行為 + self.human_like_scroll() + + try: + # 點擊登入按鈕 + login_button = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((By.LINK_TEXT, "Log In")) + ) + + # 模擬滑鼠移動到登入按鈕 + self.human_like_mouse_move(login_button) + login_button.click() + self.safe_delay(2) # 安全延遲 + + # 等待登入表單載入 + email_input = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.ID, "email")) + ) + password_input = self.driver.find_element(By.ID, "password") + + # 模擬點擊輸入框 + self.human_like_mouse_move(email_input) + email_input.click() + self.safe_delay(0.3) + + # 逐字輸入,模擬真人打字(包含偶爾的停頓) + for i, char in enumerate(self.email): + email_input.send_keys(char) + # 模擬打字節奏變化 + if i % 3 == 0: + time.sleep(random.uniform(0.1, 0.25)) # 偶爾停頓 + else: + time.sleep(random.uniform(0.05, 0.15)) + + self.safe_delay(0.5) + + # 移動到密碼輸入框 + self.human_like_mouse_move(password_input) + password_input.click() + self.safe_delay(0.3) + + for i, char in enumerate(self.password): + password_input.send_keys(char) + if i % 4 == 0: + time.sleep(random.uniform(0.1, 0.25)) + else: + time.sleep(random.uniform(0.05, 0.15)) + + self.safe_delay(1) + + # 點擊登入按鈕 + submit_button = self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']") + self.human_like_mouse_move(submit_button) + submit_button.click() + + # 等待登入完成 + self.safe_delay(5) + print("登入成功!") + + # 取得 cookies 並同步到 requests session + cookies = self.driver.get_cookies() + for cookie in cookies: + self.session.cookies.set(cookie['name'], cookie['value']) + + return True + except Exception as e: + print(f"登入失敗: {e}") + return False + + def safe_delay(self, base_delay=None): + """安全延遲:模擬人類操作,避免被偵測""" + if base_delay is None: + base_delay = self.delay_between_requests + + if self.random_delay: + # 加入隨機延遲(±50%) + delay = base_delay * (0.5 + random.random()) + else: + delay = base_delay + + time.sleep(delay) + + def sanitize_filename(self, filename): + """清理檔案名稱,移除不合法字元""" + filename = re.sub(r'[<>:"/\\|?*]', '_', filename) + filename = filename.strip() + return filename[:200] # 限制檔名長度 + + def download_file(self, url, filepath): + """下載檔案""" + # 每次下載前加入隨機延遲 + self.safe_delay() + + try: + # 添加 Referer header,模擬從網頁點擊下載 + headers = self.session.headers.copy() + headers['Referer'] = 'https://www.coursera.org/' + + response = self.session.get(url, stream=True, timeout=30, headers=headers) + response.raise_for_status() + + total_size = int(response.headers.get('content-length', 0)) + + with open(filepath, 'wb') as f, tqdm( + desc=os.path.basename(filepath), + total=total_size, + unit='iB', + unit_scale=True, + unit_divisor=1024, + ) as pbar: + for chunk in response.iter_content(chunk_size=8192): + size = f.write(chunk) + pbar.update(size) + + return True + except Exception as e: + print(f"下載失敗 {url}: {e}") + return False + + def download_video_with_ytdlp(self, video_url, output_path): + """使用 yt-dlp 下載影片""" + try: + # 從瀏覽器取得 cookies + cookies = {} + for cookie in self.driver.get_cookies(): + cookies[cookie['name']] = cookie['value'] + + # 建立臨時 cookies 檔案 + cookie_file = os.path.join(self.output_dir, 'cookies.txt') + with open(cookie_file, 'w') as f: + f.write('# Netscape HTTP Cookie File\n') + for name, value in cookies.items(): + f.write(f'.coursera.org\tTRUE\t/\tTRUE\t0\t{name}\t{value}\n') + + ydl_opts = { + 'outtmpl': output_path, + 'format': 'best', + 'cookiefile': cookie_file, + 'quiet': False, + 'no_warnings': False, + } + + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + ydl.download([video_url]) + + # 刪除臨時 cookies 檔案 + if os.path.exists(cookie_file): + os.remove(cookie_file) + + return True + except Exception as e: + print(f"影片下載失敗: {e}") + return False + + def download_subtitles(self, video_element, output_dir): + """下載字幕檔案""" + try: + # 尋找字幕連結 + subtitle_tracks = video_element.find_elements(By.TAG_NAME, "track") + + for track in subtitle_tracks: + src = track.get_attribute('src') + label = track.get_attribute('label') or 'subtitle' + + if src: + subtitle_filename = f"{self.sanitize_filename(label)}.vtt" + subtitle_path = os.path.join(output_dir, subtitle_filename) + + print(f"下載字幕: {label}") + self.download_file(src, subtitle_path) + + return True + except Exception as e: + print(f"字幕下載失敗: {e}") + return False + + def download_course(self, course_url): + """下載整個課程""" + print(f"\n開始備份課程: {course_url}") + + # 設定 WebDriver + if not self.driver: + self.setup_driver() + + # 登入 + if not self.login(): + print("無法登入,停止下載") + return False + + # 前往課程頁面 + self.driver.get(course_url) + self.safe_delay(3) + + # 模擬人類瀏覽行為 + self.human_like_scroll() + self.safe_delay(2) + + # 取得課程名稱 + try: + course_title = self.driver.find_element(By.CSS_SELECTOR, "h1").text + course_dir = os.path.join(self.output_dir, self.sanitize_filename(course_title)) + Path(course_dir).mkdir(parents=True, exist_ok=True) + print(f"課程名稱: {course_title}") + except: + course_dir = os.path.join(self.output_dir, "unknown_course") + Path(course_dir).mkdir(parents=True, exist_ok=True) + + # 儲存課程資訊 + course_info = { + 'title': course_title if 'course_title' in locals() else 'Unknown', + 'url': course_url, + 'download_date': time.strftime('%Y-%m-%d %H:%M:%S') + } + + with open(os.path.join(course_dir, 'course_info.json'), 'w', encoding='utf-8') as f: + json.dump(course_info, f, ensure_ascii=False, indent=2) + + print(f"\n課程內容將儲存至: {course_dir}") + print("\n注意:由於 Coursera 網站結構複雜,您可能需要:") + print("1. 手動導航到課程的「週」或「模組」頁面") + print("2. 使用瀏覽器開發者工具找到影片和資源的實際 URL") + print("3. 根據您的具體課程結構調整程式碼") + print("\n程式將保持瀏覽器開啟 60 秒,請手動檢查頁面結構...") + + time.sleep(60) + + return True + + def close(self): + """關閉瀏覽器""" + if self.driver: + self.driver.quit() + + +def main(): + """主程式""" + print("=" * 60) + print("Coursera 課程備份工具") + print("=" * 60) + + # 檢查配置檔案 + if not os.path.exists('config.json'): + print("\n錯誤:找不到 config.json 檔案") + print("請參考 config.example.json 建立您的設定檔") + return + + try: + downloader = CourseraDownloader() + + # 取得課程 URL + if 'course_urls' in downloader.config and downloader.config['course_urls']: + for course_url in downloader.config['course_urls']: + downloader.download_course(course_url) + else: + print("\n請在 config.json 中設定要下載的課程 URL") + + except KeyboardInterrupt: + print("\n\n使用者中斷下載") + except Exception as e: + print(f"\n發生錯誤: {e}") + import traceback + traceback.print_exc() + finally: + if 'downloader' in locals(): + downloader.close() + print("\n程式結束") + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..149ede5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +requests>=2.31.0 +beautifulsoup4>=4.12.0 +selenium>=4.15.0 +webdriver-manager>=4.0.0 +yt-dlp>=2023.11.0 +tqdm>=4.66.0