• 实用网页抓取


    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(@"httpshellopencommand");
                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,我把需要登录的哪些网站去掉了,保留了一个福利网站。

  • 相关阅读:
    react 性能优化
    JS获取当前网页大小以及屏幕分辨率等
    创建对象的6种方式总结
    版本号规则
    JS事件模型
    浅谈虚拟DOM
    浏览器的回流与重绘
    JavaScript预编译
    canvas学习笔记
    java、tomcat安装
  • 原文地址:https://www.cnblogs.com/yao2yao4/p/3644222.html
Copyright © 2020-2023  润新知