Уселся позавчера на ночь почитать спецификацию Pingback (а что, у всех свои развлечения :-) ). А она оказалось такой маленькой, простой и понятной, что я как-то сразу и написал все за пару часов, попивая чаек (какой-то из дешевых юннаньских красных). Не откладывая, спешу поделиться кодом и соображениями.
Да, и кстати... Пользуясь случаем хочу повториться, что Pingback — вещь, и он заслуживает того, чтобы быть распространенным протоколом межсайтового уведомления, а не прозябать, как сейчас, реализованным только в WordPress по большому счету. Так что, уважаемые программисты, отройте где-нибудь пару-тройку часов времени и реализуйте клиент или сервер или и то, и другое!
Bzr-репозиторий | http://softwaremaniacs.org/code/cicero/ |
---|---|
Работающий форум | http://softwaremaniacs.org/forum/ |
Посмотреть, как это работает, можно, написав в форум пост со ссылкой на свой блог (не надо спамить чужие блоги тестовыми пингами!), если он поддерживает Pingback.
Реализация
Алгоритм pingback'а очень простой:
- при создании новой статьи из ее текста вынимаются ссылки
- из них отбираются те, которые ведут вовне форума
- содержимое ссылки загружается, и в заголовках или теле ответа ищется адрес pingback-сервера (другими словами, выясняется, поддерживает ли софт на том сервере pingback)
- у pingback-сервера через XML-RPC вызывается определенный метод, уведомляющий его о том, что в форумной статье такой-то содержится ссылка на его такую-то страницу (это самая простая часть, как будет видно дальше)
В коде я все это запихнул в один метод у Article. Он вышел чуть пухловатым, но зато он прост по сути и, надеюсь, читаем:
def ping_external_links(self):
# Пара временных переменных:
# - URL к индексу форума, чтобы определять внешние ссылки
# - абсолютный URL страницы топика этой статьи
from django.contrib.sites.models import Site
domain = Site.objects.get_current().domain
from django.core.urlresolvers import reverse
index_url = reverse('cicero_index')
topic_url = 'http://%s%s' % (domain, reverse('cicero.views.topic', args=(self.topic.forum.slug, self.topic.id)))
# определение внешней ссылки: должна быть либо с другого домена,
# либо по крайней мере не внутри форума
def is_external(url):
from urlparse import urlsplit
scheme, server, path, query, fragment = urlsplit(url)
return server != '' and \
(server != domain or not path.startswith(index_url))
# поиск ссылки на pingback-сервер внутри содержимого внешней страницы
# по регулярному выражению, как прописано в спецификации
def search_link(content):
match = re.search(r'<link rel="pingback" href="([^"]+)" ?/?>', content)
return match and match.group(1)
# парсинг HTML статьи и отбор внешних ссылок
from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup(self.html())
links = [a['href'] for a in soup.findAll('a') if is_external(a['href'])]
# обход всех ссылок с попыткой их пинговать
from xmlrpclib import ServerProxy, Fault
from urllib2 import urlopen
for link in links:
try:
f = urlopen(link)
# определение ссылки на pingback-сервер
# из HTTP-заголовков или первых 500 КБ страницы
server_url = f.info().get('X-Pingback', '') or search_link(f.read(512 * 1024))
if server_url:
# если ссылка есть, вызываем XML-RPC процедуру
# со ссылками откуда и куда делается пинг
server = ServerProxy(server_url)
server.pingback.ping(topic_url, link)
except (IOError, Fault):
pass
f.close()
Тут, знаете, хочется сказать большое спасибо Питону и его библиотекам, как стандартным, так и сторонним. Разложение URL'а на части, регулярные выражения, парсинг HTML, HTTP-запрос с анализом заголовков и собственно XML-RPC вызов делаются простыми и, главное, очевидными средствами. Это, как мне кажется, очень большая составляющая того, почему разработка на Питоне оказывается быстрой.
Мелкая засада с транзакциями
Pingback работает так, что когда внешний сервер получает пинг, он идет обратно на мой сервер и забирает текст, из которого я ссылаюсь на него, чтобы проверить, что ссылка там в тексте действительно есть. Этот пинг я делаю во view сразу после сохранения статьи в базу:
if form.is_valid():
article = form.save()
article.ping_external_links()
return HttpResponseRedirect(...)
Так вот здесь во время вызова пинга внешний сервер не увидит у меня на сайте этой самой новой статьи. Потому что при включенном автоматическом управлении транзакциями они коммитятся только после завершения работы всей view.
Обошел я проблему просто: вручную коммичу транзакцию прямо перед ping_external_links. То, что коммит происходит до завершения view, в этом случае совершенно безопасно с точки зрения целостности данных, потому что после этого момента они больше не меняются.
Всякие страхи
Я думаю, что эта реализация, в общем-то, неплохая, и скорее всего на практике будет счастливо работать. Но есть пара вещей, которые меня немножко беспокоят.
Вся пинговая механика делается синхронно и, соответственно, блокирует выполнение запроса пользователя. А значит, если пользователь поставит ссылку на какой-нибудь дохлый сервер, то сохранение его статьи будет долго-долго висеть, дожидаясь таймаута. В принципе, данные к тому времени уже сохранены, но все равно неприятно. Как решать — пока не знаю. Решение, например, с отфоркиванием пингующего процесса кажется чересчур сложными для такой простой вещи.
Другая боязнь заключается в том, что какой-нибудь негодяй может запостить на форум сообщение с парой тысяч ссылок и заставить таким образом мой сервер нагружать тот, на который идут ссылки. Но этого я боюсь меньше, потому что во-первых это все же не DOS-атака, потому что пингер запросы обрабатывает, а не бросает. А во-вторых я надеюсь, что антиспамные средства на сервере сведут вероятность подобного вандализма к минимуму.
Про сервер
Напоследок пару слов о реализации серверной части протокола.
В общем-то, я и не собирался его делать с самого начала. Тех, кто не помнит почему, процитирую свой комментарий на этот счет:
«сам форум не будет регистрировать пинги извне» — может стоит оставить эту фичу? Ведь на топики в форумах, особенно полезные, часто ссылаются.
Проблемы две: как их там показывать, и главное, кто их там будет читать? По опыту большинства пингов к комментариям этого блога, я вижу, что это скорее интересный мне как автору эго-бустер “вот на меня поставили ссылку”. В форумном же топике толпа ссылок с разных сайтов “вот глянь, тут есть решение”, мне кажется, не будет никому интересна… Нет?
Однако если кто-то писал (или собирается) на Джанго блог, а не форум, и реализовал серверную часть (или собирается), очень было бы интересно посмотреть. Там должен быть интересный момент с тем, как, найдя пришедшую ссылку на исходной странице, вытащить контекст, в котором она написана. WordPress, вроде бы, справляется, но наверняка можно лучше...
Комментарии: 21
А если вытаскивать ссылки из сообщений, которые нужно пингануть, и складывать их в какую-нибудь очередь. А запускающийся раз в n времени отдельный скрипт будет их уже пинговать. Тут же вроде не так уж важна орперативность пинга.
Заодно можно смотреть чтоб не было много ссылок на один и тот же ресурс и еще что-нибудь. а?
Иван, у меня вызывает сомнение следующий фрагмент кода:
При возникновении незапланированной ошибки указатель на файл не будет закрыт. Вот пример кода, в котором такая обработка ошибок не сработает:
Следует ставить закрытие указателя в блок finally, либо (предпочтительнее) использовать
with
:Дотошный какой :-)
На самом деле, f все таки закроется. При непойманном Exception'е функция завершиться, и f рано или поздно попадет под нож сборщика мусора и при уничтожении закроет сокет (если я ничего не путаю). Да, тут нельзя будет рассчитывать на то, что это произойдет точно после ошибки, но это и не нужно: когда закроется, тогда закроется.
А у finally и with есть два больших "но": первый в Питоне 2.4 заставить делать еще один try..except вокруг, что выглядит уродливо, а второго в 2.4 просто нет.
Кстати, в 2.5 Питоне блок с with выглядел бы немного не так:
Кстати, пинговалку лучше наверно запускать через сигналы (http://www.djangoproject.com/documentation/db-api/#what-happens-when-you-save) на post_save...
Ну вообще главным объектом критики в подобных штуках всегда были не технические моменты, а невероятные кол-ва спама, которые через него посылают.
Вот сделаю блог про виагру и оставлю в посте пару сотен линков, пропингую, а как появятся беки - уберу. Как этот момент решается?
Я у себя в блоге сделал простую реализацию серверной части, но пока она тупо скачивает ссылающийся на меня документ, ищет там ссылку на мой сайт. Все теги из текста вырезаются. В качестве сниппета берутся 100 символов до ссылки, и 100 после, не очень красиво, надо бы выделять по предложениям.
Информация о всех пингах извне сохраняется в базе, дабы в случае чего они не дублировались. По стандарту за это сервер отвечает, а не клиент.
Можно попробовать превентивно ограничить количество обрабатываемых ссылок в одном посте до N(8, например).
f.close() на самом деле можно вообще убрать ;).
Как раз ровно в этом месте(при выходе из функции или по заходе в след цикл) он и умрет т.к. счетчик ссылок обнулится.
BeautifulSoup ОЧЕНЬ основательный тормоз. Может лучше поиск ссылок через регулярки сделать? Что-нить типа
<(a|link)[^>]*?href="(?P[^"]*?)
.Ох... Вот это классический пример переоптимизации. Есть некая "общепринятая точка зрения", что BeautifulSoup — медленный. Поэтому что, давайте теперь изничтожать его изо всех сил?
Посмотрите на код. В данном конкретном месте парсинг вызывается ровно один раз. При этом тот же процесс делает запрос в БД, делает обратный парсинг urlconf'ов, а также (примечая слона в лавке) вытягивает 500-килобайтные куски по HTTP на каждую ссылку. И каким будет выигрыш от ускорения парсинга? Подозреваю, что незаметным.
Зато проблемы от наколеночного парсинга HTML регулряками, когда то лишнее что-то попадет, то нужное порежется, обычно бывают куда более реальными.
Я это могу сказать совершенно точно, т.к. довольно
много его использовал. На некоторых страницах он просто "умирает" - тормозит сильно. В итоге на полученный сервер можно попробовать организовать dos атаку - запостить специфический кривой html.
Т.е. основная идея скорее уберечь себя от "крайних" случаев.
К тому-же код без BeautifulSoup будет меньше
по размеру )).
Это возможно, но конкретно эта приведенная регулярка мне служит "верой и правдой" уже года два.
привет.
а search_link найдет "многострочный" элемент?
Не найдет. Но это намеренно :-).
Спецификация pingback'а диктует точный досимвольный вид этой ссылки (точнее, два точных варианта). То есть это более жесткое требование, чем у HTML или XHTML. Этим как раз достигается то, что формат во-первых своместим с (X)HTML, а во-вторых не требует от реализатора использовать полный HTML-парсер, если он не хочет.
http://www.hixie.ch/specs/pingback/pingback#TOC2.2 (там в конце главки, зеленым текстом).
Честно говоря, не понимаю, зачем оно вообще нужно?
На данный момент оно нужно для связи блогосферы и форумосферы. Pingback — это клей для кусков информации в WWW. А вообще, Иван прямо призывает встраивать сию полезную интерфейсную фитчу и в другие системы ;)
Как-то слишком жёстко pingback приклеен к форуму. Может быть, лучше выделить его в отдельный модуль, используя для связки систему сигналов Django (как подметил dobrych) ?
Кстати, как вообще будет реализована система подключаемых расширений?
Я это делаю через файл signals.py в корне приложения, в котором объявляется owner (приложение) и названия сигналов, через signal_name = object()
В результате приложению, использующему ресурсы другого, достаточно импортировать нужные сигналы и имя их отправителя.
Тот же signals.py могут использовать и части самого app, для упрощения логики взаимодействия.
Я просто не думал, что такой простой код стоит как-то отдельно оформлять. Если его выдирать отдельно, придется много переделывать. Например непонятно, как понимать, что такое "внешняя ссылка". Тут-то я в курсе, что все это относится к приложению форума, а если pingback будет отдельным, то ему надо как-то это сообщать. Настроек будет больше, чем кода :-)
Я ничего такого не планировал :-). Я пишу маленький форум такой, как мне нравится. Методика его расширения — скачать код и поправить :-)
другая имплементация:
http://geeksite.googlecode.com/svn/trunk/apps/blog/pingback.py
http://geeksite.googlecode.com/svn/trunk/apps/blog/models.py
Кстати, Михаил, я смотрел в нее перед тем, как свою делать. Не взял, правда, но подтвердил несколько догадок, спасибо :-)
вот ещё одна реализация пинга http://www.imalm.com/blog/2007/feb/10/using-django-signals-ping-sites-update/
Только это не pingback :-). Это нотификация поисковиков/агрегаторов, а не других блогов о ссылках на них.