好冷啊!






4.65/5 (9投票s)
简要说明如何从几个与天气相关的网站解析 XML。
引言
在我撰写有关时间服务器的文章时,有人在 C/C++/MFC 板块上提出了一个关于中央天气服务器的问题。尽管目前还没有这样的服务器,但我认为仍然有可能进行某种“屏幕抓取”来获取所需数据。
几年前,我为自己编写了一些小程序 VBScript 文件,可以自动登录某些网站(例如,公司工时表、无需购买即可参加的竞赛)。它们运行得很好,但它们做了很多假设,即某些 HTML 元素会存在并且位于特定位置。如果 HTML 页面的所有者移动了内容,我的脚本就会中断。考虑到这一点,我不太愿意尝试从“移动的目标”中提取天气信息。
我知道像 CP 这样的网站通过 XML(即 RSS)发送部分内容,所以我决定对其进行研究。这次尝试的结果,虽然不算惊天动地,但将在下面详细介绍。不可否认,从 XML 文件而不是 HTML 文件中收集数据仍然容易受到元素不存在或位置不同的影响,但这似乎不是大问题。
我很快找到了三个提供 XML 格式天气信息的网站:NOAA、Google 和 Yahoo!。您需要更改下面引用的每个 URL 以适应您的具体区域,当然,除非您对我们所在区域的天气感兴趣。
XML解析器
当我开始这个项目时,我想使用 TinyXML,因为我以前从未用过它。由于它纯粹是 C++ 代码,我不得不做一些小的调整才能将其与 MFC 编译。也许是我太挑剔了,但看着我用来解析这三个 XML 文件的代码,我感觉不对劲。诚然,这是我第一次尝试使用它,所以也许我的理由不对。我最终选择了使用微软的 XML Core Services (MSXML)。要公开类型库,只需将以下内容添加到项目的 *stdafx.h* 文件中:
#import <msxml6.dll>
using namespace MSXML2;
这将公开各种“智能指针”接口,例如 IXMLDOMDocument2Ptr
、IXMLDOMNodePtr
和 IXMLDOMNamedNodeMapPtr
。在本练习中,我使用了 MSXML 版本 6(尽管 ProgID 的名称中包含 MSXML2),原因无他,只是因为它是我的机器上最新的版本。
NOAA
我们要从 NOAA 国家气象局下载的文件是 http://www.weather.gov/xml/current_obs/KTUL.xml。我已修剪掉不属于本次练习的元素,留下:
<current_observation version="1.0">
<location>Tulsa International Airport, OK</location>
<observation_time>Last Updated on Oct 14 2009, 2:53 pm CDT</observation_time>
<weather>Overcast</weather>
<temperature_string>55.0 F (12.8 C)</temperature_string>
<relative_humidity>83</relative_humidity>
<wind_string>North at 4.6 MPH (4 KT)</wind_string>
<dewpoint_string>50.0 F (10.0 C)</dewpoint_string>
<windchill_string>54 F (12 C)</windchill_string>
<visibility_mi>10.00</visibility_mi>
</current_observation>
由于该文件布局非常直接(即,嵌套元素很少或没有嵌套),因此很容易解析。文件下载后,解析代码如下:
IXMLDOMDocument2Ptr pDoc;
HRESULT hr = pDoc.CreateInstance(_T("MSXML2.DOMDocument.6.0"));
if (SUCCEEDED(hr))
{
if (pDoc->load(COleVariant(temp.m_strTempFilename)))
{
IXMLDOMNodePtr pNode = pDoc->selectSingleNode(_T("current_observation/location"));
m_lblLocation.SetWindowText(pNode->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("current_observation/observation_time"));
m_lblLastUpdated.SetWindowText(pNode->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("current_observation/weather"));
m_lblWeather.SetWindowText(pNode->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("current_observation/temperature_string"));
m_lblTemperature.SetWindowText(pNode->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("current_observation/dewpoint_string"));
m_lblDewPoint.SetWindowText(pNode->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("current_observation/relative_humidity"));
m_lblHumidity.SetWindowText(pNode->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("current_observation/wind_string"));
m_lblWind.SetWindowText(pNode->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("current_observation/windchill_string"));
m_lblWindChill.SetWindowText(pNode->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("current_observation/visibility_mi"));
m_lblVisibility.SetWindowText(pNode->GetnodeTypedValue().bstrVal);
}
}
如您所见,这里有很多冗余。加载文件后,清理后的版本如下:
struct
{
TCHAR *pszXMLChildName;
CWnd *pwndControl;
} ControlInfo[] =
{
{ _T("location"), &m_lblLocation },
{ _T("observation_time"), &m_lblLastUpdated },
{ _T("weather"), &m_lblWeather },
{ _T("temperature_string"), &m_lblTemperature },
{ _T("dewpoint_string"), &m_lblDewPoint },
{ _T("relative_humidity"), &m_lblHumidity },
{ _T("wind_string"), &m_lblWind },
{ _T("windchill_string"), &m_lblWindChill },
{ _T("visibility_mi"), &m_lblVisibility }
};
...
IXMLDOMNodePtr pParent = pDoc->selectSingleNode(_T("current_observation"));
for (int x = 0; x < sizeof(ControlInfo) / sizeof(ControlInfo[0]); x++)
{
IXMLDOMNodePtr pNode = pParent->selectSingleNode(ControlInfo[x].pszXMLChildName);
if (pNode != NULL)
ControlInfo[x].pwndControl->SetWindowText(pNode->GetnodeTypedValue().bstrVal);
}
如果我想添加或删除任何元素,只需更改 ControlInfo
结构即可。
Google 的天气 Feed 与 NOAA 的非常相似。不过,它有更多的嵌套和更多的部分。要下载的文件是 http://www.google.com/ig/api?weather=74135。其中,我们感兴趣的元素布局如下:
<xml_api_reply version="1">
<weather module_id="0" tab_id="0" mobile_row="0" mobile_zipped="1" row="0" section="0">
<forecast_information>
<city data="Tulsa, OK" />
<current_date_time data="2009-10-15 18:53:00 +0000" />
</forecast_information>
<current_conditions>
<condition data="Overcast" />
<temp_f data="52" />
<humidity data="Humidity: 80%" />
<wind_condition data="Wind: N at 7 mph" />
</current_conditions>
</weather>
</xml_api_reply>
加载此文件后,我们可以使用以下方法进行解析:
IXMLDOMNodePtr pNode =
pDoc->selectSingleNode(_T("xml_api_reply/weather/forecast_information/city"));
IXMLDOMNamedNodeMapPtr pAttributes = pNode->Getattributes();
IXMLDOMNodePtr pAttribute = pAttributes->getNamedItem(_T("data"));
m_lblCity.SetWindowText(pAttribute->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("xml_api_reply/weather/")
_T("forecast_information/current_date_time"));
pAttributes = pNode->Getattributes();
pAttribute = pAttributes->getNamedItem(_T("data"));
m_lblForecast.SetWindowText(pAttribute->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("xml_api_reply/weather/current_conditions/condition"));
pAttributes = pNode->Getattributes();
pAttribute = pAttributes->getNamedItem(_T("data"));
m_lblCurrent.SetWindowText(pAttribute->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("xml_api_reply/weather/current_conditions/temp_f"));
pAttributes = pNode->Getattributes();
pAttribute = pAttributes->getNamedItem(_T("data"));
m_lblTemperature.SetWindowText(pAttribute->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("xml_api_reply/weather/current_conditions/humidity"));
pAttributes = pNode->Getattributes();
pAttribute = pAttributes->getNamedItem(_T("data"));
m_lblHumidity.SetWindowText(pAttribute->GetnodeTypedValue().bstrVal);
pNode = pDoc->selectSingleNode(_T("xml_api_reply/weather/current_conditions/wind_condition"));
pAttributes = pNode->Getattributes();
pAttribute = pAttributes->getNamedItem(_T("data"));
m_lblWind.SetWindowText(pAttribute->GetnodeTypedValue().bstrVal);
此格式与 NOAA 的略有不同,因为它使用属性来存储数据。与之前一样,此代码可以缩写为:
struct
{
TCHAR *pszXMLChildName;
CWnd *pwndControl;
} ControlInfo[] =
{
{ _T("forecast_information/city"), &m_lblCity },
{ _T("forecast_information/current_date_time"), &m_lblForecast },
{ _T("current_conditions/condition"), &m_lblCurrent },
{ _T("current_conditions/temp_f"), &m_lblTemperature },
{ _T("current_conditions/humidity"), &m_lblHumidity },
{ _T("current_conditions/wind_condition"), &m_lblWind }
};
...
IXMLDOMNodePtr pParent = pDoc->selectSingleNode(_T("xml_api_reply/weather"));
for (int x = 0; x < sizeof(ControlInfo) / sizeof(ControlInfo[0]); x++)
{
IXMLDOMNodePtr pNode = pParent->selectSingleNode(ControlInfo[x].pszXMLChildName);
if (pNode != NULL)
{
IXMLDOMNamedNodeMapPtr pAttributes = pNode->Getattributes();
IXMLDOMNodePtr pAttribute = pAttributes->getNamedItem(_T("data"));
ControlInfo[x].pwndControl->SetWindowText(pAttribute->GetnodeTypedValue().bstrVal);
}
}
Yahoo!
我将 Yahoo! 留到最后,仅仅是因为它使用了命名空间,这需要更多的代码行才能提取数据。还需要将风向从度数转换为基数方向。要下载的文件是 http://xml.weather.yahoo.com/forecastrss?p=74135&u=f。文件中的相关元素是:
<rss version="2.0" xmlns:yweather="http://xml.weather.yahoo.com/ns/rss/1.0">
<channel>
<lastBuildDate>Fri, 16 Oct 2009 12:53 pm CDT
<yweather:location city="Tulsa" region="OK" country="US" />
<yweather:units temperature="F" distance="mi" pressure="in" speed="mph" />
<yweather:wind chill="59" direction="0" speed="6" />
<yweather:atmosphere humidity="53" visibility="10" pressure="30.25" rising="2" />
<yweather:astronomy sunrise="7:31 am" sunset="6:47 pm" />
<item>
<title>Conditions for Tulsa, OK at 12:53 pm CDT</title />
<yweather:condition text="Partly Cloudy" code="30"
temp="59" date="Fri, 16 Oct 2009 12:53 pm CDT" />
</item>
</channel>
</rss>
Yahoo! 格式的一个有趣区别是测量单位不包含在实际值中。在下面的代码中,我提取这些单位并存储它们以备后用。在下载文件之前,我需要告知文档对象有关命名空间的信息。这可以通过调用 setProperty()
方法来完成:
pDoc->setProperty("SelectionNamespaces",
"xmlns:yweather=\"http://xml.weather.yahoo.com/ns/rss/1.0\"");
yweather
命名空间将在下面的 selectSingleNode()
调用中使用。下载文件后,可以使用如下代码进行解析:
IXMLDOMNodePtr pParent = pDoc->selectSingleNode(_T("rss/channel"));
if (pParent != NULL)
{
IXMLDOMNodePtr pChild = pParent->selectSingleNode(_T("lastBuildDate"));
m_lblForecast.SetWindowText(pChild->GetnodeTypedValue().bstrVal);
pChild = pParent->selectSingleNode(_T("//yweather:location"));
IXMLDOMNamedNodeMapPtr pAttributes = pChild->Getattributes();
IXMLDOMNodePtr pAttribute = pAttributes->getNamedItem(_T("city"));
m_lblLocation.SetWindowText(pAttribute->GetnodeTypedValue().bstrVal);
pChild = pParent->selectSingleNode(_T("//yweather:units"));
pAttributes = pChild->Getattributes();
CString strUnitTemp = CString(_T(' ')) +
pAttributes->getNamedItem(_T("temperature"))->GetnodeTypedValue().bstrVal;
CString strUnitDistance = CString(_T(' ')) +
pAttributes->getNamedItem(_T("distance"))->GetnodeTypedValue().bstrVal;
CString strUnitPressure = CString(_T(' ')) +
pAttributes->getNamedItem(_T("pressure"))->GetnodeTypedValue().bstrVal;
CString strUnitSpeed = CString(_T(' ')) +
pAttributes->getNamedItem(_T("speed"))->GetnodeTypedValue().bstrVal;
pChild = pParent->selectSingleNode(_T("//yweather:wind"));
pAttributes = pChild->Getattributes();
CString strWindSpeed = CString(_T(' ')) +
pAttributes->getNamedItem(_T("speed"))->GetnodeTypedValue().bstrVal + strUnitSpeed;
m_lblWind.SetWindowText(ComputeDirection(pAttributes->getNamedItem(
_T("direction"))->GetnodeTypedValue().bstrVal) + strWindSpeed);
pChild = pParent->selectSingleNode(_T("//yweather:atmosphere"));
pAttributes = pChild->Getattributes();
m_lblHumidity.SetWindowText(pAttributes->getNamedItem(
_T("humidity"))->GetnodeTypedValue().bstrVal + CString(_T('%')));
m_lblBarometer.SetWindowText(pAttributes->getNamedItem(
_T("pressure"))->GetnodeTypedValue().bstrVal + strUnitPressure);
m_lblVisibility.SetWindowText(pAttributes->getNamedItem(
_T("visibility"))->GetnodeTypedValue().bstrVal + strUnitDistance);
pChild = pParent->selectSingleNode(_T("//yweather:astronomy"));
pAttributes = pChild->Getattributes();
m_lblSunrise.SetWindowText(pAttributes->getNamedItem(
_T("sunrise"))->GetnodeTypedValue().bstrVal);
m_lblSunset.SetWindowText(pAttributes->getNamedItem(
_T("sunset"))->GetnodeTypedValue().bstrVal);
pChild = pParent->selectSingleNode(_T("item//yweather:condition"));
pAttributes = pChild->Getattributes();
m_lblCondition.SetWindowText(pAttributes->getNamedItem(
_T("text"))->GetnodeTypedValue().bstrVal);
m_lblTemperature.SetWindowText(pAttributes->getNamedItem(
_T("temp"))->GetnodeTypedValue().bstrVal + strUnitTemp);
}
几乎无法再压缩了!
风向是 360 度值,需要转换为 16 个基数方向之一。如果我们把一个圆分成 16 个扇形,那么每个扇形是 22.5 度。但是,由于每个基数方向都位于扇形中间,这意味着在基数方向的两侧有 11.25 度。例如,N 的方向是从 348.75 度到 11.25 度;S 的方向是从 168.75 度到 191.25 度。为了处理这种情况,只需将(顺时针)加上 11.25 度,然后除以 22.5 度。结果将是一个 0-16 范围内的数字。扇区编号为 0-15,因此为了使大于 348.75 度的度数保持在扇区 0 中,请使用模运算符。执行此操作的函数如下:
CString CYahooComDlg::ComputeDirection( const TCHAR *pszDegrees )
{
CString strDirections[16] = { _T("N"), _T("NNE"), _T("NE"), _T("ENE"),
_T("E"), _T("ESE"), _T("SE"), _T("SSE"),
_T("S"), _T("SSW"), _T("SW"), _T("WSW"),
_T("W"), _T("WNW"), _T("NW"), _T("NNW") };
TCHAR *pStop;
double dDegrees = _tstof(pszDegrees) + 11.25;
return strDirections[(UINT) (dDegrees / 22.5) % 16];
}
附加功能
调用 URLDownloadToFile()
时,它需要一个可以写入的位置。我的第一个选择是 *Local Settings\Temp* 文件夹。问题是我找不到该文件夹或其父文件夹的 CSIDL 值。但是,存在 **TEMP** 环境变量。由于我不喜欢依赖环境变量,我可以回退到 CSIDL_PERSONAL
,它解析为 *我的文档* 文件夹。由于此代码将在多个位置使用,我创建了一个方便的小类(但一个函数可能就足够了)来减少一些冗余。所有工作都在构造函数中完成,因此对象创建后即可使用。
CTempFilename::CTempFilename( HWND hWnd )
{
BOOL bResult = FALSE;
TCHAR szPath[MAX_PATH],
szFilename[MAX_PATH];
// first try the environment variable
if (GetEnvironmentVariable(_T("TEMP"), szPath, sizeof(szPath)) != 0)
bResult = TRUE;
// if that didn't work, use a CSIDL
if (! bResult)
{
if (SUCCEEDED(SHGetFolderPath(hWnd, CSIDL_PERSONAL, NULL,
SHGFP_TYPE_CURRENT, szPath)))
bResult = TRUE;
}
if (bResult && GetTempFileName(szPath, _T("Weather"), 0, szFilename) != 0)
m_strTempFilename = szFilename;
}
尽情享用!