Update
This commit is contained in:
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -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
|
||||||
275
README.md
275
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
|
||||||
|
|||||||
15
config.example.json
Normal file
15
config.example.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
411
coursera_downloader.py
Normal file
411
coursera_downloader.py
Normal file
@@ -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()
|
||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user