爬虫系列:在 Python 中用 Selenium 执行 Javascript

CodeCosmicAegis
• 阅读 1330

Selenium 是一个强大的网络数据采集工具,其最初是为网站自动化测试而开发的。近几年,它还被广泛用于获取精确的网站快照,因为它们可以直接运行在浏览器上。Selenium 可以让浏览器自动加载页面,获取需要的数据,甚至页面截屏,或者判断网站上某些动作是否发生。

Selenium 自己不带浏览器,它需要与第三方浏览器结合在一起使用。例如,如果你在 Firefox 上运行 Selenium,可以直接看到一个 Firefox 窗口被打开,进入网站,然后执行你在代码中设置的动作。虽然这样可以看得更清楚,但是我更喜欢让程序在后台运行,所以我用一个叫 PhantonJS 的工具代替真实的浏览器。

PhantomJS 是一个“无头”(headles)浏览器。它会把网站加载到内存并执行页面上的 JavaScript,但是它不会向用户展示网页的图形界面。把 Selenium 和 PhantomJS 结合在一起,就可以运行一个非常强大的网络爬虫了,可以处理 cookie、JavaScript、header、以及任何你需要做的事情。

你可以从 PyPI 网站 下载 Selenium 库,也可以用第三方管理器(像 pip)用命令行安装。

PhantomJS 也可以从它的官方网站下载。因为 PhantomJS 是一个功能完善(虽然无头)的浏览器,并非一个 Python 库,所以它不需要像 Python 的其他库一样安装,也不能用 pip 安装。

虽然有很多页面都用 Ajax 加载数据(尤其是Google),我们找到了一个页面全部是由 JavaScript 生成的,同时也是一个 PWA 应用,应用名称是 SMS America,来测试我们的爬虫。这个页面上一些电话号码,与短信内容,所有内容都是 JavaScript 生成的。如果我们传统的方法采集这个页面,只能获取加载前的页面,而我们真正需要的信息(Ajax 执行之后的页面)却抓不到。

Selenium 库是一个在 WebDriver 上调用的 API。 WebDriver 有点儿像可以加载网站的浏览器,但是它也可以像 BeautifulSoup 对象一样用来查找页面元素,与页面上的元素进行交互(发送文本、点击等),以及执行其他动作来运行网络爬虫

下面的代码可以获取 Ajax “墙”后面的内容:

import os
from dotenv import load_dotenv
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import WebDriverException

from config import logger_config


class TestWebDriver(object):
    def __init__(self):
        load_dotenv()
        logger_name = 'Web Scraping to SMS America'
        self._logger_write_file = logger_config.LoggingConfig().init_logging(logger_name)
        self._chrome_path_file = os.getenv('CHROME_PATH')

    def get_asn_content(self, link):
        driver = webdriver.Chrome(executable_path=os.getenv('CHROME_PATH'))
        driver.get(link)
        try:
            WebDriverWait(driver, 10).until(EC.presence_of_all_elements_located((By.ID, "prefixes")))
            get_content = driver.find_element(By.TAG_NAME, "app-home").text
        except (WebDriverException, UnboundLocalError) as e:
            self._logger_write_file.error(f'处理 app-home 的时候出现错误,具体错误内容:{e},地址:{link}')
            return False
        finally:
            driver.quit()
        return get_content

    def main(self):
        link = "https://america.storytrain.info/home"
        self.get_asn_content(link)


if __name__ == '__main__':
    TestWebDriver().main()

上面代码使用 webDriver 和 Chrome 浏览器的方式,首先 Chrome 库创建了一个新的 Selenium WebDriver,首先用 WebDriver 加载页面,然后暂停执行 10 秒钟,再查看页面获取(希望已经加载完成的)内容。

依据你的 Chrome 安装位置,在创建新的 Chrome WebDriver 的时候,你需要在 Selenium 的 WebDriver 接入点指明 Chrome 可执行文件的路径:

driver = webdriver.Chrome(executable_path=os.getenv('CHROME_PATH'))

