mirror of https://github.com/scrapy/scrapy.git
add image pipeline that stores and can thumbs images in different sizes
--HG-- extra : convert_revision : svn%3Ab85faa78-f9eb-468e-a121-7cced6da292c%40248
This commit is contained in:
parent
4807f2eb82
commit
7932a8a5b6
|
|
@ -0,0 +1,183 @@
|
|||
from __future__ import with_statement
|
||||
|
||||
import re
|
||||
import os
|
||||
import time
|
||||
import sha
|
||||
import urllib
|
||||
import urlparse
|
||||
from cStringIO import StringIO
|
||||
|
||||
import Image
|
||||
|
||||
from scrapy.core import log
|
||||
from scrapy.stats import stats
|
||||
from scrapy.http import Request
|
||||
from scrapy.core.exceptions import DropItem, NotConfigured
|
||||
from scrapy.core.exceptions import HttpException
|
||||
from scrapy.conf import settings
|
||||
|
||||
from scrapy.contrib.pipeline.media import MediaPipeline
|
||||
|
||||
# the age at which we download images again
|
||||
IMAGE_EXPIRES = settings.getint('IMAGES_EXPIRES', 90)
|
||||
|
||||
class NoimagesDrop(DropItem):
|
||||
pass
|
||||
|
||||
class ImageException(Exception):
|
||||
"""General image error exception"""
|
||||
|
||||
|
||||
class ImagesPipeline(MediaPipeline):
|
||||
MEDIA_TYPE = 'image'
|
||||
THUMBS = None
|
||||
# THUMBS = (
|
||||
# ("50", (50, 50)),
|
||||
# ("110", (110, 110)),
|
||||
# ("270", (270, 270))
|
||||
# )
|
||||
MIN_WIDTH = 0
|
||||
MIN_HEIGHT = 0
|
||||
|
||||
def __init__(self):
|
||||
if not settings['IMAGES_DIR']:
|
||||
raise NotConfigured
|
||||
|
||||
self.BASEDIRNAME = settings['IMAGES_DIR']
|
||||
self.mkdir(self.BASEDIRNAME)
|
||||
|
||||
self.MIN_WIDTH = settings.getint('IMAGES_MIN_WIDTH', 0)
|
||||
self.MIN_HEIGHT = settings.getint('IMAGES_MIN_HEIGHT', 0)
|
||||
MediaPipeline.__init__(self)
|
||||
|
||||
def media_to_download(self, request, info):
|
||||
relative, absolute = self._get_paths(request)
|
||||
if not should_download(absolute):
|
||||
self.inc_stats(info.domain, 'uptodate')
|
||||
referer = request.headers.get('Referer')
|
||||
log.msg('Image (uptodate): Downloaded %s from %s referred in <%s>' % \
|
||||
(self.MEDIA_TYPE, request, referer), level=log.DEBUG, domain=info.domain)
|
||||
return relative
|
||||
|
||||
def media_downloaded(self, response, request, info):
|
||||
mtype = self.MEDIA_TYPE
|
||||
referer = request.headers.get('Referer')
|
||||
|
||||
if not response or not response.body.to_string():
|
||||
msg = 'Image (empty): Empty %s (no content) in %s referred in <%s>: Empty image (no-content)' % (mtype, request, referer)
|
||||
log.msg(msg, level=log.WARNING, domain=info.domain)
|
||||
raise ImageException(msg)
|
||||
|
||||
result = self.save_image(response, request, info) # save and thumbs response
|
||||
|
||||
status = 'cached' if getattr(response, 'cached', False) else 'downloaded'
|
||||
msg = 'Image (%s): Downloaded %s from %s referred in <%s>' % (status, mtype, request, referer)
|
||||
log.msg(msg, level=log.DEBUG, domain=info.domain)
|
||||
self.inc_stats(info.domain, status)
|
||||
return result
|
||||
|
||||
def media_failed(self, failure, request, info):
|
||||
referer = request.headers.get('Referer')
|
||||
errmsg = str(failure.value) if isinstance(failure.value, HttpException) else failure.getTraceback()
|
||||
msg = 'Image (http-error): Error downloading %s from %s referred in <%s>: %s' % (self.MEDIA_TYPE, request, referer, errmsg)
|
||||
log.msg(msg, level=log.WARNING, domain=info.domain)
|
||||
raise ImageException(msg)
|
||||
|
||||
def save_image(self, response, request, info):
|
||||
mtype = self.MEDIA_TYPE
|
||||
relpath, abspath = self._get_paths(request)
|
||||
dirname = os.path.dirname(abspath)
|
||||
self.mkdir(dirname, info)
|
||||
|
||||
try:
|
||||
save_image_with_thumbnails(response, abspath, self.THUMBS, self.MIN_WIDTH, self.MIN_HEIGHT)
|
||||
except ImageException, ex:
|
||||
log.msg(str(ex), level=log.WARNING, domain=info.domain)
|
||||
raise ex
|
||||
except Exception, ex:
|
||||
msg = 'Image (processing-error): Error thumbnailing %s from %s referred in <%s>: %s' % (mtype, request, referer, ex)
|
||||
log.msg(msg, level=log.WARNING, domain=info.domain)
|
||||
raise ImageException(msg)
|
||||
|
||||
return relpath # success value sent as input result for item_media_downloaded
|
||||
|
||||
def _get_paths(self, request):
|
||||
relative = image_path(request.url)
|
||||
absolute = os.path.join(self.BASEDIRNAME, relative)
|
||||
return relative, absolute
|
||||
|
||||
def mkdir(self, dirname, info=None):
|
||||
already_created = info.extra.setdefault('created_directories', set()) if info else set()
|
||||
if dirname not in already_created:
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
already_created.add(dirname)
|
||||
|
||||
def inc_stats(self, domain, status):
|
||||
stats.incpath('%s/image_count' % domain)
|
||||
stats.incpath('%s/image_status_count/%s' % (domain, status))
|
||||
|
||||
|
||||
|
||||
def should_download(path):
|
||||
"""Should the image downloader download the image to the location specified
|
||||
"""
|
||||
try:
|
||||
mtime = os.path.getmtime(path)
|
||||
age_seconds = time.time() - mtime
|
||||
age_days = age_seconds / 60 / 60 / 24
|
||||
return age_days > IMAGE_EXPIRES
|
||||
except:
|
||||
return True
|
||||
|
||||
_MULTIPLE_SLASHES_REGEXP = re.compile(r"\/{2,}")
|
||||
_FINAL_SLASH_REGEXP = re.compile(r"\/$")
|
||||
def image_path(url):
|
||||
"""Return the relative path on the target filesystem for an image to be
|
||||
downloaded to.
|
||||
"""
|
||||
_, netloc, urlpath, query, _ = urlparse.urlsplit(url)
|
||||
urlpath = _MULTIPLE_SLASHES_REGEXP.sub('/', urlpath)
|
||||
urlpath = _FINAL_SLASH_REGEXP.sub('.jpg', urlpath)
|
||||
if os.sep != '/':
|
||||
urlpath.replace('/', os.sep)
|
||||
if query:
|
||||
img_path = os.path.join(netloc, sha.sha(url).hexdigest())
|
||||
else:
|
||||
img_path = os.path.join(netloc, urlpath[1:])
|
||||
return urllib.unquote(img_path)
|
||||
|
||||
|
||||
def thumbnail_name(image, sizestr):
|
||||
"""Get the name of a thumbnail image given the name of the original file.
|
||||
|
||||
There will can be many types of thumbnails, so we will have a "name" for
|
||||
each type.
|
||||
"""
|
||||
return os.path.splitext(image)[0] + '_' + sizestr + '.jpg'
|
||||
|
||||
def save_scaled_image(image, img_path, name, size):
|
||||
thumb = image.copy() if image.mode == 'RGB' else image.convert('RGB')
|
||||
thumb.thumbnail(size, Image.ANTIALIAS)
|
||||
filename = thumbnail_name(img_path, name)
|
||||
thumb.save(filename, 'JPEG')
|
||||
|
||||
def save_image_with_thumbnails(response, path, thumbsizes, min_width=0, min_height=0):
|
||||
memoryfile = StringIO(response.body.to_string())
|
||||
im = Image.open(memoryfile)
|
||||
if im.mode != 'RGB':
|
||||
log.msg("Found non-RGB image during scraping %s" % path, level=log.WARNING)
|
||||
for name, size in thumbsizes or []:
|
||||
save_scaled_image(im, path, name, size)
|
||||
try:
|
||||
im.save(path)
|
||||
except Exception, ex:
|
||||
log.msg("Image (processing-error): cannot process %s, so writing direct file: Error: %s" % (path, ex))
|
||||
f = open(path, 'wb')
|
||||
f.write(response.body.to_string())
|
||||
f.close()
|
||||
width, height = im.size
|
||||
if width < min_width or height < min_height:
|
||||
raise ImageException("Image too small (%dx%d < %dx%d): %s" % (width, height, min_width, min_height, response.url))
|
||||
|
||||
Loading…
Reference in New Issue