Веб-разработка Пишем и компилируем консольный HtmlUnit-браузер для помощи Google в индексации javascript-контента

Современные веб-сайты — это уже не просто набор страничек с гипертекстовой разметкой. Последние тенденции в веб-разработке приводят к тому, что сайты превращаются в сложные, большие javascript-приложения, по сути состоящие из одной html-страницы и подгружающие контент с помощью ajax-запросов к серверу. В связи с этим большую популярность приобретают js-библиотеки типа Backbone.js, дающие удобные инструменты для проектирования и написания таких веб-приложений и сервисов. Пожалуй самый яркий пример среди подобных сайтов — twitter.com. Среди плюсов такого подхода к разработке сайтов — скорость их работы, снижение траффика, перенос значительной части логики приложения с сервера на клиента, что приводит к снижению нагрузки на сервер. Однако среди всех очевидных плюсов есть небольшая ложка дегтя — невозможность индексации подобного веб-сайта, по сути состоящего из одной страницы…

Но эта неприятность поправима, потому что Google может проиндексировать динамический сайт с AJAX при условии, если каждая динамическая страница будет содержать свою статичную копию. Подробнее об этом можно прочитать здесь. Суть технологии AJAX Applications Crawlable состоит в том, что поисковый робот Google по-особому обрабатывает комбинацию символов «#!» в URL. Такой хештег (например, http://twitter.com/#!/andygoalkeeper) говорит поисковому роботу о том, что контент по этому URL формируется динамически с помощью javascript, поэтому нет смысла напрямую запрашивать такой URL. Вместо этого поисковый робот делает запрос на похожий URL, котором символы «#!» заменены на «?_escaped_fragment_=» (например, http://twitter.com/?_escaped_fragment_=/andygoalkeeper). В этот момент сервер должен вернуть статичную html-версию страницы, которую поисковый робот сможет проиндексировать и сделать вид, что получил эти данные с URL http://twitter.com/#!/andygoalkeeper. Вроде бы все легко и просто, но тут и возникает множество вопросов по тому как настроить сервис, который будет автоматически возвращать html-код страниц, которые формируются динамически. Потому что делать такие статичные страницы вручную — совсем плохой вариант.

Первым делом на ум приходит написание простенького скрипта на PHP, который будет выполнять некоторую консольную команду и заставлять некий консольный headless-браузер возвращать HTML-код для заданного URL. Однако наиболее простые консольные браузеры типа wget и lynx не поддерживают javascript, поэтому их использование абсолютно бессмысленно. Сам Google в такой ситуации, когда большинство контента на сайте формируется с помощью javascript, предлагает использовать HtmlUnit. HtmlUnit — это набор библиотек, написанных на Java, основное назначение которых — «GUI-Less browser for Java programs», т.е. браузер без GUI-интерфейса для программ, написанных на Java. В общем, все это означает, что HtmlUnit — это не просто программа, которую можно скачать, установить и использовать. Это библиотека, с помощью которой нужно написать простое Java-приложение под свои нужды.

Легко сказать — написать Java-приложение разработчику, который верстал сайты на HTML и CSS, писал много чего на PHP, JavaScript, немного на C# и Delphi, но никак не на Java. Официальный сайт HtmlUnit изобилует примерами по написанию кода, но прежде, чем писать код, нужно понять с помощью чего его написать и как откомпилировать, чтобы затем можно было бы использовать headless-браузер из консоли Linux. В общем, более-менее внятной статьи на эту тему я не нашел, это и стало поводом для написания собственной. Поэтому далее в статье вы найдете пошаговую инструкции по тому как написать свой простой headless-браузер на Java с использованием библиотеки HtmlUnit. В качестве среды разработки используем Ubuntu 11.10.

С Java-разработкой на Ubuntu дела обстоят неплохо. Достаточно установить пакет с Eclipse из репозитория, чтобы получить полноценную среду разработки. Автоматически скачаются и установятся все необходимые Java-пакеты.

sudo aptitude install eclipse

В Ubuntu 11.10 установится Eclipse 3.7. При первом запуске Eclipse попросит указать каталог, где по умолчанию будут храниться проекты (например, /home/user/workspace). После запуска Eclipse выбираем в верхнем меню File -> New -> Java Project. В диалоговом окне создания проекта указываем его имя (например, htmlunit), убеждаемся, что выбрано окружение JRE JavaSE-1.6 и жмем кнопку «Finish». Теперь в каталоге /home/user/workspace/htmlunit у нас находится новый проект для нашего будущего headless-браузера.

Далее идем на официальный сайт HtmlUnit и скачиваем последнюю версию библиотеки. На момент написания статьи это была версия 2.9. Распаковываем архив с HtmlUnit куда-нибудь, например, в каталог с нашим проектом: /home/user/workspace/htmlunit/src/lib. Затем нужно включить библиотеки HtmlUnit в наш проект. Для этого в верхнем меню Eclipse выбираем Project -> Properties. В открывшемся диалоговом окне свойств проекта выбираем раздел Java Build Path, затем кликаем на вкладку Libraries и с помощью кнопки «Add External JARs» добавляем к библиотеке JRE System Library [java-6-openjdk] библиотеки HtmlUnit.

