如何加快python中的web抓取速度

时间:2014-05-04 18:48:00

标签: python web-scraping

我正在为学校做项目,我正在尝试获取有关电影的数据。我已经设法编写了一个脚本来从IMDbPY和Open Movie DB API(omdbapi.com)获取我需要的数据。我遇到的挑战是,我正在尝试获取22,305部电影的数据,每个请求大约需要0.7秒。基本上我当前的脚本大约需要8个小时才能完成。寻找可能同时使用多个请求的任何方式或任何其他建议,以显着加快获取此数据的过程。

import urllib2
import json
import pandas as pd
import time
import imdb

start_time = time.time() #record time at beginning of script

#used to make imdb.com think we are getting this data from a browser
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers = { 'User-Agent' : user_agent }

#Open Movie Database Query url for IMDb IDs
url = 'http://www.omdbapi.com/?tomatoes=true&i='

#read the ids from the imdb_id csv file
imdb_ids = pd.read_csv('ids.csv')

cols = [u'Plot', u'Rated', u'tomatoImage', u'Title', u'DVD', u'tomatoMeter',
 u'Writer', u'tomatoUserRating', u'Production', u'Actors', u'tomatoFresh',
 u'Type', u'imdbVotes', u'Website', u'tomatoConsensus', u'Poster', u'tomatoRotten',
 u'Director', u'Released', u'tomatoUserReviews', u'Awards', u'Genre', u'tomatoUserMeter',
 u'imdbRating', u'Language', u'Country', u'imdbpy_budget', u'BoxOffice', u'Runtime',
 u'tomatoReviews', u'imdbID', u'Metascore', u'Response', u'tomatoRating', u'Year',
 u'imdbpy_gross']

#create movies dataframe
movies = pd.DataFrame(columns=cols)

i=0
for i in range(len(imdb_ids)-1):

    start = time.time()
    req = urllib2.Request(url + str(imdb_ids.ix[i,0]), None, headers) #request page
    response = urllib2.urlopen(req) #actually call the html request
    the_page = response.read() #read the json from the omdbapi query
    movie_json = json.loads(the_page) #convert the json to a dict

    #get the gross revenue and budget from IMDbPy
    data = imdb.IMDb()
    movie_id = imdb_ids.ix[i,['imdb_id']]
    movie_id = movie_id.to_string()
    movie_id = int(movie_id[-7:])
    data = data.get_movie_business(movie_id)
    data = data['data']
    data = data['business']

    #get the budget $ amount out of the budget IMDbPy string
    try:
        budget = data['budget']
        budget = budget[0]
        budget = budget.replace('$', '')
        budget = budget.replace(',', '')
        budget = budget.split(' ')
        budget = str(budget[0]) 
    except:
        None

    #get the gross $ amount out of the gross IMDbPy string
    try:
        budget = data['budget']
        budget = budget[0]
        budget = budget.replace('$', '')
        budget = budget.replace(',', '')
        budget = budget.split(' ')
        budget = str(budget[0])

        #get the gross $ amount out of the gross IMDbPy string
        gross = data['gross']
        gross = gross[0]
        gross = gross.replace('$', '')
        gross = gross.replace(',', '')
        gross = gross.split(' ')
        gross = str(gross[0])
    except:
        None

    #add gross to the movies dict 
    try:
        movie_json[u'imdbpy_gross'] = gross
    except:
        movie_json[u'imdbpy_gross'] = 0

    #add gross to the movies dict    
    try:
        movie_json[u'imdbpy_budget'] = budget
    except:
        movie_json[u'imdbpy_budget'] = 0

    #create new dataframe that can be merged to movies DF    
    tempDF = pd.DataFrame.from_dict(movie_json, orient='index')
    tempDF = tempDF.T

    #add the new movie to the movies dataframe
    movies = movies.append(tempDF, ignore_index=True)
    end = time.time()
    time_took = round(end-start, 2)
    percentage = round(((i+1) / float(len(imdb_ids))) * 100,1)
    print i+1,"of",len(imdb_ids),"(" + str(percentage)+'%)','completed',time_took,'sec'
    #increment counter
    i+=1  

#save the dataframe to a csv file            
movies.to_csv('movie_data.csv', index=False)
end_time = time.time()
print round((end_time-start_time)/60,1), "min"

1 个答案:

答案 0 :(得分:7)

使用Eventlet库来获取

根据评论中的建议,您应同时获取Feed。可以使用treadingmultiprocessingeventlet来完成此操作。

安装eventlet

$ pip install eventlet

尝试来自eventlet

的网络抓取工具示例

请参阅:http://eventlet.net/doc/examples.html#web-crawler

了解eventlet

的并发性

使用threading系统负责在线程之间切换。如果您必须访问一些常见的数据结构,这会带来很大的问题,因为您不知道,其他线程当前正在访问您的数据。然后,您开始使用同步块,锁和信号量 - 只是为了同步对共享数据结构的访问。

使用eventlet它变得更加简单 - 您始终只运行一个线程,并且仅在I / O指令或其他eventlet调用时在它们之间跳转。其余代码不间断运行且没有风险,另一个线程会搞乱我们的数据。

