Display stock quotes in an HTML page
with XMLHTTP using XML / XSLT
and a custom VB XML Stock Component

By Peter A. Bromberg, Ph.D.

Peter Bromberg

In this article I will show you how to build a custom VB COM object using WinInet to retrieve and parse stock quotes and return them as XML.  We will then build a custom block of HTML code that utilizes VBScript and the Microsoft XMLHTTP and XMLDOMDocument objects to call an ASP page that instantiates our component, returning the requested stock quotes as a well–formed  XML document, and apply an XSL template to do a transform on this XML and display it as a nicely formatted HTML table in our page. In addition, we will use DHTML to display the requested stock table without reloading the page.

You will need Visual Basic 5.0 or 6.0, IIS 3.0, 4.0 or 5.0 (or Personal Web Server), and the latest XML parser from Microsoft (July, 2000). This program will run on any Win32 machine from Windows 95 on up to Windows 2000.



First, a word about XML. XML is maturing rapidly and has quickly become the lingua franca of B2B, eCommerce, and many other areas particularly on the Internet - and for good reason. Big players like Oracle, Sun, IBM, Microsoft and others have integrated XML into their development platforms and even their operating systems. If you haven't started studying XML, XPATH, SAX, XSLT and the many other facets of this powerful medium, I strongly recommend that you begin now. I can assure you from personal experience that as a developer, it can have a dramatic positive effect on your financial well being!

Now, let's build our VB Stock Component.  Everyone is probably familiar with the concept of "screen scraping".  What we will do is grab an HTML page with our requested stock quotes from Yahoo, parse the page to remove the information we need, then format this as XML and return it to the caller. You can use this same technique to grab information from almost any service that returns an HTML page with your results – it could be Zip Code lookups from the Postal service, Shipping prices from FED EX or UPS, weather information, you name it. And if you want to take the next step, you can create your components to be SOAP compliant so that they can be called cross-platform or even exposed as Webservices from your server.

Fire up Visual Basic and start a new ActiveX DLL Project. Set the Project Properties as Unattended Execution, Upgrade ActiveX Controls, and Retained In Memory. Make sure it's Apartment Threaded and give your project the name HTTPStockLite.

Now add a new Class Module and name it StockGrabber.

Here's the code for our module :

Our first declarations are the required WinInet API functions to enable us to use WinInet to make our http call to get our page with the quotes in it We are also declaring some public variables we'll need to
parse the quotes from the page:

Public arSymbols As Variant
Public rowclr As String
Public change_amount As String

Private Declare Function InternetOpen Lib "wininet.dll" _
    Alias "InternetOpenA" (ByVal sAgent As String, _
    ByVal lAccessType As Long, ByVal sProxyName As String, _
    ByVal sProxyBypass As String, ByVal lFlags As Long) As Long

Private Declare Function InternetOpenUrl Lib "wininet.dll" _
   Alias "InternetOpenUrlA" (ByVal hInternetSession As Long, _
   ByVal sURL As String, ByVal sHeaders As String, _
   ByVal lHeadersLength As Long, ByVal lFlags As Long, _
   ByVal lContext As Long) As Long

Private Declare Function InternetReadFile Lib "wininet.dll" _
   (ByVal hFile As Long, ByVal sBuffer As String, _
   ByVal lNumberOfBytesToRead As Long, _
   lNumberOfBytesRead As Long) As Integer

Private Declare Function InternetCloseHandle Lib "wininet.dll" _
      (ByVal hInet As Long) As Integer

Private Const INTERNET_OPEN_TYPE_PRECONFIG = 0
Private Const INTERNET_FLAG_RELOAD = &H80000000
Private hSession            As Long
Private hFile               As Long
Private Result              As String
Private Buffer              As String * 1024
Private bResult             As Boolean
Private lRead               As Long

Our first function , "GetURL" is what opens WinInet and sends out our http call with a querystring
containing our quote request:

