Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Html Agility Pack — uzyskujemy statystyki z bloga do DePeszy, czyli parsujemy HTML w C#

Portalowa aplikacja DePesza jest już od jakiegoś czasu w Sklepie Windows. Prace nad doszlifowaniem programu i dodaniem nowych elementów trwają i jeszcze przed końcem maja pojawi się w markecie nowa wersja. Dziś jednak chciałbym przedstawić mały element, który zostanie dodany w kolejnym wydaniu DePeszy - statystyki blogowe.

Parsowanie HTML w C# - Html Agility Pack

Logowanie lub pobieranie z portalu powiadomień można było oprzeć na wymianie zapytań pomiędzy aplikacją DePesza, a serwerem dobrychprogramów (wystawiony serwis). Niestety, jeśli zechcemy uzyskać statystyki odnośnie wpisów blogowych zalogowanej osoby, musimy pokusić się o czyste parsowanie HTML.

Z tym problemem poradzimy sobie szybko przy pomocy Html Agility Pack. Jest to najlepsza biblioteka .NET do parsowania HTML. Możliwość ma ona ogromne i działa zarówno na desktopie, aplikacji mobilnej, jak i platformie Universal Windows Platform. Obsługa jej jest bardzo prosta i szybka (daaawno temu użyta przy zabawie z globalnymi statystykami portalu: dobreprogramy.pl w liczbach 2012 - 2013).

Licznik Blogowy - zapomniana wtyczka

Ponad wa lata temu stworzyłem wtyczkę do przeglądarek internetowych (Chrome/Firefox/Opera), która gromadziła statystyki blogowe dla zalogowanej osoby na portalu (linki: Licznik Blogowy, Licznik Blogowy - aktualizacja). Zasada uzyskiwania danych była dość prosta, stworzona w JavaScripcie, ale bardzo skuteczna i szybka. Dzięki temu, iż blog przez ponad dwa lata nie zmienił się zupełnie, od strony zarządzania wpisami blogowymi, Licznik Blogowy działa nadal.

Pobieranie danych opiera się na poniższym pomyśle, którego podstawowe założenia zostaną użyte również przy uzyskiwaniu statystyk do DePeszy.

Pobieranie statystyk - opis

Punktem wyjściowym jest strona:

http://www.dobreprogramy.pl/MojBlog.html

do której dostęp mają osoby zalogowane.
W tym miejscu znajdziemy listę ze wszystkimi wpisami danego uzytkownika portalu:

Każda pozycja na liście posiada tytuł, link do wpisu z edycją (tu także w url mamy ID wpisu) oraz status. Ten ostatni określa czy post wylądował na głównej stronie bloga, a także czy jest dopiero tworzony lub został już opublikowany. Te dane pozwolą na dostanie się do szczegółowych informacji o każdym z wpisów.

Posiadając listę z linkami do edycji, będziemy wchodzili do każdego wpisu (edycja) w celu pobrania: daty ostatniej zmiany, liczby wyświetleń i komentarzy:

Oczywiście lista z blogami jest stronicowa, zatem całość będzie działać rekurencyjnie:

Pobieranie statystyk - parsowanie HTML za pomocą Html Agility Pack w C#

Przejdźmy zatem do samej implementacji. Wyłuskiwanie informacji z HTML poprzez Agility Pack można uzyskać dwojako: poprzez XPath (język służący do adresowania części dokumentu XML) lub za pomocą zapytań LINQ (technologia umożliwia zadawanie pytań na obiektach). Preferuję te drugie podejście (kod pisze się szybciej i łatwiej go przetestować) i takiego też będę używał w poniższych przykładach.

Kod podzielony jest na dwie główne metody. Pierwsza część odpowiedzialna jest za pobieranie wszystkich linków do wpisów blogowych. Druga metoda pobiera stronę z edycją każdego wpisu i uszykuje szczegółowe dane.

Pobieranie listy wpisów

