摘要: 本来我没有写年终总结的习惯,不过面对刚刚过去的2018,可能是改变我人生轨迹的3个重要年份之一,所以我有责任来简单回顾下 离开了携程,离开了美团 我18年换了两次工作,而在这之前我在携程待了将近5年,记得从携程离职的时候还挺伤感的,详见《再见,携程;再见,五年》 携程是那种非典型的互联网公司,不像 阅读全文
posted @ 2019-01-02 07:24 zhangkai2237 阅读(381) 评论(0) 推荐(1) 编辑

0. 序言

在开发我最优惠网的过程中,遇到一些问题和技术点,写出来和大家分享,也是我自己对近期工作的整理和记录,预计会有解析HTML类库、本地缓存、链接跳转和C#中执行js代码技巧等方面。

 

1. HtmlAgilityPack简介

网站中首先遇到的问题是爬虫和解析HTML的问题,一般情况在获取页面少量信息的情况下,我们可以使用正则来精确匹配目标。不过本身正则表达式就比较复杂,同时正则表达式的精确程度很难拿捏,太精确和原网页耦合太严重,页面代码稍改动就会使正则无效;太宽泛的正则由可能会匹配目标过多。所以我们今天介绍的是通过解析HTML结构来获取目标的方式——HtmlAgilityPack。

HtmlAgilityPack是一个解析HTML的类库,支持用XPath来解析HTML,可以像XML一样来解析HTML。

HtmlAgilityPack的代码托管在codeplex上:http://htmlagilitypack.codeplex.com/,不过建议通过Nuget来获取最新版本。

 

 

2. XPath简介

XPath即为XML路径语言,它是一种用来确定XML文档中某部分位置的语言。XPath基于XML的树状结构,提供在数据结构树中找寻节点的能力。下图列举了XPath主要的路径表达式:

这种针对XML的路径能在解析HTML中用的原因是HtmlAgilityPack将下载下来的HTML页面进行规格化处理,让原本对语义支持并不好的HTML文档格式变为更严谨的Xhtml格式,甚至可以转换为XML格式;并使用XPath来选择、处理dom中的element。下图表示HTML格式化之后的节点示意图:

 

3. HtmlAgilityPack中常用的API

 在HtmlAgilityPack中常用到的类有HtmlDocument、HtmlNodeCollection、HtmlNode和HtmlWeb。

首先是加载HTML,如果是已经存在的静态HTML代码,可以用HtmlDocument的Load()或LoadHtml()来加载,如果是网络上的URL则需要用HtmlWeb的Get()或Load()方法来加载。

不管是哪种加载方式,我们得到的都是HtmlDocument的实例。此时我们需要得到的是HtmlNode或者HtmlNodeCollection对象,使用HtmlDocument的DocumentNode属性,它整个HTML文档的根节点,它本身也是一个HtmlNode。

得到文档根节点后即可使用前一节介绍的XPath得到你想要的文档中任意一个节点的信息。

 

下面是一个典型的获得有效内容的例子:

1
2
3
4
HtmlWeb htmlWeb = new HtmlWeb();
HtmlDocument htmlDoc = htmlWeb.Load("http://www.baidu.com");
HtmlNode htmlNode = htmlDoc.DocumentNode.SelectSingleNode("//title");
string title = htmlNode.InnerText;

  

由于使用最多的类是HtmlNode,这里将它常用的属性和方法列在下面,方便各位查阅。

属性:

Attributes              获取节点的属性集合
ChildNodes             获取子节点集合(包括文本节点)
FirstChild              获取第一个子节点
HasAttributes            判断该节点是否含有属性
HasChildNodes          判断该节点是否含有子节点
Id                 获取该节点的Id属性
InnerHtml             获取该节点的Html代码
InnerText             获取该节点的内容,与InnerHtml不同的地方在于它会过滤掉Html代码,而InnerHtml是连Html代码一起输出
LastChild              获取最后一个子节点
Name                Html元素名
NextSibling            获取下一个兄弟节点
ParentNode            获取该节点的父节点
PreviousSibling          获取前一个兄弟节点
XPath                根据节点返回该节点的XPath

 

方法:

