Tải Phụ Đề SRT YouTube Bằng JS & PHP Proxy Vượt Lỗi 301
💡 Tóm Tắt Nhanh Cho Kỹ Sư & Công Cụ Tìm Kiếm AI (GEO)
- Nguyên lý lấy phụ đề YouTube: YouTube lưu trữ thông tin phụ đề trong đối tượng JavaScript ngầm
captionTrackschứa các URL XML (Timed Text). Để chuyển sang định dạng.srtchuẩn, chỉ cần tính toán mốc thời gianstart + dursang dạngHH:MM:SS,MSvà gỡ bỏ HTML Entities. - Hạn chế khi chạy Client-side: Trình duyệt sẽ chặn toàn bộ lệnh
fetch()sang link YouTube từ website của bạn do chính sách CORS. Giải pháp nhanh nhất là chạy trực tiếp trên Console F12 của tab YouTube, hoặc xây dựng API trung gian (API Bridge) trên máy chủ. - Xây dựng API PHP Proxy trên Hosting: Sử dụng cURL tải mã nguồn trang và tải XML phụ đề. Bắt buộc phải thêm
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);để giải quyết triệt để lỗi 301 Moved Permanently khi YouTube tự động chuyển hướng từ HTTP sang HTTPS. - Vượt rào cản chặn IP / Rate Limit: Cấu hình HTTP Basic Proxy (
CURLOPT_PROXYvàCURLOPT_PROXYUSERPWD) kết hợp giả lập User-Agent của Google Chrome mới nhất, giúp API hoạt động bền bỉ, không bị YouTube phát hiện bot.
Việc trích xuất phụ đề (captions/subtitles) từ các video trên YouTube là nhu cầu thiết yếu đối với các lập trình viên đang xây dựng ứng dụng dịch thuật tự động, tóm tắt video bằng AI (LLM Video Summarizer), phân tích từ khóa SEO hoặc lồng tiếng tự động (Text-to-Speech). Tuy nhiên, do YouTube liên tục nâng cấp hệ thống tường lửa, siết chặt chính sách CORS (Cross-Origin Resource Sharing) và cơ chế phát hiện bot cào dữ liệu (Anti-Scraping Bot Detection), nhiều lập trình viên thường xuyên gặp phải lỗi chặn kết nối hoặc trang trắng 301 Moved Permanently.
Bài viết này phân tích bản chất cơ chế lưu trữ phụ đề của YouTube và hướng dẫn chi tiết 3 giải pháp triển khai: từ mã lệnh JavaScript chạy trực tiếp trên Console trình duyệt, module Node.js phía Server, cho đến xây dựng API PHP cURL Proxy hoàn chỉnh trên Shared Hosting có khả năng tự động xử lý chuyển hướng redirect và xuất thẳng file .srt chuẩn xác.
Hình 1: Hiểu rõ cơ chế phản hồi HTTP và phân tích dữ liệu web là chìa khóa để trích xuất phụ đề tự động thành công.
1. Cơ Chế Lưu Trữ Phụ Đề Của YouTube & Thách Thức Về CORS
Khi bạn xem một video có bật tính năng phụ đề (Closed Captions – CC), YouTube không lưu file dưới định dạng .srt hay .vtt sẵn có. Thay vào đó, nền tảng sử dụng kiến trúc dữ liệu nội bộ như sau:
- Đối tượng
captionTracks: Nằm bên trong khối mã nguồn HTML ban đầu của trang (thường nằm trong biến JavaScript toàn cụcytInitialPlayerResponse). Đối tượng này chứa danh sách các ngôn ngữ có sẵn kèm đường dẫn gốcbaseUrl. - Định dạng XML Timed Text: Khi truy cập vào
baseUrl, YouTube trả về một file XML chứa các thẻ<text start="1.5" dur="3.2">Nội dung lời thoại</text>. Trong đóstartlà thời điểm xuất hiện (tính bằng giây) vàdurlà thời lượng hiển thị. - Cơ chế bảo mật CORS: Nếu bạn cố gắng dùng hàm
fetch()từ một trang web khác (ví dụ:http://localhosthoặc tên miền riêng của bạn) đến máy chủ của YouTube, trình duyệt sẽ ngay lập tức chặn yêu cầu vì YouTube không đặt tiêu đềAccess-Control-Allow-Origin: *cho các luồng dữ liệu này.
Hình 2: Máy chủ Proxy đóng vai trò trạm trung chuyển giúp vượt qua các giới hạn địa lý và rào cản IP.
2. Giải Pháp 1: Tải Nhanh File .SRT Qua Console Trình Duyệt (Không Cần Server)
Nếu bạn chỉ cần tải phụ đề của một vài video riêng lẻ cho mục đích cá nhân mà không muốn thiết lập máy chủ phức tạp, cách đơn giản nhất là tận dụng chính phiên đăng nhập và cookie hợp lệ trên tab YouTube đang mở:
- Mở video YouTube trên trình duyệt máy tính và bật nút phụ đề (CC).
- Bấm phím
F12(hoặc tổ hợpCtrl + Shift + I) để mở Developer Tools, sau đó chọn thẻ Console. - Dán toàn bộ đoạn mã JavaScript dưới đây và ấn
Enter:
(async () => {
try {
const html = document.documentElement.innerHTML;
const match = html.match(/"captionTracks":s*([.*?])/);
if (!match) throw new Error("Video này không có phụ đề (CC).");
const captionTracks = JSON.parse(match[1]);
const track = captionTracks[0]; // Mặc định lấy ngôn ngữ đầu tiên
console.log("⏳ Đang tải phụ đề: " + track.name.simpleText);
const res = await fetch(track.baseUrl);
const xmlText = await res.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
const textNodes = xmlDoc.getElementsByTagName("text");
let srt = "";
const formatSrtTime = (seconds) => {
const date = new Date(0);
date.setSeconds(Math.floor(seconds));
const ms = Math.floor((seconds % 1) * 1000).toString().padStart(3, '0');
return date.toISOString().substr(11, 8) + ',' + ms;
};
for (let i = 0; i < textNodes.length; i++) {
const node = textNodes[i];
const start = parseFloat(node.getAttribute("start"));
const duration = parseFloat(node.getAttribute("dur") || "0");
const text = node.textContent.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, "'");
srt += (i + 1) + "n" + formatSrtTime(start) + " --> " + formatSrtTime(start + duration) + "n" + text + "nn";
}
const blob = new Blob([srt], { type: "text/srt;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = (document.title.replace(/[\/:*?"<>|]/g, "") || "subtitles") + ".srt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log("✅ Tải file .srt thành công!");
} catch (err) {
console.error("❌ Lỗi: " + err.message);
}
})();
3. Giải Pháp 2: Xây Dựng Cầu Nối Backend Node.js (Express API)
Khi xây dựng ứng dụng web cho hàng nghìn người dùng nhập link và tải phụ đề, bạn cần chuyển phần việc cào dữ liệu về phía máy chủ (Server-side). Với Node.js, bạn có thể kết hợp thư viện youtube-transcript và framework express để tạo một endpoint đơn giản:
const express = require('express');
const cors = require('cors');
const { YoutubeTranscript } = require('youtube-transcript');
const app = express();
app.use(cors());
app.use(express.json());
function formatTime(seconds) {
const pad = (num, size = 2) => num.toString().padStart(size, '0');
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
const ms = Math.floor((seconds % 1) * 1000);
return pad(hours) + ':' + pad(minutes) + ':' + pad(secs) + ',' + pad(ms, 3);
}
app.post('/api/get-subtitles', async (req, res) => {
const { videoUrl } = req.body;
if (!videoUrl) return res.status(400).json({ error: 'Thiếu URL video YouTube' });
try {
const transcript = await YoutubeTranscript.fetchTranscript(videoUrl);
let srtContent = '';
transcript.forEach((item, index) => {
const start = item.offset / 1000;
const end = start + (item.duration / 1000);
const text = item.text.replace(/&/g, '&').replace(/"/g, '"');
srtContent += (index + 1) + 'n' + formatTime(start) + ' --> ' + formatTime(end) + 'n' + text + 'nn';
});
res.setHeader('Content-Type', 'text/srt');
res.send(srtContent);
} catch (error) {
res.status(500).json({ error: 'Không thể trích xuất phụ đề từ video này' });
}
});
app.listen(3000, () => console.log('Server chạy tại http://localhost:3000'));
4. Giải Pháp 3: API PHP Proxy cURL Toàn Diện Trên Web Hosting
Đối với đại đa số website vận hành trên các gói Shared Hosting (cPanel, DirectAdmin) hỗ trợ PHP mà không thể cài đặt Node.js hay Python, việc sử dụng PHP cURL là giải pháp tiết kiệm và dễ bảo trì nhất. Tuy nhiên, hai chướng ngại lớn nhất mà các lập trình viên PHP thường gặp phải là:
- Lỗi 301 Moved Permanently: Trình duyệt hoặc cURL nhận về một trang HTML thông báo lỗi 301 thay vì nội dung phụ đề. Nguyên nhân là do URL truyền vào là
http://nhưng YouTube bắt buộc chuyển hướng sanghttps://, hoặc link video thiếu tham số bắt buộc. Nếu cURL không được bậtCURLOPT_FOLLOWLOCATION, nó sẽ dừng lại ngay tại trang thông báo chuyển hướng đó. - Chặn IP máy chủ (IP Blacklisting): Do các địa chỉ IP của các nhà cung cấp Hosting phổ thông bị YouTube liệt kê vào danh sách nghi vấn bot, YouTube sẽ yêu cầu xác thực Captcha hoặc trả về mã 429 Too Many Requests. Việc tích hợp Proxy xoay vòng (Proxy Rotation) là điều bắt buộc.
Hình 3: Kiểm tra kỹ lưỡng các cờ phản hồi HTTP giúp gỡ lỗi hệ thống cào dữ liệu nhanh chóng.
Mã nguồn API hoàn chỉnh (File: get_sub.php)
File PHP dưới đây đã được tối ưu hóa đầy đủ các cờ cấu hình cURL, xử lý tự động theo vết chuyển hướng 301, hỗ trợ cả GET/POST và trả file về trình duyệt trực tiếp:
<?php
// 1. Cấu hình tiêu đề CORS toàn quyền cho Client
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
exit(0);
}
// 2. Thiết lập thông số Proxy (Điền proxy của bạn nếu bị chặn IP)
define('USE_PROXY', false); // Đổi thành true khi cần dùng proxy
define('PROXY_SERVER', '103.152.118.50:8080'); // IP:Port
define('PROXY_AUTH', 'user:password'); // Để trống nếu không có mật khẩu
function formatSrtTime($seconds) {
$hours = floor($seconds / 3600);
$minutes = floor(($seconds % 3600) / 60);
$secs = floor($seconds % 60);
$ms = floor(($seconds - floor($seconds)) * 1000);
return sprintf("%02d:%02d:%02d,%03d", $hours, $minutes, $secs, $ms);
}
function fetchUrlWithProxy($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// ĐIỂM MẤU CHỐT: Cho phép theo vết chuyển hướng 301 / 302
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// Giả lập Headers trình duyệt thực tế chống lọc bot
$headers = [
'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: vi,en-US;q=0.7,en;q=0.3',
'Cache-Control: max-age=0'
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if (USE_PROXY) {
curl_setopt($ch, CURLOPT_PROXY, PROXY_SERVER);
if (!empty(PROXY_AUTH)) {
curl_setopt($ch, CURLOPT_PROXYUSERPWD, PROXY_AUTH);
}
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$response) return false;
return $response;
}
$videoUrl = isset($_REQUEST['videoUrl']) ? trim($_REQUEST['videoUrl']) : '';
if (empty($videoUrl)) {
header('Content-Type: application/json', true, 400);
echo json_encode(["error" => "Thiếu tham số videoUrl"]);
exit;
}
// Tải mã nguồn trang video YouTube
$html = fetchUrlWithProxy($videoUrl);
if (!$html) {
header('Content-Type: application/json', true, 500);
echo json_encode(["error" => "Không thể kết nối YouTube hoặc bị chặn IP."]);
exit;
}
// Bóc tách danh sách phụ đề
preg_match('/"captionTracks":s*([.*?])/', $html, $matches);
if (empty($matches[1])) {
header('Content-Type: application/json', true, 404);
echo json_encode(["error" => "Video này không có phụ đề CC."]);
exit;
}
$captionTracks = json_decode($matches[1], true);
$selectedTrack = $captionTracks[0];
$xmlContent = fetchUrlWithProxy($selectedTrack['baseUrl']);
if (!$xmlContent) {
header('Content-Type: application/json', true, 500);
echo json_encode(["error" => "Không thể tải XML phụ đề từ máy chủ."]);
exit;
}
$xml = simplexml_load_string($xmlContent);
if (!$xml) {
header('Content-Type: application/json', true, 500);
echo json_encode(["error" => "Lỗi đồng bộ XML."]);
exit;
}
$srtResult = "";
$index = 1;
foreach ($xml->text as $textNode) {
$start = (float)$textNode['start'];
$duration = (float)($textNode['dur'] ?? 0);
$end = $start + $duration;
$text = html_entity_decode((string)$textNode, ENT_QUOTES, 'UTF-8');
$srtResult .= $index . "n";
$srtResult .= formatSrtTime($start) . " --> " . formatSrtTime($end) . "n";
$srtResult .= $text . "nn";
$index++;
}
// Ép kiểu tải file trực tiếp về máy người dùng
header('Content-Type: text/srt; charset=utf-8');
header('Content-Disposition: attachment; filename="youtube_subtitles.srt"');
echo $srtResult;
exit;
5. Bảng So Sánh Các Giải Pháp Trích Xuất Phụ Đề
6. Hướng Dẫn Kiểm Thử Với Lệnh cURL Terminal Thực Tế
Khi đưa file get_sub.php lên hosting (ví dụ: http://khuyendung.net/App/Youtube/get_Sub.php), bạn có thể dùng Terminal trên máy tính cá nhân để gỡ lỗi chuẩn xác theo các cú pháp sau:
Kiểm tra luồng chuyển hướng Header (Khắc phục mã 301)
curl -IL "http://khuyendung.net/App/Youtube/get_Sub.php?videoUrl=https://www.youtube.com/watch?v=dQw4w9WgXcQ"
Cờ -I giúp xem trước Header phản hồi của máy chủ, còn cờ -L ép cURL nhảy theo chuỗi Redirect cho đến khi gặp mã 200 OK.
Tải và lưu thẳng vào file .srt trên máy
curl -L "http://khuyendung.net/App/Youtube/get_Sub.php?videoUrl=https://www.youtube.com/watch?v=dQw4w9WgXcQ" -o "video_subtitles.srt"
Trang Bị Phần Cứng & Hạ Tầng Tối Ưu Cho Dự Án Lập Trình Web Cào Dữ Liệu
Để hệ thống xử lý API diễn ra mượt mà, không lo nghẽn mạng hay giới hạn tài nguyên máy chủ, hãy tham khảo các sản phẩm chuyên dụng:
1. MDN Web Docs: “Cross-Origin Resource Sharing (CORS) & Fetch API Specification” (developer.mozilla.org).
2. PHP Official Documentation: “cURL Functions & CURLOPT_FOLLOWLOCATION Behavior” (php.net/manual/en/book.curl.php).
3. YouTube InnerTube API & Timed Text Specification: “Reverse Engineering YouTube Captions Extraction”.
4. RFC 2616 HTTP/1.1: “Status Code Definitions: 301 Moved Permanently vs 302 Found”.
5. Node.js youtube-transcript Open-source Project: “A Lightweight Subtitles Extractor for Headless Workflows”.