为什么PowerShell会在HTML格式中破坏非ASCII字符?

时间:2013-10-08 15:26:15

标签: encoding web-scraping powershell-v3.0

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

在网络浏览器中,airports whose codes begin with Z如下所示:

enter image description here

机场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     

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

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

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

1 个答案:

答案 0 :(得分:3)

简答:使用内容属性

在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的行为与内容相似似乎是合理的,因此这里似乎存在问题。

为了进一步挖掘,我使用了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 iherits来自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 decoder使用替换后备来为未知字节生成问号。

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

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