Is it possible to randomly sample YouTube comments with YouTube API V3?

Viewed 204

I have been trying to download all the YouTube comments on popular videos using python requests, but it has been throwing up the following error after about a quarter of the total comments:

{'error': {'code': 400, 'message': "The API server failed to successfully process the request. While this can be a transient error, it usually indicates that the request's input is invalid. Check the structure of the commentThread resource in the request body to ensure that it is valid.", 'errors': [{'message': "The API server failed to successfully process the request. While this can be a transient error, it usually indicates that the request's input is invalid. Check the structure of the commentThread resource in the request body to ensure that it is valid.", 'domain': 'youtube.commentThread', 'reason': 'processingFailure', 'location': 'body', 'locationType': 'other'}]}}

I found this thread detailing the same issue, and it seems that it is not possible to download all the comments on popular videos.

This is my code:

import argparse
import urllib
import requests
import json
import time
start_time = time.time()

class YouTubeApi():

    YOUTUBE_COMMENTS_URL = 'https://www.googleapis.com/youtube/v3/commentThreads'
    comment_counter = 0
    
    with open("API_keys.txt", "r") as f:
        key_list = f.readlines()    
        key_list = [key.strip('/n') for key in key_list]


    def format_comments(self, results, likes_required):
        comments_list = []
        try:
            for item in results["items"]:
                comment = item["snippet"]["topLevelComment"]

                likes = comment["snippet"]["likeCount"]
                if likes < likes_required:
                    continue

                author = comment["snippet"]["authorDisplayName"]
                text = comment["snippet"]["textDisplay"]

                str = "Comment by {}:\n \"{}\"\n\n".format(author, text)
                str = str.encode('ascii', 'replace').decode()

                comments_list.append(str)
                self.comment_counter += 1
                print("Comments downloaded:", self.comment_counter, end="\r")
        except(KeyError):
            print(results)
            
                
             
        return comments_list

        
    def get_video_comments(self, video_id, likes_required):

        with open("API_keys.txt", "r") as f:
            key_list = f.readlines()
            
        key_list = [key.strip('/n') for key in key_list]

        if self.comment_counter <= 900000:
            key = self.key_list[0]
        elif self.comment_counter <= 1800000:
            key = self.key_list[1]
        elif self.comment_counter <= 2700000:
            key = self.key_list[2]
        elif self.comment_counter <= 3600000:
            key = self.key_list[3]
        elif self.comment_counter <= 4500000:
            key = self.key_list[4]
            
        params = {
            'part': 'snippet,replies',
            'maxResults': 100,
            'videoId': video_id,
            'textFormat': 'plainText',
            'key': key
        }
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36'
        }
        try:
            #data = self.openURL(self.YOUTUBE_COMMENTS_URL, params)
            comments_data = requests.get(self.YOUTUBE_COMMENTS_URL, params=params, headers=headers)
        
        except ChunkedEncodingError:
           tries = 5
           print("Chunked Error. Retrying...")
           for n in range(tries):
               try:
                   x = 0
                   x += 1
                   print("Trying", x, "times") 
                   response = session.post("https://www.youtube.com/comment_service_ajax", params=params, data=data, headers=headers)
                   comments_data = json.loads(response.text)
               except ChunkedEncodingError as c:
                   print(c)
        results = comments_data.json()
        
        nextPageToken = results.get("nextPageToken")

        commments_list = []
        commments_list += self.format_comments(results, likes_required)

        while nextPageToken:
            params.update({'pageToken': nextPageToken})
            try:
                comments_data = requests.get(self.YOUTUBE_COMMENTS_URL, params=params, headers=headers)
            except ChunkedEncodingError as c:
                tries = 5
                print("Chunked Error. Retrying...")
                for n in range(tries):
                    try:
                        x = 0
                        x += 1
                        print("Trying", x, "times") 
                        response = session.post("https://www.youtube.com/comment_service_ajax", params=params, data=data, headers=headers)
                        comments_data = json.loads(response.text)
                    except ChunkedEncodingError as c:
                        print(c)
            
            results = comments_data.json()
            nextPageToken = results.get("nextPageToken")
            commments_list += self.format_comments(results, likes_required)

        return commments_list


    def get_video_id_list(self, filename):
        try:
            with open(filename, 'r') as file:
                URL_list = file.readlines()
        except FileNotFoundError:
            exit("File \"" + filename + "\" not found")

        list = []
        for url in URL_list:
            if url == "\n":     # ignore empty lines
                continue
            if url[-1] == '\n':     # delete '\n' at the end of line
                url = url[:-1]
            if url.find('='):   # get id
                id = url[url.find('=') + 1:]
                list.append(id)
            else:
                print("Wrong URL")

        return list


def main():
    yt = YouTubeApi()

    parser = argparse.ArgumentParser(add_help=False, description=("Download youtube comments from many videos into txt file"))
    required = parser.add_argument_group("required arguments")
    optional = parser.add_argument_group("optional arguments")
 here: https://console.developers.google.com/apis/credentials")
    optional.add_argument("--likes", '-l', help="The amount of likes a comment needs to be saved", type=int)
    optional.add_argument("--input", '-i', help="URL list file name")
    optional.add_argument("--output", '-o', help="Output file name")
    optional.add_argument("--help", '-h', help="Help", action='help')
    args = parser.parse_args()

    # --------------------------------------------------------------------- #


    likes = 0
    if args.likes:
        likes = args.likes

    input_file = "URL_list.txt"
    if args.input:
        input_file = args.input

    output_file = "Comments.txt"
    if args.output:
        output_file = args.output

    list = yt.get_video_id_list(input_file)
    if not list:
        exit("No URLs in input file")

    try:
        
        vid_counter = 0
        with open(output_file, "a") as f:
            for video_id in list:
                vid_counter += 1
                print("Downloading comments for video ", vid_counter, ", id: ", video_id, sep='')
                comments = yt.get_video_comments(video_id, likes)
                if comments:
                    for comment in comments:
                        f.write(comment)

        print('\nDone!')

    except KeyboardInterrupt:
        exit("User Aborted the Operation")

    # --------------------------------------------------------------------- #


if __name__ == '__main__':
    main()

The next best method would be to randomly sample them. Does anyone know if this is possible with the API V3?

1 Answers

Even if the API returns a processingFailure error, you could still catch that (or any other API error for that matter) for to terminate gracefully your pagination loop. This way your script will provide the top-level comments that it fetched from of the API prior to the occurrence of the first API error.

The error response provided by the YouTube Data API is (usually) of the following form:

{
  "error": {
    "errors": [
      {
        "domain": <string>,
        "reason": <string>,
        "message": <string>,
        "locationType": <string>,
        "location": <string>
      }
    ],
    "code": <integer>,
    "message": <string>
  }
}

Hence, you could have defined the following function:

def is_error_response(response):
    error = response.get('error')
    if error is None:
        return False
    print("API Error: "
        f"code={error['code']} "
        f"domain={error['errors'][0]['domain']} "
        f"reason={error['errors'][0]['reason']} "
        f"message={error['errors'][0]['message']!r}")
    return True

that you'll invoke after each statement of form results = comments_data.json(). In case of the first occurrence of that statement, you'll have:

results = comments_data.json()
if is_error_response(results):
   return []
nextPageToken = results.get("nextPageToken")

For the second instance of that statement:

results = comments_data.json()
if is_error_response(results):
   return comments_list
nextPageToken = results.get("nextPageToken")

Notice that the function is_error_response above prints out an error message on stdout in case its argument in an API error response; this is for the purpose of having the user of your script informed about the API call failure.

Related