natural language toolkit twitter client c 20012023 nltk project ewan klein ewaninf ed ac uk lorenzo rubio lrnzciggmail com url https www nltk org for license information see license txt nltk twitter client this module offers methods for collecting and processing tweets most of the functionality depends on access to the twitter apis and this is handled via the third party twython library if one of the methods below returns an integer it is probably a twitter error code https dev twitter comoverviewapiresponsecodes for example the response of 420 means that you have reached the limit of the requests you can currently make to the twitter api currently rate limits for the search api https dev twitter comrestpublicratelimiting are divided into 15 minute windows retrieve data from the twitter streaming api the streaming api requires oauth 1 0 https en wikipedia orgwikioauth authentication register a method for handling tweets param tweethandleri handler method for viewing param data response from twitter api param statuscode the status code returned by the twitter api param data the response from twitter api wrapper for statuses sample api call stream in an endless loop until limit is reached see twython issue 288 https github comryanmcgrathtwythonissues288 colditzjb commented on 9 dec 2014 wrapper for statuses filter api call stream in an endless loop until limit is reached retrieve data from the twitter rest api param appkey optional your applications key param appsecret optional your applications secret key param oauthtoken optional when using oauth 1 combined with oauthtokensecret to make authenticated calls param oauthtokensecret optional when using oauth 1 combined with oauthtoken to make authenticated calls register a method for handling tweets param tweethandleri handler method for viewing or writing tweets to a file given a file object containing a list of tweet ids fetch the corresponding full tweets from the twitter api the api call statuseslookup will fail to retrieve a tweet if the user has deleted it this call to the twitter api is ratelimited see https dev twitter comrestreferencegetstatuseslookup for details param idsf input file object consisting of tweet ids one to a line return iterable of tweet objects in json format the twitter endpoint takes lists of up to 100 ids so we chunk the ids assumes that the handler has been informed fetches tweets from searchtweets generator output and passses them to handler param str keywords a list of query terms to search for written as a commaseparated string param int limit number of tweets to process param str lang language call the rest api searchtweets endpoint with some plausible defaults see the twitter search documentation https dev twitter comrestpublicsearch for more information about admissible search parameters param str keywords a list of query terms to search for written as a commaseparated string param int limit number of tweets to process param str lang language param int maxid id of the last tweet fetched param int retriesaftertwythonexception number of retries when searching tweets before raising an exception rtype python generator if no handler is provided basictweethandler provides minimum functionality for limiting the number of tweets retrieved pagination loop keep fetching tweets until the desired count is reached while dealing with twitter rate limits the maxid is also present in the tweet metadata results searchmetadata nextresults but as part of a query and difficult to fetch this is doing the equivalent last tweet id minus one convert a list of userids into a variety of information about the users see https dev twitter comrestreferencegetusersshow param list userids a list of integer strings corresponding to twitter userids rtype listjson return a collection of the most recent tweets posted by the user param str user the user s screen name the initial symbol should be omitted param int limit the number of tweets to recover 200 is the maximum allowed param str includerts whether to include statuses which have been retweeted by the user possible values are true and false wrapper class with restricted functionality and fewer options process some tweets in a simple manner param str keywords keywords to use for searching or filtering param list follow userids to use for filtering tweets from the public stream param bool toscreen if true display the tweet texts on the screen otherwise print to a file param bool stream if true use the live public stream otherwise search past public tweets param int limit the number of data items to process in the current round of processing param tuple datelimit the date at which to stop collecting new data this should be entered as a tuple which can serve as the argument to datetime datetime e g datelimit2015 4 1 12 40 for 12 30 pm on april 1 2015 note that in the case of streaming this is the maximum date i e a date in the future if not it is the minimum date i e a date in the past param str lang language param bool repeat a flag to determine whether multiple files should be written if true the length of each file will be set by the value of limit use only if toscreen is false see also py func handle param gzipcompress if true output files are compressed with gzip handle data by sending it to the terminal direct data to sys stdout return return false if processing should cease otherwise return true rtype bool param data tweet object returned by twitter api handle data by writing it to a file the difference between the upper and lower date limits depends on whether tweets are coming in an ascending date order i e when streaming or descending date order i e when searching past tweets param int limit number of data items to process in the current round of processing param tuple upperdatelimit the date at which to stop collecting new data this should be entered as a tuple which can serve as the argument to datetime datetime e g upperdatelimit2015 4 1 12 40 for 12 30 pm on april 1 2015 param tuple lowerdatelimit the date at which to stop collecting new data see upperdatalimit for formatting param str fprefix the prefix to use in creating file names for tweet collections param str subdir the name of the directory where tweet collection files should be stored param bool repeat flag to determine whether multiple files should be written if true the length of each file will be set by the value of limit see also py func handle param gzipcompress if true output files are compressed with gzip return timestamped file name rtype str write twitter data as linedelimited json into one or more files return return false if processing should cease otherwise return true param data tweet object returned by twitter api stop for a functional cause e g date limit repeat is true thus close output file and create a new one natural language toolkit twitter client c 2001 2023 nltk project ewan klein ewan inf ed ac uk lorenzo rubio lrnzcig gmail com url https www nltk org for license information see license txt nltk twitter client this module offers methods for collecting and processing tweets most of the functionality depends on access to the twitter apis and this is handled via the third party twython library if one of the methods below returns an integer it is probably a twitter error code https dev twitter com overview api response codes _ for example the response of 420 means that you have reached the limit of the requests you can currently make to the twitter api currently rate limits for the search api https dev twitter com rest public rate limiting _ are divided into 15 minute windows retrieve data from the twitter streaming api the streaming api requires oauth 1 0 https en wikipedia org wiki oauth _ authentication register a method for handling tweets param tweethandleri handler method for viewing param data response from twitter api param status_code the status code returned by the twitter api param data the response from twitter api wrapper for statuses sample api call stream in an endless loop until limit is reached see twython issue 288 https github com ryanmcgrath twython issues 288 colditzjb commented on 9 dec 2014 wrapper for statuses filter api call stream in an endless loop until limit is reached retrieve data from the twitter rest api param app_key optional your applications key param app_secret optional your applications secret key param oauth_token optional when using oauth 1 combined with oauth_token_secret to make authenticated calls param oauth_token_secret optional when using oauth 1 combined with oauth_token to make authenticated calls register a method for handling tweets param tweethandleri handler method for viewing or writing tweets to a file given a file object containing a list of tweet ids fetch the corresponding full tweets from the twitter api the api call statuses lookup will fail to retrieve a tweet if the user has deleted it this call to the twitter api is rate limited see https dev twitter com rest reference get statuses lookup for details param ids_f input file object consisting of tweet ids one to a line return iterable of tweet objects in json format the twitter endpoint takes lists of up to 100 ids so we chunk the ids assumes that the handler has been informed fetches tweets from search_tweets generator output and passses them to handler param str keywords a list of query terms to search for written as a comma separated string param int limit number of tweets to process param str lang language call the rest api search tweets endpoint with some plausible defaults see the twitter search documentation https dev twitter com rest public search _ for more information about admissible search parameters param str keywords a list of query terms to search for written as a comma separated string param int limit number of tweets to process param str lang language param int max_id id of the last tweet fetched param int retries_after_twython_exception number of retries when searching tweets before raising an exception rtype python generator if no handler is provided basictweethandler provides minimum functionality for limiting the number of tweets retrieved pagination loop keep fetching tweets until the desired count is reached while dealing with twitter rate limits wait 15 minutes the max_id is also present in the tweet metadata results search_metadata next_results but as part of a query and difficult to fetch this is doing the equivalent last tweet id minus one convert a list of userids into a variety of information about the users see https dev twitter com rest reference get users show param list userids a list of integer strings corresponding to twitter userids rtype list json return a collection of the most recent tweets posted by the user param str user the user s screen name the initial symbol should be omitted param int limit the number of tweets to recover 200 is the maximum allowed param str include_rts whether to include statuses which have been retweeted by the user possible values are true and false wrapper class with restricted functionality and fewer options process some tweets in a simple manner param str keywords keywords to use for searching or filtering param list follow userids to use for filtering tweets from the public stream param bool to_screen if true display the tweet texts on the screen otherwise print to a file param bool stream if true use the live public stream otherwise search past public tweets param int limit the number of data items to process in the current round of processing param tuple date_limit the date at which to stop collecting new data this should be entered as a tuple which can serve as the argument to datetime datetime e g date_limit 2015 4 1 12 40 for 12 30 pm on april 1 2015 note that in the case of streaming this is the maximum date i e a date in the future if not it is the minimum date i e a date in the past param str lang language param bool repeat a flag to determine whether multiple files should be written if true the length of each file will be set by the value of limit use only if to_screen is false see also py func handle param gzip_compress if true output files are compressed with gzip handle data by sending it to the terminal direct data to sys stdout return return false if processing should cease otherwise return true rtype bool param data tweet object returned by twitter api handle data by writing it to a file the difference between the upper and lower date limits depends on whether tweets are coming in an ascending date order i e when streaming or descending date order i e when searching past tweets param int limit number of data items to process in the current round of processing param tuple upper_date_limit the date at which to stop collecting new data this should be entered as a tuple which can serve as the argument to datetime datetime e g upper_date_limit 2015 4 1 12 40 for 12 30 pm on april 1 2015 param tuple lower_date_limit the date at which to stop collecting new data see upper_data_limit for formatting param str fprefix the prefix to use in creating file names for tweet collections param str subdir the name of the directory where tweet collection files should be stored param bool repeat flag to determine whether multiple files should be written if true the length of each file will be set by the value of limit see also py func handle param gzip_compress if true output files are compressed with gzip return timestamped file name rtype str write twitter data as line delimited json into one or more files return return false if processing should cease otherwise return true param data tweet object returned by twitter api stop for a functional cause e g date limit repeat is true thus close output file and create a new one
import datetime
import gzip
import itertools
import json
import os
import time

