Программная отправка комментариев в livejournal
Дата публикации: 03.01.2010
Дата последней правки: 09.02.2023
Содержание
Введение
Несмотря на то, что сайт livejournal.com (дальше будем назвать его просто ЖЖ) предоставляет API для написания постов, про написания комментариев в документации ничего не сказано. А ведь есть задачи, для решения которых хотелось бы иметь и такую возможность. Я не говорю про спам, но ведь есть еще боты, которые должны уведомлять пользователей, что они попали в очередной ТОП-666, да и для создания каких-нибудь клиентов тоже не мешало бы уметь отправлять комментарии. Эта статья как раз и описывает способ, с помощью которого ваша программа сможет что-нибудь отправить в качестве комментария.
Статья, которую вы читаете, является продолжением другой статьи - Еще раз про авторизацию на сервере livejournal.com, из которой будет позаимствован некоторый код, поэтому начинать чтение лучше с нее.
Немного теории
Как уже говорилось, сервер livejournal не предоставляет API для работы с комментарием, поэтому для отправки комментариев нам придется эмулировать работу браузера, как будто бы пользователь вводит текст сообщения через форму на сайте. Сделать это не так сложно, как может показаться на первый взгляд, анализируя переданные браузером данные.
Отправку комментариев можно разделить на три этапа:
- Авторизация на сайте livejournal.
- Загрузка страницы с формой для отправки комментария.
- Собственно, отправка комментария, эмуляруя отправку формы.
Первый этап мы подробно разбирали в предыдущей статье, поэтому только коротко напомню, что нельзя путать авторизацию с помощью API для отправки сообщений и авторизацию как пользователь ЖЖ для чтения подзамочных записей или написания комментариев. В данном случае, как вы понимаете, нам нужна именно второй тип авторизации. Результатом работы первого этапа является получение cookie, которые затем будут прикрепляться к запросу, подтверждая авторизацию.
Второй этап самый простой. Здесь нам достаточно просто получить код HTML-страницы с постом, куда мы собираемся отправить комментарий. Это нужно для того, чтобы получить из HTML-кода формы отправки комментариев одну строку - значение скрытого поля lj_form_auth, которую затем надо будет передать в запрос на третьем этапе.
Третий этап состоит в том, чтобы сформировать POST-запрос по адресу http://www.livejournal.com/talkpost_do.bml, передав туда всю необходимую информацию. Браузер, отправляя комментарии, передает довольно много переменных, но на некоторые из них можно пока не обращать внимания. Заодно надо сразу сказать, что мы не будем менять юзерпик пользователя, обойдемся пока картинкой по умолчанию. Запрос в этом случае должен содержать следующие параметры:
- usertype=cookieuser обозначает, что мы уже авторизовались и имеем cookie для дальнейшего подтверждения авторизации.
- subject - заголовок комментария.
- body - текст комментария.
- lj_form_auth - строка, которую мы получили на предыдущем этапе из HTML-страницы.
- cookieuser - имя пользователя, которым мы авторизовались.
- journal - имя пользователя или сообщества, в чей журнал мы отправляем комментарий.
- itemid - целочисленный идентификатор поста. Что это за идентификатор, будет написано ниже.
- parenttalkid - Номер комментария, на который мы хотим ответить. Если наш комментарий должен относиться к посту в целом, это значение должно быть равно 0. Что это за номер и где его взять мы поговорим чуть позже.
Реализация на C#
Для наглядности дальнейшее описание будем сопровождать написанием кода на языке C#. Скачать исходники и скомпилированную программу можно отсюда.
В результате мы напишем программу-бота, которая будет уметь не только писать комментарии к постам, но и отвечать на другие комментарии. Назначение полей ввода, думаю, понятно из скриншота. Единственное, о чем нужно сказать, это поле "Номер родительского комментария". Именно сюда нужно вводить целочисленный идентификатор для комментария, на который надо ответить. Если нужно написать ответ в сам пост, то поле нужно оставить пустым или ввести туда значение 0.
Для простоты и наглядности программа будет работать в одном потоке (в смысле thread), в реальных задачах отправку запросов лучше выделать в отдельный поток. Для лучшего понимания процесса обработки запросов используется класс лога, который выводит в процессе работы тексты запросов и ответов, а также промежуточную информацию, с помощью которой можно быстро понять как происходит процесс отправки комментария, а заодно проследить возможные ошибки.
Используемые классы
В начале рассмотрим общую структуру программы с основными классами.
Для вывода сообщений в лог мы будем использовать класс TextLog, реализующий интерфейс ILog. Здесь нет ничего особо примечательного, и его содержимое не относится к теме поста, поэтому останавливаться на нем не будем.
Все самое интересное будет содержаться в классе LJServer. Если вы помните первую статью про работу с сервером ЖЖ, то там был одноименный абстрактный класс, от которого производились классы для работы с сервером по разным протоколам (Flat и XML-RPC). В данном случае класс для работы с сервером у нас один, и из него выкинуто все, что не используется при отправке комментариев. Содержимое этого класса как раз и будет основой для дальнейшего повествования.
С точки зрения класса формы главного окна все самое интересное будет происходить внутри обработчика кнопки "Пуск!". Отправка комментария выглядит следующим образом:
LJServer server = new LJServer (_log);
server.PostComment (login, password, url, message, subj, parentId);
Или полностью с проверками на возможные исключения и ошибки ввода:
{
public partial class mainForm : Form
{
public mainForm ()
{
InitializeComponent ();
_log = new TextLog (logTextBox);
}
TextLog _log;
private void sendButton_Click (object sender, EventArgs e)
{
// Прочитаем введенные параметры
string url = urlTextBox.Text;
string login = loginTextBox.Text;
string password = passwordTextBox.Text;
string message = commentTextBox.Text;
string subj = subjTextBox.Text;
int parentId = 0;
// Ввели ли номер родительского комментария?
if (parentTextBox.Text.Length != 0)
{
try
{
parentId = int.Parse (parentTextBox.Text);
}
catch (FormatException)
{
errorProvider.SetError (parentTextBox, "Здесь должно быть целое число");
return;
}
} // if (parentTextBox.Text.Length != 0)
// Очистим лог от предыдущих записей
_log.Clear ();
// Создадим класс для работы с сервером ЖЖ
LJServer server = new LJServer (_log);
try
{
// Отправка комментария
server.PostComment (login, password, url, message, subj, parentId);
}
catch (FormatException)
{
_log.WriteLine ("Failed");
}
} // private void sendButton_Click (object sender, EventArgs e)
} // public partial class mainForm : Form
} // namespace LJBot
Класс LJServer и отправка комментариев
Теперь, когда мы кратко упомянули вспомогательные классы, начнем разбираться с классом LJServer, внутри которого и будет происходить вся работа с сервером ЖЖ.
Метод для отправки комментария PostComment выглядит следующим образом:
/// Отправить комментарий
/// </summary>
///
/// <param name="login">Логин пользователя,
/// от имени которого нужно отправить комментарий</param>
///
/// <param name="password">Пароль пользователя</param>
///
/// <param name="url">Адрес поста, к которому нужно отправить комментарий</param>
///
/// <param name="message">Текст комментария</param>
///
/// <param name="subj">Заголовок комментария.
/// Можно передавать пустую строку</param>
///
/// <param name="parent">Идентификатор комментария, на который надо ответить.
/// Если комментарий должен относиться к посту циликом,
/// нужно этот параметр должен быть равен 0</param>
///
public void PostComment (string login,
string password,
string url,
string message,
string subj,
int parent)
{
Параметры, передаваемые методу, описаны в комментариях.
Теперь рассмотрим как реализованы все перечисленные выше этапы для отправки сообщений внутри метода PostComment().
Подготовка
Первое, что мы сделаем - это разберем адрес поста, куда мы хотим отправить комментарий, на составные части. Как вы, наверное, знаете, адрес поста может быть записан в следующих форматах:
http://USERNAME.livejournal.com/NNNNN.html
http://user'''s'''.livejournal.com/USERNAME/NNNNN.html
http://user.livejournal.com/USERNAME/NNNNN.html
http://community.livejournal.com/COMMUNITYNAME/NNNNN.html
Здесь USERNAME - ник пользователя, а NNNNN - целочисленный номер записи. Эти данные мы и должны узнать на данном этапе.
Второй и третий формат ссылок встречаются редко. Второй вариант можно увидеть у пользователей, ники которых начинаются с символа подчеркивания, а третий вариант я в "дикой природе" не встречал ни разу. Если ник пользователя не начинается с символа подчеркивания, то при попытке зайти в его ЖЖ через второй или третий вариант записи, мы будем перенаправлены по адресу, написанному первым способом.
На самом деле есть еще один тип постов - трансляции в ЖЖ, тогда ссылки имеют вид http://syndicated.livejournal.com/NAME/NNNNN.html, но в данной статье мы их рассматривать не будем.
Более того, если в нике пользователя есть дефисы, то их надо обязательно заменить на подчеркивания, иначе при попытке открыть такую ссылку вторым или третьим способом мы получим 404-ю ошибку.
Для получения интересующих нас данных из ссылки был создан класс UrlInfo, который с помощью регулярных выражений находит все, что нас интересует, а заодно помечает относится ли ссылка к сообществу или нет. Последнее в данный момент нас интересовать не будет, это сделано исключительно для универсальности класса, чтобы его можно было использовать в других программах.
Класс UrlInfo для разбора адреса выглядит следующим образом:
/// Класс для разбора адреса вроде http://jenyay-test.livejournal.com/21741.html на составляющие:
/// имя пользователя, номер поста, сообщество или нет.
/// </summary>
class UrlInfo
{
/// <summary>
/// Имя пользователя или сообщества
/// </summary>
public readonly string Journal;
/// <summary>
/// Номер поста
/// </summary>
public readonly int Id;
/// <summary>
/// Это сообщество?
/// </summary>
public readonly bool IsCommunity;
/// <summary>
/// Исходная ссылка
/// </summary>
public readonly string Url;
public UrlInfo (string url)
{
// На всякий случай сохраним строку с адресом
Url = url;
// Сюда будем записывать регулярное выражения в зависимости от того,
// с какой подстроки начинается адрес
string urlRe;
if (url.StartsWith ("http://community.livejournal.com"))
{
// Ссылка на сообщество
IsCommunity = true;
urlRe = @"http://community.livejournal.com/(?<name>.*)/(?<id>\d+)\.html";
}
else if (url.StartsWith ("http://users.livejournal.com") ||
url.StartsWith ("http://user.livejournal.com"))
{
// Ссылка на пользователя в "экзотическом" формате
IsCommunity = false;
urlRe = @"http://users?.livejournal.com/(?<name>.*)/(?<id>\d+)\.html";
}
else
{
// Ссылка на пользователя в приваычном формате
IsCommunity = false;
urlRe = @"http://(?<name>.*).livejournal.com/(?<id>\d+)\.html";
}
// Найдем имя пользователя (или сообщества) и номер записи
Match match = Regex.Match (url, urlRe, RegexOptions.IgnoreCase);
// Проверим все ли было найдено с помощью регулярного выражения
if (!match.Success)
{
throw new FormatException ("Invalud URL format");
}
if (!match.Groups["name"].Success)
{
throw new FormatException ("Invalud URL format");
}
if (!match.Groups["id"].Success)
{
throw new FormatException ("Invalud URL format");
}
Journal = match.Groups["name"].Value;
Id = int.Parse (match.Groups["id"].Value);
}
}
В его конструктор передается строка адреса, а класс заполняет публичные члены только для чтения найденными данными. В случае ошибки генерируется исключение FormatException.
Использование класса UrlInfo в методе PostComment() выглядит следующим образом:
UrlInfo urlElements;
try
{
urlElements = new UrlInfo (url);
}
catch (FormatException e)
{
_log.WriteLine (e.Message);
throw;
}
_log.WriteLine ("Parsing URL...");
_log.WriteLine (string.Format ("Username: {0}", urlElements.Journal));
_log.WriteLine (string.Format ("Post ID: {0}", urlElements.Id));
Авторизация и получение страницы с формой отправки комментария
Для авторизации и получения cookies предназначен метод GetBaseCookie(), который принимает имя пользователя и пароль и возвращает полученные печеньки (cookies). Затем мы должны загрузить страницу с постом, при этом в запросе нужно использовать cookies, полученные только что. В прошлой статье мы таким образом получали доступ к подзамочным записям.
Эта часть кода полностью взята из предыдущей статьи, поэтому я не буду приводить код используемых методов GetBaseCookie() и GetPage() еще раз, а приведу сразу часть кода метода PostComment(), где используются перечисленные методы.
// Прочитаем страницу с формой отправки комментария
string text;
try
{
text = GetPage (url, cookies);
}
catch (WebException e)
{
_log.WriteLine (e.ToString());
throw new FormatException ("URL Error", e);
}
В результате в переменной text у нас будет сохранена HTML-страница с формой для отправки комментария.
Получение параметра lj_form_auth
Единственное, что нам нужно от только что полученной HTML-страницы - это значение параметра lj_form_auth, который представляет собой значение скрытого поле в форме ввода комментария. Для этого предназначен приватный метод GetLjFormAuth().
{
_log.WriteLine ("");
_log.WriteLine ("Parsing Form...");
// Найдем параметр lj_form_auth внутри формы
string authFormRe =
"(\\\\)?\"lj_form_auth(\\\\)?\" value=(\\\\)?\"(?<auth>.*?)(\\\\)?\"";
string lj_form_auth =
Regex.Match (text,
authFormRe,
RegexOptions.Multiline | RegexOptions.IgnoreCase).Groups["auth"].Value;
_log.WriteLine (string.Format ("auth: {0}", lj_form_auth));
if (lj_form_auth.Length == 0)
{
throw new FormatException ();
}
return lj_form_auth;
}
Этот метод просто ищет значение параметра lj_form_auth с помощью регулярного выражения. Вас может смутить обилие необязательных обратных слешей в регулярном выражении. Все дело в том, что форма в коде страницы может быть записана в двух вариантах:
- В явном виде, если адресной строке был передан параметр mode=reply. В этом случае код формы выглядит следующим образом:
<form method='post' action='http://www.livejournal.com/talkpost_do.bml' id='postform'><input type='hidden' name="lj_form_auth" value="c0:1262517300:2140:86400:OwDneGuaxz-3365201-197:c495a12ca4baad333f72c8f842657183" />
- В коде js-скрипта. В этом случае код формы выглядит следующим образом:
de.innerHTML = "<div id=\'qrformdiv\'><form id=\'qrform\' name=\'qrform\' method=\'POST\' action=\'http://www.livejournal.com/talkpost_do.bml\'><input type=\'hidden\' name=\"lj_form_auth\" value=\"c0:1262517300:2140:86400:OwDneGuaxz-3365201-197:c495a12ca4baad333f72c8f842657183\" />
Как видите, у нас перед кавычками могут стоять, а могут и отсутствовать обратные слеши.
В методе PostComment() выковыривание этого значения происходит одной строкой:
Напомню, что обработка исключений происходит на уровене выше, в форме главного окна, поэтому здесь возможное исключение FormatException() мы не ловим.
Запрос на отправку комментариев
И последнее, что нам осталось сделать - это сформировать запрос на отправку комментария. Запрос должен передаваться методом POST по адресу http://www.livejournal.com/talkpost_do.bml.
Все необходимые параметры мы уже перечислили выше в разделе Немного теории, тем более, что мы их уже все получили, поэтому можем без проблем составить запрос, главное - не забыть пропустить заголовок и текст комментария, а также параметр lj_form_auth через функцию HttpUtility.UrlEncode(), чтобы перекодировать некоторые символы строк в их 16-ричное представление.
Также предварительно нужно произвести замену в никах пользователей (и отправителя, и автора поста), заменив все символы дефиса на подчеркивания.
Завершающая часть кода метода PostComment() выглядит следующим образом:
string cookieuser = login.Replace ('-', '_');
string journal = urlElements.Journal.Replace ('-', '_');
// Сформируем запрос
StringBuilder sb = new StringBuilder ();
sb.AppendFormat ("usertype=cookieuser&");
sb.AppendFormat ("subject={0}&", HttpUtility.UrlEncode (subj));
sb.AppendFormat ("body={0}&", HttpUtility.UrlEncode (message));
sb.AppendFormat ("lj_form_auth={0}&", HttpUtility.UrlEncode (lj_form_auth));
sb.AppendFormat ("cookieuser={0}&", cookieuser);
sb.AppendFormat ("journal={0}&", journal);
sb.AppendFormat ("itemid={0}&", urlElements.Id);
sb.AppendFormat ("parenttalkid={0}&", parent);
string request = sb.ToString ();
// Запрос надо отправлять по этому адресу
string urlServer = "http://www.livejournal.com/talkpost_do.bml";
SendRequest (urlServer, request, cookies);
Запрос отправляется по адресу http://www.livejournal.com/talkpost_do.bml. Здесь мы используем слегка измененную функцию SendRequest() по сравнению с прошлой статьей, поэтому приведу ее код целиком:
/// Отправить запрос
/// </summary>
/// <param name="url">Адрес, по которому отправляется запрос</param>
/// <param name="textRequest">Текст запроса</param>
/// <param name="cookies">Куки, которые будут прицеплены к запросу</param>
/// <returns></returns>
private string SendRequest (string url,
string textRequest,
CookieCollection cookies)
{
// Выводим в лог посылаемый запрос
_log.WriteLine ("\r\n*** Request:");
_log.WriteLine (textRequest);
// Преобразуем запрос из строки в byte[]
byte[] byteArray = Encoding.UTF8.GetBytes (textRequest);
// Поулчаем класс запроса
HttpWebRequest request = (HttpWebRequest)WebRequest.Create (url);
// Заполняем параметры запроса
request.Credentials = CredentialCache.DefaultCredentials;
request.Method = "POST";
request.ContentLength = byteArray.Length;
request.ContentType = "application/x-www-form-urlencoded";
request.AllowAutoRedirect = false;
// Очищаем коллекцию от старых cookie и добавляем туда новые
request.CookieContainer = new CookieContainer ();
if (cookies != null)
{
request.CookieContainer.Add (cookies);
}
// Отправляем данные запроса
Stream requestStream = request.GetRequestStream ();
requestStream.Write (byteArray, 0, byteArray.Length);
// Получаем класс ответа
HttpWebResponse response = (HttpWebResponse)request.GetResponse ();
// Читаем ответ
Stream responseStream = response.GetResponseStream ();
StreamReader readStream = new StreamReader (responseStream, Encoding.UTF8);
string currResponse = readStream.ReadToEnd ();
// Выводим в лог ответ сервера
_log.WriteLine ("\r\n*** Response:");
_log.WriteLine (currResponse);
// Выведем в лог полученные в ответе cookie
_log.WriteLine ("\r\n*** Cookies:");
for (int i = 0; i < response.Cookies.Count; i++)
{
_log.WriteLine (response.Cookies[i].ToString());
}
readStream.Close ();
response.Close ();
return currResponse;
}
Полный код метода PostComment()
Теперь, когда мы разобрали по кускам отправку сообщений и куски кода, реализующие все этапы, посмотрим как выглядит метод PostComment() целиком:
/// Отправить запрос
/// </summary>
/// <param name="url">Адрес, по которому отправляется запрос</param>
/// <param name="textRequest">Текст запроса</param>
/// <param name="cookies">Куки, которые будут прицеплены к запросу</param>
/// <returns></returns>
private string SendRequest (string url,
string textRequest,
CookieCollection cookies)
{
// Выводим в лог посылаемый запрос
_log.WriteLine ("\r\n*** Request:");
_log.WriteLine (textRequest);
// Преобразуем запрос из строки в byte[]
byte[] byteArray = Encoding.UTF8.GetBytes (textRequest);
// Поулчаем класс запроса
HttpWebRequest request = (HttpWebRequest)WebRequest.Create (url);
// Заполняем параметры запроса
request.Credentials = CredentialCache.DefaultCredentials;
request.Method = "POST";
request.ContentLength = byteArray.Length;
request.ContentType = "application/x-www-form-urlencoded";
request.AllowAutoRedirect = false;
// Очищаем коллекцию от старых cookie и добавляем туда новые
request.CookieContainer = new CookieContainer ();
if (cookies != null)
{
request.CookieContainer.Add (cookies);
}
// Отправляем данные запроса
Stream requestStream = request.GetRequestStream ();
requestStream.Write (byteArray, 0, byteArray.Length);
// Получаем класс ответа
HttpWebResponse response = (HttpWebResponse)request.GetResponse ();
// Читаем ответ
Stream responseStream = response.GetResponseStream ();
StreamReader readStream = new StreamReader (responseStream, Encoding.UTF8);
string currResponse = readStream.ReadToEnd ();
// Выводим в лог ответ сервера
_log.WriteLine ("\r\n*** Response:");
_log.WriteLine (currResponse);
// Выведем в лог полученные в ответе cookie
_log.WriteLine ("\r\n*** Cookies:");
for (int i = 0; i < response.Cookies.Count; i++)
{
_log.WriteLine (response.Cookies[i].ToString());
}
readStream.Close ();
response.Close ();
return currResponse;
}
Идентификаторы комментариев
Нам осталось разобраться с последним вопросом - что это за идентификатор комментария мы должны вводить, чтобы ответить на чужой комментарий, и где этот идентификатор брать. По здравому смыслу может показаться, что это должен быть тот идентификатор, который находится в ссылке на отдельный комментарий (например, thread=43245#t43245), однако на самом деле это не он.
Нужный идентификатор спрятан в коде страницы, и чтобы его опознать, нужно найти в коде комментария строку вида:
<a onclick='return quickreply("47341", 184, "Re: ...")'...
Здесь нас должно интересовать второе число, в данном случае - число 184. Именно его мы и должны передавать в программу. В программе-примере мы не обрабатываем сами комментарии каким-либо образом, поэтому этот номер нужно искать пользователю самостоятельно к коде страницы.
Первый параметр ("47341") - это как раз и будет номер, который сначала можно принять за номер комментария. На самом деле он используется для выделение ветвей комментариев.
Третий параметр ("Re: ...") - это строка, которая будет подставлена по умолчанию в поле ввода заголовка в форму.
Но нас в данный момент первый и третий параметры не интересуют.
Заключение
Мы научились программно отправлять комментарии в посты ЖЖ, а также отвечать на другие комментарии. Я надеюсь, что эта информация окажется полезна вам, а также надеюсь, что вы не будете ее использовать для спамерства.
Кстати о спамерах (срази их геморрой). В ЖЖ все-таки встроен некоторый антиспамерский механизм, не позволяющий отправлять один и тот же комментарий в один и тот же пост несколько раз подряд. А вот отправлять один и тот же текст в разные журналы можно без проблем.
В заключении хотелось бы сказать еще несколько слов о программе, куски кода которой приводились в статье. В этой программе есть некоторые упрощения, которых лучше избегать в реальности. Про то, что запросы делать лучше всего в отдельном потоке, я уже говорил.
Другое упрощение в целях наглядности кода состоит в том, что перед отправкой каждого комментария происходит авторизация. По-хорошему, авторизацию надо производить один раз, сохранять полученные cookies, и использовать их при отправке всех комментариев.
Также в программе не проверяется удалось ли отправить комментарий или нет, проверяются только ошибки в процессе работы (например, если комментарии к посту запрещены, в этом случае не удастся найти строку lj_form_auth). Кроме того, возможна ситуация, когда для отправки комментария необходимо вводить каптчу. В данном примере эта ситуация тоже не рассматривается, будьте внимательны. Также мы не изменяли картинку пользователя.
Вот, собственно, и все, что хотелось рассказать в этой статье.
PS. Как дополнение к статье можете посмотреть реализацию бота для отправки комментариев в Живой Журнал на языке Python.
Ссылки
Другие статьи из серии про про работу с сервером ЖЖ:
- Основы работы с сервером livejournal.com?
- Еще раз про авторизацию на сервере livejournal.com
- Реализация бота для отправки комментариев в Живой Журнал на языке Python
Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.