爬虫识别
搜索引擎与爬虫

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

在 Python 中用 Selenium 执行 Javascript

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 生成的页面内容。

返回顶部