C# 利用Selenium实现浏览器自动化操作
概述
Selenium是一款免费的分布式的自动化测试工具,支持多种开发语言,无论是C、 java、ruby、python、或是C# ,你都可以通过selenium完成自动化测试。本文以一个简单的小例子,简述C# 利用Selenium进行浏览器的模拟操作,仅供学习分享使用,如有不足之处,还请指正。
涉及知识点
要实现本例的功能,除了要掌握Html ,JavaScript,CSS等基础知识,还涉及以下知识点:
- log4net:主要用于日志的记录和存储,本例采用log4net进行日志记录,便于过程跟踪和问题排查,关于log4net的配置和介绍,之前已有说明,本文不做赘述。
- Queue:队列,先进先出模式,本文主要用于将日志信息保存于队列中,然后再显示到页面上,其中Enqueue用于添加内容到结尾处,Dequeue用于返回并移除一个位置的对象。
- IWebDriver:浏览器驱动接口,所有的关于浏览器的操作都可以通过此接口进行,不同浏览器有不同的实现类,如:IE浏览器(InternetExplorerDriver)Chrome浏览器(ChromeDriver)等。
- BackgroundWorker:后台工作线程,区别于主线程,通过事件触发不同的状态。
Selenium安装
本例开发工具为VS2019,通过NuGet进行需要的软件包的安装与管理,如下所示:

示例效果图
本例采用Chrome浏览器,用于监控某一个网站并获取相应内容,如下所示:

Selenium示例介绍
定义一个webDriver,如下所示:
1 //谷歌浏览器 2 ChromeOptions options = new ChromeOptions(); 3 this.driver = new ChromeDriver(options);
通过ID获取元素并填充内容和触发事件,如下所示:
1 this.driver.FindElement(By.Id("email")).SendKeys(username);
2 this.driver.FindElement(By.Id("password")).SendKeys(password);
3 //# 7. 点击登录按钮
4 this.driver.FindElement(By.Id("sign-in")).Click();
通过XPath获取元素,如下所示:
1 string xpath1 = "//div[@class=\"product-list\"]/div[@class=\"product\"]/div[@class=\"price-and-detail\"]/div[@class=\"price\"]/span[@class=\"noStock\"]"; 2 string txt = this.driver.FindElement(By.XPath(xpath1)).Text;
核心代码
主要的核心代码,就是浏览器的元素定位查找和事件触发,如下所示:


