4

我正在从维基百科收集机场信息。我想保留机场名称中的非 ASCII 字符。

在网络浏览器中,代码以 Z 开头的机场如下所示:

在此处输入图像描述

机场 DBE 被称为“Dolní Benesov机场”。ZBK 机场被称为“ Ž abljak 机场”。我希望输出中的值相同。

我正在用这样的函数抓取数据:

function Get-Airports ($Uri) {
  Invoke-WebRequest -Uri $Uri -UseBasicParsing |
  Select-Xml -XPath '//table/tr[td]' |
  % {
    $Kids = $_.Node.ChildNodes
    [PSCustomObject] @{
      Iata = $Kids[0].InnerText
      Icao = $Kids[1].InnerText
      AirportName = $Kids[2].InnerText
      LocationServed = $Kids[3].InnerText
    }
  }
}

该函数获取给定的 URI,将 HTML 响应隐式转换为 XML,使用 XPath 提取表数据行,然后将每个列值映射到新 PowerShell 对象的属性。

要获取以 Z 开头的所有机场,我使用如下命令:

$Airports = Get-Airports 'http://en.wikipedia.org/wiki/List_of_airports_by_IATA_code:_Z'

$Airports变量包含一组新的 PowerShell 对象,表中的每个数据行都有一个对象。

此命令显示刮板会破坏包含非 ASCII 字符的名称:

$Airports |
? { $_.AirportName -like '*[?]*' } |
Format-Table

任何机场名称都不应包含问号。我希望这个命令不会产生任何输出。

相反,有几个对象的名称中有两个问号,其中一个非 ASCII 字符会出现在 Web 浏览器中:

Iata  Icao   AirportName                              LocationServed                                               
----  ----   -----------                              --------------                                               
ZBE   LKZA   Doln?? Benesov Airport                   Z??b??eh, Czech Republic                                     
ZBK          ??abljak Airport                         ??abljak, Montenegro                                         
ZBM   CZBM   Bromont (Roland D??sourdy) Airport       Bromont, Quebec, Canada                                      
ZLG          La G??era Airport                        La G??era, Western Sahara                                    
ZLT          La Tabati??re Airport (TC: CTU5)         La Tabati??re, Quebec, Canada                                
ZOS   SCJO   Ca??al Bajo Carlos Hott Siebert Airport  Osorno, Chile                                                
ZPC   SCPC   Puc??n Airport                           Puc??n, Chile                                                
ZQW   EDRZ   Zweibr??cken Airport                     Zweibr??cken, Germany                                        
ZTB          T??te-??-la-Baleine Airport (TC: CTB6)   T??te-??-la-Baleine, Quebec, Canada     

这肯定是字符编码问题。Wikipedia 生成UTF-8,但看起来 PowerShell 将其解码为Windows-1252或其他单字节字符集。

我在Invoke-WebRequest cmdlet 或Select-Xml cmdlet上找不到可以让我指定 UTF-8 的开关。

有没有一种简洁的方法来解决这个问题?任何方式都可以,但我认为我缺少一件简单的事情。

4

1 回答 1

4

简短答案:使用 Content 属性

在 Get-Airports 中,将管道的开头替换为以下表达式:

(Invoke-WebRequest -Uri $Uri -UseBasicParsing).Content

该函数将产生预期的结果。

不会有带问号的机场名称。

长答案:Invoke-WebRequest 错误

Invoke-WebRequest 返回一个BasicHtmlWebResponseObject的实例。它的 ToString 方法破坏了响应内容。

中国机场列表中充满了非 ASCII 字符,因此提供了一个很好的测试用例。此代码通过 Content 属性和 ToString 方法抓取该页面并提取标题:

$uri = 'http://zh.wikipedia.org/wiki/國際民航組織機場代碼_(Z)'
$response = (Invoke-WebRequest -Uri $uri -UseBasicParsing)
$pattern = '\<title\>.+\</title\>'
[Regex]::Match($response.Content, $pattern).Value
[Regex]::Match($response.ToString(), $pattern).Value

输出如下所示:

<title>國際民航組織機場代碼 (Z) - 维基百科,自由的百科全书</title>
<title>?????????????????????????????? (Z) - ????????????????????????????????????</title>

Content属性包含正确解码的响应。

ToString方法返回垃圾。

ToString 表现得像 Content 似乎是合理的,所以这里似乎有问题。

为了进一步挖掘,我使用了 ILSpy,开源的 .NET 程序集浏览器和反编译器。

BasicHtmlWebResponseObject 构造函数调用 InitializeContent 来设置 Content 属性:

// Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject
private void InitializeContent()
{
    string contentType = ContentHelper.GetContentType(base.BaseResponse);
    if (ContentHelper.IsText(contentType))
    {
        string characterSet = WebResponseHelper.GetCharacterSet(base.BaseResponse);
        this.Content = StreamHelper.DecodeStream(base.RawContentStream, characterSet);
        return;
    }
    this.Content = string.Empty;
}

该方法惊人地检测到正确的解码。

BasicHtmlWebResponseObject 从 WebResponseObject 继承 ToString:

// Microsoft.PowerShell.Commands.WebResponseObject
public sealed override string ToString()
{
    char[] chars = Encoding.ASCII.GetChars(this.Content);
    for (int i = 0; i < chars.Length; i++)
    {
        if (!this.IsPrintable(chars[i]))
        {
            chars[i] = '.';
        }
    }
    return new string(chars);
}

WebResponseObject 的 ToString 方法天真地将响应解码为 ASCII。

默认的ASCII 解码器使用替换回退来为未知字节生成问号。

我没有看到它在任何地方记录,但我认为 Select-Xml 调用 ToString 将管道对象转换为 XML。这是合理的行为,但由于 BasicHtmlWebResponseObject 的设计错误,在这里不起作用。

我猜是 Windows-1252 解码,因为它是我的默认代码页。但这不可能;该字符í在 Windows-1252 中具有编码,但?在输出中被替换为。

于 2013-10-08T19:08:25.687 回答