您只需照顾以下事项:

  • 所有I / O操作必须是非阻塞的(这很容易,eventlet为您需要的大多数I / O提供非阻塞版本。

  • 您的剩余代码不能是CPU昂贵的,因为它会阻止" green"线程更长的时间和'#34;绿色"多线程将会消失。

eventlet的一大优势是,它允许以直接的方式编写代码,而不会因为锁定,信号量等而损坏它(

)。

eventlet应用于您的代码

如果我理解正确,提前知道要提取的网址列表,并且在分析中处理的顺序并不重要。这将允许从eventlet几乎直接复制示例。我看到,索引i具有一定的意义,因此您可以考虑将url和索引混合为元组并将其作为独立作业处理。

肯定还有其他方法,但我个人发现eventlet非常容易使用它与其他技术比较,同时获得非常好的结果(特别是在获取提要时)。您只需要掌握主要概念,并且要小心遵循eventlet要求(保持非阻塞)。

使用请求和eventlet获取网址 - erequests

使用requests进行异步处理的各种软件包,其中一个使用eventlet并命名为erequests,请参阅https://github.com/saghul/erequests

简单的样本提取网址集

import erequests

# have list of urls to fetch
urls = [
    'http://www.heroku.com',
    'http://python-tablib.org',
    'http://httpbin.org',
    'http://python-requests.org',
    'http://kennethreitz.com'
]
# erequests.async.get(url) creates asynchronous request
async_reqs = [erequests.async.get(url) for url in urls]
# each async request is ready to go, but not yet performed

# erequests.map will call each async request to the action
# what returns processed request `req`
for req in erequests.map(async_reqs):
    if req.ok:
        content = req.content
        # process it here
        print "processing data from:", req.url

处理此特定问题的问题

我们可以获取并以某种方式处理我们需要的所有网址。但是在这个问题中,处理与源数据中的特定记录绑定,因此我们需要将处理后的请求与我们需要的记录索引进行匹配,以获得最终处理的更多细节。

正如我们稍后将看到的,异步处理不遵循请求的顺序,一些处理更快,一些稍后处理,map产生任何已完成的。

一个选项是将给定URL的索引附加到请求,并在以后处理返回的数据时使用它。

使用保留url索引获取和处理URL的复杂样本

注意:以下示例相当复杂,如果您可以使用上面提供的解决方案,请跳过此步骤。但请确保您没有遇到下面检测到并解决的问题(正在修改网址,重定向后请求)。

import erequests
from itertools import count, izip
from functools import partial

urls = [
    'http://www.heroku.com',
    'http://python-tablib.org',
    'http://httpbin.org',
    'http://python-requests.org',
    'http://kennethreitz.com'
]

def print_url_index(index, req, *args, **kwargs):
    content_length = req.headers.get("content-length", None)
    todo = "PROCESS" if req.status_code == 200 else "WAIT, NOT YET READY"
    print "{todo}: index: {index}: status: {req.status_code}: length: {content_length}, {req.url}".format(**locals())

async_reqs = (erequests.async.get(url, hooks={"response": partial(print_url_index, i)}) for i, url in izip(count(), urls))

for req in erequests.map(async_reqs):
    pass

附加挂钩以请求

requests(以及erequests)允许定义名为response的事件的挂钩。每次请求得到一个响应,这个钩子函数被调用,可以做某事甚至修改响应。

以下行定义了一些响应挂钩:

erequests.async.get(url, hooks={"response": partial(print_url_index, i)})

将url索引传递给钩子函数

任何钩子的签名都应为func(req, *args, *kwargs)

但是我们需要将钩子函数传递给我们正在处理的url的索引。

为此,我们使用functools.partial,它允许通过将某些参数固定为特定值来创建简化函数。这正是我们所需要的,如果您看到print_url_index签名,我们只需要修复index的值,其余的将符合钩子函数的要求。

在我们的通话中,我们使用简化函数partial的名称print_url_index,并为每个网址提供唯一索引。

索引可以在循环中由enumerate提供,如果参数数量较多,我们可以使用更高效的内存方式并使用count,这会生成每次增加的数字,默认情况下从0开始

让我们运行它:

$ python ereq.py
WAIT, NOT YET READY: index: 3: status: 301: length: 66, http://python-requests.org/
WAIT, NOT YET READY: index: 4: status: 301: length: 58, http://kennethreitz.com/
WAIT, NOT YET READY: index: 0: status: 301: length: None, http://www.heroku.com/
PROCESS: index: 2: status: 200: length: 7700, http://httpbin.org/
WAIT, NOT YET READY: index: 1: status: 301: length: 64, http://python-tablib.org/
WAIT, NOT YET READY: index: 4: status: 301: length: None, http://kennethreitz.org
WAIT, NOT YET READY: index: 3: status: 302: length: 0, http://docs.python-requests.org
WAIT, NOT YET READY: index: 1: status: 302: length: 0, http://docs.python-tablib.org
PROCESS: index: 3: status: 200: length: None, http://docs.python-requests.org/en/latest/
PROCESS: index: 1: status: 200: length: None, http://docs.python-tablib.org/en/latest/
PROCESS: index: 0: status: 200: length: 12064, https://www.heroku.com/
PROCESS: index: 4: status: 200: length: 10478, http://www.kennethreitz.org/

这表明:

  • 请求未按生成顺序处理
  • 一些请求遵循重定向,因此多次调用hook函数
  • 仔细检查我们可以看到的网址值,响应报告原始列表urls没有网址,即使是索引2我们还会额外添加/。这就是为什么在原始网址列表中简单查找响应网址对我们没有帮助的原因。