1 using OpenQA.Selenium;
2 using OpenQA.Selenium.IE;
3 using OpenQA.Selenium.Chrome;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
10
11 namespace AiSmoking.Core
12 {
13 public class Smoking
14 {
15 /// <summary>
16 /// 是否正在运行
17 /// </summary>
18 private bool running = false;
19
20 /// <summary>
21 /// 驱动
22 /// </summary>
23 private IWebDriver driver = null;
24
25
26 /// <summary>
27 /// # 无货
28 /// </summary>
29 private string no_stock = "Currently Out of Stock";
30
31
32 /// <summary>
33 /// # 线程等待秒数
34 /// </summary>
35 private int wait_sec = 2;
36
37 private Dictionary<string, string> cfg_info;
38
39 private string work_path = string.Empty;
40
41 /// <summary>
42 /// 构造函数
43 /// </summary>
44 public Smoking()
45 {
46
47 }
48
49 /// <summary>
50 /// 带参构造函数
51 /// </summary>
52 /// <param name="cfg_info"></param>
53 /// <param name="work_path"></param>
54 public Smoking(Dictionary<string, string> cfg_info,string work_path)
55 {
56 this.cfg_info = cfg_info;
57 this.work_path = work_path;
58 this.wait_sec = int.Parse(cfg_info["wait_sec"]);
59 //# 如果小于2,则等于2
60 this.wait_sec = (this.wait_sec < 2 ? 2 : this.wait_sec);
61 this.wait_sec = this.wait_sec * 1000;
62 }
63
64 /// <summary>
65 /// 开始跑
66 /// </summary>
67 public void startRun()
68 {
69 //"""运行起来"""
70 try
71 {
72 this.running = true;
73 string url = this.cfg_info["url"];
74 string username = this.cfg_info["username"];
75 string password = this.cfg_info["password"];
76 string item_id = this.cfg_info["item_id"];
77 if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(item_id))
78 {
79 LogHelper.put("配置信息不全,请检查config.cfg文件是否为空,然后再重启");
80 return;
81 }
82 if (this.driver == null)
83 {
84 string explorer = this.cfg_info["explorer"];
85 if (explorer == "Chrome")
86 {
87 //谷歌浏览器
88 ChromeOptions options = new ChromeOptions();
89 this.driver = new ChromeDriver(options);
90 }
91 else
92 {
93 //默认IE
94 var options = new InternetExplorerOptions();
95 //options.AddAdditionalCapability.('encoding=UTF-8')
96 //options.add_argument('Accept= text / css, * / *')
97 //options.add_argument('Accept - Language= zh - Hans - CN, zh - Hans;q = 0.5')
98 //options.add_argument('Accept - Encoding= gzip, deflate')
99 //options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko')
100 //# 2. 定义浏览器驱动对象
101 this.driver = new InternetExplorerDriver(options);
102 }
103 }
104 this.run(url, username, password, item_id);
105 }
106 catch (Exception e)
107 {
108 LogHelper.put("运行过程中出错,请重新打开再试"+e.StackTrace);
109 }
110 }
111
112
113 /// <summary>
114 /// 运行
115 /// </summary>
116 /// <param name="url"></param>
117 /// <param name="username"></param>
118 /// <param name="password"></param>
119 /// <param name="item_id"></param>
120 private void run(string url, string username, string password, string item_id)
121 {
122 //"""运行起来"""
123 //# 3. 访问网站
124 this.driver.Navigate().GoToUrl(url);
125 //# 4. 最大化窗口
126 this.driver.Manage().Window.Maximize();
127 if (this.checkIsExists(By.LinkText("账户登录")))
128 {
129 //# 判断是否登录:未登录
130 this.login(username, password);
131 }
132 if (this.checkIsExists(By.PartialLinkText("欢迎回来")))
133 {
134 //# 判断是否登录:已登录
135 LogHelper.put("登录成功,下一步开始工作了");
136 this.working(item_id);
137 }
138 else
139 {
140 LogHelper.put("登录失败,请设置账号密码");
141 }
142 }
143
144 /// <summary>
145 /// 停止运行
146 /// </summary>
147 public void stopRun()
148 {
149 //"""停止"""
150 try
151 {
152 this.running = false;
153 //# 如果驱动不为空,则关闭
154 //self.close_browser_nicely(self.__driver)
155 if (this.driver != null)
156 {
157 this.driver.Quit();
158 //# 关闭后切要为None,否则启动报错
159 this.driver = null;
160 }
161 }
162 catch (Exception e)
163 {
164 //print('Stop Failure')
165 }
166 finally
167 {
168 this.driver = null;
169 }
170 }
171
172
173 private void login(string username, string password)
174 {
175 //# 5. 点击链接跳转到登录页面
176 this.driver.FindElement(By.LinkText("账户登录")).Click();
177 //# 6. 输入账号密码
178 //# 判断是否加载完成
179 if (this.checkIsExists(By.Id("email")))
180 {
181 this.driver.FindElement(By.Id("email")).SendKeys(username);
182 this.driver.FindElement(By.Id("password")).SendKeys(password);
183 //# 7. 点击登录按钮
184 this.driver.FindElement(By.Id("sign-in")).Click();
185 }
186 }
187
188 /// <summary>
189 /// 工作状态
190 /// </summary>
191 /// <param name="item_id"></param>
192 private void working(string item_id)
193 {
194 while (this.running)
195 {
196 try
197 {
198 //# 正常获取信息
199 if (this.checkIsExists(By.Id("string")))
200 {
201 this.driver.FindElement(By.Id("string")).Clear();
202 this.driver.FindElement(By.Id("string")).SendKeys(item_id);
203 this.driver.FindElement(By.Id("string")).SendKeys(Keys.Enter);
204 }
205 //# 判断是否查询到商品
206 string xpath = "//div[@class=\"specialty-header search\"]/div[@class=\"specialty-description\"]/div[@class=\"gt-450\"]/span[2] ";
207 if (this.checkIsExists(By.XPath(xpath)))
208 {
209 int count = int.Parse(this.driver.FindElement(By.XPath(xpath)).Text);
210 if (count < 1)
211 {
212 Thread.Sleep(this.wait_sec);
213 LogHelper.put("没有查询到item id =" + item_id + "对应的信息");
214 continue;
215 }
216 }
217 else
218 {
219 Thread.Sleep(this.wait_sec);
220 LogHelper.put("没有查询到item id2 =" + item_id + "对应的信息");
221 continue;
222 }
223 //# 判断当前库存是否有货
224
225 string xpath1 = "//div[@class=\"product-list\"]/div[@class=\"product\"]/div[@class=\"price-and-detail\"]/div[@class=\"price\"]/span[@class=\"noStock\"]";
226 if (this.checkIsExists(By.XPath(xpath1)))
227 {
228 string txt = this.driver.FindElement(By.XPath(xpath1)).Text;
229 if (txt == this.no_stock)
230 {
231 //# 当前无货
232 Thread.Sleep(this.wait_sec);
233 LogHelper.put("查询一次" + item_id + ",无货");
234 continue;
235 }
236 }
237 //# 链接path1
238 string xpath2 = "//div[@class=\"product-list\"]/div[@class=\"product\"]/div[@class=\"imgDiv\"]/a";
239 //# 判断是否加载完毕
240 //# this.waiting((By.CLASS_NAME, "imgDiv"))
241 if (this.checkIsExists(By.XPath(xpath2)))
242 {
243 this.driver.FindElement(By.XPath(xpath2)).Click();
244 Thread.Sleep(this.wait_sec);
245 //# 加入购物车
246 if (this.checkIsExists(By.ClassName("add-to-cart")))
247 {
248 this.driver.FindElement(By.ClassName("add-to-cart")).Click();
249 LogHelper.put("加入购物车成功,商品item-id:" + item_id);
250 break;
251 }
252 else
253 {
254 LogHelper.put("未找到加入购物车按钮");
255 }
256 }
257 else
258 {
259 LogHelper.put("没有查询到,可能是商品编码不对,或者已下架");
260 }
261 Thread.Sleep(this.wait_sec);
262 }
263 catch (Exception e)
264 {
265 Thread.Sleep(this.wait_sec);
266 LogHelper.put(e);
267 }
268 }
269 }
270
271 /// <summary>
272 /// 判断是否存在
273 /// </summary>
274 /// <param name="by"></param>
275 /// <returns></returns>
276 private bool checkIsExists(By by)
277 {
278 try
279 {
280 int i = 0;
281 while (this.running && i < 3)
282 {
283 if (this.driver.FindElements(by).Count > 0)
284 {
285 break;
286 }
287 else
288 {
289 Thread.Sleep(this.wait_sec);
290 i = i + 1;
291 }
292 }
293 return this.driver.FindElements(by).Count > 0;
294 }
295 catch (Exception e)
296 {
297 LogHelper.put(e);
298 return false;
299 }
300 }
301
302 }
303 }
View Code
关于日志帮助类,代码如下:


