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)