博客地址:http://www.jianshu.com/p/9540e7566192
本文的工程目的是使用 ruby 编写一个脚本文件,实现对网页中第三方广告的检测和统计。 项目源代码:https://github.com/vito0705/selenium_vito
对页面中的第三方广告进行检测,找出其中隐藏的广告网页并将数据记录下来。
iframe
标签中,需要从iframe
标签中获取所需的数据selenium
作为 web 自动化测试工具spreadsheet
这个 gem 来实现相关功能Linux 和 windows 下均可以使用这个脚本,但对于环境配置略有不同。
可以参考这篇文章中使用 rvm 管理 ruby 的方式安装,要求 ruby 版本大于等于 2.0,具体安装不作更多说明。
在terminal
中执行:
gem install selenium-webdriver
selenium-webdriver 的 Github 源码地址
gem install spreadsheet
根据自己的浏览器版本,选择对应的 selenium 浏览器驱动版本 driver 进行下载解压,将下载解压好的 driver 文件移动到/usr/bin/
文件夹下即可。
以上四步,是 linux 下运行程序必要的环境配置,务必保证每一步的正确安装。
windows 下的环境配置与 Linux 下略有不同,但思路是相通的。
按照这篇文章《Ruby 安装 - Windows》安装 ruby 即可,记得勾选Add Ruby executables to your PATH
这一项。同样,要求 ruby 版本大于等于 2.0。
在cmd
中执行:
gem install selenium-webdriver
selenium-webdriver 的 Github 源码地址
gem install spreadsheet
根据自己的浏览器版本,选择对应的 selenium 浏览器驱动版本 driver 进行下载解压,将下载解压好的 driver 文件放在对应的浏览器安装目录下,之后需要对 Windows 环境变量进行配置。 Windows 下需要在系统变量的 path 变量中添加 exe 文件的位置,配置环境变量可参考这篇文章:Win7 怎样添加环境变量,注意路径中不要有中文。
同样,这四步也是 Windows 下必备的环境配置。但在自己的测试过程中,由于一些安全问题,Windows 下的 chrome 始终没有调通,但 Firefox 是可以使用的。
iframe
标签中,我们的目的是找到这些iframe
标签中的src
,即就是第三方广告的网址。因此我们可以将思路转变为:首先通过 selenium 获取网页的源代码,之后通过 ruby 正则表达式来实现对关键信息的提取。代码内容我们分成将四部分来分别说明。
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 :chrome
driver = 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 = Selenium::WebDriver.for :firefox
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]
这部分使用正则匹配获得待检测网址的域,有两个重要的点需要说明。
所谓第三方,指的是在iframe
中嵌入的网页的域与当前网页的域不同。那么什么是域呢?在我之前介绍跨域解决方案 rack-cors 文章里,举了这样一个例子:
那么什么是同源?我们知道,URL 由协议、域名、端口和路径组成,如果两个 URL 的协议、域名和端口相同,则表示他们同源。
我们用一个例子来说明:
URL: http://www.example.com:8080/script/jquery.js
在这个 url 中,各个字段分别代表的含义:
http://——协议
www——子域名
example.com——主域名
8080——端口号
script/jquery.js——请求的地址
当协议、子域名、主域名、端口号中任意一各不相同时,都算不同的“域”。不同的域之间相互请求资源,就叫跨域。
因此,需要获得当前网页的域,来和iframe
中的网址作对比,来判断是否属于第三方。
这里不对 ruby 中的正则表达式的语法进行详述,仅对其 MatchData 对象中的分组捕获相关的几点做简单的说明。
=~
的区别:正则表达式匹配后返回值不同,=~
返回字符串匹配中匹配的开始位置的数字索引,而 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">
nil
2.2.7 :019 > /abc/.match("abcd")
=> #<MatchData "abc">
2.2.7 :020 > /abc/.match("bcd")
=> nil
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"
可以肯定的是,上式中,从左边开始计数的成对圆括号之间匹配的结果,与结果严格对应。
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
end
end
sheet[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 访问,有时由于网络原因也会导致访问时间过长而失败,因此需要添加异常处理,从而保证程序能够正确地运行下去。
已经完成的脚本文件在环境配置成功后,可以直接使用,整个工程中共有三个文件:
ruby
> ruby detection_ad.rb
工程即可正常运行。weburl.txt
文件,并依次对文件中的所有网址进行检测。
当需要修改此文件名称时,需要在脚本中修改相关代码,将weburl.txt
修改成自己需要的名称:
ruby
web_file = "weburl.txt"
xls
文件中,只需要修改detection_ad.rb
文件中相关代码,将ad_file.xls
改为自己需要的名称:
ruby
excel_fil.write "ad_file.xls"
到这里,我们的整个工程就全部完成了,我们希望达到的目的也都实现了。但这里还有一些疑问或是问题有待解决:
暂时想到这么多,后续需要认真纠正和学习。