基于ruby+selenium的第三方广告检测

本文的工程目的是使用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  

selenium-webdriver的Github源码地址

3.安装spreadsheet:

gem install spreadsheet

spreadsheet的GitHub源码地址

4.安装selenium浏览器驱动driver

根据自己的浏览器版本,选择对应的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  

selenium-webdriver的Github源码地址

3.安装spreadsheet:

gem install spreadsheet

spreadsheet的GitHub源码地址ß

4.安装selenium浏览器驱动driver

根据自己的浏览器版本,选择对应的selenium浏览器驱动版本driver进行下载解压,将下载解压好的driver文件放在对应的浏览器安装目录下,之后需要对Windows环境变量进行配置。
Windows下需要在系统变量的path变量中添加exe文件的位置,配置环境变量可参考这篇文章:Win7怎样添加环境变量,注意路径中不要有中文。

同样,这四步也是Windows下必备的环境配置。但在自己的测试过程中,由于一些安全问题,Windows下的chrome始终没有调通,但Firefox是可以使用的。

三.程序编写


项目设计思路

  1. 为了能使脚本检测大量网站,我们使用三个文件,一个txt文件,一个xls表格文件和包含所有逻辑功能的ruby文件。
    • weburl.txt:在文件中,每个网址占一行,ruby文件会依次按行读取此文件中的网址进行检测
    • ad_file.xls:用于保存数据,最终的数据会写入这个文件
    • detection_ad.rb:所有的数据逻辑处理均包含在这个文件中,负责检测页面中的第三方广告。
  2. 第三方广告都在iframe标签中,我们的目的是找到这些iframe标签中的src,即就是第三方广告的网址。因此我们可以将思路转变为:首先通过selenium获取网页的源代码,之后通过ruby正则表达式来实现对关键信息的提取。
  3. 对于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 :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:已经创建的浏览器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对象
    • 当正则表达式通过match方法匹配时,返回一个MatchData对象;当正则表达式不匹配时,返回nil
      2.2.7 :019 > /abc/.match("abcd")
       => #<MatchData "abc"> 
      2.2.7 :020 > /abc/.match("bcd")
       => nil 
      
    • 分组捕获
      • 正则表达式通过圆括号指定捕获(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" 
        
        可以肯定的是,上式中,从左边开始计数的成对圆括号之间匹配的结果,与结果严格对应。

功能块二

    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(/\&amp\;/,"&")
            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(/\&amp\;/,"&")
                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标签中的heightwidth属性的值均为0px,或者display属性的值为none,此时在页面中并不显示这个第三方广告。
这部分代码中,有一个点需要说明:

src_matched_hide = src_match[1].gsub(/\&amp\;/,"&")

这句代码的功能是将得到的src网址中的&amp;替换为&。这是因为,在HTML中,预留字符必须被替换为字符实体。这里对HTML字符实体进行了较为详细的介绍。
在本例中,我们通过正则表达式得到的url中,最常用的&被转义成了&amp;,因此需要对其进行修正。而其他的字符实体因为在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(/\&amp\;/,"&")

            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(/\&amp\;/,"&")
                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访问,有时由于网络原因也会导致访问时间过长而失败,因此需要添加异常处理,从而保证程序能够正确地运行下去。

四.脚本使用


已经完成的脚本文件在环境配置成功后,可以直接使用,整个工程中共有三个文件:

  • 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"
    

五.总结


到这里,我们的整个工程就全部完成了,我们希望达到的目的也都实现了。但这里还有一些疑问或是问题有待解决:

  • 效率问题:尽管可以实现第三方网页检测,但程序执行的速度相对很慢
  • 代码并没有封装的很好,且引入了全局变量,当这个脚本用在比较大型的且复杂的工程中时,很可能出现问题
  • 代码质量有待提高
  • 由于时间比较急,对代码中变量的名称没有使用的很准确,注释也不是很清晰,需要修正

暂时想到这么多,后续需要认真纠正和学习。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容