Uncategorized

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 captionTracks chứa các URL XML (Timed Text). Để chuyển sang định dạng .srt chuẩn, chỉ cần tính toán mốc thời gian start + dur sang dạng HH:MM:SS,MS và 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_PROXY và 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.

Lập trình viên viết mã nguồn JavaScript và phân tích cấu trúc API trên máy tính
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ục ytInitialPlayerResponse). Đối tượng này chứa danh sách các ngôn ngữ có sẵn kèm đường dẫn gốc baseUrl.
  • Đị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 đó start là thời điểm xuất hiện (tính bằng giây) và dur là 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://localhost hoặ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ệ thống mạng máy chủ proxy và đường truyền dữ liệu API quốc tế
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ở:

  1. Mở video YouTube trên trình duyệt máy tính và bật nút phụ đề (CC).
  2. Bấm phím F12 (hoặc tổ hợp Ctrl + Shift + I) để mở Developer Tools, sau đó chọn thẻ Console.
  3. 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(/&amp;/g, '&')
                                   .replace(/&quot;/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(/&amp;/g, '&').replace(/&quot;/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 sang https://, hoặc link video thiếu tham số bắt buộc. Nếu cURL không được bật CURLOPT_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.

Màn hình giao diện dòng lệnh Linux terminal thực thi lệnh cURL và kiểm tra trạng thái HTTP
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ụ Đề

Giải Pháp Ưu Điểm Nổi Bật Nhược Điểm / Rủi Ro Môi Trường Khuyên Dùng
JavaScript Console Không tốn server, tận dụng cookie sẵn có, không lo CORS. Phải làm thủ công từng video, người dùng phải biết mở F12. Cá nhân tải vài video nhanh.
Node.js Server Xử lý bất đồng bộ mạnh, thư viện npm hỗ trợ tận răng. Cần thuê VPS riêng (không chạy được trên Shared Hosting giá rẻ). Hệ thống SaaS lớn, Microservices.
PHP cURL Proxy Chạy được trên mọi Hosting, dễ cấu hình bọc Proxy, trả file trực tiếp. Cần bật cURL theo chân Redirect (-L) để tránh lỗi 301. Website tin tức, Web tool WordPress.

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:

Nguồn tài liệu tham khảo kỹ thuật (Reference Citations):

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”.

Back to top button