public async Task<List<Post>> GetBlogMainStatistics(int pageNo, List<Post> postLink, HttpClient httpClient) { var request = new HttpRequestMessage(HttpMethod.Get, new Uri(Const.BlogPrefix + pageNo + ".html")); var response = await httpClient.SendRequestAsync(request); HtmlDocument doc = new HtmlDocument(); doc.LoadHtml(await response.Content.ReadAsStringAsync()); var divWithLinks = doc.DocumentNode.Descendants("div") .Where(d => d.Attributes.Contains("class") && d.Attributes["class"].Value.Contains("contentText")) .FirstOrDefault(); if (divWithLinks != null) { int lastOrderId = postLink.Select(x => x.OrderId).LastOrDefault(); divWithLinks.Descendants("tr").ToList().ForEach(x => { var elemA = x.Descendants("a").FirstOrDefault(); var elemSpan = x.Descendants("span").FirstOrDefault(); if (elemA != null && elemSpan != null) { var newPost = new Post() { Title = elemA.InnerText, Url = elemA.Attributes["href"].Value, IsPublished = elemSpan.InnerText == Const.PostStatusPublished, IsHomePage = elemSpan.Attributes.Contains("class") && elemSpan.Attributes["class"].Value.Contains(Const.PostHomePage), OrderId = ++lastOrderId }; newPost.Id = newPost.Url .Split(new string[] { ",", ".html" }, StringSplitOptions.RemoveEmptyEntries) .Reverse().First(); postLink.Add(newPost); } }); } var nextLink = doc.DocumentNode.Descendants("div") .Where(d => d.Attributes.Contains("class") && d.Attributes["class"].Value.Contains("controls")) .FirstOrDefault(); var nextUrl = (Const.BlogPrefix + (pageNo + 1) + ".html"); if (nextLink != null && nextLink.Descendants("a").Where(a => a.Attributes.Contains("href") && a.Attributes["href"].Value == nextUrl).Count() > 0) { await GetBlogMainStatistics((pageNo + 1), postLink, httpClient); } return postLink; }

Omówmy teraz funkcję GetBlogMainStatistics. Na początku pobieramy poprzez HttpClient jedną stronę ze statystykami (pageNo jest aktualną stroną), gdzie strona pojedyncza ma adres:

"http://www.dobreprogramy.pl/MojBlog," + pageNo + ".html"

Ładowanie danych do parsowania przez HTML Agility Pack jest proste, wystarczy podać string z czystym Htmlem (tutaj jest to pobrany dokument z odpowiedzi z serwera):

HtmlDocument doc = new HtmlDocument(); doc.LoadHtml(await response.Content.ReadAsStringAsync());

W kolejnym kroku tworzymy zapytanie LINQ, które pobierze główny element (div z klasą css: contentText) zawierający tablicę z poszczególnymi linkami do wpisów:

var divWithLinks = doc.DocumentNode.Descendants("div") .Where(d => d.Attributes.Contains("class") && d.Attributes["class"].Value.Contains("contentText")) .FirstOrDefault();

Zapytanie w LINQ wygląda znacznie przyjemniej niż gdybyśmy używali XPatha. Korzeniem jest DocumentNode, z którego pobieramy wszystkich potomków, którzy są elementami div. Szukamy elementów div, które posiadają atrybut classs, a w nim wartość contentText. Na tym schemacie opiera się całość działań na danym dokumencie HTML.

Teraz będąc w głównym elemencie div pobieramy wszystkie wpisy na stronie:

int lastOrderId = postLink.Select(x => x.OrderId).LastOrDefault(); divWithLinks.Descendants("tr").ToList().ForEach(x => { var elemA = x.Descendants("a").FirstOrDefault(); var elemSpan = x.Descendants("span").FirstOrDefault(); if (elemA != null && elemSpan != null) { var newPost = new Post() { Title = elemA.InnerText, Url = elemA.Attributes["href"].Value, IsPublished = elemSpan.InnerText == Const.PostStatusPublished, IsHomePage = elemSpan.Attributes.Contains("class") && elemSpan.Attributes["class"].Value.Contains(Const.PostHomePage), OrderId = ++lastOrderId }; newPost.Id = newPost.Url .Split(new string[] { ",", ".html" }, StringSplitOptions.RemoveEmptyEntries) .Reverse().First(); postLink.Add(newPost); } });

Z każdego elementu tr uzyskujemy nazwę wpisu, adres, a także status wpisu. Z adresu url można także wydobyć Id wpisu. Link jest ustandaryzowany i wygląda tak:

"http://www.dobreprogramy.pl/Blog,Edycja,"+ID_WPISU"+.html"

Funkcja GetBlogMainStatistics jest rekurencyjna i warunek stopu jest sprawdzeniem czy w pasku nawigacyjnym (na dole strony) jest adres z kolejną stroną z listą blogów:

