實用網頁抓取


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,我把需要登錄的哪些網站去掉了,保留了一個福利網站。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM