Уселся позавчера на ночь почитать спецификацию Pingback (а что, у всех свои развлечения :-) ). А она оказалось такой маленькой, простой и понятной, что я как-то сразу и написал все за пару часов, попивая чаек (какой-то из дешевых юннаньских красных). Не откладывая, спешу поделиться кодом и соображениями.
Да, и кстати... Пользуясь случаем хочу повториться, что Pingback — вещь, и он заслуживает того, чтобы быть распространенным протоколом межсайтового уведомления, а не прозябать, как сейчас, реализованным только в WordPress по большому счету. Так что, уважаемые программисты, отройте где-нибудь пару-тройку часов времени и реализуйте клиент или сервер или и то, и другое!
| Bzr-репозиторий | http://softwaremaniacs.org/code/cicero/ |
|---|---|
| Работающий форум | http://softwaremaniacs.org/forum/ |
Посмотреть, как это работает, можно, написав в форум пост со ссылкой на свой блог (не надо спамить чужие блоги тестовыми пингами!), если он поддерживает Pingback.
Алгоритм pingback'а очень простой:
В коде я все это запихнул в один метод у 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
7.08.07 00:15
7.08.07 00:15
А если вытаскивать ссылки из сообщений, которые нужно пингануть, и складывать их в какую-нибудь очередь. А запускающийся раз в n времени отдельный скрипт будет их уже пинговать. Тут же вроде не так уж важна орперативность пинга.
Заодно можно смотреть чтоб не было много ссылок на один и тот же ресурс и еще что-нибудь. а?
7.08.07 01:06
Иван, у меня вызывает сомнение следующий фрагмент кода:
При возникновении незапланированной ошибки указатель на файл не будет закрыт. Вот пример кода, в котором такая обработка ошибок не сработает:
Следует ставить закрытие указателя в блок finally, либо (предпочтительнее) использовать
with:7.08.07 02:41
Дотошный какой :-)
На самом деле, f все таки закроется. При непойманном Exception'е функция завершиться, и f рано или поздно попадет под нож сборщика мусора и при уничтожении закроет сокет (если я ничего не путаю). Да, тут нельзя будет рассчитывать на то, что это произойдет точно после ошибки, но это и не нужно: когда закроется, тогда закроется.
А у finally и with есть два больших "но": первый в Питоне 2.4 заставить делать еще один try..except вокруг, что выглядит уродливо, а второго в 2.4 просто нет.
Кстати, в 2.5 Питоне блок с with выглядел бы немного не так:
7.08.07 03:46
Кстати, пинговалку лучше наверно запускать через сигналы (http://www.djangoproject.com/documentation/db-api/#what-happens-when-you-save) на post_save...
7.08.07 10:48
Ну вообще главным объектом критики в подобных штуках всегда были не технические моменты, а невероятные кол-ва спама, которые через него посылают.
Вот сделаю блог про виагру и оставлю в посте пару сотен линков, пропингую, а как появятся беки - уберу. Как этот момент решается?
7.08.07 10:55
Я у себя в блоге сделал простую реализацию серверной части, но пока она тупо скачивает ссылающийся на меня документ, ищет там ссылку на мой сайт. Все теги из текста вырезаются. В качестве сниппета берутся 100 символов до ссылки, и 100 после, не очень красиво, надо бы выделять по предложениям.
Информация о всех пингах извне сохраняется в базе, дабы в случае чего они не дублировались. По стандарту за это сервер отвечает, а не клиент.
7.08.07 11:11
Можно попробовать превентивно ограничить количество обрабатываемых ссылок в одном посте до N(8, например).
f.close() на самом деле можно вообще убрать ;).
Как раз ровно в этом месте(при выходе из функции или по заходе в след цикл) он и умрет т.к. счетчик ссылок обнулится.
BeautifulSoup ОЧЕНЬ основательный тормоз. Может лучше поиск ссылок через регулярки сделать? Что-нить типа
<(a|link)[^>]*?href="(?P[^"]*?).7.08.07 11:23
Ох... Вот это классический пример переоптимизации. Есть некая "общепринятая точка зрения", что BeautifulSoup — медленный. Поэтому что, давайте теперь изничтожать его изо всех сил?
Посмотрите на код. В данном конкретном месте парсинг вызывается ровно один раз. При этом тот же процесс делает запрос в БД, делает обратный парсинг urlconf'ов, а также (примечая слона в лавке) вытягивает 500-килобайтные куски по HTTP на каждую ссылку. И каким будет выигрыш от ускорения парсинга? Подозреваю, что незаметным.
Зато проблемы от наколеночного парсинга HTML регулряками, когда то лишнее что-то попадет, то нужное порежется, обычно бывают куда более реальными.
7.08.07 11:44
Я это могу сказать совершенно точно, т.к. довольно
много его использовал. На некоторых страницах он просто "умирает" - тормозит сильно. В итоге на полученный сервер можно попробовать организовать dos атаку - запостить специфический кривой html.
Т.е. основная идея скорее уберечь себя от "крайних" случаев.
К тому-же код без BeautifulSoup будет меньше
по размеру )).
Это возможно, но конкретно эта приведенная регулярка мне служит "верой и правдой" уже года два.
7.08.07 12:50
привет.
а search_link найдет "многострочный" элемент?
7.08.07 12:59
Не найдет. Но это намеренно :-).
Спецификация pingback'а диктует точный досимвольный вид этой ссылки (точнее, два точных варианта). То есть это более жесткое требование, чем у HTML или XHTML. Этим как раз достигается то, что формат во-первых своместим с (X)HTML, а во-вторых не требует от реализатора использовать полный HTML-парсер, если он не хочет.
http://www.hixie.ch/specs/pingback/pingback#TOC2.2 (там в конце главки, зеленым текстом).
7.08.07 13:12
Честно говоря, не понимаю, зачем оно вообще нужно?
10.08.07 14:02
На данный момент оно нужно для связи блогосферы и форумосферы. Pingback — это клей для кусков информации в WWW. А вообще, Иван прямо призывает встраивать сию полезную интерфейсную фитчу и в другие системы ;)
10.08.07 14:14
Как-то слишком жёстко pingback приклеен к форуму. Может быть, лучше выделить его в отдельный модуль, используя для связки систему сигналов Django (как подметил dobrych) ?
Кстати, как вообще будет реализована система подключаемых расширений?
Я это делаю через файл signals.py в корне приложения, в котором объявляется owner (приложение) и названия сигналов, через signal_name = object()
В результате приложению, использующему ресурсы другого, достаточно импортировать нужные сигналы и имя их отправителя.
Тот же signals.py могут использовать и части самого app, для упрощения логики взаимодействия.
10.08.07 15:25
Я просто не думал, что такой простой код стоит как-то отдельно оформлять. Если его выдирать отдельно, придется много переделывать. Например непонятно, как понимать, что такое "внешняя ссылка". Тут-то я в курсе, что все это относится к приложению форума, а если pingback будет отдельным, то ему надо как-то это сообщать. Настроек будет больше, чем кода :-)
Я ничего такого не планировал :-). Я пишу маленький форум такой, как мне нравится. Методика его расширения — скачать код и поправить :-)
10.08.07 17:06
другая имплементация:
http://geeksite.googlecode.com/svn/trunk/apps/blog/pingback.py
http://geeksite.googlecode.com/svn/trunk/apps/blog/models.py
10.08.07 17:55
Кстати, Михаил, я смотрел в нее перед тем, как свою делать. Не взял, правда, но подтвердил несколько догадок, спасибо :-)
21.08.07 02:20
24.08.07 22:54
вот ещё одна реализация пинга http://www.imalm.com/blog/2007/feb/10/using-django-signals-ping-sites-update/
24.08.07 23:22
Только это не pingback :-). Это нотификация поисковиков/агрегаторов, а не других блогов о ссылках на них.