由于 Selenium 升级到 v4.0.0 以上使用上面代码会出现警告,所以我们重新修改代码为如下:

import os
from dotenv import load_dotenv
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import WebDriverException
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

from config import logger_config


class TestWebDriver(object):
    def __init__(self):
        load_dotenv()
        logger_name = 'Web Scraping to SMS America'
        self._logger_write_file = logger_config.LoggingConfig().init_logging(logger_name)
        self._chrome_path_file = os.getenv('CHROME_PATH')

    def get_asn_content(self, link):
        driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
        driver.get(link)
        try:
            WebDriverWait(driver, 10).until(EC.presence_of_all_elements_located((By.CLASS_NAME, "home-page-card-container")))
            get_content = driver.find_element(By.CLASS_NAME, "title-phone-number").text
        except (WebDriverException, UnboundLocalError) as e:
            self._logger_write_file.error(f'处理 app-home 的时候出现错误,具体错误内容:{e},地址:{link}')
            return False
        finally:
            driver.quit()
        print(get_content)
        return get_content

    def main(self):
        link = "https://america.storytrain.info/home"
        self.get_asn_content(link)


if __name__ == '__main__':
    TestWebDriver().main()

如果程序都配置正确,上面的程序会在几分钟之后显示下面的结果:

7743186342

虽然这个方法奏效了,但是效率还不够高,在处理规模较大的网站时还是可能会出问题。页面的加载时间是不确定的,具体依赖于服务器某一毫秒的负载情况,以及不断变化的网速。虽然这个页面加载可能只需要花两秒多的时间,但是我们设置了十秒的等待时间以确保页面完全加载成功。一种更加有效的方法是让 Selenium 不断地检查某个元素是否存在,以此确定页面是否已经完全加载,如果页面加载成功就执行后面的程序。

下面的程序用 class 是 home-page-card-container 的页面内容检查页面是不是已经完全加载:

WebDriverWait(driver, 10).until(EC.presence_of_all_elements_located((By.CLASS_NAME, "home-page-card-container")))

程序里新导入了一些新的模块,最需要注意的就是 WebDriverwait 和 expected_conditions,这两个模块组合起来构成了Selenium 的隐式等待(implicit wait)。

隐式等待与显式等待的不同之处在于:隐式等待是等 DOM 中某个状态发生后再继续运行代码(没有明确的等待时间,但是有最大等待时限,只要在时限内就可以),而显式等待明确设置了等待时间。在隐式等待中,DOM 触发的状态是用 expected_conditions 定义的(这里导入后用了别名 EC,是经常使用的简称)。在 Selenium 库里面元素被触发的期望条件(expected condition)有很多种,包括:

  • 弹出一个提示框
  • 一个元素被选中(比如文本框)
  • 页面的标题改变了,或者某个文字显示在页面上或者某个元素里
  • 一个元素在 DOM 中变成可见的,或者一个元素从 DOM 中消失了

当然,大多数的期望条件在使用前都需要你先指定等待的目标元素。元素用定位器(locator)指定。注意,定位器与选择器是不一样的(请看前面关于选择器的介绍)。定位器是一种抽象的查询语言,用 By 对象表示,可以用于不同的场合,包括创建选择器。

在下面的示例代码中,一个选择器被用来查找 class 是 title-phone-number 的文本内容:

get_content = driver.find_element(By.CLASS_NAME, "title-phone-number").text

总结

这篇文章主要讲解了在 Python 中使用 Selenium 配合 Chrome 浏览器对 JavaScript 生成的内容进行获取,同时如何处理内容是否已经加载的相关问题,最后讲解了 Selenium 如何选择热面元素等相关内容。

通过上面的文章,我们应该能够处理一些由 JavaScript 生成的页面内容。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
1年前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Python+Selenium自动化篇
本篇文字主要学习selenium定位页面元素的集中方法,以百度首页为例子。0.元素定位方法主要有:id定位:find\_element\_by\_id('')name定位:find\_element\_by\_name('')class定位:find\_element\_by\_class\_name(''
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这