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






4.93/5 (70投票s)
您将学习如何创建一个半自动化的爬虫,并自动化浏览
通过浏览器自动化实现高效数据录入

目录
写完这篇文章后,我找到了一种适用于所有计算机而不是仅仅是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非常适合测试用例,这解释了为什么它在辅助/半自动化数据录入工具方面有一些摩擦。