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

通过浏览器自动化实现高效数据录入

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (70投票s)

2012年2月29日

Ms-PL

7分钟阅读

viewsIcon

128872

downloadIcon

1684

您将学习如何创建一个半自动化的爬虫,并自动化浏览

通过浏览器自动化实现高效数据录入

目录

写完这篇文章后,我找到了一种适用于所有计算机而不是仅仅是IE的方法:使用Selenium。Selenium 是一个跨多种浏览器的互操作层。我不喜欢它过于工程化的设计,有很多接口,功能很难通过intellisense找到,但它做得很好!与WebBrowser相比,唯一的限制是你无法通过C#代码订阅javascript事件!(但你可以执行javascript)

引言

我是一名.NET培训师,有80名学生参加了我的课程...

你能想象吗?我必须手工和键盘给80名学生打分?

我一生中有3个恐惧:无所事事、做重复性的工作和蜘蛛。

当我可以的时候,我宁愿自己创建考试,所以我只是指示我的学生尽一切可能让单元测试通过:1个问题,1个单元测试,1分。简单、有效、实用、可扩展,而且我教他们单元测试的真实用例。

"不要在同一个bug上浪费我的时间两次(80次),一次找到它,写一个测试,让它通过,然后继续前进。"

"我的工作是让你成为一名出色的开发者,你的工作是交付有价值的软件,我们不会因为修复bug而获得报酬。"(好吧……实际上我们经常因为修复bug而获得报酬)

但这次……这次是别人创建了考试……而且没有单元测试!80份试卷!

所以,作为一个有责任心的开发者,我让我的电脑为我工作……用了两个Visual Studio项目

  • 第一个项目是一个XAML解析器/代码解析器,用于自动批改考卷。(你是否在你的xaml文件中创建了一个Grid中的Button?1分)
  • 第二个项目是一个网络爬虫,它将所有分数自动录入学校的网站。

比手工做有趣多了……你猜怎么着?学生们直到我告诉他们才知道!:)

"我为什么得到这个分数?"
"让我看看我的记录(日志)……你忘了使用Pivot,-1分,你没有使用过渡动画-2分,你的按钮没有事件处理程序-1分……"
"好吧,我明白了!哇,你记录了我们每个人,而且这一切都在一天之内完成!"
"你猜怎么着!我昨晚非常努力地工作!太累了!……我将以MS-PL发布,你想要源代码吗?"
"……什么?"

但今天对我们有兴趣的是第二个项目……那个浏览器自动化爬虫

这个爬虫通过了相关性测试:它已经有了两个客户,我和另一位懒得将分数从Excel复制到学校网站的培训师。

你可能会问:“为什么你要进行浏览器自动化?为什么不使用一个简单、经典的HTTP爬虫并发出HTTP请求?

作为一名教师/培训师,我需要在一个用ASP.NET/AJAX编写的网站上输入分数,所以创建HTTP请求非常复杂。

即使有了Fiddler这样的工具和出色的Request to Code插件,我也花了20分钟都没能成功破解发送分数的正确HTTP请求。

所以我采取了另一种方法:浏览器自动化。

使用HTTP爬虫,你会说:

  • 向LOGIN URL发送一个POST请求,参数为Login="Nicolas"和Password="Password"以及ViewState="....."
  • 保存cookie
  • 使用此cookie生成其他HTTP请求……

使用浏览器自动化,你会说:

  • 点击“loginBox”并输入Nicolas
  • 点击“password”并输入Password
  • 点击submitForm按钮
  • 点击下一步
  • 点击ddl下拉列表,我将做出选择
  • 点击继续……

总而言之,你使用浏览器为你发送请求。

用例:自动为这篇文章投票5分

致CodeProject管理员的免责声明:我从未使用过这种卑鄙的方法来人为地获得更多投票……你们可以自己查看我文章的投票数……这是第一次!

我知道你们都很忙但又非常好奇。所以这个用例就是利用你们的好奇心给我投5票。

