HabraReader 2.0 или парсим Хабр

14 Мар
2012


Предисловие


Доброго времени суток, хабрахабр.
Регулярные выражения — достаточно мощное средство современных языков программирования, а так же хочется всегда быть в курсе последних постов с хабра 😉

хабраReader(C# .NET 4) — это программа для просмотра последних постов в хабре «Лучшее» и уведомления о добавлении нового поста. На написание данной программы меня подтолкнуло крайне редкое обновление RSS ленты хабра.
Под катом я расскажу, как писал это приложение, и с какими трудностями столкнулся. Также в коде есть достаточно подробные комментарии.
В основе программы лежат РВ, поэтому эта статья может побудить тебя, хабрапользователь, познакомиться с ними.
Чтобы начать программирование нам нужно разобраться с html структурой хабра.
Итак, приступим!



хабр, какой же ты изнутри?


Для начала нам нужно узнать, как выглядят посты в html формате. Идём в «Исходный код страницы», пара минут поиска и вот оно:
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
27
28
29
30
31
32
33
34
35
36
37
38
<div class="post" id="post_139700">
    <h2 class="title">          
       <a href="http://habrahabr.ru/blogs/gdev/" class="blog_title  blogs">Game Development</a><span class="profiled_blog" title="Профильный хабр"></span>→      
       <a href="http://habrahabr.ru/blogs/gdev/139700/" class="post_title">Unreal engine 3 портирован на flash</a>    
    </h2>
    <div class="content">
                <a href="http://www.unrealengine.com/flash/"><img src="http://29.media.tumblr.com/tumblr_m0k8s4APG31r3jb51o1_500.jpg" alt="image"></a><br>
                <br>
                Посмотреть альфа-демку можно на <a href="http://www.unrealengine.com/flash/">unrealengine.com</a>, весит она метров 50, полноэкранного режима пока нет.<br>
                <br>
                Помимо molehill, во всю используется alchemy2, поэтому требуется flash player 11.2, который можно скачать в <a href="http://labs.adobe.com/downloads/flashplayer11-2.html">adobe labs</a>.
                <div class="clear"></div>      
        </div>    
                <ul class="tags">
                        <li><a href="http://habrahabr.ru/tag/unreal engine 3/" rel="tag">unreal engine 3</a></li><li>, <a href="http://habrahabr.ru/tag/flash/" rel="tag">flash</a></li><li>, <a href="http://habrahabr.ru/tag/alchemy2/" rel="tag">alchemy2</a></li>
        </ul>
    <div class="infopanel" id="infopanel_post_139700">
        <div class="voting   ">        
                <span class="plus" title="Read-only пользователи не могут голосовать"></span>
                <span class="minus" title="Read-only пользователи не могут голосовать"></span>
                <div class="mark">                    
                    <span class="score" title="Оценка будет видна после завершения голосования">—</span>                                                                
                </div>  
        </div>
        <div class="published">10 марта 2012, 21:46</div>
        <div class="favorite">        
                <a class="add" title="Добавить в избранное" href="#" onclick="return posts_add_to_favorite(this, '2', 139700)"> </a>        
        </div>
        <div class="favs_count" title="Количество пользователей, добавивших пост в избранное">49</div>  
        <div class="author">
                <a title="Автор текста" href="http://habrahabr.ru/users/mrskam/">mrskam</a>
        </div>
        <div class="comments">
                <a title="Читать комментарии" href="http://habrahabr.ru/blogs/gdev/139700/#comments"><span class="all">93</span> <span class="new">+93</span></a>            
        </div>
        </div>
        <div class="clear"></div>      
</div>

Условно пост хабра можно разделить на 4 части:
  1. Title — заголовок поста. Здесь содержатся названия хабра, является ли хабр профильным и название поста.
  2. Content — сам контент поста. Здесь содержатся текст до хабраката и ссылка на этот самый хабракат (Как я понял позднее — не всегда)
  3. Tags — все теги поста.
  4. InfoPanel — информация о посте. Здесь находится дата публикации, количество людей добавивших в избранное, имя автора и ссылка на комментарии
Загрузку страницы я решил опустить, т.к. это можно с лёгкостью найти в интернете. Приступим непосредственно к разбору страницы!

Парсим код или пишем HTMLParser


Для разбора полученного кода я так же буду использовать РВ. Чтобы полностью «разделать» пост нам понадобится 10 регулярных выражений:
1
2
3
    <a href=.[\s\S]*?. class=.blog_title  blogs.>([\s\S]*?) - РВ для поиска название хабра
 
<span class=.profiled_blog. title=.Профильный хабр.> - РВ для поиска свидетельств профильного хабра ( Если совпадение найдено - значит профильный)([\s\S]*?) - РВ для поиска названия поста([\s\S]*?)(|[\s\S]*?) - РВ для поиска контента. Тут меня поджидала первая ошибка. Т.к. предыдущая версия РВ базировалась на том, что контент находится между дивом и хабракатом, но оказалось что хабракат есть не всегда.[\s\S]*? - РВ для поиска хабракат линка.([\s\S]*?) - РВ для поиска тэгов поста.([\s\S]*?) - РВ для поиска даты публикации.([0-9]*?) - РВ для поиска кол-ва людей, добавивших пост в избранное[\s]*?([\s\S]*?)[\s]*? - РВ для поиска автора текста.[\s]*?[\s\S]*? - РВ для поиска ссылки на комментарии

На первый взгляд, ужасно непонятные строчки, не правда ли? Но на самом деле все достаточно просто. Проанализируем выражение для поиска даты публикации:
1
Нужный блок должен начинаться с ([\s\S]*?) - [\s\S]*? означает любое кол-во букв и строк, до первого попавшегося div'а - блок должен оканчиваться на
Великолепно! Если вас заинтересовали РВ, то рекомендую почитать Джеффри Фридла — Регулярные Выражения. А теперь попишем код. Для получения нужных нам значений с помощью РВ я использовал стандартный класс .NET System.Text.RegularExpressions.Regex, а для хранения данных структуры. Таким образом, получаем:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    struct Title
    {
    public String BlogTitle;// Название хабра, в котором находится пост
    public Boolean ProfiledBlog;// Профильный ли хабр?
    public String PostTitle;// Название поста
    public static String ParseBlogTitle(String PostHTMLCode)// Парсим название хабра
    {
    Match match = Regex.Match(PostHTMLCode, @"([\s\S]*?)", RegexOptions.IgnoreCase | RegexOptions.Multiline);
    if (match.Success)//если совпадение найдено, возвращаем значение первой группы
    {
    //Groups[1] потому что в Groups[0] лежит весь блок, а не только нужное значение
    return match.Groups[1].Value;
    }
    return null;
    }
    public static Boolean ParseProfiledBlog(String PostHTMLCode)// Проверяем на профильность
    {
    Match match = Regex.Match(PostHTMLCode, @"<span class=.profiled_blog. title=.Профильный хабр.>", RegexOptions.Multiline | RegexOptions.IgnoreCase);
 
 
 
           return match.Success;// Если совпадение будет найдено - значит хабр профильный        }        public static String ParsePostTitle(String PostHTMLCode)// Парсим название поста        {            Match match = Regex.Match(PostHTMLCode, @"([\s\S]*?)", RegexOptions.Multiline | RegexOptions.IgnoreCase);            if (match.Success)            {                return match.Groups[1].Value;            }            return null;        }    }         struct Content    {        public String TextContent;// Сам текст поста        public String HabraCutLink;// Линк на полную новость         public static String ParseTextContent(String PostHTMLCode)// Парсим содержание поста (до хабраката)        {            Match match = Regex.Match(PostHTMLCode, @"([\s\S]*?)(|[\s\S]*?)", RegexOptions.Multiline | RegexOptions.IgnoreCase);            if (match.Success)            {                return match.Groups[1].Value;            }            return null;        }        public static String ParseHabraCutLink(String PostHTMLCode)// Получаем ссылку на хабракат        {            Match match = Regex.Match(PostHTMLCode,@"[\s\S]*?", RegexOptions.Multiline | RegexOptions.IgnoreCase);            if (match.Success)            {                return match.Groups[1].Value;            }            return null;        }    }     struct Tag    {        public String TagName;                // Получаем все теги. Для этого я написал класс наследующий CollectionBase        public static Tags ParseTags(String PostHTMLCode)        {                        MatchCollection matches = Regex.Matches(PostHTMLCode, @"([\s\S]*?)", RegexOptions.Multiline | RegexOptions.IgnoreCase);            if (matches.Count != 0)            {                Tags tags = new Tags();                foreach(Match match in matches)                {                    tags.AddTag(new Tag() { TagName = match.Groups[1].Value });                }                return tags;            }            return null;                    }    }     struct InfoPanel    {        public String Published;// Когда опубликован?        public Int16 FavsCount;// Сколько людей добавило в избранное        public String Author;// Автор поста        public String CommentsLink;// Ссылка на комментарии         public static String ParsePublished(String PostHTMLCode)// Парсим дату публикации        {            Match match = Regex.Match(PostHTMLCode, @"([\s\S]*?)", RegexOptions.Multiline | RegexOptions.IgnoreCase);            if (match.Success)            {                return match.Groups[1].Value;            }            return null;        }        public static Int16 ParseFavsCount(String PostHTMLCode)// Парсим количество добавивших в избранное        {            Match match = Regex.Match(PostHTMLCode, @"([0-9]*?)", RegexOptions.Multiline | RegexOptions.IgnoreCase);            if (match.Success)            {                return Convert.ToInt16(match.Groups[1].Value);            }            return 0;        }        public static String ParseAuthor(String PostHTMLCode)// Парсим имя автора        {            Match match = Regex.Match(PostHTMLCode, @"[\s]*?([\s\S]*?)[\s]*?",RegexOptions.Multiline | RegexOptions.IgnoreCase);            if (match.Success)            {                return match.Groups[1].Value;            }            return null;        }        public static String ParseCommentsLink(String PostHTMLCode)// Парсим ссылку на комментарии        {            Match match = Regex.Match(PostHTMLCode, @"[\s]*?[\s\S]*?", RegexOptions.Multiline | RegexOptions.IgnoreCase);            if (match.Success)            {                return match.Groups[1].Value;            }            return null;        }    }        struct Post    {        public Title PostTitle;// Структура названия поста        public Content PostContent;// Структура контента        public Tags PostTags;// Массив тэгов        public InfoPanel PostInfoPanel;// Структура информационной панели         public static Post ParsePost(String PostHTMLCode)// Метод для получения всего и сразу :)         {            Post post = new Post();            post.PostTitle.BlogTitle = Title.ParseBlogTitle(PostHTMLCode);            post.PostTitle.PostTitle = Title.ParsePostTitle(PostHTMLCode);            post.PostTitle.ProfiledBlog = Title.ParseProfiledBlog(PostHTMLCode);             post.PostContent.TextContent = Content.ParseTextContent(PostHTMLCode);            post.PostContent.HabraCutLink = Content.ParseHabraCutLink(PostHTMLCode);             post.PostTags = Tag.ParseTags(PostHTMLCode);             post.PostInfoPanel.Published = InfoPanel.ParsePublished(PostHTMLCode);            post.PostInfoPanel.FavsCount = InfoPanel.ParseFavsCount(PostHTMLCode);            post.PostInfoPanel.CommentsLink = InfoPanel.ParseCommentsLink(PostHTMLCode);            post.PostInfoPanel.Author = InfoPanel.ParseAuthor(PostHTMLCode);             return post;        }    }

Для хранения списка тэгов и постов, я написал два отдельных класса, наследующих CollectionBase.
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
        class Tags : CollectionBase // Массив тэгов
            {
                public Tag this[int index]// Индексатор для "массивного" доступа
                {
                    get { return (Tag)List[index]; }
                    set { List[index] = value; }
                }
 
                public void AddTag(Tag tag)// Добавить тэг
                {
                    List.Add(tag);
                }
                public void RemoveTag(Tag tag)// Удалить первое вхождение тэга
                {
                    List.Remove(tag);
                }
                public void RemoveTagAt(int index)// Удалить тэг с индексом index
                {
                    List.RemoveAt(index);
                }
                public void Clear()// Очистить массив тэгов
                {
                    List.Clear();
                }
            }
 
            class Posts : CollectionBase // Массив постов
            {
                public Post this[int index]// Индексатор для "массивного" доступа
                {
                    get { return (Post)List[index]; }
                    set { List[index] = value; }
                }
 
                public void AddPost(Post post)// Добавить пост
                {
                    List.Add(post);
                }
                public void RemovePost(Post post)// Удалить первое вхождение указанного поста
                {
                    List.Remove(post);
                }
                public void RemovePostAt(int index)// Удалить пост с индексом index
                {
                    List.RemoveAt(index);
                }
                public void Clear()// Очистить массив постов
                {
                    if(List.Count != 0)
                        List.Clear();
                }
            }

И наконец сам класс парсера:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
        class HTMLParser
            {
                private String HTMLCode = null; // Хранилище HTML кода страницы
 
                public Posts Posts// Хранилище постов
                {
                    get;
                    private set;// Запрет на внешнее редактирование
                }      
 
                public HTMLParser(String html_code)// Основной конструктор
                {            
                    Posts = new Posts();
                    this.HTMLCode = html_code;// Загружаем код в класс
                    this.RemoveWhiteSpace();// Удаляем пустоты в коде
                    try// На случай непредвиденной ошибки
                    {
                        foreach (String post in ParsePostsHTML())// Парсим и добавляем посты в масс
                        {
                            Posts.AddPost(Post.ParsePost(post));//в post находится HTML код поста
                        }
                    }
                    catch (NullReferenceException)
                    {
                        Box.ShowErrorMessage("HTML Parser error!", "Невозможно разобрать html код. Неверный формат?");
                        return;
                    }
                    catch (Exception)
                    {
                        Box.ShowErrorMessage("HTML Parser error!", "Невозможно разобрать html код. Неверный формат?");
                        return;
                    }
                }      
 
                private String[] ParsePostsHTML()// Парсим html код всех постов на странице
                {
                    // Получить код всех постов со страницы
                    MatchCollection match_collection = Regex.Matches(this.HTMLCode, @"()([\s\S]*?)(\n)", RegexOptions.IgnoreCase | RegexOptions.Multiline);
                    if (match_collection.Count != 0)// Если коллекция не пуста, начинаем собирать массив html кода постов
                    {
                        String[] PostsHtml = new String[match_collection.Count];
                        for (int i = 0; i < match_collection.Count; i++)
                        {
                            PostsHtml[i] = match_collection[i].Groups[2].Value;
                        }
                        return PostsHtml;
                    }            
                    return null;          
                }        
 
                /*private String RemoveWhiteSpace(String Code)// Вспомогательная очищалка
                {
                    return Regex.Replace(Code, @"^([\s]*)$", "", RegexOptions.Multiline | RegexOptions.IgnoreCase);
                }
                 */
                private void RemoveWhiteSpace()// Очищаем html от пустых строк
                {
                    this.HTMLCode = Regex.Replace(this.HTMLCode, @"^([\s]*)$", "", RegexOptions.Multiline | RegexOptions.IgnoreCase);
                }
            }

Ничего особенного, не правда ли?

Заключение


Ну вот и всё, написать свою читалку постов с хабра не так уж сложно, самым трудным было подобрать регулярное выражение для поиска всего поста. На данный момент программа выглядит так:
HabraReader
В планах на ближайшее время:
  1. Добавить поддержку HTML тэгов (HTML -> RTF).
  2. Добавить поддержку многостраничности.
  3. Добавить возможность просматривать другие хабри, помимо «Лучшее».
  4. Добавить возможность просматривать весь пост
  5. Переделать дизайн в «Фирменный»

Так же в папке с программой можно найти конфигурационный файл в котором находятся настройки программы*.
Все исходники можно скачать\посмотреть тут
Скачать скомпилированную версию можно тут(Требуется .NET 4.0 и выше)
Приветствуется конструктивная критика!

*Время в UpdateTime нужно указывать в минутах. ShowNotice — показывать уведомления о обновлении и добавлении нового поста.
По материалам Хабрахабр.



загрузка...

Комментарии:

Наверх