HtmlNode AppendChild(HtmlNode newChild);              将参数元素追加到为调用元素的子元素(追加在最后)
void AppendChildren(HtmlNodeCollection newChildren);       将参数集合中的元素追加为调用元素的子元素(追加在最后)
HtmlNode PrependChild(HtmlNode newChild);              将参数中的元素作为子元素,放在调用元素的最前面
void PrependChildren(HtmlNodeCollection newChildren);       将参数集合中的所有元素作为子元素,放在调用元素前面
HtmlNode Clone();                           本节点克隆到一个新的节点
HtmlNode CloneNode(bool deep);                   节点克隆到一个新的几点,参数确定是否连子元素一起克隆
HtmlNode CloneNode(string newName);                克隆的同时更改元素名
HtmlNode CloneNode(string newName, bool deep);           克隆的同时更改元素名。参数确定是否连子元素一起克隆
void CopyFrom(HtmlNode node);                    创建重复的节点和其下的子树。
void CopyFrom(HtmlNode node, bool deep);               创建节点的副本。
static HtmlNode CreateNode(string html);                静态方法,允许用字符串创建一个新节点
IEnumerable<HtmlNode> DescendantNodes();             获取所有子代节点
IEnumerable<HtmlNode> DescendantNodesAndSelf();         获取所有的子代节点以及自身
IEnumerable<HtmlNode> Descendants();               获取枚举列表中的所有子代节点
IEnumerable<HtmlNode> Descendants(string name);         获取枚举列表中的所有子代节点,注意元素名要与参数匹配
IEnumerable<HtmlNode> DescendantsAndSelf();           获取枚举列表中的所有子代节点以及自身
IEnumerable<HtmlNode> DescendantsAndSelf(string name);     获取枚举列表中的所有子代节点以及自身,注意元素名要与参数匹配
HtmlNode Element(string name);                   根据参数名获取一个元素
IEnumerable<HtmlNode> Elements(string name);          根据参数名获取匹配的元素集合
bool GetAttributeValue(string name, bool def);             帮助方法,用来获取此节点的属性的值(布尔类型)。如果未找到该属性,则将返回默认值。
int GetAttributeValue(string name, int def);              帮助方法,用来获取此节点的属性的值(整型)。如果未找到该属性,则将返回默认值。
string GetAttributeValue(string name, string def);          帮助方法,用来获取此节点的属性的值(字符串类型)。如果未找到该属性,则将返回默认值。
HtmlNode InsertAfter(HtmlNode newChild, HtmlNode refChild);     将一个节点插入到第二个参数节点的后面,与第二个参数是兄弟关系
HtmlNode InsertBefore(HtmlNode newChild, HtmlNode refChild);   讲一个节点插入到第二个参数节点的后面,与第二个参数是兄弟关系
static bool IsCDataElement(string name);                确定是否一个元素节点是一个 CDATA 元素节点。
static bool IsClosedElement(string name);               确定是否封闭的元素节点
static bool IsEmptyElement(string name);                 确定是否一个空的元素节点。
static bool IsOverlappedClosingElement(string text);            确定是否文本对应于一个节点可以保留重叠的结束标记。
void Remove();                              从父集合中移除调用节点
void RemoveAll();                            移除调用节点的所有子节点以及属性
void RemoveAllChildren();                        移除调用节点的所有子节点
HtmlNode RemoveChild(HtmlNode oldChild);              移除调用节点的指定名字的子节点
HtmlNode RemoveChild(HtmlNode oldChild, bool keepGrandChildren);移除调用节点调用名字的子节点,第二个参数确定是否连孙子节点一起移除
HtmlNode ReplaceChild(HtmlNode newChild, HtmlNode oldChild);   将调用节点原有的一个子节点替换为一个新的节点,第二个参数是旧节点
HtmlNodeCollection SelectNodes(string xpath);             根据XPath获取一个节点集合
HtmlNode SelectSingleNode(string xpath);                根据XPath获取唯一的一个节点
HtmlAttribute SetAttributeValue(string name, string value);       设置调用节点的属性
string WriteContentTo();                           将该节点的所有子级都保存到一个字符串中。
void WriteContentTo(TextWriter outText);                将该节点的所有子级都保存到指定的 TextWriter。
string WriteTo();                            将当前节点保存到一个字符串中。
void WriteTo(TextWriter outText);                    将当前节点保存到指定的 TextWriter。
void WriteTo(XmlWriter writer);                     将当前节点保存到指定的则 XmlWriter。

 

4. 实战

 基础的都熟悉了,我们来做练习。例子就是在开发我最优惠网中实际使用的代码,获取什么值得买发现频道的商品名称、价格和详情页网址。代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void Main(string[] args)
{
    HtmlWeb htmlWeb = new HtmlWeb();
    HtmlDocument htmlDoc = htmlWeb.Load("http://faxian.smzdm.com");
    HtmlNode htmlNode = htmlDoc.DocumentNode.SelectSingleNode("//ul[@class='leftWrap discovery_list']");
 
    foreach (var li in htmlNode.SelectNodes("child::li"))
    {
        Console.WriteLine("名称:" + li.SelectSingleNode("child::div[@class='listItem']/h2/a/span[1]").InnerText);
        Console.WriteLine("价格:" + li.SelectSingleNode("child::div[@class='listItem']/h2/a/span[2]").InnerText);
        Console.WriteLine("网址:" + li.SelectSingleNode("child::div[@class='listItem']/h2/a").GetAttributeValue("href", ""));
        Console.WriteLine("---------------------------------------------------------------------------");
        Thread.Sleep(1000);
    }
    Console.ReadLine();
}   

  

代码下载:http://pan.baidu.com/s/1pLkq6E3

 

5. 后记

HtmlAgilityPack 的确是一个功能强大的HTML解析类库,我目前仅仅使用了它的一小部分功能,但是已经能完全满足我的需求。如果童鞋们有类似需求,可以试试。