所以,使用一个经典的爬虫,你可能会说:

  • 向https://codeproject.org.cn/Articles/338036/BrowserAutomationCrawler发送GET请求
  • 向action "submitLogin"发送带有login参数和password参数的POST请求
  • 保存cookie
  • 向action=Vote发送vote=5, articleId=MyArticleId的POST请求,并附带cookie

这可能会奏效,除了我完全编造了参数和action名称,所以你需要Fiddler来精确调整请求(而且根据网站的不同,这可能非常非常困难,尤其是对于AJAX的东西)。

做同样事情的另一种方法是说:

  • 访问https://codeproject.org.cn/Articles/338036/BrowserAutomationCrawler
  • 如果出现“logout”,则表示您已登录,
    else
    等待我点击“Sign in”(这样我就可以手动填写电子邮件和密码),
  • 然后点击“Vote 5”选项
  • 在评论文本框中填写“5 for me, great Nicolas, thanks for you work! :)”
  • 点击投票按钮

继续下载源代码并自己尝试!!!

让我们看看代码

首先,我必须实例化一个WebBrowser(我用来自动化浏览器的控件),并设置我的文章的URL、你给我的投票、评论,以及可能你的CodeProject凭据(可选,因为你可以手动输入)。

[STAThread]
static void Main(string[] args)
{
    Form form = new Form();
    form.Width = 1024;
    form.Height = 780;
    WebBrowser browser = new WebBrowser();
    browser.Dock = DockStyle.Fill;
    form.Controls.Add(browser);

    new VoteCrawler()
    {
        WebBrowser = browser,
        ArticleUrl = "https://codeproject.org.cn/Articles/338036/BrowserAutomationCrawler",
        Rating = 5,
        Comment = "5 for me, great Nicolas, thanks for you work ! :)"
        //You can choose to specify email and password here on enter them directly on the website (or if IE has your cookie, it will vote anyway)
        //Email = "email",
        //Password = "password"
    }.Crawl();

    form.ShowDialog();
}

然后这是自动投票的代码

public class VoteCrawler : Crawler
{
    public string ArticleUrl
    {
        get;
        set;
    }
    public string Comment
    {
        get;
        set;
    }
    public int Rating
    {
        get;
        set;
    }
    public string Email
    {
        get;
        set;
    }
    public string Password
    {
        get;
        set;
    }

    protected override void Automate()
    {
        GoTo(ArticleUrl);
        EnsureIsSignedOn();
        Click("ctl00_RateArticle_VoteRBL_" + (Rating - 1).ToString());
        Fill(new ClassSelector("RateComment"), Comment);
        Click("ctl00_RateArticle_SubmitRateBtn");
    }

    void EnsureIsSignedOn()
    {
        var isLogged = Actions.Ask(() => WebBrowser.Document.GetElementById("ctl00_MemberMenu_Signout") != null);
        if(!isLogged)
        {
            if(Email != null)
                Fill("Email", Email);
            if(Password != null)
                Fill("Password", Password);

            var submit = new IdSelector("subForm").SelectChildren(e => e.GetAttribute("type") == "submit");
            if(Email == null || Password == null)
                WaitClickOn(submit);
            else
                Click(submit);
            WhenLoaded();
            EnsureIsSignedOn();
        }
    }
}

如果IE保存了你的cookie,你就不需要输入登录/密码,它会自动为你投票。

我只是继承了Crawler类并重写了Automate,代码是自解释的。

var isLogged = Actions.Ask(()=> WebBrowser.Document.GetElementById("ctl00_MemberMenu_Signout") != null);

你可以看到我使用WebBrowser类来做我的事情,通过Actions.Ask。正如我将在幕后花絮中解释的那样,我使用了一个winform组件。Automate不在UI线程上运行,Actions会在UI线程内调用该操作。

问题是,如何轻松找到HTML元素的id,比如ctl00_RateArticle_VoteRBL_4,如果它没有id怎么办?

查找ID很简单,在Chrome中右键单击元素,选择“检查元素”,它会带你去你需要的地方。

对于更复杂的请求,你可以使用自定义或内置的Selectors(字符串会自动转换为IdSelector)。