var nextLink = doc.DocumentNode.Descendants("div") .Where(d => d.Attributes.Contains("class") && d.Attributes["class"].Value.Contains("controls")) .FirstOrDefault(); var nextUrl = (Const.BlogPrefix + (pageNo + 1) + ".html"); if (nextLink != null && nextLink.Descendants("a").Where(a => a.Attributes.Contains("href") && a.Attributes["href"].Value == nextUrl).Count() > 0) { await GetBlogMainStatistics((pageNo + 1), postLink, httpClient); }

W ten sposób uzyskamy listę z blogami. Kolejnym etapem jest przejrzenie jej i pobranie każdej strony z edycją wpisu, aby uzyskać dane odnośnie liczby wyświetleń i komentarzy.

Pobieranie wyświetleń i komentarzy na blogu

Cała funkcja jest znacznie prostsza niż poprzednia:

public async Task<List<Post>> GetBlogCounters(List<Post> postLink, HttpClient httpClient) { HtmlDocument doc = new HtmlDocument(); foreach (var post in postLink) { var request = new HttpRequestMessage(HttpMethod.Get, new Uri(post.Url)); var response = await httpClient.SendRequestAsync(request); doc.LoadHtml(await response.Content.ReadAsStringAsync()); var details = doc.DocumentNode.Descendants("section") .Where(d => d.Attributes.Contains("class") && d.Attributes["class"].Value.Contains("user-info")).LastOrDefault(); if (details != null) { var divs = details.Descendants("div").ToList(); if (divs.Count >= 12) { post.VisitorsCounter = int.Parse(divs[9].InnerText); post.CommentsCounter = int.Parse(divs[12].InnerText); post.DateLastModification = DateTime.ParseExact(divs[5].InnerText, "dd.MM.yyyy HH:mm", CultureInfo.InvariantCulture); } } } return postLink; }

W pętli pobieramy stronę HTML z edycją każdego wpisu i dobieramy się do tabelki ze statystykami (posiada ona klasę css user-info). Tutaj już parsujemy ilość wpisów i wyświetlenia na typ int, a także uzyskuję datę ostatniej edycji.

Na koniec uzyskamy szczegółowe dane odnośnie każdego wpisu. Tak przygotowane statystyki będą wędrować do użytkownika.

DePesza - kolejna wersja

Jak już wspomniałem niedługo wrzucę do marketu nową wersję DePeszy. Prócz szczegółowych statystyk z bloga, nowa wersja będzie posiadać dużo poprawek, ulepszeń i kilka ciekawych odświeżonych elementów. Na pewno się nie zawiedziecie. Mam nadzieję, że zmiany przypadną wszystkimi do gustu.

DePesza dostępna jest w markecie Windows 10 (desktop i mobile). Bezpośredni link: DePesza.

Aktualne źródła można znaleźć na GitHub pod adresem:https://github.com/djfoxer/dp.notification
 

windows programowanie urządzenia mobilne

Komentarze

0 nowych
MaXDemage   18 #1 23.05.2016 18:00

Kocham statystyki. <3

AntyHaker   18 #2 23.05.2016 20:31

Czekam ^^

Ostatni Mohikanin   26 #3 23.05.2016 21:52

@MaXDemage: Ale nie tak jak ja ;)

  #4 24.05.2016 09:06

ERROR: Attempted to parse HTML with regular expression; system returned Cthulhu.

mordzio   15 #5 24.05.2016 16:12

Dobra robota, ale to pewnie już wiesz;)

Pablo_Wawa   9 #6 24.05.2016 16:45

A ja się przyczepię do "jakości" kodu - wprawdzie sam nie programuję w .NET/C#, ale tam chyba można używać stałych? Używanie bezpośrednio w kodzie tekstów (tu nazw klas), zamiast stałych, jest nieprofesjonalne, bo wystarczy że DP zmienią nieco formatowanie strony (HTML) - tj. nazwę użytej klasy i będziesz musiał w swoim kodzie szukać tych tekstów do poprawienia.

Autor edytował komentarz w dniu: 24.05.2016 16:46
Quest-88   15 #7 24.05.2016 17:45

@MaXDemage: Cieszmy się z aplikacji na system, który na świecie ma aż 1% rynku. :p

djfoxer   18 #8 24.05.2016 18:24

@Quest-88: To jest apka uniwersalna - czyli Windows 10 Mobile i Windows 10, to nie jest 1% rynku, wybacz :D

