суббота, 19 мая 2007 г.

Пишем Tv программу на Django. Часть 2

Ну вот, модель мы создали. Таблицы создали. Теперь было бы их не плохо заполнить.

Заполнять будем парсером афиши@mail.ru Поэтому теперь мы будем злобными пиратами, ворующими чужой контент. Кому это не интересно можете пропускать эту запись сразу, а наполнять базу можно и какими-то легальными способами.

Мой способ - парсить майл.ру каждое воскресенье, доставая оттуда телепрограмму на следующую неделю. То есть сейчас необходимо написать какой-то скриптик, который потом нужно будет прописать в cron. И после этого мы всегда будем иметь актуальную информацию в нашей бд.

И так зайдём на http://pda.tv.mail.ru и выберем какой-нибудь канал. Смотрим урл, например: http://pda.tv.mail.ru/?gosetup=1&channel=365. 365 это тот Num который мы создавали в модели. Но как видно отображаются лишь актуальные телепередачи, поэтому переключимся в вид "весь день". Но теперь отображается программа на сегодня, а нам бы нужно бы иметь возможность получать программу на разные дни. Выберем какой-нибудь другой день... Замечательно! Спасибо мэйлу, нам необходимо только циклом ходить по страничкам вида http://pda.tv.mail.ru/?date=[date]&gosetup=1&period=3&channel=[Num] и парсить их.

Посмотрим исходный код...
Шикарно, ещё один респект мэйлу от меня.
Нужная нам информация находимтся в блоке между <!-- START: Programm Content --> и <!-- END: Programm Content -->
Там простая табличка, которую не составит большого труда отпарсить, тем более программы с описанием выделены тегом "b". Описание фильмов находится в теге "p" с классом t75. Ну вроде бы в устройстве сайта разобрались, Парсим!

Создаём новый файлики, я его назвал core.py Подключаем стандартные питоновские модули HTMLParser и urllib, ну и дальше по мануалу (хотя наверняка можно написать гораздо красивее):

class ChanalHTMLParser(HTMLParser):
""" Класс парсера для канала, создаём на основе HTMLParser """
def __init__(self):
HTMLParser.__init__(self)
self.content=0 # несколько служебных переменных - переключателей
self.flag=0
self.result=[]
self.buffer=[]
self.flag2=0
def handle_comment(self, data):
""" Ищет в комментах необходимые строки """
if "START: Programm Content" in data:
self.content=1 # говорим что начались полезные данные
elif "END: Programm Content" in data:
self.content=0 # а теперь закончились

def handle_starttag(self, tag, attrs):
""" Функция вызывается при нахождении открывающего тега """
if self.content==1: # Если мы внутри интересующий нас информации, то ищим в тегах
if tag=="td":
if len(attrs)>0:
if attrs[1][0]=="class" and attrs[1][1]=="time":
self.flag=1 # последующие данные, до закрытия тега будут временем
else: # иначе ссылкой и названием программы
if self.flag==2: # конечно если тег со временем уже закрыт
self.flag=3 # говорим, что нашли строку с названием
elif tag=="b" and self.flag==3: # встретили b - верный признак того, чтоо есть описание
self.flag2=1 # сообщаем об этом
elif tag=="a" and self.flag2==1 and self.flag==3: # а вот если встречаем a, то внутренность его нужна только если есть описание
self.buffer.append(attrs[0][1]) # если всё ok, то добавляем ссылку в буффер

def handle_data(self, data):
""" Выполняется каждый раз когда оказывается 'внутри' тегов """
if self.flag==1 or self.flag==3: # если там время или название
self.buffer.append(data) # то добавляем их в буффер

def handle_endtag(self, tag):
""" Ну а эта функция соответсвенно проверяет закрывающие теги """
if self.content==1: # снова делаем проверку
if tag=="b":
if self.flag2==1 and self.flag==3:
self.flag2=0 # обнуляем сообщение о описании
if tag=="td":
if self.flag==1: # если сейчас было 'время'
self.flag=2 # то сообщаем о том, что оно кончилось
elif self.flag==3: # если было название
self.result.append(self.buffer) # до добавляем буфер к результату
self.buffer=[] # очищаем его
self.flag=0 # и обнуляем флаг


и

class FilmHTMLParser(HTMLParser):
""" Класс парсера для описаний, создаём на основе HTMLParser """
content=0
flag=0
result=""
def handle_comment(self, data):
""" Ищем комменты """
if "BEGIN: Main table" in data:
self.content=1
elif "END: Main table" in data:
self.content=0

def handle_starttag(self, tag, attrs):
""" нужный тег с нужным классом """
if self.content==1:
if tag=="p":
if len(attrs)==1:
if attrs[0][1]=="t75":
self.flag=1

def handle_data(self, data):
""" добавляем содержимое """
if self.flag==1:
self.result+=data

def handle_endtag(self, tag):
if self.content==1:
if tag=="p":
self.flag=0


Теперь скормив этим классам странички с программой или описанием будем получать только нужную информацию.
делается это вот так:
p = ChanalHTMLParser()
sock = urllib.urlopen("http://pda.tv.mail.ru/?date="+date+"&channel="+str(chanal.Num)+"&period=3")
htmlSource = sock.read()
htmlSource = unicode(htmlSource, "cp1251") # переводим страничку в юникод. Советую все данный поступающие от пользователя или ещё откуда-нибудь сразу же переводить в unicode. Меньше проблем будет.
sock.close()
p.feed(htmlSource)
print p.result


Для описаний аналогично.

Это всё конечно замечательно, но надо это всё ещё и в бд запихнуть.
Для этого нужно подключить к нашему скрипту django'вский ORM. Делается это довольно просто. А именно:
from django.core.management import setup_environ
sys.path.append("Путь до проекта") # т.к core.py у меня лежит в папке с приложением, которое обычно находится внутри папки проекта, то в место прямого пути я написал вот такую штукенцию os.path.split(os.path.dirname(os.path.abspath(__file__)))[0]
import settings
setup_environ(settings)

Ну вот, теперь мы можем использовать всю мощь Django в своём скрипте.
Подключаем из нашей недавно созданный модели классы каналы, программы и описания:

from mysite.tv.models import Chanal, Program, Description


для работы с бд я написал отдельный класс, хотя можно было обойтись и без этого:
class DataBase2:
def get_chanals(self):
""" Функция просто получает все каналы """
chanals = Chanal.objects.all()
return chanals

def add_programm(self, chanal, ptime, title):
""" Функция добавляет программу """
if len(Program.objects.filter(Title=title, Time=ptime, Chanal=chanal))==0: # проверяем нет ли уже такой в бд
p = Program(Title=title, Time=ptime, Chanal=chanal)
p.save()
return p
else:
return None

def add_description(self, programm, content):
""" Функция добавляет описание """
if len(Description.objects.filter(Text=content, Program=programm))==0: # опять проверка на клонов
Description(Text=content, Program=programm).save()


Вобщем-то всё. Осталось написать простой цикл обхода всех страниц и скрипт готов.

Взять его как всегда можно из моего svn:
svn checkout http://tvwatcher.googlecode.com/svn/tags/post2/ tvwatcher

Запускаем django сервер, заходим в админку, добавляем какой-нибудь канал, указывая в Num номер канала на мэйле. Запускаем core.py с ключом w, снова идём в админку и видим, что добавились программы и описания этого канала на неделю.