Parsing the Web (Views: 102)


Three Classes for Grabbing HTML/XML Information


I recently bought one of the new digital satellite dishes and ran across an interesting challenge - figuring out just what was on, and when. DirectTV provided a Web site with a basic search engine, but the Web-based search was slow and very narrowly focused. As a typical programmer, I knew I could provide a better, more powerful UI, if I could just figure out how their Web search engine worked.

A quick scan of the HTML source pointed out a relatively simple search form that I could easily duplicate, but the HTML results came back in a mildly complicated HTML table. Brute force code would have been simple enough to construct to parse through the table, but I'd been looking for a reason to build a more general parser, so off I went. If I'd known just how lax the HTML rules are, and just how many hacks there are, I'd have just stuck with the brute force method and saved myself a lot of agony, but since I'm here now ...

The Basics

To put together a parser for HTML, an understanding of the rules is required. HTML originated as an SGML-like syntax, and over time has grown to fit more closely within the confines of SGML. These days the syntax is described within an SGML Data Type Definition (DTD) bringing it into a reasonably well-understood and managed domain. Given that SGML now establishes the underpinnings of HTML, the parser should apply the SGML syntax rules as a starting point. This also allows for the consideration of some simple extensions that allow parsing of XML.

Therefore, the parser is built to work on SGML in general, with specific handlers for exceptions and extensions that occur in HTML and XML. The rules for SGML are straightforward, and provide five basic constructs that we care about:

SGML directives, and
"everything else"

Elements are the facet of the SGML content with which we are most concerned, and around which the parser is established. Elements have a start tag, content, and an end tag, for example:

HTML Parsing

where TITLE is considered to be the "name" of the tag. Element names are case-insensitive. So we start with the following parsing rules:

Element start and end tags are surrounded by < and > characters.
Element end tags are denoted by the / character immediately following a <.
Element content is surrounded by start and end tags.

HTML Extensions

In HTML, we immediately note that there are exceptions to these rules. For some elements - most notably

- the end tags may be omitted, even though the element may have contents. This offers perhaps the most annoying challenge of the HTML parsing rule set, because there are several methods by which the element may be terminated.