@Pablo_Wawa: Stałe niewiele tu dadzą. I tak w większości używam nazw elementów/atrybutów jak "class", "div", "a" czy "href", które się nie zmienią. Nazwy klasy jak "contentText" czy "user-info" używam w kodzie tylko raz, więc nic by to zbytnio nie zmieniło, jedna referencja w kodzie. Poza tym nawet jeśli coś by się zmieniło, to nie skończyło by się to na nazwach klas, a na całej strukturze drzewa DOM, więc jeśli się coś zmieni to zapewne na tyle dużo, że i tam na 100% trzeba będzie ponownie napisać zapytania LINQ. Oczywiście zastanawiałem się nad stałymi (co widać w kodzie, jet tam kilka stałych), ale zrezygnowałem z tego. Była by to zbytnia sztuka dla sztuki. Nie chciałem popadać w skrajność przy czymś tak mikroskopijnym.

djfoxer   18 #9 24.05.2016 18:24

@mordzio: Poczekaj na odświeżoną DepEszę ;)

darek719   38 #10 24.05.2016 20:06

dzieję się, zmiany na +
czekam na więcej.

rob006   7 #11 24.05.2016 20:32

Chyba jestem stary, albo to przez to formatowanie kodu, ale dla mnie xpath jest 100 razy bardziej przejrzysty niż te potworki w LINQ...

mordzio   15 #12 24.05.2016 21:51

@Quest-88: Aplikacja działa również na Windows 10 :)

djfoxer   18 #13 24.05.2016 21:58

@rob006: Programując w .NET LINQ jest na każdym kroku, więc siadając do tego toolkita LINQ jest mi bardziej naturalne. Aczkolwiek wiem, że wiele osób ceni sobie xpatha za uniwersalność :)

AntyHaker   18 #14 24.05.2016 22:31

@djfoxer: Tak swoją droga - ilu bym powiadomień na portalu nie miał to apka mnie o tym nie informuje ;s

Quest-88   15 #15 24.05.2016 22:44

@mordzio: Ok, już djfoxer mi to powiedział. Chciałbym tylko zwrócić uwagę, że pulpitowe widgety należy o kant stołu roztrzaskać, bo te rozpraszają uwagę powiadomieniami. Zastanawiam się czy taka aplikacja na normalnym desktopie to nie będzie sztuka dla sztuki, bo przecież w razie potrzeby można szybko odpalić przeglądarkę i wejść na stronę. U mnie np. przeglądarka jest włączona non stop.

Zobaczymy co pokażą statystyki, ale oceniając przez pryzmat moich potrzeb, z aplikacji korzystałbym tylko na telefonie. Niestety posiadam tylko popularnego Androida, a nie martwy Windows Phone. Ale doceniam pracę djfoxera, jego oddanie i miłość do Microsoftu. :P

Btw. czy to jest tworzone w Xamarin Studio i czy można liczyć na port na Andka?

Autor edytował komentarz w dniu: 24.05.2016 22:53
djfoxer   18 #16 24.05.2016 22:47

@AntyHaker: Możesz coś więcej napisać?

AntyHaker   18 #17 24.05.2016 22:55

@djfoxer: Tzn. co więcej :P Apka zainstalowana, w niej powiadomienia się wyświetlają, ale nie mam notyfikacji push :s

djfoxer   18 #18 24.05.2016 22:56

@AntyHaker: hmm, dziwne, ok... to przyjrzę się temu, dzięki!

AntyHaker   18 #19 24.05.2016 23:02

@djfoxer: Poddaje się - nie miałem notyfikacji dobrych kilka tygodni, teraz się poskarżyłem to otrzymałem xD Będę obserwować co dalej z tym będzie.

cyryllo   17 #20 25.05.2016 18:09

Ja czekam głównie na te statsy ;)

djfoxer   18 #21 25.05.2016 22:03

@AntyHaker: Przestraszyłeś mnie :P Może to coś z systemem, u mnie np. czasem jakieś kafelki nie odświeżają się lub powiadomienia nie przychodzą, jak długo apki nie otwieram.

djfoxer   18 #22 25.05.2016 22:06

@Quest-88: Miłość dość mocno platoniczna ;) Apka na desktopie jest o tyle miła, że działa sobie w tle i jak coś nowego się pojawi to pokazuje notyfikację, nie trzeba mieć otwartej strony ciągle i spr. powiadomienia.

Apka napisana jest w UWP, VS. W najbliższym czasie będę przymierzał się do stworzenia uniwersalnej aplikacji w Xamarinie w VS.

AntyHaker   18 #23 25.05.2016 22:20

@djfoxer: Możliwe - znalazłem za to inny problem. Klikając w powiadomienie w aplikacji przenosi mnie poprawnie do przeglądarki, lecz otwiera 2 takie same karty :P