在PowerShell中的mshtml.HTMLDocumentClass对象上使用querySelectorAll会导致崩溃

时间:2016-05-12 20:12:18

标签: powershell com mshtml powershell-v5.0 selectors-api

我试图通过PowerShell进行一些网络抓取,因为我最近发现可以毫不费力地这样做。

一个好的起点是只获取HTML,使用Get-Member,看看我能从那里做些什么,如下:

$html = Invoke-WebRequest "https://www.google.com"
$html.ParsedHtml | Get-Member

我可用于获取特定元素的方法如下所示:

getElementById()
getElementsByName()
getElementsByTagName()

例如,我可以在文档中获得第一个IMG标记:

$html.ParsedHtml.getElementsByTagName("img")[0]

然而,在对我是否可以使用CSS Selectors或XPath进行更多研究之后,我发现有未列出的方法可用,因为我们只是使用HTML Document对象documented here

querySelector()
querySelectorAll()

所以不要这样做:

$html.ParsedHtml.getElementsByTagName("img")[0]

我能做到:

$html.ParsedHtml.querySelector("img")

所以我期待能够做到:

$html.ParsedHtml.querySelectorAll("img")

...以获取所有IMG元素。我发现的所有文档和google搜索都支持了这一点。但是,在我的所有测试中,此函数都会使调用进程崩溃,并在事件日志(0xc0000374)中报告堆损坏异常代码。

我在Windows 10 x64上使用PowerShell 5。我已经在Win10 x64虚拟机中尝试过它,这是一个干净的构建并且只是修补了。我也尝试过将Win7 x64升级到PowerShell 5.在PowerShell 5之前我还没有尝试过,因为我们这里的所有系统都已经升级了,但我可能会有一次有时间来换新的vanilla VM进行测试。

之前有没有人遇到过这个问题?到目前为止,我所有的研究都是死路一条。 querySelectorAll有替代品吗?我需要抓取在不可预测的布局中有可预测的标签集的页面,并且可能没有分配给标签的ID或类,所以我希望能够使用允许结构/嵌套/通配符的选择器。

P.S。我也尝试在PowerShell中使用InternetExplorer.Application COM对象,结果是相同的,除了PowerShell崩溃Internet Explorer崩溃。这实际上是我原来的方法,这里是代码:

# create browser object
$ie = New-Object -ComObject InternetExplorer.Application

# make browser visible for debugging, otherwise this isn't necessary for function
$ie.Visible = $true

# browse to page
$ie.Navigate("https://www.google.com")
# wait till browser is not busy
Do { Start-Sleep -m 100 } Until (!$ie.Busy)

# this works
$ie.document.getElementsByTagName("img")[0]

# this works as well
$ie.document.querySelector("img")

# blow it up
$ie.document.querySelectorAll("img")

# we wanna quit the process, but since we blew it up we don't really make it here
$ie.Quit()

希望我没有违反任何规则,这篇文章很有意义且相关,谢谢。

更新

我测试过早期的PowerShell版本。 v2-v4使用InternetExplorer.Application COM方法崩溃。 v3-4使用Invoke-WebRequest方法崩溃,v2不支持它。

2 个答案:

答案 0 :(得分:3)

@ midnightfreddie的解决方案以前对我来说很好,但现在在调用Exception from HRESULT: 0x80020101时它会抛出$NodeList.item($i)

我找到了以下解决方法:

function Invoke-QuerySelectorAll($node, [string] $selector)
{
    $nodeList = $node.querySelectorAll($selector)
    $nodeListType = $nodeList.GetType()
    $result = @()
    for ($i = 0; $i -lt $nodeList.length; $i++)
    {
        $result += $nodeListType.InvokeMember("item", [System.Reflection.BindingFlags]::InvokeMethod, $null, $nodeList, $i)
    }
    return $result
}

这个适用于New-Object -ComObject InternetExplorer.Application

答案 1 :(得分:2)

我也遇到了这个问题,posted about it on reddit。我相信当Powershell尝试枚举querySelectorAll()返回的HTML DOM NodeList object时会发生问题。 childNodes()返回相同的对象,可以由PS枚举,因此我猜测为.ParsedHtml.childNodes而不是.ParsedHtml.querySelectorAll()编写了一些粘合代码。 Intellisense尝试获取对象的制表符完整帮助也可以触发崩溃。

但是,我找到了解决方法!只需直接访问本机DOM方法.item().length,然后将节点对象发送到PowerShell数组中。以下代码从/ r / Powershell中提取帖子的最新页面,通过querySelectorAll()获取帖子列表锚点,然后使用本机DOM方法手动枚举它们到Powershell本机数组中。

$Result = Invoke-WebRequest -Uri "https://www.reddit.com/r/PowerShell/new/"

$NodeList = $Result.ParsedHtml.querySelectorAll("#siteTable div div p.title a")

$PsNodeList = @()
for ($i = 0; $i -lt $NodeList.Length; $i++) { 
    $PsNodeList += $NodeList.item($i)
}

$PsNodeList | ForEach-Object {
    $_.InnerHtml
}

编辑.Length似乎使用大写或小写。我原本以为DOM会区分大小写,所以要么有一些事情要继续翻译,要么我误解了一些东西。此外,CSS选择器正在抓取源链接(主要是self.PowerShell),但它是我的CSS选择器逻辑错误,而不是querySelectorAll()的问题。请注意querySelectorAll()的结果不是活动的,因此修改它们不会修改原始DOM。我还没有尝试修改它们或使用它们的方法,但显然我们至少可以抓住.InnerHtml

编辑2:这是一个更通用的包装函数:

function Get-FixedQuerySelectorAll {
    param (
        $HtmlWro,
        $CssSelector
    )
    # After assignment, $NodeList will crash powershell if enumerated in any way including Intellisense-completion while coding!
    $NodeList = $HtmlWro.ParsedHtml.querySelectorAll($CssSelector)

    for ($i = 0; $i -lt $NodeList.length; $i++) {
        Write-Output $NodeList.item($i)
    }
}

$HtmlWro是一个HTML Web响应对象,是Invoke-WebReqest的输出。我最初试图通过.ParsedHtml,但随后它会在作业时崩溃。这样做会返回Powershell数组中的节点。