To start with, we note another syntax rule from SGML: elements may not span. That is, if an element's start tag is contained within another element's start and end tags, its end tag must also appear there. Put simply, if we encounter an end tag, all omitted end tags are considered "closed" back up to the matching start tag. Also, by observation (I couldn't find a formal rule for this in the HTML specification), virtually all elements with optional end tags close when they encounter another of themselves,

  • and
  • element.

    Attributes appear within an element's start tag
    Attributes are delimited by a space character (ASCII 32)
    Attribute values are delimited by " or '


    SGML also provides that its content may include comments. Comments are of the form:

    The ). Further, a comment may contain < and > characters. Comments may not include other comments.

    terminates a comment.
    < and > are ignored while parsing a comment.

    The remaining SGML directives are denoted by the beginning markup declaration open delimiter

    Comments within the directives are delimited by --

    Lastly, we consider what remains. Content of elements not contained within a start tag, end tag, or comment is considered by the parser to be PCData (parsed character data).

    Store text not included in element start/end tags or comments as PCData.

    XML Extensions

    As mentioned before, XML is also derived from SGML. While HTML is basically a DTD described within and using SGML, XML is a subset of SGML capable both of representing data and containing other DTDs of its own. XML also demonstrates that those working on the standards in the programming community actually do learn from the mistakes of those that went before them. For instance, the rules of containment are much more formal in XML than they are in HTML, making parsing a great deal simpler. This means that while a DTD may be included in an XML document for syntax checking purposes, it isn't necessarily required for the actual parsing of the XML content, as it is for HTML.

    Knowing this, we can add two more rules and provide DTD-less XML parsing as well. For one, empty elements in HTML are simply called out as such in the DTD with their end tags forbidden (
    for example). If an element in XML is to be empty (that is, it will have no content) its start tag may have a / just before the closing > indicating that no content and no end tag will follow. Additionally, XML directives may appear with the ? character rather than !.

    Empty elements in XML may be terminated by a / just before the > in the start tag, e.g. .
    Additional directives appear using ?, instead of the ! character.

    Everything Else

    Any items encountered in the content that are not contained in element start or end tags, comments, or DTD items are considered by the parser to be PCData. The content of elements fits this bill, as do carriage returns and line feeds encountered by the parser. This leads to the final parsing rule:

    Any content located outside of start/end tags, comments, or DTD items is PCData.

    Additional Considerations

    It is also worth noting that syntax errors and occurrences of "browser-tolerated" HTML inconsistencies are frequently encountered, and as such, should not raise exceptions except in extreme cases. Instead, a warning should be noted and parsing should continue if possible.

    The Parser

    The goal of parsing the SGML content is to place the data it represents into a form more readily accessible to other components. Since SGML calls out a hierarchical structure a hierarchy is probably the most accurate way to store the parsed content. With that in mind, the parser is built from two primary classes, and a third supporting class.

    First and foremost is the THTMLParser class. Its Parse method accepts the content to be parsed, and places the processed results in the Tree property.
    Next is the TTagNode class in which the parsed results are contained. This class is a hierarchical storage container with Parent pointing to the TTagNode that contains the current node, and Children containing a list of children immediately owned by the current node.
    TTagNodeList is provided as a list container for a collection of TTagNode objects, typically produced by a call to the GetTags method of the TTagNode class.

    A Simple Example

    Consider the sample HTML shown in Figure 1. The parser would produce from the HTML a hierarchy that can be visualized as in Figure 2. Each of the boxes in the tree represents a TTagNode instance.

    Example HTML

    Example HTML

    Plain old text right out there in the middle of the document.

    Text contained within in paragraph

    Unordered List

    • Item #1
    • Item #2
    • Item #3

    Figure 1: Sample HTML.

    Figure 2: Hierarchical representation of parsed HTML.

    Each node has a NodeType property that indicates what type of node it is. All node types except ntePCData also have text in the Caption property that provides more information about the node's contents. See the table in Figure 3 for details.

    Node Contents
    HTML elements
    Element name

    SGML/XML/DTD directives
    ! or ? and directive

    Figure 3: TTagNode node types.

    For example, the HTML node in Figure 2 has a NodeType of nteElement, while the comment tag is of type nteComment, and the PCData nodes are of type ntePCData.

    The content or text for each HTML element is contained in a node in its Children list. For example, the TITLE node in Figure 2 has a PCData node whose Text property contains "Example HTML". The GetPCData method of a TTagNode returns the PCData text for all children of the node for which it's called. Note, this method is recursive and will return the PCData text for all nodes in the tree beneath the node upon which it's called.

    Retrieving Elements

    The GetTags method of a TTagNode will return a list of all children that match an element name. If '*' or '' is specified as the element name, then all children will be returned. Note that this method is recursive. The result list is a TTagNodeList.

    The code in Figure 4 illustrates how the GetTags method is used to collect a list of all
  • elements in the HTML from Figure 1 and insert their contents into a list box. The process is as follows:

    Create a container for the results, i.e. the Elements list.
    Call the GetTags method, passing the desired element name and the container.
    Iterate through the container, placing the text for each element in a list box.
    Destroy the container.

    procedure Button1OnClick(Sender: TObject);
      Elements: TTagNodeList;
      Counter: Integer;
      Elements := TTagNodeList.Create;
      HTMLParser1.Tree.GetTags('LI', Elements);
      for Counter := 0 to Elements.Count - 1 do
    Figure 4: Using GetTags.

    TTagNodeList has two methods that offer another approach (assuming that a result set has already been acquired): GetTagCount and FindTagByIndex. GetTagCount returns a count of the occurrences of an element name. FindTagByIndex returns the index within the list of the specified occurrence of an element name. For instance, the statement:

    ShowMessage(Elements.FindTagByIndex('li', 1).GetPCData);

    were it included in Figure 4, would display the text for the second occurrence of the
  • element in the Elements container. This can prove exceptionally useful for locating a specific tag from target HTML content. For example, if the third in the HTML contained the desired data, the following code would make quick work of locating the root

    HTMLParser1.GetTags('*', Elements);
    Node := Elements.FindTagByIndex('table', 2);
    if Assigned(Node) then
      // Perform processing on

    Working with Results as a Hierarchy

    The procedure in Figure 5 provides yet another method for accessing the contents of the Tree (albeit a more brute force approach). In this example, the HTML content is assumed to be fairly straightforward:
    elements contain elements, which contain
    and elements. Given this reasonably accurate assumption, the code will walk the children of a node, and for each node found will walk its children looking for occurrences of either , and add their text (contained in PCData nodes) to a TStrings container, e.g. the Lines property of a TMemo control.

    // Return TableNode's contents in a TStrings container.

    procedure GetTable(TableNode: TTagNode; Lines: TStrings);
        DataCtr: Integer;
        RowNode: TTagNode;
      TempStr: string;
      if CompareText(TableNode.Caption, 'table') = 0 then
        for RowCtr := 0 to TableNode.ChildCount - 1 do
          RowNode := TableNode.Children[RowCtr];
          if CompareText(RowNode.Caption, 'tr') = 0 then
            TempStr := '';
            for DataCtr := 0 to RowNode.ChildCount - 1 do
              Node := RowNode.Children[DataCtr];
              if CompareText(Node.Caption, 'td') = 0 then
                TempStr := TempStr + Node.GetPCData + #9
              else if CompareText(Node.Caption, 'th') = 0 then
                TempStr := TempStr + Node.GetPCData + #9;
            TempStr := Trim(TempStr);
            if TempStr <> '' then
    Figure 5: Working with results as a hierarchy.

    As an illustration of just how difficult HTML processing can be, the following caveats apply to the code provided in Figure 5 and would have to be handled to provide a robust solution:

    Subtables are not handled. That is,
    elements encountered within a , and a host of other table elements are not considered (although admittedly they are rare).

    Working with Results as a List

    The procedure in Figure 6 demonstrates working with the parsed results as a list. The goal here is to retrieve a list of all comments, links, meta tags, and images from the document, with the thrown in for good measure. The code does this in several simple steps: <br><br>Parse the HTML. <br>Create a container for the list of matching nodes from the tree. <br>Call the GetTags method passing '*', and the container ('*' indicates that all items in the tree should be returned). <br>Iterate through the container collecting matches and place their contents in the StringList. <br>Destroy the container. <br><br><br>procedure TForm1.Parse(HTML: string; Lines: TStrings);<br>const<br>  cKnownTags = '|title|img  |a    |meta |!    ';<br>  cTITLE = 0;<br>  cIMG = 1;<br>  cA = 2;<br>  cMETA = 3;<br>  cComment = 4;<br>var<br>  Index,<br>    Counter: Integer;<br>  TempStr: string;<br>  Nodes: TTagNodeList;<br>begin<br>  HTMLParser1.Parse(HTML);<br>  Nodes := TTagNodeList.Create;<br>  // Retrieve all nodes.<br>  HTMLParser1.Tree.GetTags('*', Nodes);<br>  for Counter := 0 to Nodes.Count - 1 do<br>  begin<br>    TempStr := '|' + LowerCase(Nodes[Counter].Caption);<br>    // Index of element name.<br>    Index := Pos(TempStr, cKnownTags);<br>    if Index > 0 then<br>    begin<br>      Index := Index div 6;<br>      case Index of<br>        cTITLE:<br>          Lines.Add('Title=' +<br>            HTMLDecode(Nodes[Counter].GetPCData));<br>        cIMG:<br>          begin<br>            TempStr := Nodes[Counter].Params.Values['src'];<br>            if TempStr <> '' then<br>              Lines.Add(<br>                Nodes[Counter].Params.Values['src']);<br>          end;<br>        cA:<br>          begin<br>            TempStr :=<br>              Nodes[Counter].Params.Values['href'];<br>            if TempStr <> '' then<br>              Lines.Add(TempStr + '=' +<br>                HTMLDecode(Nodes[Counter].GetPCData));<br>          end;<br>        cMETA:<br>          with Nodes[Counter].Params do<br>            Lines.Add(Values['name'] + '=' +<br>              Values['content']);<br>        cComment:<br>          Lines.Add('[Comment] ' +<br>            HTMLDecode(Nodes[Counter].Text));<br>      end; {  case Index }<br>    end; {  if Index > 0 }<br>  end; {  for Counter := 0 to Nodes.Count - 1  }<br>  Nodes.Free;<br>end;<br>Figure 6: Working with results as a list. <br><br>The important thing to understand here is that the TTagNodeList class is just a list of pointers to nodes from the Tree. This is quite beneficial in that once a desired node is located in the list, it may be used as if it had been acquired by traversing the tree. For example, when the case statement in Figure 6 encounters a TITLE element, its contents are retrieved by making a call to the TITLE node's GetPCData method (which depends on the parsed tree structure behaving as it appears to in Figure 2). Note that the PCData often contains encoded items such as > and < (< and > respectively). HTMLDecode is provided for handling most simple cases, but doesn't handle all cases (notably non-US character encoding). <br><br>This example also demonstrates the use of the attributes from an element. When an <A> element is encountered, the HREF attribute is examined. If it exists, the <A> element is treated as a link to some other resource. If the HREF attribute were not specified, this might be an instance of <A> serving as an anchor instead of a link. For more details on how the Params property of the TTagNode behaves, see the Delphi help for the Names and Values properties of the TStrings class. <br><br>Searching the www.directv.com Program Guide<br><br>Applying the HTML parser to the original need turns out to be another simple (albeit involved) exercise. First, an understanding of the CGI scripts that allow searching of the program guide is required. As it turns out, the search script is rather crude and accepts only three parameters: timezone, category, and search text. timezone is simply a number representing Eastern, Central, Mountain, or Pacific. category allows the search to span all programs, or to be narrowed to certain types of programs such as movies or sports. The search text should be all, or a portion of, the desired program name. The search is specific to program names, and doesn't consider program descriptions. <br><br>The results of the search are returned as an HTML table including channel, date, time, duration, and program name. The program name is contained within a link to the program description, which will need to be retrieved as well. This is slightly complicated by the fact that the link provided is written using JavaScript which we cannot simply call. However, the URL produced by the JavaScript function is easy to replicate, as it contains a program ID number that can be passed to another CGI script that returns the desired description. <br><br>The next step is to parse the HTML and process the results into a more useful format. TStringTable is provided as a simple container for just this purpose. The TStringTable offers a non-visual equivalent to TStringGrid with a few additional methods to make manipulating the data a bit easier. Once the HTML table has been processed and placed in the string table, a bit of house cleaning is required. For one, there are rows at the end of the HTML table that need to be ignored, as they contain images, not program content. Also, the channel appears only in the first row of a set of programs that occur on that channel. <br><br>The contents can now be added to a ListView. Once that task is complete, the descriptions can be fetched using the program IDs to call the description CGI, and then added to the ListView. <br><br>Further Study<br><br>While these examples are not terribly glamorous, the parser can be applied to more meaningful problems. For instance, a friend of mine has put together an extremely handy application using the Pricewatch site (http://www.pricewatch.com/) to monitor prices on PC hardware. Pricewatch offers a current snapshot of pricing of a particular piece of hardware from various vendors, usually sorted from least expensive to most. However, it doesn't allow for viewing of several different pieces of hardware at once, and it doesn't track the history of the price changes for the hardware. So, the application provides a way to build a list of hardware to be tracked, and then offers a simple trend analysis by gathering and saving off the price information on a regular basis. This provides a useful picture for the consumer of just how quickly the prices are moving downward on a particular item. If the pricing is in a steep downward curve, waiting to purchase might be wise. If the curve is flat, the time to purchase might be at hand. <br><br>The parser is used not only to retrieve the pricing data, but also to help deal with one of the more significant issues facing those attempting to interface with sites they do not control: unexpected changes in the target site's contents. In this case, a review of the <form> elements from the main page is performed to ensure that the query mechanism remains intact. As a further safeguard, the search results page is also examined to verify a match against the expected HTML format. If unexpected items are found in either case, processing cannot continue, but at least the user can be warned of the situation. <br><br>In a more interesting demonstration of the parser's abilities, it has been combined with a database to create a poor man's OODB (object-oriented database). XML is used to wrap the data, and is then stored in text fields in the database. When needed, the XML is retrieved and the parser used to extract the data. Without going in to detail, this is useful because the data stored in the database can carry semantic information with it (the XML elements) that provides information about the data's structure. In systems where the data structure is dynamic, this provides a simple way to avoid excessive database maintenance and further provides a clean, easily understood mechanism for the exchange of data between various applications and platforms. In the case mentioned here, a legacy defect (bug) tracking system hosted on a Solaris platform was wrapped in a Delphi based UI. <br><br>Additional Demonstration Applications<br><br>To further demonstrate the power of the classes presented in this article, two additional applications accompany this article. An HTML Engine Demo application (see Figure 7) displays a great deal of information about any selected URL, including meta tags, links, and images. <br><br> <br>Figure 7: The HTML Engine Demo application. <br><br>The Parser Test application parses any URL, or SGML/HTML/XML document, and displays the results in a TreeView (see Figure 8). It can also display links, selected tags, text, etc. <br><br> <br>Figure 8: The Parser Test application. <br><br>Room for Improvement<br><br>This parser builds a reasonable basis for parsing of HTML and XML but, offers significant room for further development. <br><br>Incorporation of a DTD processor (DTDs can be parsed with the existing parser, but no handling of the parsed contents is provided). This would provide two main benefits: more thorough parsing of elements based a true understanding of their legal contents and no need for hard coding a representation of the HTML DTD within the parser. Further, DTD-based XML parsing would then be possible. <br><br>A DOM container model to complement the TTagNode model. DOM represents a fairly well understood and commonly encountered model for representing the parsed contents of XML. While it doesn't suit all needs, it does provide a useful, standard way to communicate about the parsed elements. <br><br>XQL or other suitable extended model query mechanism. The GetTags method is reasonably sufficient, but for more exhaustive queries against XML contents a more advanced mechanism is desired. For example, it would be extremely handy if GetTags could be passed 'order/partno' to indicate that we're searching for all PARTNO elements that are immediately below an ORDER element. <br><br>Further performance tuning. While some attention has been paid to this area, no extreme efforts to speed things up were applied. Most notably, Delphi's string routines are not considered to be as fast as those provided in some third-party string-handling collections (notably HyperString). <br><br>Resources and Alternatives<br><br>For more information and examples, please visit http://www.dallas.net/~richardp/delphi/components/delphicomps.htm.<br><br>HTML 4.0 Specification -http://www.w3.org/TR/REC-html40. This is the single most useful resource to those seeking HTML enlightenment. It is extremely detailed and well written. <br><br>HTML 4.0 Loose DTD -http://www.w3.org/TR/REC-html40/loose.dtd. A part of the HTML 4.0 specification, this offers the exact specification of just what the HTML rules are. It is upon this DTD (as opposed to the "strict" DTD) that the parser is designer to operate. <br><br>"XML: Creating Structures of Meaning," Visual Developer, Nov/Dec 1998, Vol. 9 No. 4. A quick survey of XML for the beginner. Syntax and use are explored here with an eye to bringing the novice on board. <br><br>"Using Internet Explorer's HTML Parser," Dr. Dobb's Journal, #302, August 1999 (http://www.ddj.com/articles/1999/9908/9908toc.htm). This article offers an examination of using the HTML parser that is available within Microsoft's Internet Explorer via COM interface. The source for the article is in C++, but it's not difficult to follow. <br><br>"XML from Delphi," Delphi Informant Magazine, July 1999, Vol. 5 No. 7. A beginner's explanation of XML, and an excellent example of using the XML parser included in Internet Explorer 4.0. <br><br>Begin Listing One - Searching www.directv.com<br>unit Unit1;<br><br>interface<br><br>uses<br>  Windows, Messages, SysUtils, Classes, Controls, Forms,<br>  Dialogs, ComCtrls, StdCtrls, ExtCtrls, GetURL, HTMLMisc,<br>  HTMLParser, StringTable;<br><br>type<br>  TForm1 = class(TForm)<br>    WIGetURL1: TWIGetURL;<br>    StatusBar1: TStatusBar;<br>    Panel2: TPanel;<br>    pbSearch: TButton;<br>    ebSearchText: TEdit;<br>    Panel3: TPanel;<br>    ListView1: TListView;<br>    HTMLParser1: THTMLParser;<br>    procedure pbSearchClick(Sender: TObject);<br>    procedure WIGetURL1Status(Sender: TObject;<br>      Status: Integer; StatusInformation: Pointer;<br>      StatusInformationLength: Integer);<br>  private<br>    procedure GetTable(TableNode: TTagNode;<br>      Table: TStringTable);<br>    function GetDescription(Node: TTagNode): string;<br>  end;<br><br>var<br>  Form1: TForm1;<br><br>implementation<br><br>{$R *.DFM}<br><br>const<br>  cDirecTVSearchURL =<br>    'http://';<br>  cDirecTVDescURL =<br>    'http://';<br><br>  // Return TableNode's contents in Table<br><br>procedure TForm1.GetTable(TableNode: TTagNode;<br>  Table: TStringTable);<br>var<br>  RowCtr,<br>    DataCtr: Integer;<br>  Node,<br>    RowNode: TTagNode;<br>begin<br>  Table.Clear;<br>  if LowerCase(TableNode.Caption) = 'table' then<br>  begin<br>    for RowCtr := 0 to TableNode.ChildCount - 1 do<br>    begin<br>      RowNode := TableNode.Children[RowCtr];<br>      if LowerCase(RowNode.Caption) = 'tr' then<br>      begin<br>        Table.NewRow;<br>        for DataCtr := 0 to RowNode.ChildCount - 1 do<br>        begin<br>          Node := RowNode.Children[DataCtr];<br>          if LowerCase(Node.Caption) = 'td' then<br>            Table.AddColumnObject(Node.GetPCData, Node)<br>          else if LowerCase(Node.Caption) = 'th' then<br>            Table.AddHeader(Node.GetPCData);<br>        end;<br>        if Table.Row[Table.RowCount - 1].Count <= 0 then<br>          Table.DeleteRow(Table.RowCount - 1);<br>      end;<br>    end;<br>  end;<br>end;<br><br>function TForm1.GetDescription(Node: TTagNode): string;<br>var<br>  TempStr: string;<br>begin<br>  Result := '';<br>  if Node.ChildCount > 0 then<br>    TempStr := Node.Children[0].Params.Values['href']<br>  else<br>    TempStr := '';<br><br>  if TempStr <> '' then<br>  begin<br>    // Parse out the description id<br>    Delete(TempStr, 1, Pos('(', TempStr));<br>    Delete(TempStr, Pos(')', TempStr), Length(TempStr));<br>    WIGetURL1.URL := cDirecTVDescURL + TempStr;<br>    Screen.Cursor := crHourglass;<br>    Application.ProcessMessages;<br>    WIGetURL1.GetURL;<br>    Screen.Cursor := crDefault;<br>    StatusBar1.Panels.Items[0].Text := '';<br>    if WIGetURL1.Status = wiSuccess then<br>    begin<br>      TempStr := WIGetURL1.Text;<br>      // Use brute force to scrape out program description.<br>      if Pos('<BLOCKQUOTE>', TempStr) > 0 then<br>      begin<br>        Delete(TempStr, 1, Pos('<BLOCKQUOTE>', TempStr) + 11);<br>        Delete(TempStr, Pos('</BLOCKQUOTE>', TempStr),<br>          Length(TempStr));<br>      end;<br>      Result := TempStr;<br>    end;<br>  end<br>end;<br><br>procedure TForm1.pbSearchClick(Sender: TObject);<br>const<br>  tzPacific = '0'; // Time zones.<br>  tzMountain = '1';<br>  tzCentral = '2';<br>  tzEastern = '3';<br>  cgMovies = '0'; // Categories.<br>  cgSports = '1';<br>  cgSpecials = '2';<br>  cgSeries = '3';<br>  cgNews = '4';<br>  cgShopping = '5';<br>  cgAllCategories = '-1';<br>var<br>  Cols,<br>    Rows: Integer;<br>  NewItem: TListItem;<br>  Node: TTagNode;<br>  Nodes: TTagNodeList;<br>  ResultTable: TStringTable;<br>  TempStr: string;<br>begin<br>  if ebSearchText.Text = '' then<br>    Exit;<br>  WIGetURL1.URL := cDirecTVSearchURL + tzCentral + '/' +<br>    cgAllCategories + '/' + urlEncode(ebSearchText.Text);<br>  Screen.Cursor := crHourglass;<br>  Application.ProcessMessages;<br>  WIGetURL1.GetURL;<br>  Screen.Cursor := crDefault;<br>  StatusBar1.Panels.Items[0].Text := '';<br>  if WIGetURL1.Status = wiSuccess then<br>  begin<br>    if Pos('No program titles that match',<br>      WIGetURL1.Text) > 0 then<br>      ShowMessage('No matches found')<br>    else<br>    begin<br>      // Attempt to parse HTML table we're looking for.<br>      HTMLParser1.Parse(WIGetURL1.Text);<br>      Nodes := TTagNodeList.Create;<br>      HTMLParser1.Tree.GetTags('table', Nodes);<br>      if Nodes.Count > 0 then<br>      begin<br>        ResultTable := TStringTable.Create;<br>        GetTable(Nodes[0], ResultTable);<br>        // Get rid of image tags at bottom of search<br>        // response (the 2nd column has no contents).<br>        with ResultTable do<br>          for Rows := RowCount - 1 downto 0 do<br>            if Cells[1, Rows] = '' then<br>              DeleteRow(Rows);<br>        // Ensure all cells are filled appropriately<br>        // (in the HTML table, a RowSpan attribute<br>        // allows the "Channel" to be displayed in<br>        // one cell for several programs).<br>        with ResultTable do<br>          for Rows := 0 to RowCount - 1 do<br>            for Cols := 0 to ColCount - 1 do<br>              if Cells[Cols, Rows] = '' then<br>                if Rows > 0 then<br>                  Cells[Cols, Rows] :=<br>                    Cells[Cols, Rows - 1];<br>        // Add items to ListView (Program, Channel,<br>        // Data, Time).<br>        ListView1.Items.Clear;<br>        for Rows := 0 to<br>          ResultTable.RowCount - 1 do<br>        begin<br>          NewItem := ListView1.Items.Add;<br>          NewItem.Caption :=<br>            ResultTable.Cells[4, Rows];<br>          NewItem.SubItems.Add(<br>            ResultTable.Cells[0, Rows]);<br>          NewItem.SubItems.Add(<br>            ResultTable.Cells[1, Rows]);<br>          NewItem.SubItems.Add(<br>            ResultTable.Cells[2, Rows]);<br>        end;<br>        // Retrieve program descriptions (program id<br>        // contained in 4th column's node).<br>        for Rows := 0 to<br>          ResultTable.RowCount - 1 do<br>        begin<br>          // It's rude to whack the server :-)<br>          Sleep(500);<br>          Node :=<br>            TTagNode(ResultTable.Objects[4, Rows]);<br>          TempStr := GetDescription(Node);<br>          ListView1.Items[Rows].<br>            SubItems.Add(TempStr);<br>        end;<br>        ResultTable.Free;<br>      end // if Nodes.Count > 0 ...<br>      else<br>        ShowMessage(<br>          'Error - Expected table...found none');<br>    end; // else of Pos('No program titles that...<br>  end // WIGetURL1.Status = wiSuccess...<br>  else<br>    ShowMessage('Unable to contact search server [' +<br>      WIGetURL1.ErrorMessage + ']');<br>end;<br><br>end.<br>End Listing One<br><br><br>Component Download: http://www.baltsoft.com/files/dkb/attachment/Parsing_the_Web.zip</font> </td> </tr> </table> <p align=center><a href=index.php?oldal=45><font size=2 color=#003399 face=Verdana> << Back to main page</font></a></p></form> </body> </html>
    element are ignored.
    Row and column spanning is not handled.