import requests
from twython import Twython, TwythonStreamer
from twython.exceptions import TwythonError, TwythonRateLimitError

from nltk.twitter.api import BasicTweetHandler, TweetHandlerI
from nltk.twitter.util import credsfromfile, guess_path


class Streamer(TwythonStreamer):
    

    def __init__(self, app_key, app_secret, oauth_token, oauth_token_secret):

        self.handler = None
        self.do_continue = True
        TwythonStreamer.__init__(
            self, app_key, app_secret, oauth_token, oauth_token_secret
        )

    def register(self, handler):
        
        self.handler = handler

    def on_success(self, data):
        
        if self.do_continue:
            if self.handler is not None:
                if "text" in data:
                    self.handler.counter += 1
                    self.handler.handle(data)
                    self.do_continue = self.handler.do_continue()
            else:
                raise ValueError("No data handler has been registered.")
        else:
            self.disconnect()
            self.handler.on_finish()

    def on_error(self, status_code, data):
        
        print(status_code)

    def sample(self):
        
        while self.do_continue:

            
            
            

            try:
                self.statuses.sample()
            except requests.exceptions.ChunkedEncodingError as e:
                if e is not None:
                    print(f"Error (stream will continue): {e}")
                continue

    def filter(self, track="", follow="", lang="en"):
        
        while self.do_continue:
            

            try:
                if track == "" and follow == "":
                    msg = "Please supply a value for 'track', 'follow'"
                    raise ValueError(msg)
                self.statuses.filter(track=track, follow=follow, lang=lang)
            except requests.exceptions.ChunkedEncodingError as e:
                if e is not None:
                    print(f"Error (stream will continue): {e}")
                continue


