第一次接觸HtmlAgilityPack是在5年前,一些意外,讓我從技術部門臨時調到銷售部門,負責建立一些流程和尋找潛在客戶,最后在阿里巴巴找到了很多客戶信息,非常全面,剛開始是手動復制到Excel,是真尼瑪的累,雖然那個時候C#還很菜,也想能不能通過程序來批量獲取(所以平時想法要多才好)。幾經周折,終於發現了HtmlAgilityPack神器,這幾年也用HtmlAgilityPack采集了很多類型數據,特別是足球賽事資料庫的數據采集以及天氣數據采集,都是使用HtmlAgilityPack,所以把自己的使用過程總結下來,分享給大家,讓更多人接觸和學會使用,給自己的工作帶來遍歷。
今天的主要內容是HtmlAgilityPack的基本介紹、使用,實際代碼。最后我們以采集天氣數據為例子,來介紹實際的采集分析過程和簡單的代碼。我們將在下一篇文章中開源該天氣數據庫和C#操作代碼。采集核心就只是在這里介紹,其實核心代碼都有了,自己加工下就可以了,同時也免費對有需要的人開放。至於具體詳情,請關注下一篇文章。
.NET開源目錄:【目錄】本博客其他.NET開源項目文章目錄
本文原文地址:C#+HtmlAgilityPack+XPath帶你采集數據(以采集天氣數據為例子)
1.HtmlAgilityPack簡介
HtmlAgilityPack是一個開源的解析HTML元素的類庫,最大的特點是可以通過XPath來解析HMTL,如果您以前用C#操作過XML,那么使用起HtmlAgilityPack也會得心應手。目前最新版本為1.4.6,下載地址如下:http://htmlagilitypack.codeplex.com/ 目前穩定的版本是1.4.6,上一次更新還是2012年,所以很穩定,基本功能全面,也沒必要更新了。
提到HtmlAgilityPack,就必須要介紹一個輔助工具,不知道其他人在使用的時候,是如何分析頁面結構的。反正我是使用官方提供的一個叫做HAPExplorer的工具。非常有用。下面我們在使用的時候會介紹如何使用。
2.XPath技術介紹與使用
2.1 XPath介紹
XPath即為XML路徑語言,它是一種用來確定XML(標准通用標記語言的子集)文檔中某部分位置的語言。XPath基於XML的樹狀結構,提供在數據結構樹中找尋節點的能力。起初 XPath 的提出的初衷是將其作為一個通用的、介於XPointer與XSL間的語法模型。但是 XPath 很快的被開發者采用來當作小型查詢語言。
XPath是W3C的一個標准。它最主要的目的是為了在XML1.0或XML1.1文檔節點樹中定位節點所設計。目前有XPath1.0和XPath2.0兩個版本。其中Xpath1.0是1999年成為W3C標准,而XPath2.0標准的確立是在2007年。W3C關於XPath的英文詳細文檔請見:http://www.w3.org/TR/xpath20/ 。
2.2 XPath的路徑表達
XPath是XML的查詢語言,和SQL的角色很類似。以下面XML為例,介紹XPath的語法。下面的一些資料是幾年前學習這個的時候,從網絡以及博客園獲取的一些資料,暫時找不到出處,例子和文字基本都是借鑒,再次謝過。如果大家發現類似的一起文章,告訴我鏈接,我加上引用。下面的Xpath的相關表達也很基礎,基本足夠用了。
<?xml version="1.0"encoding="ISO-8859-1"?> <catalog> <cd country="USA"> <title>Empire Burlesque</title> <artist>Bob Dylan</artist> <price>10.90</price> </cd> </catalog>
定位節點:XML是樹狀結構,類似檔案系統內數據夾的結構,XPath也類似檔案系統的路徑命名方式。不過XPath是一種模式(Pattern),可以選出XML檔案中,路徑符合某個模式的所有節點出來。例如要選catalog底下的cd中所有price元素可以用:
/catalog/cd/price
如果XPath的開頭是一個斜線(/)代表這是絕對路徑。如果開頭是兩個斜線(//)表示文件中所有符合模式的元素都會被選出來,即使是處於樹中不同的層級也會被選出來。以下的語法會選出文件中所有叫做cd的元素(在樹中的任何層級都會被選出來)://cd
選擇未知的元素:使用星號(*)可以選擇未知的元素。下面這個語法會選出/catalog/cd的所有子元素:
/catalog/cd/*
以下的語法會選出所有catalog的子元素中,包含有price作為子元素的元素。
/catalog/*/price
以下的語法會選出有兩層父節點,叫做price的所有元素。
/*/*/price
要注意的是,想要存取不分層級的元素,XPath語法必須以兩個斜線開頭(//),想要存取未知元素才用星號(*),星號只能代表未知名稱的元素,不能代表未知層級的元素。
選擇分支:使用中括號可以選擇分支。以下的語法從catalog的子元素中取出第一個叫做cd的元素。XPath的定義中沒有第0元素這種東西。
/catalog/cd[1]
以下語法選擇catalog中的最后一個cd元素:(XPathj並沒有定義first()這種函式喔,用上例的[1]就可以取出第一個元素。
/catalog/cd[last()]
以下語法選出price元素的值等於10.90的所有/catalog/cd元素
/catalog/cd[price=10.90]
選擇屬性:在XPath中,除了選擇元素以外,也可以選擇屬性。屬性都是以@開頭。例如選擇文件中所有叫做country的屬性:
//@country
以下語法選擇出country屬性值為UK的cd元素
//cd[@country='UK']
3.采集天氣網站案例
3.1 需求分析
我們要采集的是全國各地城市的天氣信息,網站為:http://www.tianqihoubao.com/,該網站數據分為2種類型,1個是歷史數據,覆蓋范圍為2011年至今,1個是天氣預報的數據,歷史數據是天氣后報,也就是實際的天氣數據。采集的范圍必須覆蓋全國主要城市,最好是所有的城市。通過分析該網站的頁面,的確是滿足要求。天氣信息,包括實際的天氣狀況,風力狀況以及氣溫狀況情況,包括最低和最高區間。
結合基本要求,我們進入網站,分析一些大概特點,以及主要頁面的結構。
3.2 網站頁面結構分析
要采集大量的信息,必須對網站頁面進行詳細的分析和總結。因為機器采集不是人工,需要動態構造URL,請求或者頁面html,然后進行解析。所以分析網站頁面結構是第一步,也是很關鍵的一步。我們首先進入到總的歷史頁面:http://www.tianqihoubao.com/lishi/,如下圖:
很明顯,這個總的頁面按省份進行了分開,可以看到每個省份、地級市名稱的鏈接中,都是固定格式,只不過拼音縮寫不同而已。而且每個省份的第一個城市為省會城市。這一點要注意,程序中要區分省會城市和其他地級城市。當然省會城市也可以省略,畢竟只有30多個,手動標記也很快的事情。這個頁面我們將主要采集省份的縮寫信息,然后我們選擇一個省份,點擊進去,看每個省份具體的城市信息,如我們選擇遼寧省:http://www.tianqihoubao.com/lishi/ln.htm如下圖:
同樣,每個省份下面的地區也有單獨的鏈接,格式和上面的類似,按照城市拼音。我們看到每個省份下面,有大的地級行政區,每個地級市區后面細分了小的縣市區。我們隨意點擊大連市的鏈接,進去看看具體的天氣歷史信息:
該頁面包括了城市2011年1月到2015年至今的歷史數據,按月分開。鏈接的特點也很固定,包括了城市名稱的拼音和年份月份信息。所以構造這個鏈接就很容易了。下面看看每個月份的情況:
廣告我屏蔽了一些,手動給抹掉吧。每個城市的每個月的天氣信息比較簡單,直接表格填充了數據,日期,天氣狀況,氣溫和風力。這幾步都是按照頁面的鏈接一步一步引導過來的,所以上述流程清楚了,要采集的信息也清楚了,有了大概的思路:
先采集整個省份的拼音代碼,然后依次獲取每個省份每個地級市,以及對應縣級市的名稱和拼音代碼,最后循環每個縣級市,按照月份獲取所有歷史數據。下面將重點分析幾個頁面的節點情況,就是如何用HtmlAgilityPack和Xpath來獲取你要的數據信息,至於保存到數據庫,八仙過海各顯神通吧,我用的是XCode組件。
3.3 分析省-縣市結構頁面
還是以遼寧省為例:http://www.tianqihoubao.com/lishi/ln.htm ,打開頁面,右鍵獲取網頁源代碼后,粘貼到 HAPExplorer 中,也可以直接在HAPExplorer 中打開鏈接,如下面的動畫演示:
我們可以看到,右側的XPath地址,div結束后,下面都是dl標簽,就是我們要采集的行了。下面我們用代碼來獲取上述結構。先看看獲取頁面源代碼的代碼:
public static string GetWebClient(string url) { string strHTML = ""; WebClient myWebClient = new WebClient(); Stream myStream = myWebClient.OpenRead(url); StreamReader sr = new StreamReader(myStream, Encoding.Default);//注意編碼 strHTML = sr.ReadToEnd(); myStream.Close(); return strHTML; }
下面是分析每個省份下屬縣市區的程序,限於篇幅我們省掉了數據庫部分,只采集城市和拼音代碼,並輸出:
/// <summary>添加省級-地區-縣市 的城市信息,注意 省會城市 標記5</summary> /// <param name="cityCode">省份代碼</param> public static void ParsePageByArea(String cityCode) { //更加鏈接格式和省份代碼構造URL String url = String.Format("http://www.tianqihoubao.com/lishi/{0}.htm", cityCode); //下載網頁源代碼 var docText = HtmlHelper.GetWebClient(url); //加載源代碼,獲取文檔對象 var doc = new HtmlDocument(); doc.LoadHtml(docText); //更加xpath獲取總的對象,如果不為空,就繼續選擇dl標簽 var res = doc.DocumentNode.SelectSingleNode(@"/html[1]/body[1]/div[1]/div[6]/div[1]/div[1]/div[3]"); if (res != null) { var list = res.SelectNodes(@"dl");//選擇標簽數組 if (list.Count < 1) return; foreach (var item in list) { var dd = item.SelectSingleNode(@"dd").SelectNodes("a"); foreach (var node in dd) { var text = node.InnerText.Trim(); //拼音代碼要從href屬性中進行分割提取 var herf = node.Attributes["href"].Value.Trim().Split('/', '.'); Console.WriteLine("{0}:{1}", text, herf[herf.Length - 2]); } } } }
我們以遼寧為例,調用代碼:ParsePageByArea("ln");結果如下:
3.4 分析城市單月的歷史天氣頁面
這也是最重要核心的一個要分析的頁面。我們以大連市2011年8月份為例:http://www.tianqihoubao.com/lishi/dalian/month/201108.html,我們要找到我們需要采集的信息節點,如下圖所示的動畫演示,其實這個過程習慣幾次就好了,每一次點擊節點后,要觀察右邊的內容是不是我們想要的,還可以通過滾動條的長度判斷大概的長度。
這里不是直接從URL加載,由於編碼原因,URL加載會有亂碼,所以我是手動輔助源代碼到HAPExplorer中的,效果一樣,所以直接在獲取頁面源代碼的時候,要注意編碼問題。總的過程比較簡單,還是查找到Table標簽的位置,因為那里保存了所需要的數據,每一行每一列都非常標准。過程類似,我們直接更加XPath找到Table,然后一次獲取每行,每列,進行對應即可,看代碼,都進行了詳細的注釋:
/// <summary>采集單個城市單個月的歷史天氣數據</summary> /// <param name="cityCode">城市拼音代碼</param> /// <param name="year">年份</param> /// <param name="month">月份</param> public static void ParsePageByCityMonth(String cityCode, Int32 year, Int32 month) { //更加拼音代碼,月份信息構造URL String url = String.Format("http://www.tianqihoubao.com/lishi/{0}/month/{1}{2:D2}.html", cityCode, year, month); //獲取該鏈接的源代碼 var docText = HtmlHelper.GetWebClient(url); //加載源代碼,獲取頁面結構對象 var doc = new HtmlDocument(); doc.LoadHtml(docText); //更加Xpath獲取表格對象 var res = doc.DocumentNode.SelectSingleNode(@"/html[1]/body[1]/div[2]/div[6]/div[1]/div[1]/table[1]"); if (res != null) { //獲取所有行 var list = res.SelectNodes(@"tr"); list.RemoveAt(0);//移除第一行,是表頭 // 遍歷每一行,獲取日期,以及天氣狀況等信息 foreach (var item in list) { var dd = item.SelectNodes(@"td"); //日期 - - 氣溫 - 風力風向 if (dd.Count != 4) continue; //獲取當前行日期 var date1 = dd[0].InnerText.Replace("\r\n", "").Replace(" ", "").Trim(); //獲取當前行天氣狀況 var tq = dd[1].InnerText.Replace("\r\n", "").Replace(" ", "").Trim(); //獲取當前行氣溫 var qw = dd[2].InnerText.Replace("\r\n", "").Replace(" ", "").Trim(); //獲取當前行風力風向 var fx = dd[3].InnerText.Replace("\r\n", "").Replace(" ", "").Trim(); //輸出 Console.WriteLine("{0}:{1},{2},{3}", date1, tq, qw, fx); } } }
我們調用大連市2011年8月的記錄:ParsePageByCityMonth("dalian",2011,8); 結果如下:
至於其他頁面都是這個思路,先分析xpath,再獲取對應的信息。熟悉幾次后應該會快很多的。HtmlAgilityPack里面的方法用多了,自己用對象瀏覽器查看一些,會一些基本的就可以解決很多問題。
另外,很多網頁都是直接輸出json數據,對json數據的處理我寫過一篇文章,可以參考下,純手工打造的解析json:用原始方法解析復雜字符串,json一定要用JsonMapper么?
4.資源
網易新聞頁面信息抓取 -- htmlagilitypack搭配scrapysharp
C#類似Jquery的html解析類HtmlAgilityPack基礎類介紹及運用
我把分析HAPExplorer 工具共享一些吧,這里下載:HtmlAgilityPack分析工具.rar
我將在下一篇文章中開放這個天氣數據庫,目前正在采集,很大,很慢。敬請關注。