Public Function GetURL(sURL As String) As String
hSession = InternetOpen("PeteWinInet/1.0", INTERNET_OPEN_TYPE_PRECONFIG, "", "", 0)

If hSession Then
    hFile = InternetOpenUrl(hSession, sURL, "", 0, INTERNET_FLAG_RELOAD, 0)
    If hFile Then
        lRead = 1
        While lRead
            If InternetReadFile(hFile, Buffer, Len(Buffer), lRead) Then
                Result = Result + Buffer
            End If
        Wend

        GetURL = Result
        InternetCloseHandle (hFile)
    End If
    InternetCloseHandle (hSession)
End If
End Function

Our next function, "GetStock" is the one we will actually call in the component from the instantiating ASP page. This instantiates our StockGrabber class and does the call to the quote service, and then passes the HTML results page to our parsing routines:

Public Function GetStock(symbol As String, Optional bXML As Boolean) As String
    Dim strRetval As String
    Dim HTTP As StockGrabber
    Set HTTP = New HttpStockLite.StockGrabber
   strRetval = HTTP.GetURL("http://quote.yahoo.com/q?s=" & symbol)
    resp = strRetval 
'Convert the symbol(s) string to an array
    arSymbols = Split(symbol, " ", -1, 1)
    If bXML Then
    txtQuotes = "<?xml version=""1.0""?>" & vbCrLf & "<STOCKDATA>"
    Else
    txtQuotes = "<TABLE cellspacing="2" CELLPADDING=1 border="0">" & vbCrLf
    txtQuotes = txtQuotes & "<TR BGCOLOR=Gray><TH>SYMBOL</TH><TH>TIME</TH><TH>LAST</TH><TH>CHG</TH><TH>PCT CHG</TH><TH>VOL</TH></TR>" & vbCrLf
    End If
    txtQuotes = txtQuotes & ParseResponse(resp, bXML)
    If bXML Then
    GetStock = txtQuotes & "</STOCKDATA>" & vbCrLf
    Else
    GetStock = txtQuotes & "</TABLE>" & vbCrLf
    End If
End Function

Our next function "ParseResponse", is what isolates the table containing our information and calls the subfunctions GetRow and GetRowItem to isolate the information we need:

Private Function ParseResponse(resp, Optional bXML As Boolean)

    Dim start_pos
    Dim end_pos
    Dim i
    Dim quotes
    Dim new_row
' Find the table that contains the info we need  

    start_pos = InStr(resp, "Symbol")
    If start_pos = 0 Then
        ParseResponse = "Error parsing response."
        Exit Function
    End If   

' See where the table ends.
    end_pos = InStr(start_pos, resp, "</table>")
    resp = Mid(resp, start_pos, end_pos - start_pos + Len("</table>"))

' Parse the rows from the table.
    Do
        new_row = GetRow(resp, bXML)

        If Len(new_row) = 0 Then Exit Do
       If Left(Trim(change_amount), 1) = "+" Then
            rowcolr = "#99FF99"
            Else
            rowcolr = "#FF6666"
        End If     

        If bXML Then
        quotes = quotes & "<STOCK><SYMBOL>" & Trim(arSymbols(i)) & "</SYMBOL>" & new_row
        Else
        quotes = quotes & "<TR BGCOLOR=" & rowcolr & "><TD>" & Trim(arSymbols(i)) & "</TD>" & new_row & "</TR>"
        End If
        i = i + 1
    Loop

    ParseResponse = Trim(quotes)
End Function

Our next function , "GetRow" actually isolates the HTML contained in each row of our
Stock quote HTML page and applies XML tags to the output based on the optional bXML parameter:

Private Function GetRow(resp, Optional bXML As Boolean)

    Dim pos
    Dim symbol
    Dim last_time
    Dim last_price
   ' Dim change_amount
    Dim change_percent
    Dim Volume
    Dim col
' Find the "<tr" starting the row.
    pos = InStr(resp, "<tr")
    If pos = 0 Then
        resp = ""
        GetRow = ""
        Exit Function
    End If
    resp = Mid(resp, pos)   

