使用Win32::OLE自动捕获Internet Explorer截图

背景

以前我曾写过关于使用Perl的Win32::OLE来控制Internet Explorer的文章,这是响应Stackoverflow上的一个问题

当时我还在坚持使用Windows XP。从那时起,我已经升级到Windows 8.1 Pro 64位版,并且不再使用PPMsActivePerl,而是使用Visual Studio 2013来构建perl以及我需要的模块。

我一直在使用Perl的Win32::OLE来控制Internet Explorer,用于各种目的,已经有近10年的历史了。除了需要阅读大量的Microsoft文档外,这实际上并没有什么复杂之处。这么多年过去了,还没有任何语言或环境像Perl那样有如此完善的文档,不仅信息量丰富,而且你可以很容易地找到清晰、正确和有用的信息。

无论如何,虽然信息的组织结构还有很多需要改进的地方,但查找通过OLE驱动Internet Explorer信息的良好起点是MSDN上的Internet Explorer对象文档。如果您想与InternetExplorer对象内的内容交互,可以查阅MSHTML脚本对象接口主题。了解一点关于OLE接口的知识也有帮助。

阅读brian d foy关于使用Perl控制Firefox的文章时,我发现Win32::IE::Mechanize已经从CPAN消失了。我并不理解PerlMonks上的讨论,因为我非常清楚地记得使用Win32::OLE来控制Internet Explorer 8进行了一次大规模的抓取工作。

我决定查看我旧时的截图工具,看看在Windows 8上使用Internet Explorer 10时需要做哪些更改。我的修改后的脚本可以在GitHub gist中找到。在这里,我将介绍一些重点。

跟踪执行

想法是使用DWebBrowserEvents2来确定捕获浏览器窗口的正确时机。我决定看看我2012年的答案是否仍然有效。我指向我的个人网站,它失败了

Win32::OLE(0.1712) error 0x80020009: "Exception occurred"
    in METHOD/PROPERTYGET "StatusText" at iescreenshot.pl line 38.

问题的原因在于访问Internet Explorer对象StatusText属性。显然,IE10不再公开这个属性。好吧,我之所以使用它,只是为了给出一些发生什么的线索。我决定写一个快速记录函数,该函数可以用于所有事件

sub log_browser_event {
    my $event = shift;
    no warnings 'uninitialized';
    my $args = eval { join(' ' => map valof($_), @_) };
    say "$event: $args";
    return;
}

这不是完美的代码示例,但我在尽量保持简短。

事件处理

我们只关心两个事件:DocumentComplete,这样我们就可以知道何时截图,以及onQuit,这样我们就可以在用户在我们到达那个点之前关闭浏览器窗口时干净地退出。

您可以使用以下调用初始化OLE事件

Win32::OLE->WithEvents(
    $object,
    $handler,
    $interface
);

然后,假设您的$handler有一些巨大的switch语句,根据实际接收的事件进行分发。相反,我选择使用分发表

const my %BrowserEvents => (
    DocumentComplete => sub {
        $do_take_screenshot = 1;
        Win32::MessageLoop->QuitMessageLoop;
    },
    OnQuit => sub {
        $do_take_screenshot = 0;
        Win32::MessageLoop->QuitMessageLoop;
    },
    _ => sub { },
);

注意使用Win32::MessageLoop->QuitMessageLoop而不是Win32::OLE->QuitMessageLoop,以避免虚假的睡眠调用。

然后,我使用以下方式初始化OLE事件接口:

Win32::OLE->WithEvents(
    $browser,
    sub { $handler->(\%BrowserEvents, @_) },
    'DWebBrowserEvents2'
);

在这种情况下,$handler只是记录事件,并咨询调度表以查看我们是否对事件感兴趣。

sub WebBrowserEventHandler {
    my $handlers = shift;
    my $browser = shift;
    my $event = shift;

    log_browser_event($event, @_);

    my $handler = exists $handlers->{$event}
                ? $handlers->{$event}
                : $handlers->{_}
    ;
    $handler->($browser, $event, @_);
    return;
}

在接收到DocumentCompleteonQuit后,我们终止消息循环,控制权返回到导航函数。此时,唯一剩下的就是检查是否需要捕获屏幕截图。之后,程序终止。

捕获Internet Explorer窗口

当我运行这个修改过的脚本并尝试使用Imager::Screenshot进行截图时,我得到了只有浏览器框架的截图,没有任何内容。我不确定发生了什么,我将在以后尝试诊断这个问题。目前,由于我正在使用令人尊敬的Win32::GuiTest模块,我决定使用它提供的Win32::GuiTest::DibSect类。

sub take_screenshot {
    my $browser = shift;

    wait_until_ready($browser);

    my $hwnd = $browser->{HWND};
    my $title = $browser->{Document}{title};
    $title =~ s/[^A-Za-z0-9_-]+/-/g;

    my $ds = Win32::GuiTest::DibSect->new;

    my $fgwnd = GetForegroundWindow();
    SetForegroundWindow $hwnd;
    $ds->CopyWindow($hwnd);
    SetForegroundWindow $fgwnd;

    $ds->SaveAs("$title.bmp");
    $ds->Destroy;

    return;
}

等待文档渲染

尽管如此,我仍然偶尔会得到一个空白文档区域的截图。如果我的理解正确,DocumentReady事件被触发并不意味着文档已完全渲染。它只是意味着你可以操作DOM。因此,我添加了一个简单的循环,使浏览器停止忙碌。这绝对不是万无一失的,但它已经在大多数尝试过的网站上起作用了。具有大量AJAX内容的网站往往会有这个问题。有特定于网站的解决方法,但这超出了本文的范围。

sub wait_until_ready {
    my $browser = shift;
    {
        local $| = 1;
        while ($browser->Busy) {
            print '.';
            sleep 1;
        }
    }
    return;
}

此时,您可以从命令行运行脚本,只需简单地输入perl iescreenshot.pl perltricks.com

WebDriver API

WebDriver API可能消除了使用其他任何解决方案来驱动Internet Explorer的需求,但直到它无处不在,Win32::OLE仍然是足够的。

结论

在过去,使用Win32::OLE来驱动Internet Explorer对我来说非常有帮助。截图只是简单的、验证概念性的练习。使用Perl的美丽之处在于,一旦你到达包含所需信息的页面,你可以使用Perl优秀的HTML解析模块从中获取你想要的内容,然后,比如说,保存到Excel工作表、生成PDF文档,或者将其存储在某个数据库中。


本文最初发布在PerlTricks.com

标签

Sinan Unur

Sinan Unur是一位专注于医疗保健经济学的经济学家和开发者。你经常可以在他的博客上找到他关于编程的文章,或者在StackOverflow上回答问题。

浏览他们的文章

反馈

这篇文章有什么问题吗?请通过在GitHub上打开问题或拉取请求来帮助我们。