class KinoPoisk: """ API: scraper - скрапер movie - профайл фильма search - поиск фильма best - поиск лучших фильмов person - поиск персон work - информация о работах персоны """ def __init__(self): self.cache = Cache('kinopoisk.db') self.html = Clear() self.http = HTTP() self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:10.0.2) Gecko/20100101 Firefox/10.0.2', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3', 'Cache-Control': 'no-cache', 'Referer': 'http://www.kinopoisk.ru/level/7/' } # API def scraper(self, name, year=None, trailer_quality=None): try: tag = 'scraper:' + urllib.quote_plus(name.encode('windows-1251')) except: return None else: if year: tag += ':' + str(year) id = self.cache.get(tag, self._scraper, name, year) if not id: return None return self.movie(id, trailer_quality) def movie(self, id, trailer_quality=None): id = str(id) if trailer_quality is None: trailer_quality = 6 movie = self.cache.get('movie:' + id, self._movie, id) if not movie: return None if movie['trailers']: # компилируем список с нужным нам качеством video = [] for m in movie['trailers']: url = [x for x in m['video'] if x[0] <= trailer_quality] if url: m['video'] = url[-1] video.append(m) movie['trailers'] = video if movie['trailers']: # готовим главный трейлер r = [x for x in movie['trailers'] if x['trailer']] if r: movie['info']['trailer'] = r[0]['video'][1] else: # если трейлер не найден, то отдаем что попало... movie['info']['trailer'] = movie['trailers'][0]['video'][1] return movie def search(self, name, trailer_quality=None): return self._search_movie(name) def best(self, **kwarg): page = kwarg.get('page', 1) limit = kwarg.get('limit', 50) url = 'http://www.kinopoisk.ru/top/navigator/m_act%5Bis_film%5D/on/m_act%5Bnum_vote%5D/' + str( kwarg.get('votes', 100)) + '/' if kwarg.get('dvd'): url += 'm_act%5Bis_dvd%5D/on/' if kwarg.get('decade'): url += 'm_act%5Bdecade%5D/' + str(kwarg['decade']) + '/' if kwarg.get('genre'): url += 'm_act%5Bgenre%5D/' + str(GENRE[kwarg['genre']]) + '/' if kwarg.get('country'): url += 'm_act%5Bcountry%5D/' + str(kwarg['country']) + '/' if kwarg.get('rate'): url += 'm_act%5Brating%5D/' + str(kwarg['rate']) + ':/' if kwarg.get('mpaa'): url += 'm_act%5Bmpaa%5D/' + str(kwarg['mpaa']) + '/' url += 'perpage/' + str(limit) + '/order/ex_rating/' if page > 1: url += 'page/' + str(page) + '/' response = self.http.fetch(url, headers=self.headers) if response.error: return None res = {'pages': (1, 0, 1, 0), 'data': []} r = re.compile('<div class="pagesFromTo(.+?)<div class="pagesFromTo', re.U | re.S).search( response.body.decode('windows-1251')) if r: body = r.group(1) # compile pagelist p = re.compile('>([0-9]+)—[0-9]+[^0-9]+?([0-9]+)', re.U).search(body) if p: page = (int(p.group(1)) - 1) / limit + 1 total = int(p.group(2)) pages = total / limit if limit * pages != total: pages += 1 res['pages'] = (pages, 0 if page == 1 else page - 1, page, 0 if page == pages else page + 1) # end compile for id in re.compile('<div id="tr_([0-9]+)"', re.U | re.S).findall(body): res['data'].append(int(id)) return res def person(self, name): #response = self.http.fetch('https://www.kinopoisk.ru/index.php?level=7&from=forma&result=adv&m_act%5Bfrom%5D=forma&m_act%5Bwhat%5D=actor&m_act%5Bfind%5D=' + urllib.quote_plus(name.encode('windows-1251')), headers=self.headers) response = self.http.fetch( 'http://www.kinopoisk.ru/s/type/people/list/1/find/' + urllib.quote_plus(name.encode('windows-1251')) + '/order/relevant/', headers=self.headers) if response.error: return None res = [] body = re.compile( '<div class="navigator">(.+?)<div class="navigator">', re.U | re.S).search(response.body.decode('windows-1251')) if body: for block in re.compile('<p class="pic">(.+?)<div class="clear">', re.U | re.S).findall(body.group(1)): id, name, original, year, poster = None, None, None, None, None r = re.compile( '<p class="name"><a href="/name/([0-9]+)[^>]+>([^<]+)</a>', re.U | re.S).search(block) if r: id = r.group(1) name = r.group(2).strip() if id and name: r = re.compile('<span class="gray">([^<]+)</span>', re.U | re.S).search(block) if r: original = r.group(1).strip() if not original: original = None r = re.compile('<span class="year">([0-9]{4})</span>', re.U | re.S).search(block) if r: year = int(r.group(1)) if block.find('no-poster.gif') == -1: poster = 'http://st.kinopoisk.ru/images/actor/' + id + '.jpg' res.append({ 'id': int(id), 'name': name, 'originalname': original, 'year': year, 'poster': poster }) return {'pages': (1, 0, 1, 0), 'data': res} def work(self, id): response = self.http.fetch('http://www.kinopoisk.ru/name/' + str(id) + '/', headers=self.headers) if response.error: return None res = {} r = re.compile('id="sort_block">(.+?)<div id="block_right"', re.U | re.S).search(response.body.decode('windows-1251')) if r: for block in r.group(1).split( u'<tr><td colspan="3" class="specializationBox')[1:]: work = None for w in ('actor', 'director', 'writer', 'producer', 'producer_ussr', 'composer', 'operator', 'editor', 'design', 'voice', 'voice_director'): if block.find(u'id="' + w + u'"') != -1: work = 'producer' if w == 'producer_ussr' else w break if work: movies = [] for id, name in re.compile( '<span class="name"><a href="/film/([0-9]+)/[^>]+>([^<]+?)</a>', re.U).findall(block): for tag in (u'(мини-сериал)', u'(сериал)'): if name.find(tag) != -1: break else: movies.append(int(id)) if movies: res.setdefault(work, []).extend(movies) return res def review(self, id, query): query_s = 'all' if query == 'stat' else query data = self.cache.get('review:' + str(id) + ':' + query_s, self._review, id, query_s) if not data: return data return data[query] def countries(self): return COUNTRIES def country(self, id, default=None): country = [x[1] for x in COUNTRIES if x[0] == id] return country[0] if country else default # PRIVATE def _search_movie(self, name, year=None): url = 'http://www.kinopoisk.ru/s/type/film/list/1/find/' + urllib.quote_plus( name.encode('windows-1251')) + '/order/relevant' if year: url += '/m_act%5Byear%5D/' + str(year) url += '/m_act%5Btype%5D/film/' response = self.http.fetch(url, headers=self.headers) if response.error: return None res = [] r = re.compile('<div class="navigator">(.+?)<div class="navigator">', re.U | re.S).search( response.body.decode('windows-1251')) if r: for id in re.compile( '<p class="name"><a href="/level/1/film/([0-9]+)', re.U | re.S).findall(r.group(1)): res.append(int(id)) return {'pages': (1, 0, 1, 0), 'data': res} def _scraper(self, name, year): timeout = True # если фильм свежий, то кладем в кэш НЕ на долго (могут быть обновления на сайте) if year and year >= time.gmtime(time.time()).tm_year: timeout = 7 * 24 * 60 * 60 #week ids = self._search_movie(name, year) if ids is None: return False, None elif not ids['data']: # сохраняем пустой результат на 3-е суток return 259200, None else: return timeout, ids['data'][0] def _review(self, id, query): url = 'http://www.kinopoisk.ru/film/' + str(id) + '/ord/rating/' if query in ('good', 'bad', 'neutral'): url += 'status/' + query + '/' url += 'perpage/200/' response = self.http.fetch(url, headers=self.headers) if response.error: return False, None html = response.body.decode('windows-1251') res = { 'stat': { 'all': 0, 'good': 0, 'bad': 0, 'neutral': 0 }, query: [] } r = re.compile('<ul class="resp_type">(.+?)</ul>', re.U | re.S).search(html) if r: ul = r.group(1) for q, t in (('pos', 'good'), ('neg', 'bad'), ('neut', 'neutral')): r = re.compile( '<li class="' + q + '"><a href="[^>]+>[^<]+</a><b>([0-9]+)</b></li>', re.U).search(ul) if r: res['stat'][t] = int(r.group(1)) res['stat']['all'] = res['stat']['good'] + res['stat'][ 'bad'] + res['stat']['neutral'] r = re.compile('<div class="navigator">(.+?)<div class="navigator">', re.U | re.S).search(html) if r: for block in r.group(1).split('itemprop="reviews"'): review = { 'nick': None, 'count': None, 'title': None, 'review': None, 'time': None } r = re.compile('itemprop="reviewBody">(.+?)</div>', re.U | re.S).search(block) if r: text = r.group(1) for tag1, tag2 in ((u'<=end=>', u'\n'), (u'<b>', u'[B]'), (u'</b>', u'[/B]'), (u'<i>', u'[I]'), (u'</i>', u'[/I]'), (u'<u>', u'[U]'), (u'</u>', u'[/U]')): text = text.replace(tag1, tag2) r = self.html.text(text) if r: review['review'] = r user = None r = re.compile( '<p class="profile_name"><s></s><a href="[^>]+>([^<]+)</a></p>' ).search(block) if r: user = self.html.string(r.group(1)) else: r = re.compile('<p class="profile_name"><s></s>([^<]+)</p>' ).search(block) if r: user = self.html.string(r.group(1)) if user: review['nick'] = user r = re.compile('<p class="sub_title"[^>]+>([^<]+)</p>').search( block) if r: title = self.html.string(r.group(1)) if title: review['title'] = title r = re.compile('<span class="date">([^<]+)</span>', re.U | re.S).search(block) if r: review['time'] = r.group(1).replace(u' |', u',') r = re.compile(u'<a href="[^>]+>рецензии \(([0-9]+)\)</a>', re.U | re.S).search(block) if r: review['count'] = int(r.group(1)) if review['nick'] and review['review']: res[query].append(review) return 3600, res # one hour def _movie(self, id): response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/', headers=self.headers) if response.error: return False, None html = response.body.decode('windows-1251') res = { 'id': int(id), 'thumb': None, 'fanart': None, 'trailers': [], 'info': {} } # имя, оригинальное имя, девиз, цензура, год, top250 # runtime - длительность фильма (в отдельную переменную, иначе не видно размер файла) for tag, reg, cb in ( ('title', '<h1 class="moviename-big" itemprop="name">(.+?)</h1>', self.html.string), ('originaltitle', 'itemprop="alternativeHeadline">([^<]*)</span>', self.html.string), ('tagline', '<td style="color\: #555">«(.+?)»</td></tr>', self.html.string), ('mpaa', 'images/mpaa/([^\.]+).gif', self.html.string), ('runtime', '<td class="time" id="runtime">[^<]+<span style="color\: #999">/</span>([^<]+)</td>', self.html.string), ('year', '<a href="/lists/m_act%5Byear%5D/([0-9]+)/"', int), ('top250', '<a href="/level/20/#([0-9]+)', int)): r = re.compile(reg, re.U).search(html) if r: value = r.group(1).strip() if value: res['info'][tag] = cb(value) # режисеры, сценаристы, жанры for tag, reg in (('director', u'<td itemprop="director">(.+?)</td>'), ( 'writer', u'<td class="type">сценарий</td><td[^>]*>(.+?)</td>'), ('genre', u'<span itemprop="genre">(.+?)</span>')): r = re.compile(reg, re.U | re.S).search(html) if r: r2 = [] for r in re.compile('<a href="[^"]+">([^<]+)</a>', re.U).findall(r.group(1)): r = self.html.string(r) if r and r != '...': r2.append(r) if r2: res['info'][tag] = u', '.join(r2) # описание фильма r = re.compile( '<span class="_reachbanner_"><div class="brand_words film-synopsys" itemprop="description">(.+?)</div></span>', re.U).search(html) if r: plot = self.html.text(r.group(1).replace('<=end=>', '\n')) if plot: res['info']['plot'] = plot # IMDB r = re.compile('IMDb: ([0-9.]+) \(([0-9\s]+)\)</div>', re.U).search(html) if r: res['info']['rating'] = float(r.group(1).strip()) res['info']['votes'] = r.group(2).strip() # премьера r = re.compile(u'премьера \(мир\)</td>(.+?)</tr>', re.U | re.S).search(html) if r: r = re.compile(u'data\-ical\-date="([^"]+)"', re.U | re.S).search(r.group(1)) if r: data = r.group(1).split(' ') if len(data) == 3: i = 0 for mon in (u'января', u'февраля', u'марта', u'апреля', u'мая', u'июня', u'июля', u'августа', u'сентября', u'октября', u'ноября', u'декабря'): i += 1 if mon == data[1]: mon = str(i) if len(mon) == 1: mon = '0' + mon day = data[0] if len(day) == 1: day = '0' + day res['info']['premiered'] = '-'.join( [data[2], mon, day]) break # постер r = re.compile(u'onclick="openImgPopup\(([^\)]+)\)', re.U | re.S).search(html) if r: poster = r.group(1).replace("'", '').strip() if poster: res['thumb'] = 'http://kinopoisk.ru' + poster # актеры r = re.compile(u'<h4>В главных ролях:</h4>(.+?)</ul>', re.U | re.S).search(html) if r: actors = [] for r in re.compile( '<li itemprop="actors"><a [^>]+>([^<]+)</a></li>', re.U).findall(r.group(1)): r = self.html.string(r) if r and r != '...': actors.append(r) if actors: res['info']['cast'] = actors[:] menu = re.compile( '<ul id="newMenuSub" class="clearfix(.+?)<!\-\- /menu \-\->', re.U | re.S).search(html) if menu: menu = menu.group(1) # фанарт if menu.find('/film/' + id + '/wall/') != -1: response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/wall/', headers=self.headers) if not response.error: html = response.body.decode('windows-1251') fanart = re.compile( '<a href="/picture/([0-9]+)/w_size/([0-9]+)/">', re.U).findall(html) if fanart: fanart.sort(cmp=lambda (id1, size1), (id2, size2): cmp(int(size1), int(size2))) # пробуем взять максимально подходящее fanart_best = [x for x in fanart if int(x[1]) <= 1280] if fanart_best: fanart = fanart_best response = self.http.fetch( 'http://www.kinopoisk.ru/picture/' + fanart[-1][0] + '/w_size/' + fanart[-1][1] + '/', headers=self.headers) if not response.error: html = response.body.decode('windows-1251') r = re.compile('id="image" src="([^"]+)"', re.U | re.S).search(html) if r: res['fanart'] = r.group(1).strip() if res['fanart'].startswith('//'): res['fanart'] = 'http:' + res['fanart'] # если нет фанарта (обоев), то пробуем получить кадры if not res['fanart'] and menu.find('/film/' + id + '/stills/') != -1: response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/stills/', headers=self.headers) if not response.error: html = response.body.decode('windows-1251') fanart = re.compile( '<a href="/picture/([0-9]+)/"><img src="[^<]+</a>[^<]+<b><i>([0-9]+)×([0-9]+)</i>', re.U).findall(html) if fanart: fanart.sort(cmp=lambda (id1, size1, t1), ( id2, size2, t2): cmp(int(size1), int(size2))) # пробуем взять максимально подходящее fanart_best = [ x for x in fanart if int(x[1]) <= 1280 and int(x[1]) > int(x[2]) ] if fanart_best: fanart = fanart_best response = self.http.fetch( 'http://www.kinopoisk.ru/picture/' + fanart[-1][0] + '/', headers=self.headers) if not response.error: html = response.body.decode('windows-1251') r = re.compile('id="image" src="([^"]+)"', re.U | re.S).search(html) if r: res['fanart'] = r.group(1).strip() if res['fanart'].startswith('//'): res['fanart'] = 'http:' + res['fanart'] # студии if menu.find('/film/' + id + '/studio/') != -1: response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/studio/', headers=self.headers) if not response.error: html = response.body.decode('windows-1251') r = re.compile(u'<b>Производство:</b>(.+?)</table>', re.U | re.S).search(html) if r: studio = [] for r in re.compile( '<a href="/lists/m_act%5Bstudio%5D/[0-9]+/" class="all">(.+?)</a>', re.U).findall(r.group(1)): r = self.html.string(r) if r: studio.append(r) if studio: res['info']['studio'] = u', '.join(studio) # трэйлеры trailers1 = [] # русские трейлеры trailers2 = [] # другие русские видео trailers3 = [] # трейлеры trailers4 = [] # другие видео if menu.find('/film/' + id + '/video/') != -1: response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/video/', headers=self.headers) if not response.error: html = response.body.decode('windows-1251') for row in re.compile( u'<!-- ролик -->(.+?)<!-- /ролик -->', re.U | re.S).findall(html): # отсекаем лишние блоки if row.find(u'>СМОТРЕТЬ</a>') != -1: # русский ролик? if row.find('class="flag flag2"') == -1: is_ru = False else: is_ru = True # получаем имя трейлера r = re.compile( '<a href="/film/' + id + '/video/[0-9]+/[^>]+ class="all">(.+?)</a>', re.U).search(row) if r: name = self.html.string(r.group(1)) if name: trailer = { 'name': name, 'time': None, 'trailer': False, 'ru': is_ru, 'video': [] } # трейлер или тизер? for token in (u'Трейлер', u'трейлер', u'Тизер', u'тизер'): if name.find(token) != -1: trailer['trailer'] = True break # получаем время трейлера r = re.compile( u'clock.gif"[^>]+></td>\s*<td style="color\: #777">[^0-9]*([0-9\:]+)</td>', re.U | re.S).search(row) if r: trailer['time'] = r.group(1).strip() # делим ролики по качеству for r in re.compile( 'trailer/([1-3])a.gif"(.+?)link=([^"]+)" class="continue">.+?<td style="color\:#777">([^<]+)</td>\s*</tr>', re.U | re.S).findall(row): quality = int(r[0]) if r[1].find('icon-hd') != -1: quality += 3 trailer['video'].append( (quality, r[2].strip(), r[3])) if id == '462754': #raise pass if trailer['video']: if trailer['ru']: if trailer['trailer']: trailers1.append(trailer) else: trailers2.append(trailer) else: if trailer['trailer']: trailers3.append(trailer) else: trailers4.append(trailer) # склеиваем трейлеры res['trailers'].extend(trailers1) res['trailers'].extend(trailers2) res['trailers'].extend(trailers3) res['trailers'].extend(trailers4) timeout = True # если фильм свежий, то кладем в кэш НЕ на долго (могут быть обновления на сайте) if 'year' not in res['info'] or int( res['info']['year']) >= time.gmtime(time.time()).tm_year: timeout = 7 * 24 * 60 * 60 #week return timeout, res
class RuTracker: def __init__(self, expire=0, size=0): self.cache_catalog = Cache('rutracker_catalog.db') self.cache_profile = Cache('rutracker_profile.db', expire, size) self.http = RuTrackerHTTP() self.re = { 'is_int': re.compile(r'^([0-9]+)', re.U), 'is_float': re.compile(r'^([0-9]{1,10}\.[0-9]+)', re.U) } self.html = Clear() self.status = { '%': 'check', 'D': 'repeat', '!': 'nodesc', '©': 'copyright', '∏': 'moder', 'x': 'close', '∑': 'absorb', '#': 'doubtful', '*': 'nocheck', '?': 'neededit', 'T': 'temp', '√': 'ok' } # API def get(self, id=None, page=1): """ Получение списка директорий и раздач На вход функции надо подавать следующие параметры: id - [int] id директории page - [int] номер страницы Возвращает словарь, состоящий из следующих полей: pages - [list] кортеж [int] для навигации = (кол-во страниц, предыдущая, текущая, следующая) data - [list] данные выборки, состоящая из следующих полей: id - [int] id (для директорий и топиков ID могут совпадать) name - [str] имя записи type - [str] тип записи (torrent - торрент, folder - директория) size - [int] размер раздачи в байтах seeder - [int] кол-во сидов leecher - [int] кол-во личеров download - [int] кол-во скачиваний торрента comment - [int] кол-во комментариев status - [str] символ отметки модератора status_human - [str] отметка модератора в удобочитаемом виде (описание смотри ниже). Описание возвращаемых отметок status_human: Скачать торрент нельзя (красный): moder - премодерация, check - проверяется, repeat - повтор, nodesc - не оформлено, copyright - закрыто правообладателем, close - закрыто, absorb - поглощено. Скачать торрент можно (желтый): nocheck - не проверено, neededit - недооформлено, doubtful - сомнительно, temp - временная Скачать торрент можно (зеленый): ok - проверено """ # INDEX if id is None: html = self.http.get('http://rutracker.org/forum/index.php') if not html: return html res = [] r = re.compile(r'<div\sid="forums_wrap">(.+)<div\sid="board_stats">', re.U|re.S).search(html) if r: r = re.compile(r'<h4\sclass="forumlink"><a\shref="\./viewforum\.php\?f=([0-9]+)">(.+?)</a></h4>', re.U|re.S).findall(r.group(1)) if r: res = [{'id': int(i), 'name': self.html.string(x), 'type': 'folder'} for i, x in r] if not res: return None return {'pages': (1, 0, 1, 0), 'data': res} else: page_query = '' if page > 1: page_query = '&start=' + str(50*(page-1)) html = self.http.get('http://rutracker.org/forum/viewforum.php?f=' + str(id) + '&sort=2' + page_query) if not html: return html pages = self._compile_pages(html) folder = [] torrent = [] group_list = re.compile(r'<table class="forumline forum">(.+?)</table>', re.U|re.S).findall(html) if group_list: for group in group_list: # вытаскиваем папки (если есть) r = re.compile(r'<h4\sclass="forumlink"><a\shref="viewforum\.php\?f=([0-9]+)">(.+?)</a></h4>', re.U|re.S).findall(group) if r: folder.extend([{'id': int(i), 'name': self.html.string(x), 'type': 'folder'} for i, x in r]) # нарубаем на строчки топиков topic_list = group.split(u'topicSep">') if len(topic_list) > 1: topic_list = topic_list[1:] for html in topic_list: # вытаскиваем id for text in re.compile(r'<tr\sid="tr\-[0-9]+"(.+?)</tr>', re.U|re.S).findall(html): item = self._compile_topic(text) if item: torrent.append(item) folder.extend(torrent) return {'pages': pages, 'data': folder} def search(self, search, folder=None, index=None, ignore=None): """ Поиск по РуТрекеру На вход функции надо подавать следующие параметры: search - [str] поисковая строка запроса (Unicode) folder - [list] список ID директорий, в которых необходимо искать (None - искать везде) Возвращает словарь, аналогичный выводу метода GET """ if isinstance(search, unicode): search = search.encode('windows-1251') # проверяем авторизацию html = self.http.get('http://rutracker.org/forum/index.php') if not html: return html # готовим запрос для получения дерева разделов if folder: if not isinstance(folder, list) and not isinstance(folder, tuple): folder = [folder] else: if index is not None: if not isinstance(index, list) and not isinstance(index, tuple): index = [index] if not isinstance(ignore, list) and not isinstance(ignore, tuple): ignore = [ignore] if not index and not ignore: folder = [] else: folder = self._load_catalog(index, ignore) if not folder: return folder # готовим запрос params = [('nm', search), ('o', 10), ('s', 2), ('prev_my', 0), ('prev_new', 0), ('prev_oop', 0), ('submit', r'Поиск')] params.extend([('f[]', x) for x in folder]) # делаем поиск html = self.http.post('http://rutracker.org/forum/tracker.php', params) if not html: return html res = [] table = re.compile('id="tor\-tbl">(.+?)</table>', re.U|re.S).search(html) if table: for tr in re.compile('<tr\sclass="tCenter\shl\-tr">(.+?)</tr>', re.U|re.S).findall(table.group(1)): item = self._compile_topic(tr, True) if item: res.append(item) return {'pages': (1, 0, 1, 0), 'data': res} def profile(self, id): """ Получение дополнительной информации о раздачи На вход функции надо подавать следующие параметры: id - [int] id топика с раздачей Возвращает словарь, состоящий из: descript - [str] описание на RuTracker cover - [str] url обложки screenshot - [list] Список url скриншотов """ return self.cache_profile.get('profile:' + str(id), self._profile, id) def comment(self, id, page=1): """ Получение комментариев раздачи На вход функции надо подавать следующие параметры: id - [int] id топика page - [int] номер страницы Возвращает словарь, состоящий из следующих полей: pages - [list] кортеж [int] для навигации = (кол-во страниц, предыдущая, текущая, следующая) data - [list] данные выборки - список словарей, состоящих из следующих полей: nick - [str] ник автора комментария usertime - [str] стаж юзера count - [str] кол-во сообщений у юзера location - [str] откуда юзер time - [str] время добавления комментария message - [str] комментарий """ page_query = '' if page > 1: page_query = '&start=' + str(30*(page-1)) html = self.http.get('http://rutracker.org/forum/viewtopic.php?t=' + str(id) + page_query) if not html: return html res = { 'pages': self._compile_pages(html), 'comments': [] } # нарубаем страницу по постам rows = re.compile('<tbody id="post_[0-9]+" class="row1|2">(.+?)<!\-\-/post_body\-\->', re.U|re.S).findall(html) if rows: if page == 1: rows.pop(0) if rows: # функция для очистки комментариев def _def_subn1(m): return u'<div class="q-wrap"><div class="q">' + self.html.string(m.group(1)) + u':\n' def _def_subn2(m): r = u'[BR][I]' + m.group(1).replace(u'[I]', u'').replace(u'[/I]', u'') + u'[/I][BR]' n = 1 while n: r, n = re.compile(u'\[BR\]\[BR\]', re.U|re.S).subn(u'[BR]', r) return r for html in rows: comment = { 'nick': None, 'usertime': None, 'count': None, 'location': u'', 'time': None, 'message': None } # вытаскиваем ник r = re.compile('<p class="nick[^>]+>([^<]+)</p>', re.U).search(html) if r: comment['nick'] = self.html.string(r.group(1).strip()) # смотрим стаж r = re.compile(u'<p class="joined"><em>Стаж:</em>([^<]+)</p>', re.U).search(html) if r: comment['usertime'] = r.group(1).strip() # смотрим кол-во коментов у юзера r = re.compile(u'<p class="posts"><em>Сообщений:</em>([^<]+)</p>', re.U).search(html) if r: comment['count'] = r.group(1).strip() # смотрим город юзера r = re.compile(u'<p class="from"><em>Откуда:</em>([^<]+)</p>', re.U).search(html) if r: comment['location'] = r.group(1).strip() # смотрим страну юзера r = re.compile('<p class="flag"><img [^>]*title="([^"]+)"[^>]*></p>', re.U).search(html) if r: if comment['location']: comment['location'] += u', ' comment['location'] += r.group(1).strip() # смотрим время коммента r = re.compile('<a class="small" href="\./viewtopic.php\?p=[^>]+>([0-9]{1,2}\-[^\-]+\-[0-9]{2} [0-9]{1,2}\:[0-9]{1,2})</a>', re.U).search(html) if r: comment['time'] = r.group(1).strip() # вытаскиваем тело коммента r = re.compile('<div class="post_body"[^>]+>(.+)$', re.U|re.S).search(html) if r: html = r.group(1).strip() if html: # заменяем что можем... for reg, rep in ( ('<span class="post\-b">([^(?:</span>)]+)</span>', u'[B]\g<1>[/B]'), ): html = re.compile(reg, re.U|re.S).sub(rep, html) # конвертируем цитаты html, n = re.compile('<div class="q-wrap">\s*<div class="q" head="([^"]+)">', re.U|re.S).subn(_def_subn1, html) n = 1 while n: html, n = re.compile('<div class="q-wrap">\s*<div class="q">(.+?)</div>\s*</div>', re.U|re.S).subn(_def_subn2, html) # прогоняем через полную очистку comment['message'] = self.html.text(html) if comment['nick'] and comment['message']: res['comments'].append(comment) return res def download(self, id): """ Скачивание торрента раздачи На вход функции надо подавать следующие параметры: id - [str] топика с раздачей Возвращает торрент или None (в случае неудачи) """ return self.http.download(id) # PRIVATE def _compile_pages(self, text): r = re.compile(u'<p style="float\: left">Страница <b>([0-9]+)</b> из <b>([0-9]+)</b></p>', re.U|re.S).search(text) if r: current = int(r.group(1)) total = int(r.group(2)) next = current + 1 if next > total: next = 0 return total, current-1, current, next return 1, 0, 1, 0 def _compile_topic(self, text, is_search=False): r = re.compile(r'<a\s[^>]*href="\./viewtopic\.php\?t=([0-9]+)"[^>]*>(.+?)</a>', re.U|re.S).search(text) if r: id = r.group(1) name = self.html.string(r.group(2)) r = re.compile(r'<a[^>]+href="http://dl\.rutracker\.org/forum/dl\.php\?t=' + id + '"[^>]*>(.+?)</a>', re.U|re.S).search(text) if r: size = self._compile_size(r.group(1)) if size and name: item = self._create_torrent(int(id), name) item['size'] = size r = re.compile(r'"tor-icon[^>]+>([^<]+)<', re.U|re.S).search(text) if r: stat = r.group(1) try: status = self.status[stat] except KeyError: pass else: item['status'] = self.html.char(stat) item['status_human'] = status if is_search: item['comment'] = -1 query = (('download', '<td\sclass="row4\ssmall">([0-9]+)</td>'), ('seeder', '<td\sclass="row4\sseedmed"><b>([0-9]+)</b></td>'), ('leecher', '<td\sclass="row4\sleechmed"[^>]+><b>([0-9]+)</b></td>')) else: query = (('comment', u'<span title="Ответов">([0-9]+)</span>'), ('download', u'title="Торрент скачан">[^<]*<b>([0-9]+)</b>[^<]*</p>'), ('seeder', 'title="Seeders"><b>([0-9]+)</b></span>'), ('leecher', 'title="Leechers"><b>([0-9]+)</b></span>')) for tag, reg in query: r = re.compile(reg, re.U|re.S).search(text) if r: item[tag] = int(r.group(1)) return item return None def _create_torrent(self, id, name): return { 'id': id, 'name': name, 'type': 'torrent', 'size': 0, 'seeder': 0, 'leecher': 0, 'download': 0, 'comment': 0, 'status': None, 'status_human': None } def _compile_size(self, text): text = self.html.string(text.replace(u'↓', u'')) if text: text = text.lower() prefix = 1 for p, v in ((u'kb', 1024), (u'mb', 1024*1024), (u'gb', 1024*1024*1024), (u'tb', 1024*1024*1024*1024)): if text.find(p) != -1: prefix = v text = text.replace(p, u'').strip() break num = self.re['is_float'].search(text) if num: return int(float(prefix)*float(num.group(1))) num = self.re['is_int'].search(text) if num: return prefix*int(num.group(1)) return None def _profile(self, id): html = self.http.guest('http://rutracker.org/forum/viewtopic.php?t=' + str(id)) if not html: return False, html res = { 'descript': None, 'cover': None, 'screenshot': None } r = re.compile('<table class="topic" id="topic_main" cellpadding="0" cellspacing="0">(.+?)<legend>Download</legend>', re.U|re.S).search(html) if r: html = r.group(1) # ищем коверы (перебирая все возможные варианты хостингов картинок) for api in (self.pic_hosting_fastpic, ): cover = api('cover', html) if cover: res['cover'] = cover break # вытаскиваем блок со скриншотами r = re.compile(u'<h3 class="sp-title">Скриншоты(.+?)</div>', re.U|re.S).search(html) if r: body = r.group(1) # ищем скрины (перебирая все возможные варианты хостингов картинок) for api in (self.pic_hosting_fastpic, ): screenshot = api('screenshot', body) if screenshot: res['screenshot'] = screenshot break # пытаемся получить текст описания r = re.compile('<div class="post_body"[^>]+>(.+)', re.U|re.S).search(html) if r: html = r.group(1) # режем и заменяем все что можем... for reg, rep in ( (u'<div class="sp-wrap">.+?</div>', u''), # удаляем все спойлеры (u'<var[^>]+>[^<]+</var>', u''), # удаляем все изображения (u'<span class="post\-hr">\-</span>', u'\n'), # удаляем HR (u'<span class="post\-b">([^<]+)</span>', u'[COLOR FF0DA09E]\g<1>[/COLOR]') # заменяем болды ): html = re.compile(reg, re.U|re.S).sub(rep, html) # прогоняем через полную очистку html = self.html.text(html) if html: res['descript'] = html return True, res # CATALOG def _load_catalog(self, index, ignore): catalog = self.cache_catalog.get('catalog', self._load_catalog_http) if not catalog: return [] res = [] for key, folders in catalog.iteritems(): if index is None or key in index: res.extend([x for x in folders if x not in ignore]) return res def _load_catalog_http(self): html = self.http.get('http://rutracker.org/forum/tracker.php') if not html: return html r = re.compile('<select id="fs-main"(.+?)</select>', re.U|re.S).search(html) if not r: return None res = {} root = None for cat, is_root_forum in re.compile('<option id="fs\-([0-9]+)"([^>]+)>', re.U).findall(r.group(1)): cat = int(cat) if is_root_forum.find('root_forum') != -1: root = cat res[root] = [cat] elif root: res[root].append(cat) return (86400 if res else False), res # day # SCREENSHOT def pic_hosting_fastpic(self, img, html): if img == 'cover': r = re.compile('<var[^>]+class="postImg postImgAligned img\-right"[^>]+title="(http\://[0-9a-z]+\.fastpic\.ru/big/[0-9a-f/]+\.[a-z]{3,4})"[^>]*>', re.U|re.S).search(html) if r: return r.group(1) return None else: res = [] for r in re.compile('<a[^>]*href="http\://fastpic\.ru/view/[^\.]+\.([a-z]{3,4})\.html"[^>]*>[.]*?<var[^>]+title="(http\://[0-9a-z]+\.fastpic\.ru/thumb/[0-9a-f/]+)\.[a-z]{3,4}"[^>]*>', re.U|re.S).findall(html): res.append('.'.join([r[1].replace('thumb', 'big'), r[0]])) return res if res else None
class KinoPoisk: """ API: scraper - скрапер movie - профайл фильма search - поиск фильма best - поиск лучших фильмов person - поиск персон work - информация о работах персоны """ def __init__(self): self.cache = Cache('kinopoisk.db') self.html = Clear() self.http = HTTP() self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:10.0.2) Gecko/20100101 Firefox/10.0.2', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3', 'Cache-Control': 'no-cache', 'Referer': 'http://www.kinopoisk.ru/level/7/' } # API def scraper(self, name, year=None, trailer_quality=None): try: tag = 'scraper:' + urllib.quote_plus(name.encode('windows-1251')) except: return None else: if year: tag += ':' + str(year) id = self.cache.get(tag, self._scraper, name, year) if not id: return None return self.movie(id, trailer_quality) def movie(self, id, trailer_quality=None): id = str(id) if trailer_quality is None: trailer_quality = 6 movie = self.cache.get('movie:' + id, self._movie, id) if not movie: return None if movie['trailers']: # компилируем список с нужным нам качеством video = [] for m in movie['trailers']: url = [x for x in m['video'] if x[0] <= trailer_quality] if url: m['video'] = url[-1] video.append(m) movie['trailers'] = video if movie['trailers']: # готовим главный трейлер r = [x for x in movie['trailers'] if x['trailer']] if r: movie['info']['trailer'] = r[0]['video'][1] else: # если трейлер не найден, то отдаем что попало... movie['info']['trailer'] = movie['trailers'][0]['video'][1] return movie def search(self, name, trailer_quality=None): return self._search_movie(name) def best(self, **kwarg): page = kwarg.get('page', 1) limit = kwarg.get('limit', 50) url = 'http://www.kinopoisk.ru/top/navigator/m_act%5Bis_film%5D/on/m_act%5Bnum_vote%5D/' + str(kwarg.get('votes', 100)) + '/' if kwarg.get('dvd'): url += 'm_act%5Bis_dvd%5D/on/' if kwarg.get('decade'): url += 'm_act%5Bdecade%5D/' + str(kwarg['decade']) + '/' if kwarg.get('genre'): url += 'm_act%5Bgenre%5D/' + str(GENRE[kwarg['genre']]) + '/' if kwarg.get('country'): url += 'm_act%5Bcountry%5D/' + str(kwarg['country']) + '/' if kwarg.get('rate'): url += 'm_act%5Brating%5D/' + str(kwarg['rate']) + ':/' if kwarg.get('mpaa'): url += 'm_act%5Bmpaa%5D/' + str(kwarg['mpaa']) + '/' url += 'perpage/' + str(limit) + '/order/ex_rating/' if page > 1: url += 'page/' + str(page) + '/' response = self.http.fetch(url, headers=self.headers) if response.error: return None res = {'pages': (1, 0, 1, 0), 'data': []} r = re.compile('<div class="pagesFromTo(.+?)<div class="pagesFromTo', re.U|re.S).search(response.body.decode('windows-1251')) if r: body = r.group(1) # compile pagelist p = re.compile('>([0-9]+)—[0-9]+[^0-9]+?([0-9]+)', re.U).search(body) if p: page = (int(p.group(1))-1)/limit + 1 total = int(p.group(2)) pages = total/limit if limit*pages != total: pages += 1 res['pages'] = (pages, 0 if page == 1 else page-1, page, 0 if page==pages else page+1) # end compile for id in re.compile('<div id="tr_([0-9]+)"', re.U|re.S).findall(body): res['data'].append(int(id)) return res def person(self, name): response = self.http.fetch('http://www.kinopoisk.ru/s/type/people/list/1/find/' + urllib.quote_plus(name.encode('windows-1251')) + '/order/relevant/', headers=self.headers) if response.error: return None res = [] body = re.compile('<div class="navigator">(.+?)<div class="navigator">', re.U|re.S).search(response.body.decode('windows-1251')) if body: for block in re.compile('<p class="pic">(.+?)<div class="clear">', re.U|re.S).findall(body.group(1)): id, name, original, year, poster = None, None, None, None, None r = re.compile('<p class="name"><a href="http://www\.kinopoisk\.ru/level/4/people/([0-9]+)[^>]+>([^<]+)</a>', re.U|re.S).search(block) if r: id = r.group(1) name = r.group(2).strip() if id and name: r = re.compile('<span class="gray">([^<]+)</span>', re.U|re.S).search(block) if r: original = r.group(1).strip() if not original: original = None r = re.compile('<span class="year">([0-9]{4})</span>', re.U|re.S).search(block) if r: year = int(r.group(1)) if block.find('no-poster.gif') == -1: poster = 'http://st.kinopoisk.ru/images/actor/' + id + '.jpg' res.append({'id': int(id), 'name': name, 'originalname': original, 'year': year, 'poster': poster}) return {'pages': (1, 0, 1, 0), 'data': res} def work(self, id): response = self.http.fetch('http://www.kinopoisk.ru/name/' + str(id) + '/', headers=self.headers) if response.error: return None res = {} r = re.compile('id="sort_block">(.+?)<style>', re.U|re.S).search(response.body.decode('windows-1251')) if r: for block in r.group(1).split(u'<table cellspacing="0" cellpadding="0" border="0" width="100%">'): work = None for w in ('actor', 'director', 'writer', 'producer', 'producer_ussr', 'composer', 'operator', 'editor', 'design', 'voice', 'voice_director'): if block.find(u'id="' + w + u'"') != -1: work = 'producer' if w == 'producer_ussr' else w break if work: movies = [] for id, name in re.compile('<span class="name"><a href="/film/([0-9]+)/" >([^<]+?)</a>', re.U).findall(block): for tag in (u'(мини-сериал)', u'(сериал)'): if name.find(tag) != -1: break else: movies.append(int(id)) if movies: res.setdefault(work, []).extend(movies) return res def review(self, id, query): query_s = 'all' if query == 'stat' else query data = self.cache.get('review:' + str(id) + ':' + query_s, self._review, id, query_s) if not data: return data return data[query] def countries(self): return COUNTRIES def country(self, id, default=None): country = [x[1] for x in COUNTRIES if x[0] == id] return country[0] if country else default # PRIVATE def _search_movie(self, name, year=None): url = 'http://www.kinopoisk.ru/s/type/film/list/1/find/' + urllib.quote_plus(name.encode('windows-1251')) + '/order/relevant' if year: url += '/m_act%5Byear%5D/' + str(year) url += '/m_act%5Btype%5D/film/' response = self.http.fetch(url, headers=self.headers) if response.error: return None res = [] r = re.compile('<div class="navigator">(.+?)<div class="navigator">', re.U|re.S).search(response.body.decode('windows-1251')) if r: for id in re.compile('<p class="name"><a href="/level/1/film/([0-9]+)', re.U|re.S).findall(r.group(1)): res.append(int(id)) return {'pages': (1, 0, 1, 0), 'data': res} def _scraper(self, name, year): timeout = True # если фильм свежий, то кладем в кэш НЕ на долго (могут быть обновления на сайте) if year and year >= time.gmtime(time.time()).tm_year: timeout = 7*24*60*60 #week ids = self._search_movie(name, year) if ids is None: return False, None elif not ids['data']: # сохраняем пустой результат на 3-е суток return 259200, None else: return timeout, ids['data'][0] def _review(self, id, query): url = 'http://www.kinopoisk.ru/film/' + str(id) + '/ord/rating/' if query in ('good', 'bad', 'neutral'): url += 'status/' + query + '/' url += 'perpage/200/' response = self.http.fetch(url, headers=self.headers) if response.error: return False, None html = response.body.decode('windows-1251') res = { 'stat': {'all': 0, 'good': 0, 'bad': 0, 'neutral': 0}, query: [] } r = re.compile('<ul class="resp_type">(.+?)</ul>', re.U|re.S).search(html) if r: ul = r.group(1) for q, t in (('pos', 'good'), ('neg', 'bad'), ('neut', 'neutral')): r = re.compile('<li class="' + q + '"><a href="[^>]+>[^<]+</a><b>([0-9]+)</b></li>', re.U).search(ul) if r: res['stat'][t] = int(r.group(1)) res['stat']['all'] = res['stat']['good'] + res['stat']['bad'] + res['stat']['neutral'] r = re.compile('<div class="navigator">(.+?)<div class="navigator">', re.U|re.S).search(html) if r: for block in r.group(1).split('itemprop="reviews"'): review = { 'nick': None, 'count': None, 'title': None, 'review': None, 'time': None } r = re.compile('itemprop="reviewBody">(.+?)</div>', re.U|re.S).search(block) if r: text = r.group(1) for tag1, tag2 in ((u'<=end=>', u'\n'), (u'<b>', u'[B]'), (u'</b>', u'[/B]'), (u'<i>', u'[I]'), (u'</i>', u'[/I]'), (u'<u>', u'[U]'), (u'</u>', u'[/U]')): text = text.replace(tag1, tag2) r = self.html.text(text) if r: review['review'] = r user = None r = re.compile('<p class="profile_name"><s></s><a href="[^>]+>([^<]+)</a></p>').search(block) if r: user = self.html.string(r.group(1)) else: r = re.compile('<p class="profile_name"><s></s>([^<]+)</p>').search(block) if r: user = self.html.string(r.group(1)) if user: review['nick'] = user r = re.compile('<p class="sub_title"[^>]+>([^<]+)</p>').search(block) if r: title = self.html.string(r.group(1)) if title: review['title'] = title r = re.compile('<span class="date">([^<]+)</span>', re.U|re.S).search(block) if r: review['time'] = r.group(1).replace(u' |', u',') r = re.compile(u'<a href="[^>]+>рецензии \(([0-9]+)\)</a>', re.U|re.S).search(block) if r: review['count'] = int(r.group(1)) if review['nick'] and review['review']: res[query].append(review) return 3600, res # one hour def _movie(self, id): response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/', headers=self.headers) if response.error: return False, None html = response.body.decode('windows-1251') res = { 'id': int(id), 'thumb': None, 'fanart': None, 'trailers': [], 'info': {} } # имя, оригинальное имя, девиз, цензура, год, top250 # runtime - длительность фильма (в отдельную переменную, иначе не видно размер файла) for tag, reg, t in ( ('title', '<title>(.+?)</title>', 'str'), ('originaltitle', 'itemprop="alternativeHeadline">([^<]*)</span>', 'str'), ('tagline', '<td style="color\: #555">«(.+?)»</td></tr>', 'str'), ('mpaa', 'images/mpaa/([^\.]+).gif', 'str'), ('runtime', '<td class="time" id="runtime">[^<]+<span style="color\: #999">/</span>([^<]+)</td>', 'str'), ('year', '<a href="/lists/m_act%5Byear%5D/([0-9]+)/"', 'int'), ('top250', 'Топ250\: <a\shref="/level/20/#([0-9]+)', 'int') ): r = re.compile(reg, re.U).search(html) if r: value = r.group(1).strip() if value: res['info'][tag] = value if t == 'int': res['info'][tag] = int(res['info'][tag]) else: res['info'][tag] = self.html.string(res['info'][tag]) # режисеры, сценаристы, жанры for tag, reg in ( ('director', u'<td itemprop="director">(.+?)</td>'), ('writer', u'<td class="type">сценарий</td><td[^>]*>(.+?)</td>'), ('genre', u'<span itemprop="genre">(.+?)</span>') ): r = re.compile(reg, re.U|re.S).search(html) if r: r2 = [] for r in re.compile('<a href="[^"]+">([^<]+)</a>', re.U).findall(r.group(1)): r = self.html.string(r) if r and r != '...': r2.append(r) if r2: res['info'][tag] = u', '.join(r2) # актеры r = re.compile(u'<h4>В главных ролях:</h4>(.+?)</ul>', re.U|re.S).search(html) if r: actors = [] for r in re.compile('<li itemprop="actors"><a [^>]+>([^<]+)</a></li>', re.U).findall(r.group(1)): r = self.html.string(r) if r and r != '...': actors.append(r) if actors: res['info']['cast'] = actors[:] #res['info']['castandrole'] = actors[:] # описание фильма r = re.compile('<span class="_reachbanner_"><div class="brand_words" itemprop="description">(.+?)</div></span>', re.U).search(html) if r: plot = self.html.text(r.group(1).replace('<=end=>', '\n')) if plot: res['info']['plot'] = plot # IMDB r = re.compile('IMDb: ([0-9.]+) \(([0-9\s]+)\)</div>', re.U).search(html) if r: res['info']['rating'] = float(r.group(1).strip()) res['info']['votes'] = r.group(2).strip() # премьера r = re.compile(u'премьера \(мир\)</td>(.+?)</tr>', re.U|re.S).search(html) if r: r = re.compile(u'data\-ical\-date="([^"]+)"', re.U|re.S).search(r.group(1)) if r: data = r.group(1).split(' ') if len(data) == 3: i = 0 for mon in (u'января', u'февраля', u'марта', u'апреля', u'мая', u'июня', u'июля', u'августа', u'сентября', u'октября', u'ноября', u'декабря'): i += 1 if mon == data[1]: mon = str(i) if len(mon) == 1: mon = '0' + mon day = data[0] if len(day) == 1: day = '0' + day res['info']['premiered'] = '-'.join([data[2], mon, day]) break # постер r = re.compile(u'onclick="openImgPopup\(([^\)]+)\)', re.U|re.S).search(html) if r: poster = r.group(1).replace("'", '').strip() if poster: res['thumb'] = 'http://kinopoisk.ru' + poster menu = re.compile('<ul id="newMenuSub" class="clearfix(.+?)<!\-\- /menu \-\->', re.U|re.S).search(html) if menu: menu = menu.group(1) # фанарт if menu.find('/film/' + id + '/wall/') != -1: response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/wall/', headers=self.headers) if not response.error: html = response.body.decode('windows-1251') fanart = re.compile('<a href="/picture/([0-9]+)/w_size/([0-9]+)/">', re.U).findall(html) if fanart: fanart.sort(cmp=lambda (id1, size1), (id2, size2): cmp(int(size1), int(size2))) # пробуем взять максимально подходящее fanart_best = [x for x in fanart if int(x[1]) <= 1280] if fanart_best: fanart = fanart_best response = self.http.fetch('http://www.kinopoisk.ru/picture/' + fanart[-1][0] + '/w_size/' + fanart[-1][1] + '/', headers=self.headers) if not response.error: html = response.body.decode('windows-1251') r = re.compile('id="image" src="([^"]+)"', re.U|re.S).search(html) if r: res['fanart'] = r.group(1).strip() # если нет фанарта (обоев), то пробуем получить кадры if not res['fanart'] and menu.find('/film/' + id + '/stills/') != -1: response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/stills/', headers=self.headers) if not response.error: html = response.body.decode('windows-1251') fanart = re.compile('<a href="/picture/([0-9]+)/"><img src="[^<]+</a>[^<]+<b><i>([0-9]+)×([0-9]+)</i>', re.U).findall(html) if fanart: fanart.sort(cmp=lambda (id1, size1, t1), (id2, size2, t2): cmp(int(size1), int(size2))) # пробуем взять максимально подходящее fanart_best = [x for x in fanart if int(x[1]) <= 1280 and int(x[1]) > int(x[2])] if fanart_best: fanart = fanart_best response = self.http.fetch('http://www.kinopoisk.ru/picture/' + fanart[-1][0] + '/', headers=self.headers) if not response.error: html = response.body.decode('windows-1251') r = re.compile('id="image" src="([^"]+)"', re.U|re.S).search(html) if r: res['fanart'] = r.group(1).strip() # студии if menu.find('/film/' + id + '/studio/') != -1: response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/studio/', headers=self.headers) if not response.error: html = response.body.decode('windows-1251') r = re.compile(u'<b>Производство:</b>(.+?)</table>', re.U|re.S).search(html) if r: studio = [] for r in re.compile('<a href="/lists/m_act%5Bstudio%5D/[0-9]+/" class="all">(.+?)</a>', re.U).findall(r.group(1)): r = self.html.string(r) if r: studio.append(r) if studio: res['info']['studio'] = u', '.join(studio) # трэйлеры trailers1 = [] # русские трейлеры trailers2 = [] # другие русские видео trailers3 = [] # трейлеры trailers4 = [] # другие видео if menu.find('/film/' + id + '/video/') != -1: response = self.http.fetch('http://www.kinopoisk.ru/film/' + id + '/video/', headers=self.headers) if not response.error: html = response.body.decode('windows-1251') for row in re.compile(u'<!-- ролик -->(.+?)<!-- /ролик -->', re.U|re.S).findall(html): # отсекаем лишние блоки if row.find(u'>СМОТРЕТЬ</a>') != -1: # русский ролик? if row.find('class="flag flag2"') == -1: is_ru = False else: is_ru = True # получаем имя трейлера r = re.compile('<a href="/film/' + id + '/video/[0-9]+/[^>]+ class="all">(.+?)</a>', re.U).search(row) if r: name = self.html.string(r.group(1)) if name: trailer = { 'name': name, 'time': None, 'trailer': False, 'ru': is_ru, 'video': [] } # трейлер или тизер? for token in (u'Трейлер', u'трейлер', u'Тизер', u'тизер'): if name.find(token) != -1: trailer['trailer'] = True break # получаем время трейлера r = re.compile(u'clock.gif"[^>]+></td>\s*<td style="color\: #777">[^0-9]*([0-9\:]+)</td>', re.U|re.S).search(row) if r: trailer['time'] = r.group(1).strip() print 'F**K' # делим ролики по качеству for r in re.compile('trailer/([1-3])a.gif"(.+?)link=([^"]+)" class="continue">.+?<td style="color\:#777">([^<]+)</td>\s*</tr>', re.U|re.S).findall(row): print str(r) quality = int(r[0]) if r[1].find('icon-hd') != -1: quality += 3 trailer['video'].append((quality, r[2].strip(), r[3])) print str(trailer) if id == '462754': #raise pass if trailer['video']: if trailer['ru']: if trailer['trailer']: trailers1.append(trailer) else: trailers2.append(trailer) else: if trailer['trailer']: trailers3.append(trailer) else: trailers4.append(trailer) # склеиваем трейлеры res['trailers'].extend(trailers1) res['trailers'].extend(trailers2) res['trailers'].extend(trailers3) res['trailers'].extend(trailers4) timeout = True # если фильм свежий, то кладем в кэш НЕ на долго (могут быть обновления на сайте) if 'year' not in res['info'] or int(res['info']['year']) >= time.gmtime(time.time()).tm_year: timeout = 7*24*60*60 #week return timeout, res
class RuTracker: def __init__(self, expire=0, size=0): self.cache_catalog = Cache('rutracker_catalog.db') self.cache_profile = Cache('rutracker_profile.db', expire, size) self.http = RuTrackerHTTP() self.re = { 'is_int': re.compile(r'^([0-9]+)', re.U), 'is_float': re.compile(r'^([0-9]{1,10}\.[0-9]+)', re.U), 'hash': re.compile(r'<span id="tor-hash">([0-9A-F]{40})</span>', re.U) } self.html = Clear() self.status = { '%': 'check', 'D': 'repeat', '!': 'nodesc', '©': 'copyright', '∏': 'moder', 'x': 'close', '∑': 'absorb', '#': 'doubtful', '*': 'nocheck', '?': 'neededit', 'T': 'temp', '√': 'ok' } # API def get(self, id=None, page=1): """ Получение списка директорий и раздач На вход функции надо подавать следующие параметры: id - [int] id директории page - [int] номер страницы Возвращает словарь, состоящий из следующих полей: pages - [list] кортеж [int] для навигации = (кол-во страниц, предыдущая, текущая, следующая) data - [list] данные выборки, состоящая из следующих полей: id - [int] id (для директорий и топиков ID могут совпадать) name - [str] имя записи type - [str] тип записи (torrent - торрент, folder - директория) size - [int] размер раздачи в байтах seeder - [int] кол-во сидов leecher - [int] кол-во личеров download - [int] кол-во скачиваний торрента comment - [int] кол-во комментариев status - [str] символ отметки модератора status_human - [str] отметка модератора в удобочитаемом виде (описание смотри ниже). Описание возвращаемых отметок status_human: Скачать торрент нельзя (красный): moder - премодерация, check - проверяется, repeat - повтор, nodesc - не оформлено, copyright - закрыто правообладателем, close - закрыто, absorb - поглощено. Скачать торрент можно (желтый): nocheck - не проверено, neededit - недооформлено, doubtful - сомнительно, temp - временная Скачать торрент можно (зеленый): ok - проверено """ # INDEX if id is None: html = self.http.get('http://rutracker.lib/forum/index.php') if not html: return html res = [] r = re.compile( r'<div\sid="forums_wrap">(.+)<div\sclass="bottom_info">', re.U | re.S).search(html) if r: r = re.compile( r'<h4\sclass="forumlink"><a\shref="viewforum\.php\?f=([0-9]+)">(.+?)</a></h4>', re.U | re.S).findall(r.group(1)) if r: res = [{ 'id': int(i), 'name': self.html.string(x), 'type': 'folder' } for i, x in r] if not res: return None return {'pages': (1, 0, 1, 0), 'data': res} else: page_query = '' if page > 1: page_query = '&start=' + str(50 * (page - 1)) html = self.http.get( 'http://rutracker.lib/forum/viewforum.php?f=' + str(id) + '&sort=2' + page_query) if not html: return html pages = self._compile_pages(html) folder = [] torrent = [] group_list = re.compile( r'<table class="[^"]*forumline forum">(.+?)</table>', re.U | re.S).findall(html) if group_list: for group in group_list: # вытаскиваем папки (если есть) r = re.compile( r'<h4\sclass="forumlink"><a\shref="viewforum\.php\?f=([0-9]+)">(.+?)</a></h4>', re.U | re.S).findall(group) if r: folder.extend([{ 'id': int(i), 'name': self.html.string(x), 'type': 'folder' } for i, x in r]) # нарубаем на строчки топиков topic_list = group.split(u'topicSep">') if len(topic_list) > 1: topic_list = topic_list[1:] for html in topic_list: # вытаскиваем id for text in re.compile( r'<tr\sid="tr\-[0-9]+"(.+?)</tr>', re.U | re.S).findall(html): item = self._compile_topic(text) if item: torrent.append(item) folder.extend(torrent) return {'pages': pages, 'data': folder} def search(self, search, folder=None, index=None, ignore=None): """ Поиск по РуТрекеру На вход функции надо подавать следующие параметры: search - [str] поисковая строка запроса (Unicode) folder - [list] список ID директорий, в которых необходимо искать (None - искать везде) Возвращает словарь, аналогичный выводу метода GET """ if isinstance(search, unicode): search = search.encode('windows-1251') # проверяем авторизацию html = self.http.get('http://rutracker.lib/forum/index.php') if not html: return html # готовим запрос для получения дерева разделов if folder: if not isinstance(folder, list) and not isinstance(folder, tuple): folder = [folder] else: if index is not None: if not isinstance(index, list) and not isinstance( index, tuple): index = [index] if ignore is not None: if not isinstance(ignore, list) and not isinstance( ignore, tuple): ignore = [ignore] if not index and not ignore: folder = [] else: folder = self._load_catalog(index, ignore) if not folder: return folder # готовим запрос params = [('nm', search), ('o', 10), ('s', 2), ('prev_my', 0), ('prev_new', 0), ('prev_oop', 0), ('submit', r'Поиск')] params.extend([('f[]', x) for x in folder]) # делаем поиск html = self.http.post('http://rutracker.lib/forum/tracker.php', params) if not html: return html res = [] table = re.compile('id="tor\-tbl">(.+?)</table>', re.U | re.S).search(html) if table: for tr in re.compile('<tr\sclass="tCenter\shl\-tr">(.+?)</tr>', re.U | re.S).findall(table.group(1)): item = self._compile_topic(tr, True) if item: res.append(item) return {'pages': (1, 0, 1, 0), 'data': res} def profile(self, id): """ Получение дополнительной информации о раздачи На вход функции надо подавать следующие параметры: id - [int] id топика с раздачей Возвращает словарь, состоящий из: descript - [str] описание на RuTracker cover - [str] url обложки screenshot - [list] Список url скриншотов """ return self.cache_profile.get('profile:' + str(id), self._profile, id) def comment(self, id, page=1): """ Получение комментариев раздачи На вход функции надо подавать следующие параметры: id - [int] id топика page - [int] номер страницы Возвращает словарь, состоящий из следующих полей: pages - [list] кортеж [int] для навигации = (кол-во страниц, предыдущая, текущая, следующая) data - [list] данные выборки - список словарей, состоящих из следующих полей: nick - [str] ник автора комментария usertime - [str] стаж юзера count - [str] кол-во сообщений у юзера location - [str] откуда юзер time - [str] время добавления комментария message - [str] комментарий """ page_query = '' if page > 1: page_query = '&start=' + str(30 * (page - 1)) html = self.http.get('http://rutracker.lib/forum/viewtopic.php?t=' + str(id) + page_query) if not html: return html res = {'pages': self._compile_pages(html), 'comments': []} # нарубаем страницу по постам rows = re.compile( '<tbody id="post_[0-9]+" class="row1|2">(.+?)<!\-\-/post_body\-\->', re.U | re.S).findall(html) if rows: if page == 1: rows.pop(0) if rows: # функция для очистки комментариев def _def_subn1(m): return u'<div class="q-wrap"><div class="q">' + self.html.string( m.group(1)) + u':\n' def _def_subn2(m): r = u'[BR][I]' + m.group(1).replace(u'[I]', u'').replace( u'[/I]', u'') + u'[/I][BR]' n = 1 while n: r, n = re.compile(u'\[BR\]\[BR\]', re.U | re.S).subn(u'[BR]', r) return r for html in rows: comment = { 'nick': None, 'usertime': None, 'count': None, 'location': u'', 'time': None, 'message': None } # вытаскиваем ник r = re.compile('<p class="nick[^>]+>([^<]+)</p>', re.U).search(html) if r: comment['nick'] = self.html.string(r.group(1).strip()) # смотрим стаж r = re.compile( u'<p class="joined"><em>Стаж:</em>([^<]+)</p>', re.U).search(html) if r: comment['usertime'] = r.group(1).strip() # смотрим кол-во коментов у юзера r = re.compile( u'<p class="posts"><em>Сообщений:</em>([^<]+)</p>', re.U).search(html) if r: comment['count'] = r.group(1).strip() # смотрим город юзера r = re.compile( u'<p class="from"><em>Откуда:</em>([^<]+)</p>', re.U).search(html) if r: comment['location'] = r.group(1).strip() # смотрим страну юзера r = re.compile( '<p class="flag"><img [^>]*title="([^"]+)"[^>]*></p>', re.U).search(html) if r: if comment['location']: comment['location'] += u', ' comment['location'] += r.group(1).strip() # смотрим время коммента r = re.compile( '<a class="small" href="\./viewtopic.php\?p=[^>]+>([0-9]{1,2}\-[^\-]+\-[0-9]{2} [0-9]{1,2}\:[0-9]{1,2})</a>', re.U).search(html) if r: comment['time'] = r.group(1).strip() # вытаскиваем тело коммента r = re.compile('<div class="post_body"[^>]+>(.+)$', re.U | re.S).search(html) if r: html = r.group(1).strip() if html: # заменяем что можем... for reg, rep in ( ('<span class="post\-b">([^(?:</span>)]+)</span>', u'[B]\g<1>[/B]'), ): html = re.compile(reg, re.U | re.S).sub(rep, html) # конвертируем цитаты html, n = re.compile( '<div class="q-wrap">\s*<div class="q" head="([^"]+)">', re.U | re.S).subn(_def_subn1, html) n = 1 while n: html, n = re.compile( '<div class="q-wrap">\s*<div class="q">(.+?)</div>\s*</div>', re.U | re.S).subn(_def_subn2, html) # прогоняем через полную очистку comment['message'] = self.html.text(html) if comment['nick'] and comment['message']: res['comments'].append(comment) return res def download(self, id): """ Скачивание торрента раздачи На вход функции надо подавать следующие параметры: id - [str] топика с раздачей Возвращает торрент или None (в случае неудачи) """ return self.http.download(id) def hash(self, id): """ Получение инфо-хеша раздачи На вход функции надо подавать следующие параметры: id - [str] топика с раздачей Возвращает шеснадцатеричное число хэша (в виде строки) или None (в случае неудачи) """ return self.cache_profile.get('hash:' + str(id), self._hash, id) def magnet(self, id): """ Получение инфо-хеша раздачи На вход функции надо подавать следующие параметры: id - [str] топика с раздачей Возвращает шеснадцатеричное число хэша (в виде строки) или None (в случае неудачи) """ hash = self.hash(id) if hash: return 'magnet:?xt=urn:btih:' + hash return hash # PRIVATE def _compile_pages(self, text): r = re.compile( u'<p style="float\: left">Страница <b>([0-9]+)</b> из <b>([0-9]+)</b></p>', re.U | re.S).search(text) if r: current = int(r.group(1)) total = int(r.group(2)) next = current + 1 if next > total: next = 0 return total, current - 1, current, next return 1, 0, 1, 0 def _compile_topic(self, text, is_search=False): r = re.compile( r'<a\s[^>]*href="viewtopic\.php\?t=([0-9]+)"[^>]*>(.+?)</a>', re.U | re.S).search(text) if r: id = r.group(1) name = self.html.string(r.group(2)) r = re.compile( r'<a[^>]+href="dl\.php\?t=' + id + '"[^>]*>(.+?)</a>', re.U | re.S).search(text) if r: size = self._compile_size(r.group(1)) if size and name: item = self._create_torrent(int(id), name) item['size'] = size r = re.compile(r'"tor-icon[^>]+>([^<]+)<', re.U | re.S).search(text) if r: stat = r.group(1) try: status = self.status[stat] except KeyError: pass else: item['status'] = self.html.char(stat) item['status_human'] = status if is_search: item['comment'] = -1 query = (( 'download', '<td\sclass="row4\ssmall">([0-9]+)</td>' ), ( 'seeder', '<b class="seedmed">([0-9]+)</b></td>' ), ('leecher', '<td\sclass="row4 leechmed"[^>]+><b>([0-9]+)</b></td>' )) else: query = (( 'comment', u'<span title="Ответов">([0-9]+)</span>' ), ('download', u'title="Торрент скачан">[^<]*<b>([0-9]+)</b>[^<]*</p>' ), ('seeder', 'title="Seeders"><b>([0-9]+)</b></span>'), ('leecher', 'title="Leechers"><b>([0-9]+)</b></span>')) for tag, reg in query: r = re.compile(reg, re.U | re.S).search(text) if r: item[tag] = int(r.group(1)) return item return None def _create_torrent(self, id, name): return { 'id': id, 'name': name, 'type': 'torrent', 'size': 0, 'seeder': 0, 'leecher': 0, 'download': 0, 'comment': 0, 'status': None, 'status_human': None } def _compile_size(self, text): text = self.html.string(text.replace(u'↓', u'')) if text: text = text.lower() prefix = 1 for p, v in ((u'kb', 1024), (u'mb', 1024 * 1024), (u'gb', 1024 * 1024 * 1024), (u'tb', 1024 * 1024 * 1024 * 1024)): if text.find(p) != -1: prefix = v text = text.replace(p, u'').strip() break num = self.re['is_float'].search(text) if num: return int(float(prefix) * float(num.group(1))) num = self.re['is_int'].search(text) if num: return prefix * int(num.group(1)) return None def _hash(self, id): html = self.http.get('http://rutracker.lib/forum/viewtopic.php?t=' + str(id)) if not html: return False, html r = self.re['hash'].search(html) if not r: return False, None return True, str(r.group(1)) def _profile(self, id): html = self.http.guest('http://rutracker.lib/forum/viewtopic.php?t=' + str(id)) if not html: return False, html res = {'descript': None, 'cover': None, 'screenshot': None} r = re.compile( '<div class="post_body"[^>]+>(.+?)<legend>Download</legend>', re.U | re.S).search(html) if r: html = r.group(1) # ищем коверы (перебирая все возможные варианты хостингов картинок) for api in (self.pic_hosting_fastpic, ): cover = api('cover', html) if cover: res['cover'] = cover break # вытаскиваем блок со скриншотами r = re.compile(u'<span>Скриншоты</span></div>(.+?)</div>', re.U | re.S).search(html) if r: body = r.group(1) # ищем скрины (перебирая все возможные варианты хостингов картинок) for api in (self.pic_hosting_fastpic, ): screenshot = api('screenshot', body) if screenshot: res['screenshot'] = screenshot break # пытаемся получить текст описания # режем и заменяем все что можем... for reg, rep in ( (u'<div class="sp\-wrap">.+?<div class="sp-body">.+?</div>', u''), # удаляем все спойлеры (u'<var[^>]+>[^<]+</var>', u''), # удаляем все изображения (u'<span class="post\-hr">\-</span>', u'\n'), # удаляем HR (u'<span class="post\-b">([^<]+)</span>', u'[COLOR FF0DA09E]\g<1>[/COLOR]') # заменяем болды ): html = re.compile(reg, re.U | re.S).sub(rep, html) # прогоняем через полную очистку html = self.html.text(html) if html: res['descript'] = html return True, res # CATALOG def _load_catalog(self, index, ignore): catalog = self.cache_catalog.get('catalog', self._load_catalog_http) if not catalog: return [] res = [] for key, folders in catalog.iteritems(): if index is None or key in index: res.extend([x for x in folders if x not in ignore]) return res def _load_catalog_http(self): html = self.http.get('http://rutracker.lib/forum/tracker.php') if not html: return html r = re.compile('<select id="fs-main"(.+?)</select>', re.U | re.S).search(html) if not r: return None res = {} root = None for cat, is_root_forum in re.compile( '<option id="fs\-([0-9]+)"([^>]+)>', re.U).findall(r.group(1)): cat = int(cat) if is_root_forum.find('root_forum') != -1: root = cat res[root] = [cat] elif root: res[root].append(cat) return (86400 if res else False), res # day # SCREENSHOT def pic_hosting_fastpic(self, img, html): if img == 'cover': r = re.compile( '<var[^>]+class="postImg postImgAligned img\-right"[^>]+title="(http\://[0-9a-z]+\.fastpic\.ru/big/[0-9a-f/]+\.[a-z]{3,4})"[^>]*>', re.U | re.S).search(html) if r: return r.group(1) return None else: res = [] for r in re.compile( '<a[^>]*href="http\://fastpic\.ru/view/[^\.]+\.([a-z]{3,4})\.html"[^>]*>[.]*?<var[^>]+title="(http\://[0-9a-z]+\.fastpic\.ru/thumb/[0-9a-f/]+)\.[a-z]{3,4}"[^>]*>', re.U | re.S).findall(html): res.append('.'.join([r[1].replace('thumb', 'big'), r[0]])) return res if res else None