«Правильный» html парсинг

    Логотип

    Первый раз я запустил Eclipse еще весной, почитал книжки на английском, поставил SDK, немного поигрался и забросил. В начале зимы я купил себе первый смартфон на базе Android, но вновь вернутся к разработке меня подтолкнул недавний пост, в котором говорилось, что можно обойтись и знанием C#, с которым в отличии от Java я знаком. Мне было достаточно одного вечера, чтобы понять, что за связку Visual Studio и Monodroid я больше не сяду, позже я прочитал этот пост, где полностью согласился с автором.

    После небольшого вступления перейду к теме топика. Довольно большое количество приложений под мобильные устройства взаимодействуют с сайтами и не секрет, что порой нужно получить какую-то информацию со страницы — это может быть курс валют или что-нибудь другое, и нет никакого желания делать это посредством браузера.

    Большинство разработчиков, получают html код страницы и перегоняют его в xml, что является неправильным подходом, так как html является «правильным» xml не всегда, вроде на хабре писали, что для браузера не обязателен тег html (современный браузер и без него должен отобразить страницу) или просто будут ошибки, тогда на помощь приходят библиотеки. Из них я выбрал HtmlCleaner.

    Под катом я расскажу, как подключить эту библиотеку, а также напишем простой парсер stackoverflow.com.

    Рассказывать как установить Android SDK, Eclipse и ADT Plugin я не буду, если эти слова Вам ничего не говорят, то посетите эти две ссылки:
    Installing the SDK
    ADT Plugin for Eclipse

    Главная страница stackoverflow.com выглядит следующим образом:

    Главная страница

    Парсить я буду информацию, выделенную красными прямоугольниками.

    Всё рассчитано на новичков, поэтому будет много картинок. На данном этапе у Вас должен быть полностью настроенный Eclipse, для создания проекта нажимаем File -> New -> Project… и выбираем Android Project, после чего заполним форму:

    Новый проект

    Пишу для своего устройства, поэтому выбрал версию 2.2, второй важный параметр — это package name, который должен быть уникальным, принято, что это имя сайта наоборот, плюс название приложения. Тесты создавать не будет, поэтому смело нажимаем Finish. Создался проект, рекомендую Вам изучить какие файлы и где лежат, но по своему опыту скажу, что сразу я малость испугался, того количества файлов, которое появилось при первом запуске Eclipse.

    Приступим к редактированию файла res\layout\main.xml, тут я удалю TextView и добавлю два элемента управления: Button и ListView, изменю идентификаторы, для кнопки установлю android:layout_width=«fill_parent» и android:text=«Получить данные». Готовый результат выглядит таким образом:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent">
      <Button android:id="@+id/parse"
       android:layout_height="wrap_content"
       android:layout_width="fill_parent"
        android:text="Получить данные">
      </Button>
      <ListView android:id="@+id/listViewData"
       android:layout_height="wrap_content"
       android:layout_width="match_parent">
      </ListView>
    </LinearLayout>


    * This source code was highlighted with Source Code Highlighter.


    Это простейший интерфейс, в случае, если Вы сделаете приложение и решите опубликовать его в маркете, то обязательно его нужно изменить, поставить тот же фон через android:background="@drawable/Имя_файла_без_расширения" и т.д.

    Для парсинга нам понадобиться скачать библиотеку htmlcleaner-2.2.jar, далее её следует подключить добавив в Build Paths. Хороший мануал как это сделать можно найти тут, если у Вас появились какие-то трудности.

    Прежде всего нужно указать, что нашему приложению нужен интернет, иначе у Вас ничего не выйдет, добавим в файл AndroidManifest.xml:

    <uses-permission android:name="android.permission.INTERNET"/>

    Теперь создадим класс HtmlHelper, который будет делать основную работу:

    public class HtmlHelper {
      TagNode rootNode;

      //Конструктор
      public HtmlHelper(URL htmlPage) throws IOException
      {
        //Создаём объект HtmlCleaner
        HtmlCleaner cleaner = new HtmlCleaner();
        //Загружаем html код сайта
        rootNode = cleaner.clean(htmlPage);
      }

      List<TagNode> getLinksByClass(String CSSClassname)
      {
        List<TagNode> linkList = new ArrayList<TagNode>();

        //Выбираем все ссылки
        TagNode linkElements[] = rootNode.getElementsByName("a", true);
        for (int i = 0; linkElements != null && i < linkElements.length; i++)
        {
          //получаем атрибут по имени
          String classType = linkElements[i].getAttributeByName("class");
          //если атрибут есть и он эквивалентен искомому, то добавляем в список
          if (classType != null && classType.equals(CSSClassname))
          {
            linkList.add(linkElements[i]);
          }
        }

        return linkList;
      }
    }



    В главном классе установим слушателя для кнопки и вызовем асинхронно парсинг с помощью AsyncTask, сразу я делал с помощью создания потока и потом через handler обновлял интерфейс, но прочитал, что это не лучшее решение и лучше для этих целей подходит AsyncTask, также, чтобы было видно, что приложение работает я вызову диалог, который будет информировать о процессе. Собственно главный класс выглядит следующим образом:

    public class StackParser extends Activity {
      /** Called when the activity is first created. */
      @Override
      public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        //Находим кнопку
        Button button = (Button)findViewById(R.id.parse);
        //Регистрируем onClick слушателя
        button.setOnClickListener(myListener);
      }
      
      //Диалог ожидания
      private ProgressDialog pd;
      //Слушатель OnClickListener для нашей кнопки
      private OnClickListener myListener = new OnClickListener() {
        public void onClick(View v) {
          //Показываем диалог ожидания
          pd = ProgressDialog.show(StackParser.this, "Working...", "request to server", true, false);
          //Запускаем парсинг
          new ParseSite().execute("http://www.stackoverflow.com");
        }
      };
      
      private class ParseSite extends AsyncTask<String, Void, List<String>> {
        //Фоновая операция
        protected List<String> doInBackground(String... arg) {
          List<String> output = new ArrayList<String>();
          try
          {
            HtmlHelper hh = new HtmlHelper(new URL(arg[0]));
            List<TagNode> links = hh.getLinksByClass("question-hyperlink");

            for (Iterator<TagNode> iterator = links.iterator(); iterator.hasNext();)
            {
              TagNode divElement = (TagNode) iterator.next();
              output.add(divElement.getText().toString());
            }
          }
          catch(Exception e)
          {
            e.printStackTrace();
          }
          return output;
        }

        //Событие по окончанию парсинга
        protected void onPostExecute(List<String> output) {
          //Убираем диалог загрузки
          pd.dismiss();
          //Находим ListView
          ListView listview = (ListView) findViewById(R.id.listViewData);
          //Загружаем в него результат работы doInBackground
          listview.setAdapter(new ArrayAdapter<String>(StackParser.this,
              android.R.layout.simple_list_item_1 , output));
        }
      }
    }



    Если Вы всё делали со мной, то у Вас должна была получится, следующая иерархия файлов:

    Файлы

    А после запуска приложение должно выглядеть следующим образом:

    cкриншоты приложения

    Ссылка на скачивание: приложение

    Вывод


    Мы узнали, что есть довольно много библиотек для парсинга, познакомились с одной из них, написали приложение, которое в фоне парсит сайт и при готовности показывает нам результат своей работы. В принципе, его можно развить и дальше, после чего не исключено, что оно станет популярным в определенных кругах, первое, что приходит в голову — это при клике на вопрос, открывать данный вопрос в новом окне через WebView.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 12

      0
      отлично.
        +4
        Спасибо, очень полезная библиотека!
        Единственное замечание — имена своих классов в java принято писать с большой буквы, уж очень в глаза бросается
          0
          Ок, исправимся, в посте менять уже не буду, но в будущем учту.
            0
            Хотя, пожалуй, лучше исправить, под рукой нет дропбокса, поэтому в исходниках останется как было.
          +1
          интересно, попробую!
            +1
            Я похожую задачу решал немного по другому, для парсинга тяжелых страниц написал небольшое веб приложение, которое отдает JSON на андроид, потому что тянуть на мобилку тяжелые страницы, да еще потом парсить их слишком тяжелый труд для аккума ) в учебных целях то оно конечно все равно, но в реальном приложении мне кажется это уже играет существенную роль
              0
              Не так уж и много страницы весят, основной объем всё равно приходится на CSS, скрипты и графику. А голый HTML весит мало, даже на здоровых страницах. Парсинг на серверной стороне имеет смысл, если результаты обработки сервером одной страницы будут отдаваться множеству клиентов. Ну или хотя бы если эта самая «серверная сторона» вообще имеется в наличии.
              0
              никакого желания делать это по средствам браузера

              Пожалуйста, прочитайте и исправьте.
                0
                Спасибо, мне ещё в семь утра сообщили об ошибке в личных сообщениях, исправить смог только сейчас.
                0
                Я бы еще упомянул JSoup. Она быстрее htmlcleaner раза в 2, по крайней мере в JavaSE.
                Ее плюсы в относительной простоте кода и удобных CSS-подобных селекторах.
                  +1
                  Все таки использовать XPath было бы логичнее, чем обходить все ссылки. Как я понял, библиотечка это позволяет.

                  Ну и у StackOverflow есть API blog.stackoverflow.com/2010/05/stack-exchange-api-public-beta-starts/, так что странички дергать совсем не обязательно.
                    +1
                    как бы ни продвигали AsyncTask, мне кажется потоки + runOnUiThread ничуть не хуже, а часто и более гибко

                    Only users with full accounts can post comments. Log in, please.