65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# 通过 POST 获取和解析 HTML 数据

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (15投票s)

2015年10月16日

CPOL

5分钟阅读

viewsIcon

78673

downloadIcon

2424

本文介绍了一个完整的C#解决方案,用于通过POST请求从远程服务器获取HTML数据,并通过解析其DOM结构来提取字段。

引言

从格式化或非格式化数据源中提取数据并进行进一步处理是一项基本技能。典型的例子是访问网站,抓取HTML页面并获取一些有用的信息。完成此操作的基本步骤是:

  1. 创建HTTP请求以获取HTML数据
  2. 从HTML代码中提取信息
  3. 将转储数据处理到本地数据库以进行进一步处理

背景

有两种基本方法可以通过HTTP从Internet检索数据:GETPOST。大多数网站支持GET方法返回数据。您可以通过在浏览器中输入URL来发起GET请求。但是,一些网站出于安全考虑或请求参数长度限制,只接受POST方法返回数据。

例如,MSDN博客搜索表单支持POST请求。

另一个例子是跟踪快递状态。由于需要处理敏感数据,承运商要么提供公共Web服务允许用户检索数据,要么只提供在线查询,通过提交表单使用POST。为了获取信息,我们必须模拟POST请求并解析返回的HTML数据以提取详细信息。

Using the Code

在本教程中,您将从speedy.ca获取跟踪事件日志。步骤如下:

  1. 创建一个ASP.NET Web Forms项目
  2. 添加一个Web窗体,其中包含两个文本框,一个按钮,两个标签和一个GridView。您将使用GridView显示结果。

  3. 双击按钮并实现事件处理程序
    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;
        }
    }
  4. 实现GrabHtmlData()ParseData()方法。

    1. 获取HTML数据

    发送POST请求有多种方法。您可以使用Fiddler、Postman等第三方工具创建POST请求。您也可以以编程方式创建POST请求。在C#中,System.Net命名空间中的WebRequestWebClient类允许您执行此操作。 

    获取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包含HtmlDocumentHtmlElementHtmlNodeCollection类(在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.StringSystem.Text.StringBuilder中的方法来完成这些任务,但这需要大量的C#代码。您可以使用正则表达式通过几行代码来实现这些。

    步骤
    1. 实例化一个System.Text.RegularExpressions.Regex对象。
    2. 传入要处理的string
    3. 传入一个正则表达式。
    4. 开始匹配并处理返回的组。

    例如

    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数据的步骤

    1. 创建一个HtmlWeb对象。它是一个通过HTTP获取HTML的实用工具。
    2. 创建一个HtmlDocument对象来接收HTML数据。
    3. 通过getElementById()方法找到一个HtmlNode对象。
    4. 使用节点的属性,如ChildNodesFirstChildNextSiblingParentNode来导航节点。或者,使用Ancestors()Descendants()方法获取节点祖先或后代的列表。
    5. 从节点创建HtmlAttribute对象,并通过节点的Attributes属性提取数据,或者使用innerTextinnerHtml属性提取文本。

示例代码

// 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&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
			Delivery ETA: 09-Oct-2015&nbsp;(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'>&nbsp</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'>&nbsp</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 != "&nbsp;")
            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日:添加最终项目截图
© . All rights reserved.