例如,在点击5选项后填充评论框,这是选择并填充它的代码:将文本填充到名为RateComment的类文本框中。

Fill(new ClassSelector("RateComment"), "5 for me, great Nicolas, thanks for you work ! :)");

这是Crawler类和选择器

好的,描述的文章内容比代码本身还要多,因为代码本身只有大约300行。

它是如何工作的?

幕后

System.Windows.Forms.WebBrowser 是一个在winform应用程序中嵌入浏览器的类。

有趣的是,它是IE7(或者我记不清是IE6)COM接口的包装器。

所以你可以用C#代码运行javascript,或者在HtmlElement上调用你想要的事件。

例如,在Crawler.Click中,我调用了浏览器中的javascript点击事件。

public Crawler Click(Selector selector)
{
    Actions.Do((end) =>
    {
        selector.Select(WebBrowser.Document).First().InvokeMember("click");
        end();
    });
    return this;
}

或者我直接修改DOM

public Crawler Fill(Selector selector, string value)
{
    Actions.Do((end) =>
    {
        selector.ForEach(WebBrowser.Document, e => e.InnerText = value);
        end();
    });
    return this;
}

WebBrowser是一个winform组件,所以它运行在UI线程上。由于Automate应该顺序运行但又不阻塞运行线程,所以我把它运行在一个单独的线程上。

public void Crawl()
{
    if(WebBrowser == null)
        throw new InvalidOperationException("WebBrowser should be affected");
    ThreadPool.QueueUserWorkItem(s =>
    {
        Automate();
    });
}

AutomationActions只是SynchronizationContext或UI线程的包装器。

public class AutomationActions
{
    SynchronizationContext _UiContext;
    AutoResetEvent _AutoReset = new AutoResetEvent(false);

    public AutomationActions()
    {
        _UiContext = SynchronizationContext.Current;
    }

    public T Ask<T>(Func<T> request)
    {
        T result = default(T);
        Do(end =>
        {
            result = request();
            end();
        });
        return result;
    }
    public void Do(Action action)
    {
        Do(end =>
        {
            try
            {
                action();
            }
            finally
            {
                end();
            }
        });
    }
    public void Do(Action<Action> action)
    {
        if(_UiContext == SynchronizationContext.Current)
        {
            throw new InvalidCastException("Cannot call AutomationActions in the UI thread");
        }
        _UiContext.Post(s =>
        {
            action(() =>
            {
                _AutoReset.Set();
            });
        }, null);
        _AutoReset.WaitOne();
    }
}

Do(Action<Action> action)允许在UI线程决定继续时,通过调用Action参数来执行下一个操作。

例如,我用它来执行WhenLoaded操作。

public Crawler WhenLoaded()
{
    Actions.Do((end) =>
    {
        WebBrowserNavigatedEventHandler onNavigated = null;
        onNavigated = (s, e) =>
        {
            WebBrowser.Navigated -= onNavigated;
            WebBrowser.Document.Window.Load += (s1, e2) =>
            {
                end();
            };
        };
        WebBrowser.Navigated += onNavigated;
    });
    return this;
}

结论并向您提问

这种爬行方法的明显局限性在于,它不适用于需要浏览大量数据的网络爬虫或文本挖掘应用程序。

它非常适合于半自动化数据录入。

另一个限制是WebBrowser使用的是IE76或7……你知道我如何使用其他浏览器吗?这可能会写一篇有趣的互操作性文章。

我找到了一种使用你想要的任何浏览器的方法:使用Selenium。Selenium是一个跨多种浏览器的互操作层。我不喜欢它过于工程化的设计,有很多接口,功能很难通过intellisense找到,但它做得很好!与WebBrowser相比,唯一的限制是你无法通过C#代码订阅javascript事件!(但你可以执行javascript)……如果我重构代码使用Selenium,我将更新这篇文章。

Selenium非常适合测试用例,这解释了为什么它在辅助/半自动化数据录入工具方面有一些摩擦。

© . All rights reserved.