本文的工程目的是使用ruby编写一个脚本文件,实现对网页中第三方广告的检测和统计。
项目源代码:https://github.com/vito0705/selenium_vito
本文主要内容
一.项目分析
项目目的
项目要求
项目解决思路
二.环境配置
Linux下环境配置
Windows下环境配置
三.程序编写
(一)加载库文件
(二)初始化部分
(三)网页检测部分
(四)代码执行部分
项目设计思路
代码实现
四.脚本使用
五.总结
一.项目分析
项目目的
对页面中的第三方广告进行检测,找出其中隐藏的广告网页并将数据记录下来。
项目要求
检测所有广告及广告的域并记录下来
统计所有广告的数目及其中隐藏广告的数目
以表格形式保存数据
项目解决思路
第三方广告都在网页中的
iframe
标签中,需要从iframe
标签中获取所需的数据根据需要,我们选择
selenium
作为web自动化测试工具数据需要保存在表格中,我们选择
spreadsheet
这个gem来实现相关功能
二.环境配置
Linux和windows下均可以使用这个脚本,但对于环境配置略有不同。
Linux下环境配置
1.安装ruby
可以参考这篇文章中使用rvm管理ruby的方式安装,要求ruby版本大于等于2.0,具体安装不作更多说明。
2.安装ruby版本的selenium
在terminal
中执行:
gem install selenium-webdriver
3.安装spreadsheet:
gem install spreadsheet
4.安装selenium浏览器驱动driver
Chrome
Firefox
driver下载及版本对应:https://github.com/mozilla/geckodriver/releases
根据自己的浏览器版本,选择对应的selenium浏览器驱动版本driver进行下载解压,将下载解压好的driver文件移动到/usr/bin/
文件夹下即可。
以上四步,是linux下运行程序必要的环境配置,务必保证每一步的正确安装。
Windows下环境配置
windows下的环境配置与Linux下略有不同,但思路是相通的。
1.安装ruby
按照这篇文章《Ruby 安装 - Windows》安装ruby即可,记得勾选Add Ruby executables to your PATH
这一项。同样,要求ruby版本大于等于2.0。
2.安装ruby版本的selenium
在cmd
中执行:
gem install selenium-webdriver
3.安装spreadsheet:
gem install spreadsheet
4.安装selenium浏览器驱动driver
Chrome
Firefox
driver下载及版本对应:https://github.com/mozilla/geckodriver/releases
根据自己的浏览器版本,选择对应的selenium浏览器驱动版本driver进行下载解压,将下载解压好的driver文件放在对应的浏览器安装目录下,之后需要对Windows环境变量进行配置。
Windows下需要在系统变量的path变量中添加exe文件的位置,配置环境变量可参考这篇文章:Win7怎样添加环境变量,注意路径中不要有中文。
同样,这四步也是Windows下必备的环境配置。但在自己的测试过程中,由于一些安全问题,Windows下的chrome始终没有调通,但Firefox是可以使用的。
三.程序编写
项目设计思路
为了能使脚本检测大量网站,我们使用三个文件,一个txt文件,一个xls表格文件和包含所有逻辑功能的ruby文件。
weburl.txt:在文件中,每个网址占一行,ruby文件会依次按行读取此文件中的网址进行检测
ad_file.xls:用于保存数据,最终的数据会写入这个文件
detection_ad.rb:所有的数据逻辑处理均包含在这个文件中,负责检测页面中的第三方广告。
第三方广告都在
iframe
标签中,我们的目的是找到这些iframe
标签中的src
,即就是第三方广告的网址。因此我们可以将思路转变为:首先通过selenium获取网页的源代码,之后通过ruby正则表达式来实现对关键信息的提取。对于selenium和spreadsheet两个gem的使用,我们不作过多解释,可以参考以下两篇文章,给出了两个gem的基本使用方法。
代码实现
代码内容我们分成将四部分来分别说明。
(一)加载库文件
require 'rubygems' require 'selenium-webdriver' require 'spreadsheet'
(二)初始化部分
# 存放网址的文件web_file = "weburl.txt"# 创建excel表格实例Spreadsheet.client_encoding = "UTF-8" excel_fil = Spreadsheet::Workbook.new sheet = excel_fil.create_worksheet :name => "ads_show"# 创建浏览器driver实例# driver = Selenium::WebDriver.for :chromedriver = Selenium::WebDriver.for :firefox# 创建三个全局变量# web_num:excel表单中的行数# all_ads_num:所有网页的广告总数# hide_ads_num:所有网页的隐藏广告总数$web_num = 1$all_ads_num = 0$hide_ads_num = 0
(三)网页检测部分
这部分的功能是检测一个网页中的所有第三方广告,找到广告的域并统计广告的数量,进一步需要分离出页面中隐藏的第三方广告。
我们将这部分定义为一个方法:search_ads(driver, web_url_para, sheet)
,这个方法要求三个参数:
driver:已经创建的浏览器driver实例,如
driver = Selenium::WebDriver.for :firefox
web_url_para:待检测网页网址url
sheet:已经创建的excel表单实例,如
sheet = excel_fil.create_worksheet :name => "ads_show"
接下来会从多个模块来介绍这一部分内容。
功能块一
web_url = web_url_para #-------------------------------------------------------- #web_url_domain:the domian of the web page #-------------------------------------------------------- web_url_domain_raw = web_url.match(/https?\:\/\/(.*?)\/.*?/) web_url_domain = web_url_domain_raw[1]
这部分使用正则匹配获得待检测网址的域,有两个重要的点需要说明。
1.不同的域
所谓第三方,指的是在iframe
中嵌入的网页的域与当前网页的域不同。那么什么是域呢?在我之前介绍跨域解决方案rack-cors文章里,举了这样一个例子:
那么什么是同源?我们知道,URL由协议、域名、端口和路径组成,如果两个URL的协议、域名和端口相同,则表示他们同源。
我们用一个例子来说明:
URL: http://www.example.com:8080/script/jquery.js
在这个url中,各个字段分别代表的含义:
http://——协议
www——子域名
example.com——主域名
8080——端口号
script/jquery.js——请求的地址
当协议、子域名、主域名、端口号中任意一各不相同时,都算不同的“域”。不同的域之间相互请求资源,就叫跨域。
因此,需要获得当前网页的域,来和iframe
中的网址作对比,来判断是否属于第三方。
2.MatchData对象的分组捕获
这里不对ruby中的正则表达式的语法进行详述,仅对其MatchData对象中的分组捕获相关的几点做简单的说明。
match方法
可以双向使用match方法,即正则表达式和字符串对象均可以响应match方法。match方法会将字符串参数转换为正则表达式
match与
=~
的区别:正则表达式匹配后返回值不同,=~
返回字符串匹配中匹配的开始位置的数字索引,而match则返回MatchData实例:2.2.7 :017 > "The alphabet starts with abc" =~ /abc/ => 25 2.2.7 :018 > /abc/.match("The alphabet starts with abc") => #<MatchData "abc">
MatchData对象
正则表达式通过圆括号指定捕获(capture)。当一个字符串和模式之间进行正则匹配测试时,通常是想使用字符串,或者更常见的是用字符串的一部分完成一些操作。捕获表示法让用户可以从能够匹配特殊子模式的字符串中,抽取和保存字符子串。
从MatchData对象中得到捕获结果的一个方式是直接通过数组的方式索引对象:
0
索引会返回匹配的整个字符串;从1
开始往后,n
的索引会基于从左边的括号开始计数,返回第n
个捕获结果。关于“从左开始计数圆括号”的周期性,用一个例子来说明:a=/((a)((b)c)(d)?)/.match("abce") => #<MatchData "abc" 1:"abc" 2:"a" 3:"bc" 4:"b" 5:nil> a[0] => "abc"a[1] => "abc" a[2] => "a" a[3] => "bc" a[4] => "b" a[5] => nil (不匹配) a[6] => nil (超出范围) a[-2] => "b"
可以肯定的是,上式中,从左边开始计数的成对圆括号之间匹配的结果,与结果严格对应。
当正则表达式通过match方法匹配时,返回一个MatchData对象;当正则表达式不匹配时,返回
nil
2.2.7 :019 > /abc/.match("abcd") => #<MatchData "abc"> 2.2.7 :020 > /abc/.match("bcd") => nil
分组捕获
功能块二
driver.get web_url sleep 3 #-------------------------------------------------------- #get <iframe ...>...<\iframe> #-------------------------------------------------------- html_source = driver.page_source match_iframe = html_source.scan(/(<\s*iframe\s.*?>.*?<\s*\/\s*iframe\s*>)/)
这部分功能是访问目标网页,获取网页源代码,并获得源代码中所有的iframe
标签中的数据。
功能块三
#-------------------------------------------------------- #select the third party hide ads url from iframe.src #iframe_src_hide:hide ad url #ad_hide_num: number #-------------------------------------------------------- iframe_src_hide_raw = match_iframe.map do |ifr| if (src_match = ifr[0].to_s.match(/(<\s*iframe\s.*?(src=\"(.*?)\".*?>))/) ) src_matched_hide = src_match[1].gsub(/\&\;/,"&") hide_condition_1 = src_matched_hide.match(/.*?\swidth\s*\=\s*\"\s*0\s*px\s*\"\s.*?height\s*=\s*\"\s*0\s*px\s*\".*/) hide_condition_2 = src_matched_hide.match(/.*?\sheight\s*\=\s*\"\s*0\s*px\s*\"\s.*?width\s*=\s*\"\s*0\s*px\s*\".*/) hide_condition_3 = src_matched_hide.match(/.*?style\s*=\s*\".*?width\s*:\s*0\s*px\s*;.*?height\s*:\s*0\s*px.*?\"/) hide_condition_4 = src_matched_hide.match(/.*?style\s*=\s*\".*?height\s*:\s*0\s*px\s*;.*?width\s*:\s*0\s*px.*?\"/) hide_condition_5 = src_matched_hide.match(/.*?\sdisplay\s*=\s*\"\s*none\s*\"\s*/) hide_condition_6 = src_matched_hide.match(/.*?style\s*=\s*\".*?display\s*:\s*none\s*.*?\"/) if (hide_condition_1 || hide_condition_2 || hide_condition_3 || hide_condition_4 || hide_condition_5 || hide_condition_6) # alert("123"); src_matched = src_match[3].gsub(/\&\;/,"&") src_matched = src_matched.match(/https?\:\/\/(.*)\/.*/) if src_matched domain_judge_raw = src_matched[0].to_s.match(/https?\:\/\/(.*?)\/.*?/) domain_judge = domain_judge_raw[1] if domain_judge.to_s == web_url_domain.to_s #the same domain src_matched = nil else #not the same domain src_matched[0] end end else src_matched = nil end end end iframe_src_hide = iframe_src_hide_raw.compact ad_hide_num = iframe_src_hide.size $hide_ads_num = $hide_ads_num + ad_hide_num
这部分功能是:检测页面中所有的第三方隐藏广告。所谓隐藏广告,就是其iframe
标签中的height
和width
属性的值均为0px
,或者display
属性的值为none
,此时在页面中并不显示这个第三方广告。
这部分代码中,有一个点需要说明:
src_matched_hide = src_match[1].gsub(/\&\;/,"&")
这句代码的功能是将得到的src
网址中的&
替换为&
。这是因为,在HTML中,预留字符必须被替换为字符实体。这里对HTML字符实体进行了较为详细的介绍。
在本例中,我们通过正则表达式得到的url中,最常用的&
被转义成了&
,因此需要对其进行修正。而其他的字符实体因为在url中使用较少,此处没有进行更多的校验。
功能块四
#-------------------------------------------------------- #select the third party ads url from iframe.src #iframe_src:ad url #ad number #-------------------------------------------------------- iframe_src_show_raw = match_iframe.map do |ifr| if (src_match = ifr[0].to_s.match(/(<\s*iframe\s.*?(src=\"(.*?)\".*?>))/) ) src_matched_hide = src_match[1].gsub(/\&\;/,"&") hide_condition_1 = src_matched_hide.match(/.*?\swidth\s*\=\s*\"\s*0\s*px\s*\"\s.*?height\s*=\s*\"\s*0\s*px\s*\".*/) hide_condition_2 = src_matched_hide.match(/.*?\sheight\s*\=\s*\"\s*0\s*px\s*\"\s.*?width\s*=\s*\"\s*0\s*px\s*\".*/) hide_condition_3 = src_matched_hide.match(/.*?style\s*=\s*\".*?width\s*:\s*0\s*px\s*;.*?height\s*:\s*0\s*px.*?\"/) hide_condition_4 = src_matched_hide.match(/.*?style\s*=\s*\".*?height\s*:\s*0\s*px\s*;.*?width\s*:\s*0\s*px.*?\"/) hide_condition_5 = src_matched_hide.match(/.*?\sdisplay\s*=\s*\"\s*none\s*\"\s*/) hide_condition_6 = src_matched_hide.match(/.*?style\s*=\s*\".*?display\s*:\s*none\s*.*?\"/) unless (hide_condition_1 || hide_condition_2 || hide_condition_3 || hide_condition_4 || hide_condition_5 || hide_condition_6) src_matched = src_match[3].gsub(/\&\;/,"&") src_matched = src_matched.match(/https?\:\/\/(.*)\/.*/) if src_matched domain_judge_raw = src_matched[0].to_s.match(/https?\:\/\/(.*?)\/.*?/) domain_judge = domain_judge_raw[1] if domain_judge.to_s == web_url_domain.to_s #the same domain src_matched = nil else #not the same domain src_matched[0] end end else src_matched = nil end end end iframe_src_show = iframe_src_show_raw.compact ad_show_num = iframe_src_show.size #-------------------------------------------------------- #all ads #-------------------------------------------------------- iframe_src = iframe_src_hide + iframe_src_show ad_num = iframe_src.size $all_ads_num = $all_ads_num + ad_num
这部分功能是:获得所有非隐藏的第三方广告的数据,计算其数量;之后与隐藏的广告数据整合,得到全部广告的数据。
功能块五
#-------------------------------------------------------- #select ad domian #src_domain:ad url domain #-------------------------------------------------------- src_domain_raw = iframe_src.map do |sr| if (domain_match = sr.to_s.match(/https?\:\/\/(.*?)\/.*?/) ) domain_matched = domain_match[1] end end src_domain = src_domain_raw.compact
这部分功能是:根据获得的所有广告数据,获得这些广告的域。
功能块六
#-------------------------------------------------------- #file operation #-------------------------------------------------------- sheet[$web_num + 0,0] = "Web url" sheet[$web_num + 0,1] = web_url sheet[$web_num + 1,0] = "The num of ads" sheet[$web_num + 1,1] = ad_num sheet[$web_num + 1,2] = "The num of hide ads" sheet[$web_num + 1,3] = ad_hide_num sheet[$web_num + 2,0] = "The url domain of ads" sheet[$web_num + 2,1] = "The url of ads" + "(The top " + String(ad_hide_num) + " are hidden ads)" ad_num.times do |n| i = n + 3 + $web_num sheet[i,0] = src_domain[n] sheet[i,1] = iframe_src[n] end $web_num = $web_num + ad_num + 3 + 1 puts "This page has searched successfully: #{web_url_para}"
这部分功能是文件操作,负责将得到的数据写入表格中。
至此,这个方法的内容已经全部介绍完了。尽管我们将这部分内容全部放在一个方法中,但由于全局变量的引入,这部分内容并不能完全的独立。
(四)代码执行部分
File.open(web_file) do |fil| if fil fil.each do |url| begin search_ads(driver, url, sheet) rescue puts "This page has searched unsuccessfully: #{url}" puts "Please waiting process..." driver.quit # driver = Selenium::WebDriver.for :chrome driver = Selenium::WebDriver.for :firefox puts "Start the next web url" next end end endendsheet[0,0] = "The ads number of all pages"sheet[0,1] = $all_ads_num sheet[0,2] = "The hide ads number of all pages"sheet[0,3] = $hide_ads_num excel_fil.write "ad_file.xls"puts "Detection is complete!"driver.quit
这部分负责读取txt文件中的网址,并依次执行上述方法,获得我们所需的数据后将其写入表格中。
这里,我们加入了异常处理。对于我们的功能,一些网站会禁止通过selenium访问,有时由于网络原因也会导致访问时间过长而失败,因此需要添加异常处理,从而保证程序能够正确地运行下去。
四.脚本使用
已经完成的脚本文件在环境配置成功后,可以直接使用,整个工程中共有三个文件:
detection_ad.rb:
可执行文件,在终端terminal中(windows下为cmd)执行命令:> ruby detection_ad.rb
工程即可正常运行。
weburl.txt:
这个文件用来放置待检测的网页网址,每一行仅能放置一个网址。程序运行后,脚本会打开weburl.txt
文件,并依次对文件中的所有网址进行检测。
当需要修改此文件名称时,需要在脚本中修改相关代码,将weburl.txt
修改成自己需要的名称:web_file = "weburl.txt"
ad_file.xls:
这个文件用于保存数据,脚本运行后处理得到的所有数据会全部写入这个文件中。如果需要将最终数据写入到其他名称的xls
文件中,只需要修改detection_ad.rb
文件中相关代码,将ad_file.xls
改为自己需要的名称:excel_fil.write "ad_file.xls"
五.总结
到这里,我们的整个工程就全部完成了,我们希望达到的目的也都实现了。但这里还有一些疑问或是问题有待解决:
效率问题:尽管可以实现第三方网页检测,但程序执行的速度相对很慢
代码并没有封装的很好,且引入了全局变量,当这个脚本用在比较大型的且复杂的工程中时,很可能出现问题
代码质量有待提高
由于时间比较急,对代码中变量的名称没有使用的很准确,注释也不是很清晰,需要修正
暂时想到这么多,后续需要认真纠正和学习。
作者:vito1994
链接:https://www.jianshu.com/p/9540e7566192