如何加速 Ajax 请求 Python Youtube 抓取
How to speed up Ajax requests Python Youtube scraper
我正在编辑一个简单的爬虫,它可以抓取 Youtube 视频的评论页面。爬虫使用 Ajax 遍历 Youtube 视频评论页面上的每条评论,然后将它们保存到 json 文件中。即使评论数量很少(< 10),解析评论仍然需要 3 分钟以上。
我试过包含 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 多条评论,因此性能是一个很大的因素。
- 使用多线程可以解决问题吗?如果是这样,我将如何重构它以从中受益?
- 这严格来说是网络问题吗?
- 是的,多线程会加速这个过程。 运行 网络操作(即下载)在一个单独的
Thread
.
- 是的,这是一个与网络相关的问题。
您的请求已 I/O 绑定。您向 Youtube 发出请求 - 需要一些时间才能得到响应,这主要取决于网络,您无法加快处理速度。但是,您可以使用 Thread
s 并行发送多个请求。这不会使实际过程更快,但您会在更短的时间内处理更多。
线程教程:
一个与您的任务有些相似的示例 -- http://www.toptal.com/python/beginners-guide-to-concurrency-and-parallelism-in-python
此外,由于您将进行大量的抓取和处理,我建议使用类似 Scrapy 的东西 - 我个人将它用于此类任务。
一次发出多个请求会加快处理速度,但如果需要 3 分钟来解析 10 条评论,那么您还有一些其他问题,而解析 100,000 条评论则需要数天时间。除非有紧迫的理由使用 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 会很慢;你可能想现在就开始将它们转储到数据库中以避免这种情况。 re-examine 为什么你一次只做一个评论:BeautifulSoup
应该在每次加载页面时为你提供完整的评论列表及其文本,这样你就可以分批处理它们,一次就很方便你开始解析更大的组。
我正在编辑一个简单的爬虫,它可以抓取 Youtube 视频的评论页面。爬虫使用 Ajax 遍历 Youtube 视频评论页面上的每条评论,然后将它们保存到 json 文件中。即使评论数量很少(< 10),解析评论仍然需要 3 分钟以上。
我试过包含 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 多条评论,因此性能是一个很大的因素。
- 使用多线程可以解决问题吗?如果是这样,我将如何重构它以从中受益?
- 这严格来说是网络问题吗?
- 是的,多线程会加速这个过程。 运行 网络操作(即下载)在一个单独的
Thread
. - 是的,这是一个与网络相关的问题。
您的请求已 I/O 绑定。您向 Youtube 发出请求 - 需要一些时间才能得到响应,这主要取决于网络,您无法加快处理速度。但是,您可以使用 Thread
s 并行发送多个请求。这不会使实际过程更快,但您会在更短的时间内处理更多。
线程教程:
一个与您的任务有些相似的示例 -- http://www.toptal.com/python/beginners-guide-to-concurrency-and-parallelism-in-python
此外,由于您将进行大量的抓取和处理,我建议使用类似 Scrapy 的东西 - 我个人将它用于此类任务。
一次发出多个请求会加快处理速度,但如果需要 3 分钟来解析 10 条评论,那么您还有一些其他问题,而解析 100,000 条评论则需要数天时间。除非有紧迫的理由使用 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 会很慢;你可能想现在就开始将它们转储到数据库中以避免这种情况。 re-examine 为什么你一次只做一个评论:BeautifulSoup
应该在每次加载页面时为你提供完整的评论列表及其文本,这样你就可以分批处理它们,一次就很方便你开始解析更大的组。