實用網頁抓取

0、前言

  本文主要介紹如何抓取網頁中的內容、如何解決亂碼問題、如何解決登陸問題以及對所採集的數據進行處理顯示的過程。效果以下所示:html

 

 

一、下載網頁並加載至HtmlAgilityPack

  這裏主要用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

 

四、Html解析

  這裏使用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;
    }
}

 

七、使用.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;

 

八、結語

  以上就是給本身常常訪問的網站作信息抓取的實踐了,實際上作出的東西對我來講是頗有用的,我不再會像之前那樣,隔一下子就要打開網站看追的美劇有沒有更新了。對博客按推薦數排序,是一種比較高效的方式了。

  因爲代碼中有個人Cookie,就不放出下載了。

  應要求,給個demo,我把須要登陸的哪些網站去掉了,保留了一個福利網站。

相關文章
相關標籤/搜索