使用 C# 通过 POST 获取和解析 HTML 数据
本文介绍了一个完整的C#解决方案,用于通过POST请求从远程服务器获取HTML数据,并通过解析其DOM结构来提取字段。
引言
从格式化或非格式化数据源中提取数据并进行进一步处理是一项基本技能。典型的例子是访问网站,抓取HTML页面并获取一些有用的信息。完成此操作的基本步骤是:
- 创建HTTP请求以获取HTML数据
- 从HTML代码中提取信息
- 将转储数据处理到本地数据库以进行进一步处理
背景
有两种基本方法可以通过HTTP从Internet检索数据:GET
和POST
。大多数网站支持GET
方法返回数据。您可以通过在浏览器中输入URL来发起GET
请求。但是,一些网站出于安全考虑或请求参数长度限制,只接受POST
方法返回数据。
例如,MSDN博客搜索表单支持POST
请求。
另一个例子是跟踪快递状态。由于需要处理敏感数据,承运商要么提供公共Web服务允许用户检索数据,要么只提供在线查询,通过提交表单使用POST
。为了获取信息,我们必须模拟POST
请求并解析返回的HTML数据以提取详细信息。
Using the Code
在本教程中,您将从speedy.ca获取跟踪事件日志。步骤如下:
- 创建一个ASP.NET Web Forms项目
- 添加一个Web窗体,其中包含两个文本框,一个按钮,两个标签和一个GridView。您将使用GridView显示结果。
- 双击按钮并实现事件处理程序
protected void btpParse_Click(object sender, EventArgs e) { String website = txtWebsite.Text; String param = txtParam.Text; String htmlData = GrabHtmlData(website, param); // to be implemented if (htmlData != null) { lblGrabResult.Text = "Size of html data: " + htmlData.Length + " characters"; List<EventLog> logs = ParseData(htmlData); // to be implemented lblParseResult.Text = "Count of items: " + logs.Count; // bind results to gridview gvParseResult.DataSource = logs; gvParseResult.DataBind(); } else { lblGrabResult.Text = "Failed to grab html data"; } }
在这里,您定义一个名为
EventLog
的类来保存事件日志。public class EventLog { public DateTime EventDate { get; set; } public String EventName { get; set; } public String EventLocation { get; set; } public override string ToString() { return EventDate + " " + EventName + " " + EventLocation; } }
- 实现
GrabHtmlData()
和ParseData()
方法。1. 获取HTML数据
发送
POST
请求有多种方法。您可以使用Fiddler、Postman等第三方工具创建POST
请求。您也可以以编程方式创建POST
请求。在C#中,System.Net
命名空间中的WebRequest
和WebClient
类允许您执行此操作。获取HTML数据的Way1 - 通过HttpWebRequest
// create http request HttpWebRequest request = WebRequest.CreateHttp(website) as HttpWebRequest; // set post request.Method = "POST"; request.KeepAlive = false; request.ContentType = "application/x-www-form-urlencoded"; // add post data string postData = "pro=" + param; byte[] byteArray = Encoding.UTF8.GetBytes(postData); request.ContentLength = byteArray.Length; using (Stream stream = request.GetRequestStream()) { stream.Write(byteArray, 0, byteArray.Length); } // get response var response = request.GetResponse() as HttpWebResponse; using (var stream = response.GetResponseStream()) { StreamReader reader = new StreamReader(stream); return reader.ReadToEnd(); }
获取HTML数据的Way2 - 通过WebClient
private string GrabHtmlDatabyWebClient(string website, string param) { using (var client = new WebClient()) { // create post parameters var values = new NameValueCollection(); values["pro"] = param; // get response var response = client.UploadValues(website, values); return Encoding.Default.GetString(response); } }
2. 解析HTML数据
在C#中有多种从HTML文档中提取数据的方法。
Way1 - 通过HtmlDocument
.NET包含
HtmlDocument
、HtmlElement
和HtmlNodeCollection
类(在System.Windows.Forms
命名空间下)来解析HTML页面。HtmlDocument
提供了基本的DOM方法,如GetElementById()
和GetElementsByTagName()
。例如:查找并打印表格中的所有链接。
<table> ... <div class="photoBox"> <a href="/user_details?userid=9HuMj3ePDGWR7vs3kLfZGg"> <img width="100" height="100" alt="Photo of Terry" src="http://s3-cdn.azure.com/photo/uiBlab5eTCJJuUrpdauA/ms.jpg"> </a> </div> ... </table>
解析HTML片段的示例代码
String htmlData = ..; // load html data HtmlDocument doc = new HtmlDocument(); doc.LoadHtml(htmlData); // locate "table" node HtmlNodeCollection col = doc.DocumentNode.SelectNodes("//table"); foreach (HtmlNode node in col) { // find all links String link = node.Attributes["href"].Value; Console.WriteLine(link); }
类似地,.NET也提供了
XmlDocument
类和XmlNode
来解析XML文档。Way2 - 通过正则表达式
正则表达式语言旨在识别字符模式。它通常用于实现文本的搜索、读取、替换和修改。使用正则表达式,您可以对
string
执行非常复杂和高级的操作:- 验证文本输入,如电子邮件、密码和电话号码。
- 将文本数据解析为更结构化的形式,例如,从HTML页面提取链接、图片、标题。
- 处理文档中的文本模式,例如,计算重复的单词并替换单词。
当然,您可以使用
System.String
和System.Text.StringBuilder
中的方法来完成这些任务,但这需要大量的C#代码。您可以使用正则表达式通过几行代码来实现这些。步骤
- 实例化一个
System.Text.RegularExpressions.Regex
对象。 - 传入要处理的
string
。 - 传入一个正则表达式。
- 开始匹配并处理返回的组。
例如
string htmlData = ...; // this regular expression matches <div class="adxTOCTitleMycenNews"> // <a href="http://www.example.com">News title...</a> // define a backreference "title" in the pattern string pattern = @"<[dD][iI][vV]\sclass=[\""\']?adxTOCTitleMycenNews[\""\']?> <[aA]\shref=[\""\']?[^>]*>(?<title>[^<]*)<\/[aA]>"; // create regex object Regex r = new Regex(pattern); // find match MatchCollection mc = r.Matches(htmlData); //parse the news title foreach (Match m in mc) { GroupCollection gc = m.Groups; Console.WriteLine("News title: " + gc["title"].Value); }
然而,Regex在从HTML数据中提取数据方面效率较低。在实际应用中,HTML文档可能格式不正确,各式各样,甚至包含空标签,如
<br>
、<br/>
。编写一个合适的Regex来识别目标HTML标签很复杂。例如,考虑到<input/>
、<input type=text name=firstName value=>
和<input type="text" name="firstName" value=""/>
的不同格式,编写一个能考虑所有情况的Regex是一项艰巨的任务。Way3 - 通过第三方库
Html Agility Pack (HAP) 是一个HTML解析器,它构建了一个可读写的DOM。HAP增强了内部的.NET
HtmlDocument
。它允许您以一种方便的方式解析HTML文件。HAP还支持Linq to Objects。主要优点是,该解析器对“真实世界”的格式不正确的HTML(即缺少正确的闭合标签或大写标签)非常宽容。HAP会遍历页面内容并构建文档对象模型,然后可以进行处理。文档加载后,您可以通过循环节点开始解析数据。要开始使用HAP,您应该安装名为
HtmlAgilityPack
的NuGet包。您也可以在https://nuget.net.cn/packages/Fizzler.Systems.HtmlAgilityPack/下载。
解析HTML数据的步骤
- 创建一个
HtmlWeb
对象。它是一个通过HTTP获取HTML的实用工具。 - 创建一个
HtmlDocument
对象来接收HTML数据。 - 通过
getElementById()
方法找到一个HtmlNode
对象。 - 使用节点的属性,如
ChildNodes
、FirstChild
、NextSibling
和ParentNode
来导航节点。或者,使用Ancestors()
和Descendants()
方法获取节点祖先或后代的列表。 - 从节点创建
HtmlAttribute
对象,并通过节点的Attributes
属性提取数据,或者使用innerText
和innerHtml
属性提取文本。
示例代码
// Create an HtmlDocument object from URL
//HtmlWeb htmlWeb = new HtmlWeb();
//HtmlDocument htmlDocument =
// htmlWeb.Load("https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks");
// Create an HtmlDocument object from a html file
HtmlDocument document = new HtmlDocument();
doc.Load("file.htm");
// Find a specific node
HtmlNode myNode = document.GetElementbyId("mynode");
// Identify all links within that node
foreach(HtmlNode link in myNode.Descendants("//a[@href"])
{
// Extract data from HtmlAttribute objects
HtmlAttribute attr = link.Attributes["href"];
Console.WriteLine(attr.Value);
}
为了从以下表格中提取跟踪事件数据,您需要遍历每一行并从每个列中获取数据。
示例HTML代码
<div class="ServicesResults">
...
<table width=498 border=0 class='textTracing'>
<tr id='TableTitle'>
<td colspan=4 id='ProbillHeader'>
Shipment Timeline for 3009000
Delivery ETA: 09-Oct-2015 (Appointment)
</td>
</tr>
<tr>
<td class='ColumnHeader' id='DateHeader'>Date</td>
<td class='ColumnHeader' id='TimeHeader'>Time</td>
<td class='ColumnHeader' id='StatusHeader'>Status</td>
<td class='ColumnHeader' id='LocHeader'>Location</td>
</tr>
<form id='FindPOD' name='FindPOD' action='findpod.asp' method='post'>
<tr>
<td class='ColumnValue'>Wed 07-Oct-2015</td>
<td class='ColumnValue'>01:08Hrs</td>
<td class='ColumnValue'>Bill Entered</td>
<td class='ColumnValue'>Toronto, ON</td>
</tr>
<tr>
<td class='ColumnValue'> </td>
<td class='ColumnValue'>05:10Hrs</td>
<td class='ColumnValue'>Loaded-Tor-Mtl</td>
<td class='ColumnValue'>Toronto, ON</td>
</tr>
<tr>
<td class='ColumnValue'> </td>
<td class='ColumnValue'>06:05Hrs</td>
<td class='ColumnValue'>Enroute to Montreal ex-Toronto</td>
<td class='ColumnValue'>Toronto, ON</td>
</tr>
<tr>
<td class='ColumnValue'>Thu 08-Oct-2015</td>
<td class='ColumnValue'>13:05Hrs</td>
<td class='ColumnValue'>Appointment Set 09-Oct-2015 08:00 - 12:00Hrs</td>
<td class='ColumnValue'>Toronto, ON</td>
</tr>
...
</tr>
</table>
</div>
请注意,该表格有一个CSS类“textTracing
”,并且包含在一个具有另一个CSS类“ServicesResults
”的div
中。为了通过识别CSS节点来获取节点并提取数据,您可以安装另一个名为Fizzler的库,该库是HTML Agility Pack中HtmlNode
的扩展。Fizzler是一个轻量级的.NET CSS选择器引擎,它使您能够通过CSS选择器从节点树中选择项。
要安装Fizzler,您应该安装名为Fizzler.Systems.HtmlAgilityPack
的NuGet包。
您也可以在https://nuget.net.cn/packages/Fizzler.Systems.HtmlAgilityPack/安装。
您可以调用QuerySelectorAll()
方法(接受类名作为参数)来获取节点并从节点中提取数据。到目前为止,您已经获取了HTML数据,下一步是将数据传递给HtmlDocument
对象,并进一步提取数据。
private List<EventLog> ParseData(string htmlData)
{
var logs = new List<EventLog>();
// load data
var document = new HtmlAgilityPack.HtmlDocument();
document.LoadHtml(htmlData);
// find the log table
var resultNode = document.DocumentNode.QuerySelector(".ServicesResults .textTracing");
// extract data
String date = null;
String time = null;
String status = null;
String location = null;
foreach (var rowNode in resultNode.Descendants("tr").Skip(2))
{
var colNodes = rowNode.Descendants("td");
if (colNodes.Count() != 4)
{
throw new InvalidOperationException("The website modify the format result results");
}
var colList = colNodes.ToList();
if (colList[0].InnerText != " ")
date = colList[0].InnerText;
time = colList[1].InnerText;
status = colList[2].InnerText;
location = colList[3].InnerText;
var log = new EventLog()
{
EventDate = DateTime.Now,
EventName = status,
EventLocation = location
};
logs.Add(log);
}
return logs;
}
最终演示
编译项目后,您将得到一个填满了解析后的事件日志的GridView
。
历史
- 2015年10月11日:发布第一个版本
- 2015年10月20日:添加最终项目截图