class Query(Twython):
    

    def __init__(self, app_key, app_secret, oauth_token, oauth_token_secret):
        
        self.handler = None
        self.do_continue = True
        Twython.__init__(self, app_key, app_secret, oauth_token, oauth_token_secret)

    def register(self, handler):
        
        self.handler = handler

    def expand_tweetids(self, ids_f, verbose=True):
        
        ids = [line.strip() for line in ids_f if line]

        if verbose:
            print(f"Counted {len(ids)} Tweet IDs in {ids_f}.")

        
        
        id_chunks = [ids[i : i + 100] for i in range(0, len(ids), 100)]

        chunked_tweets = (self.lookup_status(id=chunk) for chunk in id_chunks)

        return itertools.chain.from_iterable(chunked_tweets)

    def _search_tweets(self, keywords, limit=100, lang="en"):
        
        while True:
            tweets = self.search_tweets(
                keywords=keywords, limit=limit, lang=lang, max_id=self.handler.max_id
            )
            for tweet in tweets:
                self.handler.handle(tweet)
            if not (self.handler.do_continue() and self.handler.repeat):
                break
        self.handler.on_finish()

    def search_tweets(
        self,
        keywords,
        limit=100,
        lang="en",
        max_id=None,
        retries_after_twython_exception=0,
    ):
        
        if not self.handler:
            
            
            self.handler = BasicTweetHandler(limit=limit)

        count_from_query = 0
        if max_id:
            self.handler.max_id = max_id
        else:
            results = self.search(
                q=keywords, count=min(100, limit), lang=lang, result_type="recent"
            )
            count = len(results["statuses"])
            if count == 0:
                print("No Tweets available through REST API for those keywords")
                return
            count_from_query = count
            self.handler.max_id = results["statuses"][count - 1]["id"] - 1

            for result in results["statuses"]:
                yield result
                self.handler.counter += 1
                if self.handler.do_continue() == False:
                    return

        
        
        retries = 0
        while count_from_query < limit:
            try:
                mcount = min(100, limit - count_from_query)
                results = self.search(
                    q=keywords,
                    count=mcount,
                    lang=lang,
                    max_id=self.handler.max_id,
                    result_type="recent",
                )
            except TwythonRateLimitError as e:
                print(f"Waiting for 15 minutes -{e}")
                time.sleep(15 * 60)  
                continue
            except TwythonError as e:
                print(f"Fatal error in Twython request -{e}")
                if retries_after_twython_exception == retries:
                    raise e
                retries += 1

            count = len(results["statuses"])
            if count == 0:
                print("No more Tweets available through rest api")
                return
            count_from_query += count
            
            
            
            
            self.handler.max_id = results["statuses"][count - 1]["id"] - 1

            for result in results["statuses"]:
                yield result
                self.handler.counter += 1
                if self.handler.do_continue() == False:
                    return

    def user_info_from_id(self, userids):
        
        return [self.show_user(user_id=userid) for userid in userids]

    def user_tweets(self, screen_name, limit, include_rts="false"):
        
        data = self.get_user_timeline(
            screen_name=screen_name, count=limit, include_rts=include_rts
        )
        for item in data:
            self.handler.handle(item)