1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading.Tasks;
6 using log4net;
7
8 [assembly: log4net.Config.XmlConfigurator(Watch = true)]
9 namespace AiSmoking.Core
10 {
11 /// <summary>
12 /// 日志帮助类
13 /// </summary>
14 public static class LogHelper
15 {
16 /// <summary>
17 /// 日志实例
18 /// </summary>
19 private static ILog logInstance = LogManager.GetLogger("smoking");
20
21 private static Queue<string> queue = new Queue<string>(2000);
22
23 public static void put(string msg)
24 {
25 queue.Enqueue(msg);
26 WriteLog(msg, LogLevel.Info);
27 }
28
29 public static void put(Exception ex)
30 {
31 WriteLog(ex.StackTrace, LogLevel.Error);
32 }
33
34 public static string get()
35 {
36 if (queue.Count > 0)
37 {
38 return queue.Dequeue();
39 }
40 else
41 {
42 return string.Empty;
43 }
44 }
45
46 public static void WriteLog(string message, LogLevel level)
47 {
48 switch (level)
49 {
50 case LogLevel.Debug:
51 logInstance.Debug(message);
52 break;
53 case LogLevel.Error:
54 logInstance.Error(message);
55 break;
56 case LogLevel.Fatal:
57 logInstance.Fatal(message);
58 break;
59 case LogLevel.Info:
60 logInstance.Info(message);
61 break;
62 case LogLevel.Warn:
63 logInstance.Warn(message);
64 break;
65 default:
66 logInstance.Info(message);
67 break;
68 }
69 }
70
71
72 }
73
74
75 public enum LogLevel
76 {
77 Debug = 0,
78 Error = 1,
79 Fatal = 2,
80 Info = 3,
81 Warn = 4
82 }
83 }
View Code
关于log4net的实例定义,需要由配置文件【Log4NetConfig.xml】支撑,如下所示:


1 <?xml version="1.0" encoding="utf-8" ?>
2 <log4net>
3 <root>
4 <level value="DEBUG" />
5 <appender-ref ref="LogFileAppender" />
6 <appender-ref ref="ConsoleAppender" />
7 </root>
8 <logger name="smoking">
9 <level value="ALL" />
10 </logger>
11 <appender name="LogFileAppender" type="log4net.Appender.FileAppender" >
12 <param name="File" value="logs/${TMO}log-file.txt" />
13 <StaticLogFileName value="false"/>
14 <param name="AppendToFile" value="true" />
15 <layout type="log4net.Layout.PatternLayout">
16 <param name="Header" value="[Header]"/>
17 <param name="Footer" value="[Footer]"/>
18 <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] - %m%n"/>
19 </layout>
20 <filter type="log4net.Filter.LevelRangeFilter">
21 <param name="LevelMin" value="DEBUG" />
22 <param name="LevelMax" value="ERROR" />
23 </filter>
24 </appender>
25 <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender" >
26 <layout type="log4net.Layout.PatternLayout">
27 <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] - %m%n" />
28 </layout>
29 </appender>
30 </log4net>
View Code
还需要在AssemblyInfo.cs中添加声明,如下所示:
1 [assembly: log4net.Config.DOMConfigurator(ConfigFile = "Log4NetConfig.xml", ConfigFileExtension = "xml", Watch = true)]
备注
行路难·其一
【作者】李白 【朝代】唐
金樽清酒斗十千,玉盘珍羞直万钱。
停杯投箸不能食,拔剑四顾心茫然。
欲渡黄河冰塞川,将登太行雪满山。
闲来垂钓碧溪上,忽复乘舟梦日边。
行路难,行路难,多歧路,今安在?
长风破浪会有时,直挂云帆济沧海。
赞 (0)