' Find the items in this row.

    symbol = GetRowItem(resp)
    last_time = GetRowItem(resp)
    If InStr(last_time, "No such ticker symbol.") > 0 Then
        GetRow = "No such ticker symbol."
        Exit Function
    End If

    last_price = GetRowItem(resp)
    change_amount = GetRowItem(resp)
    change_percent = GetRowItem(resp)
' this is my row , either XML or HTML...
    Volume = GetRowItem(resp)
    If bXML Then
    GetRow = "<TIME>" & Trim(last_time) & "</TIME>" & _
            "<LAST>" & Trim(last_price) & "</LAST>" & _
            "<CHG>" & Trim(change_amount) & "</CHG>" & _
            "<PCT_CHG>" & Trim(change_percent) & "</PCT_CHG>" & _
            "<VOL>" & Trim(Volume) & "</VOL></STOCK>"  

    Else

    GetRow = "<TD>" & Trim(last_time) & "</TD>" & _
            "<TD>" & Trim(last_price) & "</TD>" & _
            "<TD>" & Trim(change_amount) & "</TD>" & _
            "<TD>" & Trim(change_percent) & "</TD>" & _
            "<TD>" & Trim(Volume) & "</TD></TR>"

   End If  

End Function

Our final function, "GetRowItem" isolates the individual components of each quote and assigns them to variables:

Private Function GetRowItem(resp)

    Dim start_pos
    Dim end_pos
    Dim pos
    Dim count
    Dim ch
    Dim txt  

' Find the "<td" and "</td" that bracket the item.

    start_pos = InStr(resp, "<td")
    end_pos = InStr(start_pos, resp, "</td")   

' Save characters between these where the
' outstanding brackets match.

    count = 1
    For pos = start_pos + 1 To end_pos
        ch = Mid(resp, pos, 1)
        If ch = "<" Then
            count = count + 1
            ElseIf ch = ">" Then
            count = count - 1
        Else
            If count = 0 Then txt = txt & ch
        End If
    Next
   GetRowItem = txt
    resp = Mid(resp, end_pos)
End Function

Compile your component into a DLL. I suggest making an MTS package and dropping your component into it, although it is not necessary.

Now that we have our HTTPStockLite.Stockgrabber COM Component, we are ready to instantiate it in an ASP page and call the "GetStock" method. Remember, we want to accept the stock symbols on the querystring from the calling page, so here is our code:

<%

‘ ------ httpStockLite.asp------

Dim HTTP
Set HTTP=Server.CreateObject("HTTPStockLite.StockGrabber")
Response.ContentType="text/XML"
response.Write HTTP.GetStock(Request.Querystring("symbols"), True)
Set HTTP = Nothing

%>

That's all it takes – just five lines of code! Note that we set the response.ContentType to "text/XML" as that is what we want to return in this case. The "True" parameter in the GetStock function call tells our component that we want well-formed XML in our response. If we leave this optional parameter blank or set it to false, our component will simply return an HTML table. This way we can use our component either for XML or for HTML only.  In a moment when we get to our calling page you will see that the XML option provides the capability to take our XML reponse and apply an XSL Transformation to achieve a much richer, custom type of content returned to our calling page.

Now let's look at the calling page and finally a sample XSL Stylesheet to apply a nice formatting transform. First, here is a sample "caller" block of code:

<script language="VBScript" runat=Server>
function getStocks()
Dim xmlDOC
Dim xslDOC
Dim bOK
Dim HTTP
Set HTTP = CreateObject("MSXML2.XMLHTTP")
Set xmlDOC =CreateObject("MSXML2.DOMDocument.3.0")
xmlDOC.Async=False
Set xslDOC =CreateObject("MSXML2.DOMDocument.3.0")
xslDOC.Async=False
bOK = xslDOC.Load("http://localhost/test/Stock2.xsl")
if Not bOK then
rtext.innerText = "Error loading XSL"
end if
HTTP.Open "POST","http://localhost/test/httpstocklite.asp?symbols=" & document.all("symbol").value , False
HTTP.Send()
bOK = xmlDOC.loadXML(HTTP.responseText)
if Not bOK then
rtext.innerText = "Error loading XML from HTTP"
end if
rtext.innerHTML =  xmlDOC.transformNode(xslDOC)
end Function
</script>

