65.9K
CodeProject 正在变化。 阅读更多。
Home

好冷啊!

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (9投票s)

2009年10月23日

CPOL

5分钟阅读

viewsIcon

24222

简要说明如何从几个与天气相关的网站解析 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;

这将公开各种“智能指针”接口,例如 IXMLDOMDocument2PtrIXMLDOMNodePtrIXMLDOMNamedNodeMapPtr。在本练习中,我使用了 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

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;
}

尽情享用!

© . All rights reserved.