Фабрика картинок — как оно работает? Часть 1

    Хочется рассказать немного о технической части своего проекта, возможно для критики а может кто-то почерпнет что-то для себя.

    Задача. Что «фабрика» должна уметь?


    Добавление изображений:
    • Изображения должны добавляться автоматически.
    • Источники заранее известны, для них должны быть написаны парсеры.
    • Из источника нужно извлечь максимум информации: категория, название, ключевые слова.
    • При добавлении нужно проверить не добавили ли мы ранее это изображение.
    • Паук не привязан к основному серверу, все найденное должно попадать в общую очередь для дальнейшего разбора.

    Поиск в клиентской части:
    • Поиск по ключевым словам
    • Поиск по цветам
    • Поиск по размеру изображения (минимальный, максимальный)
    • Разделение по категориям
    • Максимальное время на поиск 0.05 — 0.08ms

    Задача есть. Что будем использовать?


    • Основной ЯП: Python, Cython (+ Twisted)
    • База информации изображений: MySQL, Sphinx, Redis
    • Веб-сервер: Twisted-Web + Nginx

    Пауки


    «Пауки» лежат на разных «маленьких» (дешевых) vps-серверах. Это решено было сделать для получения почти бесплатного трафика, плюс на тот случай если нас забанит какой-то «источник».

    Иерархия паучков:

    p0is0n@localhost:~/developer/arx-images$ ls -la ./spiders/
    base.py
    indexers.py
    sources

    — В base.py лежат базовые классы для работы с контентом. Все классы наследуются от twisted.application.service.Service.
    — indexers.py это непосредственно запуск всех паучков.
    — В sources лежат сами парсеры «источников», имена файлов примерно совпадают с названием сайта (просто для удобства).

    p0is0n@localhost:~/developer/arx-images$ ls -la ./spiders/sources/
    bikewalls.py
    carwalls.py



    Все парсеры наследуются от класса BaseIndexer объявленного в base.py

    Пример исходного кода carwalls.py (Код "как-есть")
    import sys
    import random
    import re
    import os
    
    from itertools import cycle, chain
    from types import ListType, TupleType
    from pprint import pprint
    from cStringIO import StringIO
    from hashlib import md5
    
    from twisted.python.log import msg, err
    from twisted.internet.defer import Deferred, DeferredQueue, inlineCallbacks, returnValue
    
    from core.queues import httpQueue
    from core.utils import sleep, get_best_resolution
    from core.constants import DEBUG
    from core.models import ImageModel, contextModel
    from core.html import normalize, normalize_title
    
    from base import sources, BaseIndexer
    
    
    class CarwallsIndexer(BaseIndexer):
    	"""CarwallsIndexer class"""
    
    	name = 'Carwalls.com'
    
    	charset = 'utf8'
    	index = 'http://www.desktopmachine.com/'
    	source = sources.add(4)
    
    	pages = cycle(chain(*(
    		('?p={0}'.format(page) for page in xrange(0, 500, 18)),
    	)))
    
    	reFindImagesList = re.compile(u'<a href=([\S]+framepic\.php\?id=\d+&size=[\S]+)[^>]+>2560\s*x\s*1600<\/a>', re.S).findall
    	reFindTitle = re.compile(u"<title>(.+?)2560\s*x\s*1600 wallpaper<\/title>", re.S).search
    	reFindPhoto = re.compile(u'<td colspan=2>\s*<img src=([\S]+\/pics\/[\S]+2560x1600\.(?:jpg))>\s*<\/td>', re.S).search
    
    	@inlineCallbacks
    	def _findImages(self):
    		self._stats.page = self.pages.next()
    
    		# Request
    		result = yield httpQueue.request(url=self.getAbsoluteUrl(self._stats.page))
    		result = result.decode(self.charset, 'ignore')
    
    		if not result:
    			raise ValueError('Wow! Empty result')
    
    		# Count images
    		count = 0
    
    		for url in self.reFindImagesList(result):
    			# Sleep
    			(yield self.sleepWithFireOnServiceStop(self.sleepValue,
    				self.sleepSplit))
    
    			# Try find images
    			msg('Spider', self.name, 'findImages, try', url)
    
    			if self.loop == -1:
    				returnValue(None)
    
    			try:
    				result = yield httpQueue.request(url=self.getAbsoluteUrl(url))
    				result = result.decode(self.charset, 'ignore')
    			except Exception, e:
    				msg('Spider', self.name, 'findImages request error', url)
    				err(e)
    
    				# Stats
    				self._stats.errors.http += 1
    
    				# Skip
    				continue
    
    			title = self.reFindTitle(result)
    			image = self.reFindPhoto(result)
    
    			title = title and title.group(1) or None
    			image = image and image.group(1) or None
    
    			if not title or not image:
    				msg('Spider', self.name, 'findImages wrong title or image', repr((title, image)))
    
    				# Skip
    				continue
    
    			# Make item
    			try:
    				item = (yield self._makeItem(title=title, url=url.split('&size').pop(0)))
    			except Exception, e:
    				msg('Spider', self.name, 'findImages make item error')
    				err(e)
    
    				# Skip
    				continue
    
    			url = image
    
    			if not item['url']:
    				msg('Spider', self.name, 'findImages wrong url', repr(item['url']))
    
    				# Skip
    				continue
    
    			if not item['categories']:
    				# Set default categories
    				item['categories'].extend((103, 112))
    
    			# Translate to list
    			item['categories_names'] = list(
    				item['categories_names'])
    
    			# Sleep
    			(yield self.sleepWithFireOnServiceStop(self.sleepValue,
    				self.sleepSplit))
    
    			if self.loop == -1:
    				returnValue(None)
    
    			msg('Spider', self.name,
    				'findImages, try', url)
    
    			# Create file
    			result = self._makeFile()
    
    			try:
    				(yield httpQueue.request(url=self.getAbsoluteUrl(url), file=result))
    			except Exception, e:
    				msg('Spider', self.name, 'findImages request error', url)
    				err(e)
    
    				if hasattr(result, 'delete') and not result.delete:
    					# Delete file if is temporary
    					os.unlink(result.name)
    
    				# Stats
    				self._stats.errors.http += 1
    
    				# Skip
    				continue
    			finally:
    				result.close()
    
    			try:
    				item.update(image=result)
    
    				# if DEBUG:
    				# 	pprint(item)
    
    				self.imageQueuePut(item)
    			except Exception, e:
    				msg('Spider', self.name, 'findImages create error')
    				err(e)
    
    				# Skip
    				continue
    
    		returnValue(count)
    


    Запуск происходит в indexers.py, все очень просто:

    from twisted.application.service import Application, MultiService
    from core import constants
    
    from sources.carwalls import CarwallsIndexer
    from sources.bikewalls import BikewallsIndexer
    
    application = Application("ARX-Images Indexers")
    
    services = MultiService()
    services.setServiceParent(application)
    
    services.addService(CarwallsIndexer())
    services.addService(BikewallsIndexer())
    

    После того как «источник» извлек изображение вместе со всеми категориями и ключевыми словами, вызывается метод BaseIndexer.imageQueuePut, который добавляет информацию в «локальную» очередь для последующей обработки.

    Обработка заключается в проверке всех полей: категории, ключевые слова, заголовок. Далее посылается запрос на проверку существования изображения в базе (или основной очереди) и если результат отрицателен — изображение отправляется в очередь на основной сервер.

    На основном сервере полученная информация проходит обработку: разбираем ключевые слова, извлекаем информацию о изображении (цвета, размер), создаем превьюшки и изображение добавляется в основную базу.

    Проверка существования изображения


    Эта часть была не самой легкой. Для сравнения изображений был взят за основу алгоритм pHash (статья на хабре).

    Функция получения хеша
    def getImageHash(image):
    	cdef unsigned int lefts, row, i
    	cdef unsigned long long bits
    
    	cdef list results = []
    
    	if not isinstance(image, Image.Image):
    		image = Image.open(image)
    
    	image = image.resize((128, 128))
    	image = image.filter(ImageFilter.Kernel(
    		(5, 5), (
    		1,  0,  0,  0,  1,
    		1,  0,  0,  0,  1,
    		1,  0,  0,  0,  1,
    		1,  0,  0,  0,  1,
    		1,  0,  0,  0,  1
    		),
    		8,
    		0
    	))
    	image = ImageOps.grayscale(image)
    	image = image.resize((16, 16)).convert('P', dither=Image.NONE)
    
    	lefts = (sum(image.getdata()) / HASH_BITS)
    	datas = image.getdata()
    
    	for i in xrange(0, 256, 32):
    		bits = int(''.join('1' if row > lefts else '0' for row in islice(datas, i, i + 32)), 2)
    
    		# Add to results
    		results.append(bits)
    
    	return tuple(results)
    

    Для хранения хешей был написан сервер, который имеет несколько функций:
    — добавление хеша
    — удаление
    — поиск «похожих» по хешу

    Поиск построен на простом алгоритме «Brute-force», то-есть обычный перебор.

    Все «тяжелые» функции сервера, написаны на Cython.

    P.S. Если кому-то интересно, буду писать продолжение. Сам проект: picsfab.com

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 21

    • UFO just landed and posted this here
      • UFO just landed and posted this here
          0
          На konachan тоже тайтлов не увидел, может не туда смотрю? Ткните пальцем.
          • UFO just landed and posted this here
              0
              Ok, спасибо. Сделаю из konachan'а источник.
            • UFO just landed and posted this here
              • UFO just landed and posted this here
                  0
                  Тегов сейчас как таковых вообще нет, думаю нужно будет доработать.
            • UFO just landed and posted this here
              • UFO just landed and posted this here
                0
                Если источник и делать то только с пиксива и девианта. Но на вторичках зато теги бывают (но на каночане такой бред… модеры нашего сайта туда уже и не смотрят)
                0
                Убейте своего кодера.

                #sbox-overlay {
                width: 100%;
                height: 100%;
                }

                и всё.
                  +4
                  «Кодер» — это я:)

                  Даже не знаю почему я так не поступил.
                  0
                  А чего XPath не любим?
                    0
                    С RE мне удобней.
                    0
                    Разделы «Техника» и «Автомобили» слишком похожи. Это баг?
                      0
                      Нет, в «технику» входит любая техника.
                    • UFO just landed and posted this here
                        0
                        Нигде не написано зачем это вообще нужно? Просто обои? Тогда почему везде написано «картинки» а не обои? И зачем это, если это всё есть у яндекса и у гугля?
                          0
                          Как минимум оно полезно в качестве опыта как для автора, так и для тех, кто интересуется. Я как понимаю, это будет цикл статей.
                          0
                          image
                          Не удержался, сори.

                          Only users with full accounts can post comments. Log in, please.