<p><FONT FACE=Tahoma SIZe=2><BR>Enter symbols separated<BR> by spaces</FONT><BR>
<input type="text" name="symbol" value="" SIZE=12><BR><input type="button"
value="Get Stocks" onClick="getStocks()">
<div id=rtext></div><BR>

Basically what we are doing here is providing a simple form with a textbox formfield named "symbol", and submit button with an onClick event that calls our VBScript getStocks() function that is inside the script block. Note that my code assumes that all your files are in a subfolder named "test" off the localhost webroot, but you can easily modify this.

The getStock function creates three objects: an XMLHTTP object to send out the quote request to our HttpStockLite.asp page and return the result, and two DOMDocument objects, one to load the resulting XML into, and the other to load our XSL Stylesheet into. Finally, we set the <Div> tag innerHTML to the value of the resultant XML transform which in this case is a nicely formatted table.

Finally, here is the XSL Stylesheet I wrote to transform the XML:

<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl">
 <xsl:template match="/">
 <TABLE STYLE="border:1px solid black">
 <TR STYLE="font-size:8pt; font-family:Verdana; font-weight:bold; text-decoration:underline">  <TD>Symbol</TD>
  <TD STYLE="background-color:lightgrey">Last</TD>
  <TD>Time</TD>
<TD>Chg</TD>
<TD>Pct-Chg</TD>
<TD>Vol</TD>
  </TR>
 <xsl:for-each select="STOCKDATA/STOCK">
 <TR STYLE="font-family:Verdana; font-size:8pt; padding:0px 2px">
<TD>
<xsl:value-of select="SYMBOL" />
</TD>
 <TD STYLE="background-color:lightgrey"> $
  <xsl:value-of select="LAST" />
  </TD>
 <TD >
  <xsl:value-of select="TIME" />
</TD>
 <TD>
  <xsl:value-of select="CHG" />
  </TD>
 <TD>
  <xsl:value-of select="PCT_CHG" />
</TD>
 <TD>
  <xsl:value-of select="VOL" />
  </TD>
  </TR>
  </xsl:for-each>
  </TABLE>
  </xsl:template>
  </xsl:stylesheet>

Finally, for those who would prefer to use an "all script" approach without a custom VB COM component, I have included in the sample code a second page "httpstock2.htm" that calls stock.asp. The stock.asp page has virtually identical code to that found in the VB component, but uses the Microsoft Internet Transfer Control (MSINET.OCX) in the page to make the http call.

And, since they say the "proof is in the pudding", a working demo can be accessed below:

Stock Demo

 

While a discussion of XSL, XSLT and XPATH is beyond the scope of this article, the XSL Transformations (XSLT) specification defines an XML-based language for expressing transformation rules that map one XML document to another. With XSLT, you can use your favorite programming language to create output in an arbitrary text-based format (usually XML). XSLT solves many of the problems caused by the proliferation of multiple XML Schemas describing similar data. See our HOTLINKS section on the EggheadCafe.com website for some links to the various topics mentioned in this article.

As a last caveat, I should mention that while the example I describe here is fine as an example or for a non-public site, it is quite illegal to redisplay Yahoo! quote data. Yahoo! has a legal/contractual obligation to prevent this (e.g. shut down sites that do this), as it's basically stealing from them, which means it's stealing from their quote provider,Reuters.

Download the code for this article

Peter Bromberg is an independent consultant specializing in distributed .NET solutions Inc. in Orlando and a co-developer of the EggheadCafe.com developer website. He can be reached at pbromberg@yahoo.com