最后再次打个广告:网购前记得去我最优惠网查查最低价哦^_^

 

 

参考文档:

http://www.cnblogs.com/oec2003/p/3322956.html

http://zhoufoxcn.blog.51cto.com/792419/595344/

http://www.cnblogs.com/kissdodog/archive/2013/02/28/2936950.html

http://www.tuicool.com/articles/YZ3uau

 

posted @ 2016-04-05 07:41 zhangkai2237 阅读(1882) 评论(3) 推荐(0) 编辑
摘要: 前段时间决定将自己用了三年多的Lumia 800正式退役,这是我用的时间最长的手机,虽然系统上有缺陷,但是好不妨碍他成为我最有感情的一部手机。由于之前是WinPhone 开发者的关系,这部手机是微软送的,眼睁睁的看着WinPhone系统从出生到长大,还没成熟时就蔫了的历程,心里很不是滋味。这篇文章先晒晒被我用过的Lumia手机,然后再分析下WinPhone发展成现在这样微软需要承担的责任。 阅读全文
posted @ 2015-10-06 11:44 zhangkai2237 阅读(10473) 评论(114) 推荐(58) 编辑
摘要: 相机功能是手机区别于PC的一大功能,在做手机应用时,如果合理的利用了拍照功能,可能会给自己的应用增色很多。使用Windows Phone的相机功能,有两种方法,一种是使用PhotoCamera类来构建自己的相机UI,另外一种是通过CameraCaptureTask选择器来实现该功能。 阅读全文
posted @ 2014-04-05 17:53 zhangkai2237 阅读(1832) 评论(0) 推荐(0) 编辑
摘要: 在面试中, 面试官提到装箱和拆箱的问题时,可能很多人想到的第一句话是“装箱是将值类型转化为引用类型的过程;拆箱是将引用类型转化为值类型的过程”。这句话没有问题,但是仅仅只说出这句话而没有下文的话那就不是一个中级.Net程序员的水平。 实际上装箱和拆箱这个名字就很形象,“箱”指的就是托管堆,装箱即指在托管堆中将在栈上的值类型对象封装,生成一份该值类型对象的副本,并返回该副本的地址。而拆箱即是指返回已装箱值类型在托管堆中的地址(注意:严格意义来说拆箱是不包括值类型字段的拷贝的)。 阅读全文
posted @ 2013-03-21 23:29 zhangkai2237 阅读(9436) 评论(13) 推荐(9) 编辑
摘要: 前段时间准备换个工作,所以参加了几场面试,面试的中主要考察的是C#相关知识的掌握以及实际项目中的经验。项目经验没办法整理,每个人都不一样,但是考察的基础知识我们可以归纳总结,对自己是个整体总结提高的过程。 阅读全文
posted @ 2013-03-17 15:27 zhangkai2237 阅读(4315) 评论(9) 推荐(5) 编辑
摘要: 页面重定向是一个不大不小的问题, 说它不大是因为我们作为web开发者经常会遇到页面重定向的问题, 而我们平时也似乎能很好的把这个问题给解决掉. 说他不小是因为虽然我们都知道部分重定向的方法, 但是我们并没有完整的了解所有页面重定向的方法, 同时也并不是特别的清楚他们之间的区别. 这就造成了我们在选择页面重定向的方式上大多数的时候是盲目的. 本篇文章希望和大家一起将几种页面重定向的方法做一个总结和比较, 为以后的工作和学习带来一点点的帮助. 阅读全文
posted @ 2013-01-27 18:19 zhangkai2237 阅读(3092) 评论(7) 推荐(7) 编辑
摘要: 前几篇把asp.net mvc3 中基于Model的主要数据验证的方法都已经讲完了,本节纯粹只是讲一个我曾经遇到的问题,因为和数据验证相关,所以就放在了这系列的文章中。 经过前几篇文章的分享,大家应该觉得asp.net 自带的Remote远程数据注解比较好用,但是我在实际使用中却遇到了问题。 阅读全文
posted @ 2013-01-24 11:10 zhangkai2237 阅读(5609) 评论(3) 推荐(7) 编辑
摘要: 有时候我们会在javascript中调用方法时, 只要方法名写对了, 即使参数列表不对也可以正常调用. 当然是不是跟你预想的一样工作时另外一回事儿. 本来倒是没注意这个现象, 因为我每次在js中调用方法时都是严格安装输入的参数列表来传数据的, 也没有吧两个js方法使用相同的命名, 但是刚好今天一个同事遇到了, 所以就来研究一下. 阅读全文
posted @ 2012-12-29 22:35 zhangkai2237 阅读(862) 评论(0) 推荐(0) 编辑
摘要: 相信大家都对面向对象的三个特征封装、继承、多态很熟悉,每个人都能说上一两句,但是大多数都仅仅是知道这些是什么,不知道CLR内部是如何实现的,所以本篇文章主要说说多态性中的一些概念已经内部实现的机理。 阅读全文
posted @ 2012-12-20 17:20 zhangkai2237 阅读(38269) 评论(25) 推荐(33) 编辑
点击右上角即可分享
微信分享提示