本文主要介绍如何抓取网页中的内容、如何解决乱码问题、如何解决登陆问题以及对所采集的数据进行处理显示的过程。效果以下所示:html
这里主要用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
这里使用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; } }
程序会自动记录浏览记录,且已浏览的连接再也不显示出来。这里比较耗时的功能有:从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,我把须要登陆的哪些网站去掉了,保留了一个福利网站。