Теперь у нас есть полностью настроенная среда разработки Java-приложений с библиотеками HtmlUnit, и мы можем приступить непосредственно к написанию кода. Для этого создадим главный и единственный файл класса в нашем проекте. Выбираем в главном меню File -> New -> Class и в диалоговом окне указываем название (Name) нашего нового класса (например, htmlunit), после чего Eclipse создаст новый файл /home/user/workspace/htmlunit/src/htmlunit.java.

После этого остается только погрузиться в примеры программ на официальном сайте HtmlUnit и написать приложение, соответствующее нашим требованиям. Поэтому консольное приложение для автоматической генерации статичных html-страниц (snapshots) должно отвечать следующим требованиям:

1. Принимать URL как параметр командной строки
2. Корректно выполнять javascript подобно современным браузерам
3. Дожидаться выполнения ajax-запросов на страницах

В результате соблюдения этих требований у меня получился следующий код для самого простого headless-браузера на HtmlUnit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.logging.Level;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
 
public class htmlunit {
    public static void main(String[] args) throws Exception {
        // Отключаем отображение ошибок
        java.util.logging.Logger.getLogger("com.gargoylesoftware.htmlunit").setLevel(Level.OFF);
 
        // Если есть URL, то запрашиваем снимок страницы (вместе с контентом, генерируемым ajax)
        if (args.length > 0) {
            // Используем движок Firefox 3.6 (он совместим с большинством js-библиотек)
            WebClient webClient = new WebClient(BrowserVersion.FIREFOX_3_6);
            // Ждем, пока отработают ajax-запросы, выполняемые при загрузке страницы
            webClient.setAjaxController(new NicelyResynchronizingAjaxController());
            // Запрашиваем и рендерим веб-страницу
            HtmlPage page = webClient.getPage(args[0]);
            // Выводим исходный код страницы в консоль
            System.out.println(page.asXml());
            // Закрываем headless-браузер, освобождаем память
            webClient.closeAllWindows();
        }
    }
}

Пара слов о том, почему код написан именно так. Во-первых, по умолчанию HtmlUnit выводит кучу ошибок и предупреждений для CSS, и эти ошибки никак нельзя отдавать поисковому роботу — следовательно, нужно ошибки скрыть. Во-вторых, чтобы javascript корректно отработал, лучше установить для объекта WebClient совместимость с Firefox 3.6, потому что в противном случае HtmlUnit не может корректно обработать даже jQuery. В-третьих, крайне важно, чтобы headless-браузер автоматически определил и дождался выполнения всего javascript-кода и особенно ajax-запросов, поэтому без NicelyResynchronizingAjaxController здесь не обойтись.

Вот и мы почти и добрались до нашей цели — приложение написано, осталось его откомпилировать и получить в результате исполняемый JAR-файл, который можно будет запускать в консоли. Для этого в верхнем меню Eclipse выбираем File -> Export, в первом диалоговом окне выбираем Java -> Runnable JAR File, а во втором — в строке Export Destination указываем расположение и название JAR-файла, и в Library handling отмечаем «Package required libraries into generated JAR».

Запустить наш headless-браузер из консоли Linux довольно просто:

java -jar htmlunit.jar http://google.com

Будет запрошена страница http://google.com и ее html-код будет выведен в консоль. Осталось лишь связать PHP с консольным приложением на Java так, чтобы скрипт на PHP принимал GET-запрос с «_escaped_fragment_», делал запрос к headless-браузеру и получал в ответ html-snapshot страницы.

1
2
3
4
5
6
7
8
<?php
    if (isset($_GET['_escaped_fragment_'])) {
        $url = ' http://'.$_SERVER['SERVER_NAME'].'/'.escapeshellarg($_GET['_escaped_fragment_']);
        // Запрашиваем снимок страницы у HtmlUnit
        putenv('LANG=ru_RU.UTF-8');
        $result = shell_exec('java -jar htmlunit.jar'.$url);
        echo $result;
    }

Особое внимание обратите на строку «putenv(‘LANG=ru_RU.UTF-8′);» — без нее не получится корректно вывести в консоль текст в кодировке UTF-8, полученный от консольного приложения.

Веб-приложение, в котором понадобилось индексировать javascript-контент использует в качестве PHP-фреймворка Yii, поэтому можно написать входной скрипт index.php следующим образом, чтобы веб-приложение корректно реагировало и на запросы от обычных пользователей, и от поискового робота Google:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
    // Возвращаем страницы ajax-приложения
    if (!isset($_GET['_escaped_fragment_'])) {
        // Подключаем конфигурационный файл
        $config = dirname(__FILE__).'/protected/config/main.php';
        // Подключаем фреймворк Yii
        require_once('/usr/share/yii/framework/yii.php');
        // Создаем экземпляр объекта веб-приложения и запускаем его
        Yii::createWebApplication($config)->run();
    // Обработка Google Ajax Crawler
    } else {
        $url = ' http://'.$_SERVER['SERVER_NAME'].'/'.escapeshellarg($_GET['_escaped_fragment_']);
 
        // Запрашиваем снимок страницы у HtmlUnit
        putenv('LANG=ru_RU.UTF-8');
        $result = shell_exec('java -jar htmlunit.jar'.$url);
        echo $result;
    }

Это всего лишь простейший пример использования библиотеки HtmlUnit. У нее огромное количество возможностей. И приведенный код headless-браузера можно улучшить под свои собственные цели.

Исходный код проекта и откомпилированный JAR-файл можно скачать из репозитория на Github.