7
\$\begingroup\$

I would like to improve my Reddit-scraping code to make it faster, but I don't know how. I am using deque instead of list to improve append performance. Otherwise my code calls the PRAW API and type checks one class attribute (I don't know why this is necessary but otherwise I get type errors). Is there a faster way to do this? These functions are surprisingly slow, and performance time is also highly variable in addition to having a high mean execution time.

For a given user, I'm trying to retrieve their most recent comments and posts. I've pasted the code at the bottom of this post, but I have two problems. First, the code seems to run awfully slow considering how little information I'm retrieving. Second, the variance in execution time from one function call to the next is surprisingly high. You'll see I have two sleep commands for 1 second each, so the minimum time for get_user_comments_and_posts() to execute is 2 seconds. Sometimes I see this, but sometimes I see 14 seconds! And when I print out the output for a given function call, there doesn't seem to be anything special about the output for a short vs. long execution time.

COMMENT_LIMIT = 2

@timeit
def get_user_comments_and_posts(self):
    time.sleep(TIME_SLEEP)
    self.get_comments()
    time.sleep(TIME_SLEEP)
    self.get_submissions()


@timeit
def get_comments(self):
    comments_list =  deque([])
    comments = self.get_c_praw_call()
    for c in comments:
        if isinstance(c.subreddit, basestring):
            subreddit_name = c.subreddit
        else:
            subreddit_name =  c.subreddit.display_name
        new_comment = comment(c.created_utc, c.ups, subreddit_name)
        comments_list.append(new_comment)
    self.comments = comments_list
    for commentD in self.comments:
        print commentD


@timeit
def get_c_praw_call(self):
    return self.praw_object.get_redditor(self.username).get_comments(limit=COMMENT_LIMIT)

@timeit
def get_submissions(self):
    submissions_list =  deque([])
    submissions = self.get_s_praw_call()
    for s in submissions:
        if isinstance(s.subreddit, basestring):
            subreddit_name = s.subreddit
        else:
            subreddit_name =  s.subreddit.display_name
        new_thread = redditThread(s.title, s.ups, s.created_utc, s.id, subreddit_name, [])
        submissions_list.append(new_thread)
    self.threads = submissions_list
    for threadD in  self.threads:
        print threadD

@timeit
def get_s_praw_call(self):
    return self.praw_object.get_redditor(self.username).get_submitted(limit=COMMENT_LIMIT)

Here's the output from the timeit decorator:

this comment has 0 votes and belongs in the todayilearned subreddit where it was made at 1434032968
this comment has 1 votes and belongs in the politics subreddit where it was made at 1434026834
'get_comments'  2.43 sec
'get_s_praw_call'  0.00 sec
'__init__'  0.00 sec
'__init__'  0.00 sec
this has title: Rupert Murdoch stepping down from 21st Century Fox and came from subreddit: news
this has title: Top athletics coach Alberto Salazar faces doping claims and came from subreddit: news
'get_submissions'  0.62 sec
'get_user_comments_and_posts'  5.05 sec
'get_c_praw_call'  0.09 sec
'__init__'  0.00 sec
'__init__'  0.00 sec
this comment has 1 votes and belongs in the todayilearned subreddit where it was made at 1434044423
this comment has 1 votes and belongs in the todayilearned subreddit where it was made at 1434044125
'get_comments'  2.17 sec
'get_s_praw_call'  0.00 sec
'__init__'  0.00 sec
'__init__'  0.00 sec
this has title: [SunTuThursday Forum] Halodrol - DAA - Sub-Q Injections - Squid vs. Octopus vs. Cuttlefish: Which is the ultimate
cephalod? - Ask Anything! and came from subreddit: steroids
this has title: [Off-Topic Discussion] "Everyone has an opinion, and the guy screaming for censorship may be the next guy to have his
ideas cut off." - Richard King and came from subreddit: steroids
'get_submissions'  0.86 sec
'get_user_comments_and_posts'  5.03 sec
'get_c_praw_call'  0.05 sec
'__init__'  0.00 sec
'__init__'  0.00 sec
this comment has 0 votes and belongs in the PaoMustResign subreddit where it was made at 1434040381
this comment has 1 votes and belongs in the Art subreddit where it was made at 1434038905
'get_comments'  2.08 sec
'get_s_praw_call'  0.00 sec
'__init__'  0.00 sec
'__init__'  0.00 sec
this has title: How much lower of an iq is the average FPH poster? and came from subreddit: circlebroke
this has title: When will GallowBoob reach Bill Brasky status? and came from subreddit: circlebroke
'get_submissions'  0.99 sec
'get_user_comments_and_posts'  5.07 sec

So it is often taking upwards of 3 seconds to process the API calls. Is this normal or am I making some really stupid blunder?

\$\endgroup\$
  • \$\begingroup\$ It looks like you're using PRAW library. This library enforces Reddit's restriction of only permitting 30 API requests per minute (60 for OAuth sessions). "PRAW was designed to make following the API guidelines simple. It will not send a request more often than every 2 seconds and it caches every page for 30 seconds." See here \$\endgroup\$ – Dustin Oct 4 '15 at 23:53
5
\$\begingroup\$

The issue probably isn't with your code. An operation like this is almost certainly IO bound, not CPU bound. This means that the bulk of the time is being used waiting for the network to respond, not waiting for your CPU to process something locally.

You can speed a program like this up by multithreading it. This strategy would allow you to have multiple concurrent requests open to reddit at once. I would expect the speed to scale almost linearly with the number of threads, up to some maximum (likely bound by the PRAW API). Check out the concurrent futures library for simple multithreading: https://docs.python.org/3.4/library/concurrent.futures.html

In case you have never dealt with multithreading, beware that you will need to monitor when multiple threads access a single object to store results from the PRAW API. You will need to make sure to lock that object before accessing it.

EDIT: I just realized you were using Python 2... concurrent.futures won't do you much good. You should take a look at the Queue module: https://docs.python.org/2/library/queue.html#module-Queue Threading will also be helpful: https://docs.python.org/2/library/threading.html

Also, a few other comments. isinstance is generally frowned upon in Python. Python subscribes to a philosophy called duck typing. Basically, instead of only proceeding only if something is the correct type, we wrap questionable statements in try/except blocks instead. If there is an error, we simply catch it. Lastly, you aren't going to realize any gains here from a deque; it will just consume more memory as a normal list. A deque would be valuable if you were popping comments off the front of the list as well as the back, but in your case, you only ever access comments_list to iterate over it (using that for loop)

\$\endgroup\$
  • \$\begingroup\$ Thanks for the suggestions. You were right. I have added an edit to my question to show performance for others' reference. Funny thing is multiple threads improves performance slightly but multiple processes improve performance enormously. However the improvement is in constant time - no difference for 2 vs 3 vs 4 workers though this might be the API limit. Thanks for your help. \$\endgroup\$ – sunny Jun 12 '15 at 14:54
  • \$\begingroup\$ weird. That really surprises me. For reasons I won't get into, in Python generally IO bound operations benefit from multiple threads while CPU bound processes benefit from multiprocessing. I've got a feeling you're right about he API... I bet it's doing something funky. Glad you figured this out! \$\endgroup\$ – Nick Jun 15 '15 at 18:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.