class Twitter:
    

    def __init__(self):
        self._oauth = credsfromfile()
        self.streamer = Streamer(**self._oauth)
        self.query = Query(**self._oauth)

    def tweets(
        self,
        keywords="",
        follow="",
        to_screen=True,
        stream=True,
        limit=100,
        date_limit=None,
        lang="en",
        repeat=False,
        gzip_compress=False,
    ):
        
        if stream:
            upper_date_limit = date_limit
            lower_date_limit = None
        else:
            upper_date_limit = None
            lower_date_limit = date_limit

        if to_screen:
            handler = TweetViewer(
                limit=limit,
                upper_date_limit=upper_date_limit,
                lower_date_limit=lower_date_limit,
            )
        else:
            handler = TweetWriter(
                limit=limit,
                upper_date_limit=upper_date_limit,
                lower_date_limit=lower_date_limit,
                repeat=repeat,
                gzip_compress=gzip_compress,
            )

        if to_screen:
            handler = TweetViewer(limit=limit)
        else:
            if stream:
                upper_date_limit = date_limit
                lower_date_limit = None
            else:
                upper_date_limit = None
                lower_date_limit = date_limit

            handler = TweetWriter(
                limit=limit,
                upper_date_limit=upper_date_limit,
                lower_date_limit=lower_date_limit,
                repeat=repeat,
                gzip_compress=gzip_compress,
            )

        if stream:
            self.streamer.register(handler)
            if keywords == "" and follow == "":
                self.streamer.sample()
            else:
                self.streamer.filter(track=keywords, follow=follow, lang=lang)
        else:
            self.query.register(handler)
            if keywords == "":
                raise ValueError("Please supply at least one keyword to search for.")
            else:
                self.query._search_tweets(keywords, limit=limit, lang=lang)


class TweetViewer(TweetHandlerI):
    

    def handle(self, data):
        
        text = data["text"]
        print(text)

        self.check_date_limit(data)
        if self.do_stop:
            return

    def on_finish(self):
        print(f"Written {self.counter} Tweets")


class TweetWriter(TweetHandlerI):
    

    def __init__(
        self,
        limit=2000,
        upper_date_limit=None,
        lower_date_limit=None,
        fprefix="tweets",
        subdir="twitter-files",
        repeat=False,
        gzip_compress=False,
    ):
        
        self.fprefix = fprefix
        self.subdir = guess_path(subdir)
        self.gzip_compress = gzip_compress
        self.fname = self.timestamped_file()
        self.repeat = repeat
        self.output = None
        TweetHandlerI.__init__(self, limit, upper_date_limit, lower_date_limit)

    def timestamped_file(self):
        
        subdir = self.subdir
        fprefix = self.fprefix
        if subdir:
            if not os.path.exists(subdir):
                os.mkdir(subdir)

        fname = os.path.join(subdir, fprefix)
        fmt = "%Y%m%d-%H%M%S"
        timestamp = datetime.datetime.now().strftime(fmt)
        if self.gzip_compress:
            suffix = ".gz"
        else:
            suffix = ""
        outfile = f"{fname}.{timestamp}.json{suffix}"
        return outfile

    def handle(self, data):
        
        if self.startingup:
            if self.gzip_compress:
                self.output = gzip.open(self.fname, "w")
            else:
                self.output = open(self.fname, "w")
            print(f"Writing to {self.fname}")

        json_data = json.dumps(data)
        if self.gzip_compress:
            self.output.write((json_data + "\n").encode("utf-8"))
        else:
            self.output.write(json_data + "\n")

        self.check_date_limit(data)
        if self.do_stop:
            return

        self.startingup = False

    def on_finish(self):
        print(f"Written {self.counter} Tweets")
        if self.output:
            self.output.close()

    def do_continue(self):
        if self.repeat == False:
            return TweetHandlerI.do_continue(self)

        if self.do_stop:
            
            return False

        if self.counter == self.limit:
            
            
            self._restart_file()
        return True

    def _restart_file(self):
        self.on_finish()
        self.fname = self.timestamped_file()
        self.startingup = True
        self.counter = 0