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.
|
 |
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:
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