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