本文主要介紹如何抓取網頁中的內容、如何解決亂碼問題、如何解決登陸問題以及對所採集的數據進行處理顯示的過程。效果以下所示:html
這裏主要用WebClient類的DownloadString方法和HtmlAgilityPack中HtmlDocument類LoadHtml方法來實現。主要代碼以下。chrome
var url = page == 1 ? "http://www.cnblogs.com/" : "http://www.cnblogs.com/sitehome/p/" + page; var wc = new WebClient { BaseAddress = url, Encoding = Encoding.UTF8 }; var doc = new HtmlDocument(); var html = wc.DownloadString(url); doc.LoadHtml(html);
在抓取cnbeta的時候,我發現用上述方法抓取的html是亂碼,開始我覺得是網頁編碼問題,結果發現html網頁是UTF-8格式,編碼一致。最後發現緣由是網頁被壓縮過,WebClient類不能處理被壓縮過了網頁,不過能夠從WebClient類擴展出新的類,來支持網頁壓縮問題。核心代碼以下,使用時用XWebClient替換WebClient便可。shell
public class XWebClient : WebClient {protected override WebRequest GetWebRequest(Uri address) { var request = base.GetWebRequest(address) as HttpWebRequest; request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;return request; } }
某些網站的一些網頁,須要登陸才能查看,僅靠網址是沒辦法抓到的,這須要從html協議相關的知識了,不過這裏不須要那麼深的知識,先來一個具體的例子,先用chrome打開博客園、用F12或右鍵點擊「審查元素」打開「開發者工具/Developer Tools」,選擇「網路/Network」選項卡,刷新網頁,點擊開發者工具中的第一個請求,以下圖所示:瀏覽器
此時就能夠看到剛纔那次請求的請求頭(Request Header)了,有興趣的童鞋能夠對照着http協議來查看每個部分表明什麼含義,而這裏只關注其中的Cookie部分,這裏包括了自動登陸須要的信息,而回到問題,我不只須要url,還須要攜帶cookie,而WebClient對象是沒有Cookie相關的屬性的,這時候又要擴展WebClient對象了。核心代碼以下:cookie
public class XWebClient : WebClient { public XWebClient() { Cookies = new CookieContainer(); } public CookieContainer Cookies { get; private set; } protected override WebRequest GetWebRequest(Uri address) { var request = base.GetWebRequest(address) as HttpWebRequest; request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; if (request.CookieContainer == null) { request.CookieContainer = Cookies; } return request; } }
這裏GetWebRequest函數中得到WebRequest中的CookieContainer是null,因此我暴露了一個CookieContainer,用來添加Cookie,使用時調用其Add(new Cookie(string name, string value, string path, string domain))方法便可,這裏path通常爲「/」,domain爲url,上圖中的Cookie按分號分割,等號左邊的就是name,右邊的就是value。把全部的Cookie添加進去後,就能夠抓取登陸後的網頁了。dom
這裏使用HtmlAgilityPack的HtmlDocument對象的DocumentNode.SelectSingleNode方法來選擇元素,獲得的HtmlNode對象取.Attributes["href"].Value即獲得屬性值,取InnerText即獲得InnerText。異步
這裏的SelectSingleNode方法是能夠接收XPath做爲參數的,而這能夠大大簡化解析難度。async
在網頁上的一個元素上懸停,右鍵點擊「審查元素」,而後在被選中的那一塊,右鍵點擊「Copy XPath」,而後粘貼在SelectSingleNode方法的參數位置便可。對XPath感興趣的童鞋,能夠隨便看看其它元素的XPath,觀察XPath的語法規則。若是找不到某個元素對應的html節點,能夠點擊開發者工具左上角的放大鏡,並在網頁上點擊該元素,其html節點就自動被選中了。ide
這裏用Linq to Objects就能夠,這裏是最有個性化的步驟,以博客園爲例,能夠對發佈時間、點擊數、頂的數目、評論數、top N等等進行過濾或排序,甚至對某某人進行屏蔽,很是自由。函數
我最後篩選出數據有三個屬性:Text,爲顯示的文本,能夠包含評論數、發表時間、標題之類的信息;Summary:爲鼠標懸停時提示的文本;Url:爲點擊連接後用瀏覽器打開的網址。
我採用Wpf做爲UI,代碼以下:
<Window x:Class="NewsCatcher.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="720" Width="1024" WindowStartupLocation="CenterScreen"> <ListView Name="listView"> <ListView.View> <GridView> <GridView.Columns> <GridViewColumn Header="新聞列表"> <GridViewColumn.CellTemplate> <DataTemplate> <TextBlock Width="960"> <Hyperlink NavigateUri="{Binding Url}" ToolTip="{Binding Summary}" RequestNavigate="Hyperlink_OnRequestNavigate"> <TextBlock FontSize="20" Text="{Binding Text}" /> </Hyperlink> </TextBlock> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> </GridView.Columns> </GridView> </ListView.View> </ListView> </Window>
事件處理程序Hyperlink_OnRequestNavigate的代碼以下,啓用新進程使用默認瀏覽器來打開網站(若是不加那個參數,那麼老是用IE打開網站):
private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e) { (sender as Hyperlink).Foreground = Brushes.Red; var uri = e.Uri.AbsoluteUri; Process.Start(new ProcessStartInfo(WindowsHelper.GetDefaultBrowser(), uri)); e.Handled = true; }
WindowsHelper類的代碼:
public static class WindowsHelper { private static string defaultBrowser; public static string GetDefaultBrowser() { if (defaultBrowser == null) { var key = Registry.ClassesRoot.OpenSubKey(@"http\shell\open\command\"); var s = key.GetValue("").ToString(); defaultBrowser = new string(s.SkipWhile(c => c != '"').Skip(1).TakeWhile(c => c != '"').ToArray()).Trim().Trim('"'); } return defaultBrowser; } }
程序會自動記錄瀏覽記錄,且已瀏覽的連接再也不顯示出來。這裏比較耗時的功能有:從xml文件中反序列化出歷史數據、從各個網站下載並解析,它們是能夠並行的,然而解析完成以後要排除歷史數據中已有的數據,這個過程須要等待反序列化過程完成,代碼以下:
deserialization = new Task(delegate { try { history = NEWSHISTORY_XML.Deserialize<List<HistoryItem>>(); history.RemoveAll(h => h.Time < DateTime.Now.AddDays(-7).ToInt32()); } catch (Exception) { history = new List<HistoryItem>(); } }); cnblogs = new Task(async delegate { try { var result = Cnblogs(); await deserialization; AddIfNotClicked(result); } catch (Exception exception) { itemsSource.Add(new ShowItem { Text = "Cnblogs Fails", Summary = exception.Message }); } listView.Dispatcher.Invoke(() => listView.Items.Refresh()); }); cnbeta = new Task(async delegate { try { var result = CnBeta(); await deserialization; AddIfNotClicked(result); } catch (Exception exception) { itemsSource.Add(new ShowItem { Text = "CnBeta Fails", Summary = exception.Message }); } listView.Dispatcher.Invoke(() => listView.Items.Refresh()); }); deserialization.Start(); cnblogs.Start(); cnbeta.Start();
private void AddIfNotClicked(IEnumerable<ShowItem> result) { foreach (var item in result.Where(i => history.All(h => h.Url != i.Url))) { itemsSource.Add(item); } }
itemsSource = new List<ShowItem>(); listView.ItemsSource = itemsSource;
以上就是給本身常常訪問的網站作信息抓取的實踐了,實際上作出的東西對我來講是頗有用的,我不再會像之前那樣,隔一下子就要打開網站看追的美劇有沒有更新了。對博客按推薦數排序,是一種比較高效的方式了。
因爲代碼中有個人Cookie,就不放出下載了。
應要求,給個demo,我把須要登陸的哪些網站去掉了,保留了一個福利網站。