Web 内容监控工具:Web Watch - Gas Price
本文旨在演示如何编写一个网页内容监控工具来监控汽油价格指数。
引言
近年来,汽油价格 everywhere 暴涨。为了在我附近找到一个更便宜的加油站,我经常访问 www.TorontoGasPrices.com。该网站为司机提供报告大多伦多地区(GTA)安大略省加拿大汽油价格的场所。虽然晚上的汽油价格比早上便宜得多,但有些加油站的价格总是比其他加油站低。我觉得我需要一个工具来监控那个网站上报告的汽油价格。与此同时,我刚从微软的 2005 年发布活动中获得了一个 Visual Studio 2005 标准版,并正在为其寻找第一个项目。于是我开始自己编写这个工具。我必须说,我从这个小项目中获得了很多乐趣。希望您在使用该工具时也能乐在其中。
该工具是用 C# 在 Microsoft .NET Framework 2.0 上编写的。为了安装软件和源代码,您应该安装 Windows 2000/XP 和 Microsoft .NET Framework 2.0。如果您只想使用该工具(不使用源代码),.NET Framework 可再发行组件包就足够了 (微软下载站点)。
我知道我们大多数人仍然使用 VS 2003。例如,我计划在一段时间内同时保留 VS 2003 和 2005。如果您无法用 VS 2003 加载它,我深表歉意。
“Web Watch - 汽油价格”工具的要求
我根据我的需求定义了以下要求
- 该工具将有一个浏览器窗口用于显示我们正在监控的网页。
- 用户可以选择一个城市/区域和地点/加油站。
- 用户可以设置一个价格进行通知。
- 当选定地点汽油价格降至标记以下时,该工具会通知用户。
- 该工具可以以显示窗口或隐藏窗口的方式运行;当窗口隐藏时,任务栏系统托盘中的通知图标为用户提供了访问该工具的界面。
在开发过程中,我发现只需稍加努力就可以支持更多功能。因此,我又增加了两个要求
- 用户可以使用浏览器访问和浏览其他网站。
- 用户可以配置工具下次运行的行为,例如,自动隐藏窗口并使用上一次的设置开始监控。
工具用户手册
如何设置汽油价格监控
- 在“城市/区域”组合框中选择一个城市/区域。
- 在“地点”组合框中选择一个地点/加油站。
- 点击“设置监控”菜单来设置一个要监控的价格。
- 点击“开始”菜单项开始监控。当选定地点汽油价格降至低于监控价格时,Web Watch 将弹出一个带有音乐的通知窗口。
- 点击“停止”菜单项停止监控。
- 点击“待机”可在监控过程中隐藏 Web Watch 窗口。
- 点击“清除->当前”菜单项可以清除当前窗口数据,并在监控中时停止监控。
- 点击“退出”菜单关闭程序。
如何使用系统托盘中的 Web Watch 图标
- 双击系统托盘中的 Web Watch 图标将显示窗口。
- 右键单击系统托盘中的 Web Watch 图标将弹出上下文菜单,其中包含以下选项
- 显示/隐藏 WebWatch
- 设置价格
- 开始/停止监控
- Close
这些选项是监控功能的副本,当监控窗口隐藏时使用。
如何为下次运行配置 Web Watch
- 如果您希望 Web Watch 窗口在下次启动 Web Watch 时显示(或隐藏),请选择“显示窗口”(或“隐藏窗口”)单选按钮。
- Web Watch 将所有会话数据保存在数据库中。启动时,它将加载上次运行的所有设置(城市/地点/公司/监控价格/等)。
- 当 Web Watch 以隐藏窗口方式启动时,您可以执行步骤 9 来显示窗口并设置/更改要监控的价格。
- 您可以点击“清除->历史记录”菜单项来清除所有历史数据,以便下次运行时从头开始。
“附带电池”功能
- 您可以将 Web Watch 用作网页浏览器。要访问网页,请在 URL 文本框中输入网址并按“Enter”键。
- 您可以使用“上一步”或“下一步”导航菜单在网站上向后或向前导航。
工具开发人员手册
除了该工具的实用性之外,该项目本身还演示了 C# 初学者可以使用的以下功能
- Windows Forms 控件:组合框、定时器、文本框、工具菜单栏、浏览器窗口、通知图标、图标菜单等。
- 文件 I/O、字符串、流读取器/写入器
- 属性作为窗体间通信的手段
- 双向链表作为集合类型
为了帮助您阅读源代码,请访问网页 www.TorontoGasPrices.com。在浏览器窗口中,点击“查看->源代码”菜单获取 HTML 源代码。在记事本加载 HTML 源代码后,请确保“视图->自动换行”菜单未选中。保持记事本窗口打开;在审查 C# 源代码时,您需要经常重新访问 HTML。
C# 源代码主要分为三个部分
- 网页内容请求
- 搜索功能
- 事件处理程序
网页源请求代码根据 URL 请求网页内容。然后,它将 HTML 源作为字符串行存储在双向链表中。我选择双向链表是因为我们需要能够根据内容的组织方式向任一方向搜索。这是 `ReadHtmlSrc()` 的代码
public void ReadHtmlSrc(string url)
{
// Clear the html source list
htmlSrc.Clear();
try
{
// Create a request for the URL.
WebRequest request = WebRequest.Create(url);
// If required by the server, set the credentials.
request.Credentials = CredentialCache.DefaultCredentials;
// Get the response.
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
// Get the stream containing content returned by the server.
Stream dataStream = response.GetResponseStream();
// Open the stream using a StreamReader for easy access.
StreamReader reader = new StreamReader(dataStream);
// Read and store the content.
while (!reader.EndOfStream)
{
string oneLine = reader.ReadLine();
htmlSrc.AddLast(oneLine);
}
// Clean up
reader.Close();
dataStream.Close();
response.Close();
}
catch (WebException caught)
{
MessageBox.Show(caught.ToString(), "Web Watch - Web Request Error");
throw;
}
}
设计了几个搜索函数来处理用户选择。例如,以下函数用于查询给定城市/区域的可用地点。如您所见,每当查询找到一个地点时,函数就会将其添加到“地点”组合框中。
private void GetLocationList(string cityName,
ref System.Windows.Forms.ToolStripComboBox locations)
{
Int32 index;
Int32 offset = 22;
Int32 tail = 5;
string location = "";
bool bCityFound = false;
locations.Items.Clear();
LinkedListNode<STRING> current = htmlSrc.First;
while (current != null)
{
string line = current.Value;
Int32 length = line.Length;
if (!bCityFound)
{
if (line.IndexOf("PList") >= 0 && line.IndexOf(cityName) >= 0)
{
bCityFound = true;
}
}
else
{
index = line.IndexOf("PAddress");
if (index >= 0)
{
location = line.Substring(index + offset,
length - index - offset - tail);
if(!locations.Items.Contains(location))
locations.Items.Add(location);
bCityFound = false;
}
}
current = current.Next;
}
}
下一个函数搜索给定地点的汽油公司名称。在这种情况下,我们从列表的最后一个项目开始,因为网页内容以相反的顺序出现在 HTML 源中。
private bool GetGasCompany(string stationName, ref string company)
{
Int32 index;
bool bGotCompany = false;
company = "";
if (stationName == "") return bGotCompany;
bool bFoundLocation = false;
// Since the order of appearance for price, gas company, location in the HTML
// source may vary, depending on individual web sites, we store it in a doubly
// linked list so that the search order can be one way or another. In this case,
// we want to start from location to gas company. We start the search on the
// list from the last item.
LinkedListNode<STRING> current = htmlSrc.Last;
while (current != null)
{
string line = current.Value;
Int32 length = line.Length;
if (!bFoundLocation)
{
// Search for the location string
index = line.IndexOf(stationName);
if (index >= 0)
{
bFoundLocation = true;
}
}
else
{
foreach (string comName in gasCompanies)
{
// Search for the company string
index = line.IndexOf(comName);
if (index >= 0)
{
company = comName;
bGotCompany = true;
break;
}
}
// Search for the string after the company string
index = line.IndexOf("PStation");
if (index >= 0 || bGotCompany)
{
break;
}
}
current = current.Previous;
}
return bGotCompany;
}
需要将声音文件“WebWatch.wav”和图标文件“WebWatch.ico”放在与可执行文件相同的文件夹中,即当前构建中的 *Release* 文件夹。
关注点
正如您所见,“Web Watch - 汽油价格”的实现无论如何都很简单。它的占用空间小,但能执行我们期望的任务。我对该工具的最终成果很满意。然而,从开发者的角度来看,实现存在一个局限性。它绑定到一个特定的网页。有一天,如果网页发生变化,搜索代码将被迫更改,或者它将变得无用。
真正的问题似乎是如何监控一个网页取决于业务背景和网页内容的组织方式。当给出标准时,我们如何搜索网页以查找目标取决于两个方面
- 为目标选择的线索。根据标准选择适当的目标线索非常重要。更好的线索可以节省大量的搜索工作。
- 网页的组织方式。在 HTML 源中,线索和/或标准可能出现在目标之前或之后。线索和目标可能出现在同一行或不同的行上。
我们可以使用某种形式的配置来告诉搜索函数目标线索是什么以及如何执行搜索,但这并不能解决整个问题。我想看看是否能在本文的未来更新中提出更好的设计通用网页监控引擎的方法。
结论
本文介绍了汽油价格监控工具 Web Watch - Gas Price,该工具监控 www.TorontoGasPrices.com 上的汽油价格。虽然最终产品已经超出了项目要求,但其实现绑定到了一个特定的网页。对其实现的讨论引发了一个问题:如何为网页内容监控编写一个通用的搜索引擎?我仍在寻找更好、更通用的网页监控解决方案。
历史
这是本文和源代码的第一个修订版。