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_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.
������� 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------

Set HTTP=Server.CreateObject("HTTPStockLite.StockGrabber")
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
Set HTTP = CreateObject("MSXML2.XMLHTTP")
Set xmlDOC =CreateObject("MSXML2.DOMDocument.3.0")
Set xslDOC =CreateObject("MSXML2.DOMDocument.3.0")
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
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

<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>
<xsl:for-each select="STOCKDATA/STOCK">
<TR STYLE="font-family:Verdana; font-size:8pt; padding:0px 2px">
<xsl:value-of select="SYMBOL" />
<TD STYLE="background-color:lightgrey"> $
<xsl:value-of select="LAST" />
<TD >
<xsl:value-of select="TIME" />
<xsl:value-of select="CHG" />
<xsl:value-of select="PCT_CHG" />
<xsl:value-of select="VOL" />

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 NullSkull.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 NullSkull.com developer website. He can be reached at info@eggheadcafe.com