【问题标题】:How to speed up Ajax requests Python Youtube scraper如何加速 Ajax 请求 Python Youtube 刮板
【发布时间】:2026-02-06 06:25:01
【问题描述】:

我正在一个简单的抓取器上进行编辑,该抓取器会抓取 Youtube 视频的评论页面。爬虫使用 Ajax 浏览 Youtube 视频评论页面上的每条评论,然后将它们保存到 json 文件中。即使 cmets 数量很少(

我尝试包含 request-cache 并使用 ujson 而不是 json 来查看是否有任何好处,但没有明显区别。

这是我目前使用的代码:

import os
import sys
import time
import ujson
import requests
import requests_cache
import argparse
import lxml.html

requests_cache.install_cache('comment_cache')

from lxml.cssselect import CSSSelector

YOUTUBE_COMMENTS_URL = 'https://www.youtube.com/all_comments?v={youtube_id}'
YOUTUBE_COMMENTS_AJAX_URL = 'https://www.youtube.com/comment_ajax'


def find_value(html, key, num_chars=2):
    pos_begin = html.find(key) + len(key) + num_chars
    pos_end = html.find('"', pos_begin)
    return html[pos_begin: pos_end]


def extract_comments(html):
    tree = lxml.html.fromstring(html)
    item_sel = CSSSelector('.comment-item')
    text_sel = CSSSelector('.comment-text-content')
    photo_sel = CSSSelector('.user-photo')


    for item in item_sel(tree):
        yield {'cid': item.get('data-cid'),
               'name': item.get('data-name'),
               'ytid': item.get('data-aid'),
               'text': text_sel(item)[0].text_content(),
               'photo': photo_sel(item)[0].get('src')}


def extract_reply_cids(html):
    tree = lxml.html.fromstring(html)
    sel = CSSSelector('.comment-replies-header > .load-comments')
    return [i.get('data-cid') for i in sel(tree)]


def ajax_request(session, url, params, data, retries=10, sleep=20):
    for _ in range(retries):
        response = session.post(url, params=params, data=data)
        if response.status_code == 200:
            response_dict = ujson.loads(response.text)
            return response_dict.get('page_token', None), response_dict['html_content']
        else:
            time.sleep(sleep)


def download_comments(youtube_id, sleep=1, order_by_time=True):
    session = requests.Session()

    # Get Youtube page with initial comments
    response = session.get(YOUTUBE_COMMENTS_URL.format(youtube_id=youtube_id))
    html = response.text
    reply_cids = extract_reply_cids(html)

    ret_cids = []
    for comment in extract_comments(html):
        ret_cids.append(comment['cid'])
        yield comment

    page_token = find_value(html, 'data-token')
    session_token = find_value(html, 'XSRF_TOKEN', 4)

    first_iteration = True

    # Get remaining comments (the same as pressing the 'Show more' button)
    while page_token:
        data = {'video_id': youtube_id,
                'session_token': session_token}

        params = {'action_load_comments': 1,
                  'order_by_time': order_by_time,
                  'filter': youtube_id}

        if order_by_time and first_iteration:
            params['order_menu'] = True
        else:
            data['page_token'] = page_token

        response = ajax_request(session, YOUTUBE_COMMENTS_AJAX_URL, params, data)
        if not response:
            break

        page_token, html = response

        reply_cids += extract_reply_cids(html)
        for comment in extract_comments(html):
            if comment['cid'] not in ret_cids:
                ret_cids.append(comment['cid'])
                yield comment

        first_iteration = False
        time.sleep(sleep)

    # Get replies (the same as pressing the 'View all X replies' link)
    for cid in reply_cids:
        data = {'comment_id': cid,
                'video_id': youtube_id,
                'can_reply': 1,
                'session_token': session_token}

        params = {'action_load_replies': 1,
                  'order_by_time': order_by_time,
                  'filter': youtube_id,
                  'tab': 'inbox'}

        response = ajax_request(session, YOUTUBE_COMMENTS_AJAX_URL, params, data)
        if not response:
            break

        _, html = response

        for comment in extract_comments(html):
            if comment['cid'] not in ret_cids:
                ret_cids.append(comment['cid'])
                yield comment
        time.sleep(sleep)


def main(argv):
    parser = argparse.ArgumentParser(add_help=False, description=('Download Youtube comments without using the Youtube API'))
    parser.add_argument('--help', '-h', action='help', default=argparse.SUPPRESS, help='Show this help message and exit')
    parser.add_argument('--youtubeid', '-y', help='ID of Youtube video for which to download the comments')
    parser.add_argument('--output', '-o', help='Output filename (output format is line delimited JSON)')
    parser.add_argument('--timeorder', '-t', action='store_true', help='Download Youtube comments ordered by time')

    try:
        args = parser.parse_args(argv)

        youtube_id = args.youtubeid
        output = args.output

        start_time = time.time()

        if not youtube_id or not output:
            parser.print_usage()
            raise ValueError('you need to specify a Youtube ID and an output filename')

        print 'Downloading Youtube comments for video:', youtube_id
        count = 0
        with open(output, 'wb') as fp:
            for comment in download_comments(youtube_id, order_by_time=bool(args.timeorder)):
                print >> fp, ujson.dumps(comment, escape_forward_slashes=False)
                count += 1
                sys.stdout.write('Downloaded %d comment(s)\r' % count)
                sys.stdout.flush()

        elapsed_time = time.time() - start_time

        print '\nDone! Elapsed time (seconds):', elapsed_time


    except Exception, e:
        print 'Error:', str(e)
        sys.exit(1)


if __name__ == "__main__":
    main(sys.argv[1:])

我是 Python 新手,所以我不确定瓶颈在哪里。完成的脚本将用于解析超过 100,000 个 cmets,因此性能是一个重要因素。

  • 使用多线程可以解决问题吗?如果是这样,我将如何重构它以从中受益?
  • 这完全是网络问题吗?

【问题讨论】:

  • 对不起,您是如何找出需要插入“session.post”以便从“youtube.com/comment_ajax”网址获得正确响应的正确参数和数据的?我在这个 url/API 上找不到任何文档。

标签: python ajax youtube comments lxml


【解决方案1】:
  • 是的,多线程将加快进程。在单独的Thread 中运行网络操作(即下载)。
  • 是的,这是与网络相关的问题。

您的请求受 I/O 限制。您向 Youtube 发出请求 - 需要一些时间才能获得响应,它主要取决于网络,您无法加快处理速度。但是,您可以使用Threads 并行发送多个请求。这不会使实际过程更快,但您会在更短的时间内处理更多。

线程教程:

一个和你的任务有点相似的例子——http://www.toptal.com/python/beginners-guide-to-concurrency-and-parallelism-in-python

此外,由于您将进行大量抓取和处理,我建议您使用 Scrapy 之类的东西 - 我个人将它用于此类任务。

【讨论】:

  • 感谢您的提示。我做了更多的测试,奇怪的是,使用更多的 cmets,这个过程运行得更快。对于总共 95 个 cmets,总时间下降到 9 秒(每条评论 0.09 秒)。对于 330 cmets,计算时间仅为 42 秒(0.12 秒)。如果它是 I/O 问题,时间不应该增加吗?
  • 您正在从一个 html 响应中提取多个 cmets。因此,一页上的内容越多,您提取的内容就越多。但是下载 html 本身是 I/O 问题。我提到它是一个整体 I/O 问题的原因是因为您无法控制页面上有多少 cmets。所以在任何情况下使用Thread 都会让它更快。
  • 啊,好的,谢谢!如果其他人遇到此问题,我将对其进行调查并添加答案。
【解决方案2】:

一次发出多个请求将加快该过程,但如果解析 10 个 cmets 需要 3 分钟,那么您还有其他一些问题,解析 100,000 个 cmets 需要几天时间。除非有迫切的理由使用lxml,否则我建议您查看BeautifulSoup,让它为您提供评论标签及其文本内容的列表,而不是自己做。我猜大部分的缓慢在于lxml 转换您传递给它的内容,然后在您的手动计数中查找字符串中的位置。我也怀疑打给sleep 的电话——这些电话是干什么用的?

假设这个

print >> fp, ujson.dumps(comment, escape_forward_slashes=False)
count += 1
sys.stdout.write('Downloaded %d comment(s)\r' % count)

仅用于调试,将其移至download_comments 并使用logging 以便您可以打开和关闭它。随时将每个单独的评论转储到 JSON 会很慢;您现在可能想开始将这些转储到数据库中以避免这种情况。并重新检查为什么您一次只做一条评论:BeautifulSoup 应该为您提供一个完整的 cmets 列表及其在每个页面加载时的文本,这样您就可以分批处理它们,一旦您开始解析更大的文件,这将很方便组。

【讨论】: