0、前言
本文主要介紹如何抓取網頁中的內容、如何解決亂碼問題、如何解決登錄問題以及對所采集的數據進行處理顯示的過程。效果如下所示:
1、下載網頁並加載至HtmlAgilityPack
這里主要用WebClient類的DownloadString方法和HtmlAgilityPack中HtmlDocument類LoadHtml方法來實現。主要代碼如下。
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);
2、解決亂碼問題
在抓取cnbeta的時候,我發現用上述方法抓取的html是亂碼,開始我以為是網頁編碼問題,結果發現html網頁是UTF-8格式,編碼一致。最后發現原因是網頁被壓縮過,WebClient類不能處理被壓縮過了網頁,不過可以從WebClient類擴展出新的類,來支持網頁壓縮問題。核心代碼如下,使用時用XWebClient替換WebClient即可。
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; } }
3、解決登錄問題
某些網站的一些網頁,需要登錄才能查看,僅靠網址是沒辦法抓到的,這需要從html協議相關的知識了,不過這里不需要那么深的知識,先來一個具體的例子,先用chrome打開博客園、用F12或右鍵點擊“審查元素”打開“開發者工具/Developer Tools”,選擇“網路/Network”選項卡,刷新網頁,點擊開發者工具中的第一個請求,如下圖所示:
此時就可以看到剛才那次請求的請求頭(Request Header)了,有興趣的童鞋可以對照着http協議來查看每一個部分代表什么含義,而這里只關注其中的Cookie部分,這里包括了自動登錄需要的信息,而回到問題,我不僅需要url,還需要攜帶cookie,而WebClient對象是沒有Cookie相關的屬性的,這時候又要擴展WebClient對象了。核心代碼如下:
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添加進去后,就可以抓取登錄后的網頁了。
4、Html解析
這里使用HtmlAgilityPack的HtmlDocument對象的DocumentNode.SelectSingleNode方法來選擇元素,得到的HtmlNode對象取.Attributes["href"].Value即得到屬性值,取InnerText即得到InnerText。
這里的SelectSingleNode方法是可以接收XPath作為參數的,而這可以大大簡化解析難度。
在網頁上的一個元素上懸停,右鍵點擊“審查元素”,然后在被選中的那一塊,右鍵點擊“Copy XPath”,然后粘貼在SelectSingleNode方法的參數位置即可。對XPath感興趣的童鞋,可以隨便看看其它元素的XPath,觀察XPath的語法規則。如果找不到某個元素對應的html節點,可以點擊開發者工具左上角的放大鏡,並在網頁上點擊該元素,其html節點就自動被選中了。
5、對采集的數據進行過濾和排序
這里用Linq to Objects就可以,這里是最有個性化的步驟,以博客園為例,可以對發布時間、點擊數、頂的數目、評論數、top N等等進行過濾或排序,甚至對某某人進行屏蔽,非常自由。
我最后篩選出數據有三個屬性:Text,為顯示的文本,可以包含評論數、發表時間、標題之類的信息;Summary:為鼠標懸停時提示的文本;Url:為點擊鏈接后用瀏覽器打開的網址。
6、顯示
我采用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; } }
7、使用.NET 4.5的異步特性來處理多個網站的加載問題
程序會自動記錄瀏覽記錄,且已瀏覽的鏈接不再顯示出來。這里比較耗時的功能有:從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;
8、結語
以上就是給自己經常訪問的網站做信息抓取的實踐了,實際上做出的東西對我來說是很有用的,我再也不會像以前那樣,隔一會兒就要打開網站看追的美劇有沒有更新了。對博客按推薦數排序,是一種比較高效的方式了。
由於代碼中有我的Cookie,就不放出下載了。
應要求,給個demo,我把需要登錄的哪些網站去掉了,保留了一個福利網站。