返回介绍

12.1 基于浏览器的解析

发布于 2024-01-27 21:43:11 字数 24245 浏览 0 评论 0 收藏 0

有时,站点使用大量的 JavaScript 或其他页面加载后执行的代码来给页面填充内容。在这些情况中,使用一个普通的网页抓取器来分析站点几乎是不可能的。你最后得到的是一个空白的页面。如果你想要同页面进行交互(即,如果你需要点击按钮或者输入一些搜索文本),也会碰到相同的问题。无论哪一种情况,你需要找出屏幕阅读(screen read)页面的方法。屏幕读取器使用浏览器打开页面,在浏览器中加载页面之后读取并同它交互。

 屏幕读取器很擅长执行通过一系列操作来获取信息才能完成的任务。出于这个原因,屏幕读取器脚本也是自动化常规网页任务的简单方式。

在 Python 中最常用的屏幕读取库是 Selenium(http://selenium.googlecode.com/svn/trunk/docs/api/py/index.html)。Selenium 是一个 Java 程序,用来打开浏览器,并且通过读取页面同页面交互。如果你已经了解 Java,可以使用 Java IDE 来与浏览器交互。我们会通过 Python 使用 Python 包(Python binding)与 Selenium 交互。

12.1.1 使用Selenium进行屏幕读取

Selenium 是一个强大的基于 Java 的引擎,可通过支持 Selenium 的浏览器直接与网站交互。它是一个非常流行的用于用户测试的框架,让公司可以为它们的网站构建测试。对于我们的目标,我们会使用 Selenium 来抓取一个我们需要交互或不是所有内容都在第一次请求中加载(例如图 11-6 中的示例,大多数的内容在第一次请求完成后加载)的站点。让我们查看页面,看看是否可以通过 Selenium 读取它。

首先,需要使用 pip install 安装 Selenium(http://selenium-python.readthedocs.io/en/latest/installation.html):

pip install selenium

现在,开始编写 Selenium 代码。首先,需要打开浏览器。Selenium 支持许多不同的浏览器,但是附带了一个 Firefox 的内置驱动。如果你没有安装 Firefox,可以安装它,或者为 Chrome(https://code.google.com/p/selenium/wiki/ChromeDriver)、IE(https://code.google.com/p/selenium/wiki/InternetExplorerDriver)或 Safari(https://code.google.com/p/selenium/wiki/SafariDriver)安装 Selenium 驱动。让我们看一下是否能够使用 Selenium 打开网页(在例子中,我们会使用 Firefox,但是切换和使用不同的驱动器是很简单的):

from selenium import webdriver ➊

browser = webdriver.Firefox() ➋
browser.get('http://www.fairphone.com/we-are-fairphone/') ➌

browser.maximize_window() ➍

❶ 导入来自 Selenium 的 webdriver 模块。这个模块用来调用任何已经安装的驱动器。

❷ 通过使用 webdriver 模块的 Firefox 类初始化 Firefox 浏览器对象。这会在计算机上打开一个新的浏览器窗口。

❸ 通过 get 方法和一个 URL 参数,访问想要抓取的 URL。打开的浏览器应该开始加载页面了。

❹ 使用 maximize_browser 方法最大化打开的浏览器。这会帮助 Selenium“看到”更多的内容。

现在已经有了一个页面加载完的浏览器对象(browser 变量)。让我们看一下是否能够同页面上的元素交互。如果你使用浏览器的检视标签,会看到社交媒介内容在一个类名称为 content 的 div 中。让我们看一下是否可以使用新的 browser 对象看到全部内容:

content = browser.find_element_by_css_selector('div.content') ➊

print content.text ➋

all_bubbles = browser.find_elements_by_css_selector('div.content') ➌

print len(all_bubbles)

for bubble in all_bubbles: ➍
  print bubble.text

❶ browser 对象有一个函数 find_element_by_css_selector,使用 CSS 选择器来选择 HTML 对象。这行代码选择了第一个类名称为 content 的 div,这会返回第一个匹配的对象(一个 HTMLElement 对象)。

❷ 这行代码会打印第一个匹配对象的文本内容。我们期待看到第一个聊天气泡。

❸ 这行代码使用 find_elements_by_css_selector 方法,传递一个 CSS 选择器,找到所有匹配的对象。这个方法返回一个 HTMLElement 对象列表。

❹ 遍历列表,并且打印每一个对象的内容。

嗯,有些奇怪。看起来只有两个匹配我们想要查找的对象(因为在打印 all_bubbles 的长度时,看到输出为 2),但是我们在页面上看到了大量的内容气泡。让我们更深入地查看页面上的 HTML 对象,看是否可以弄明白为什么没有匹配出更多的对象(见图 12-1)。

图 12-1:iframe

啊!当查看内容的父元素时,我们看到这是一个 iframe(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe),位于页面的中部。iframe(内联框架)是一个 HTML 标签,它将另外一个 DOM 结构嵌入到页面中,允许一个页面在它自身中加载另外一个页面。我们的代码可能不能解析它,因为解析器希望只遍历一个 DOM 对象。让我们看一下是否可以让 iframe 在一个新的窗口中加载,这样就不需要遍历两个 DOM。

iframe = browser.find_element_by_xpath('//iframe') ➊

new_url = iframe.get_attribute('src') ➋

browser.get(new_url) ➌

❶ 使用 find_element_by_xpath 方法,返回第一个匹配 iframe 标签的元素。

❷ 得到 src 属性,这包含在 iframe 中加载的页面的 URL。

❸ 在浏览器中加载 iframe 的 URL。

我们找到了如何加载想要的内容的方法。现在看一下是否可以加载所有的内容气泡:

all_bubbles = browser.find_elements_by_css_selector('div.content')

for elem in all_bubbles:
  print elem.text

现在有了气泡内容——很棒!让我们收集一些信息:想要提取人的姓名、他们分享的内容、照片(如果有的话),以及到原始内容的链接。

在浏览页面 HTML 代码的过程中,看起来每一个内容元素都有 fullname 和 name 元素来标识个人,还有一个有文本内容的 twine-description 元素。我们看到这里有一个 picture 元素,还有一个 when 元素,其中保存着时间数据。when 元素还包含一个原始链接。具体讲解如下。

from selenium.common.exceptions import NoSuchElementException ➊

all_data = []

for elem in all_bubbles: ➋
  elem_dict = {}

  elem_dict['full_name'] = \
    elem.find_element_by_css_selector('div.fullname').text ➌
  elem_dict['short_name'] = \
    elem.find_element_by_css_selector('div.name').text
  elem_dict['text_content'] = \
    elem.find_element_by_css_selector('div.twine-description').text
  elem_dict['timestamp'] = elem.find_element_by_css_selector('div.when').text
  elem_dict['original_link'] = \
  elem.find_element_by_css_selector('div.when a').get_attribute('href') ➍
try:
  elem_dict['picture'] = elem.find_element_by_css_selector(
      'div.picture img').get_attribute('src') ➎
except NoSuchElementException:
  elem_dict['picture'] = None ➏
all_data.append(elem_dict)

❶ 这行代码导入来自 Selenium 的 NoSuchElementException 异常类。当在 try...except 代码块中使用异常类时,确保你导入和使用了库异常,以便正确地处理预期误差。我们知道,不是每一个对象都有照片,并且 Selenium 会在找不到我们想要的 picture HTML 元素的时候抛出这个异常,所以我们可以使用这个异常来对有和没有图片的气泡区别处理。

❷ 在 for 循环中,遍历了内容气泡。对于这其中的每一个 elem 对象,通过更深入地遍历树,可以找到其中包含的元素。

❸ 对于每一个文本对象,这行代码调用了 HTMLElement 的 text 属性,这会抛弃文本中的标签,只返回元素的文本内容。

❹ HTMLElement 的 get_attribute 方法期待得到一个嵌套的属性,并且返回属性的值。这行代码使用 href 属性来得到 URL,使用嵌套的 CSS 在 when 类中的 div 元素中查找锚标签。

❺ 在 try 代码块中,这段代码在 div 中查找照片。如果没有照片,下一行会捕获 Selenium 抛出的 NoSuchElementException 异常,因为没有匹配的元素。

❻ 如果没有找到匹配的元素,这行代码添加一个 None 值。这确保新列表中的所有对象有一个 picture 值。

我们的脚本很早就遇到了问题,你应该会看到一个包含下面文本信息的异常:

Message: Unable to locate element:
  {"method":"css selector","selector":"div.when"}

这告诉我们在查找 when 元素时碰到了问题。让我们通过检视标签仔细查看发生了什么(见图 12-2)。

图 12-2:相邻 div

通过更仔细地观察,可以看到 content div 和 when div 实际上是相邻的,而不是在 DOM 结构中的父子关系。这暴露了一个问题,因为只遍历了 content div,而不是父 div。如果仔细地观察,可以看到 twine-item-border 是 content 和 when 元素的共同父元素。你需要通过加载父元素,修改为 all_bubbles 使用的元素:

all_bubbles = browser.find_elements_by_css_selector('div.twine-item-border')

做出这个改变后重新运行之前的代码。发生了什么?你会看到更多的 NoSuchElementException 错误。因为不确定每一个元素有相同的属性,所以我们假设它们都不相同,并且重新编写代码来对异常做出解释:

from selenium.common.exceptions import NoSuchElementException

all_data = []
all_bubbles = browser.find_elements_by_css_selector(
  'div.twine-item-border')

for elem in all_bubbles:
  elem_dict = {'full_name': None,
         'short_name': None,
         'text_content': None,
         'picture': None,
         'timestamp': None,
         'original_link': None,
         } ➊
  content = elem.find_element_by_css_selector('div.content') ➋
  try:
    elem_dict['full_name'] = \
      content.find_element_by_css_selector('div.fullname').text
  except NoSuchElementException:
    pass ➌
  try:
    elem_dict['short_name'] = \
      content.find_element_by_css_selector('div.name').text
  except NoSuchElementException:
    pass
  try:
    elem_dict['text_content'] = \
      content.find_element_by_css_selector('div.twine-description').text
  except NoSuchElementException:
    pass
  try:
    elem_dict['timestamp'] = elem.find_element_by_css_selector(
      'div.when').text
  except NoSuchElementException:
    pass
  try:
    elem_dict['original_link'] = \
      elem.find_element_by_css_selector(
        'div.when a').get_attribute('href')
  except NoSuchElementException:
    pass
  try:
    elem_dict['picture'] = elem.find_element_by_css_selector(
      'div.picture img').get_attribute('src')
  except NoSuchElementException:
    pass
  all_data.append(elem_dict)

❶ 对于对象的每一次迭代,这行代码添加一个新的字典,设置所有的键为 None。这给了我们一个干净的字典设置,这样每一个对象都有相同的键,我们可以在发现数据的时候添加它到键中。

❷ 拉取 content div,这样可以在这个 div 中选择。这让代码更加明确,以防有其他的 div 有相似的名称。

❸ 使用 Python 的 pass(https://docs.python.org/2/tutorial/controlflow.html#pass-statements)来略过异常。因为所有的键都已经设置为 None,所以我们在这里不需要做任何事。Python 的 pass 让代码略过异常,所以程序会继续执行下一个代码块。

一旦将数据收集到 all_data 中,你可以打印它,看一下收集到的内容。下面是一些样例输出(这是一个社交媒体时间线,所以你的时间线会和下面展示的有所不同):

[{'full_name': u'Stefan Brand',
  'original_link': None,
  'picture': u'https://pbs.twimg.com/media/COZlle9WoAE5pVL.jpg:large',
  'short_name': u'',
  'text_content': u'Simply @Fairphone :) #WeAreFairphone http://t.co/vUvKzjX2Bw',
  'timestamp': u'POSTED ABOUT 14 HOURS AGO'},
 {'full_name': None,
  'original_link': None,
  'picture': None,
  'short_name': u'',
  'text_content': None,
  'timestamp': None},
 {'full_name': u'Sietse/MFR/Orphax',
  'original_link': None,
  'picture': None,
  'short_name': u'',
  'text_content': u'Me with my (temporary) Fairphone 2 test phone.
  # happytester #wearefairphone @ Fairphone instagram.com/p/7X-KXDQzXG/',
  'timestamp': u'POSTED ABOUT 17 HOURS AGO'},...]

我们的数据看起来有一些混乱。for 循环很晦涩,很难阅读和理解。同样,看起来我们可以改进一些数据收集方式——我们的日期对象只是一个字符串,但是它更应该是一个日期。我们还需要实验 Selenium 的能力来与页面交互,这可能会帮助我们加载更多的内容。

还需要调试看到的错误。我们找不到正确的短名称;代码看起来返回了一个空字符串。在研究了页面之后,看起来 name div 是隐藏的。在 Selenium 中,隐藏的元素通常是不能阅读到的,所以需要使用该元素的 innerHTML 属性,这会返回标签中的内容。我们也注意到,时间戳数据存储在 title 属性中,并且 URL 事实上存储在 data-href 中,而不是 href 属性中。

 随着时间推移,编写首次运行就可成功抓取的代码会变得更简单。预期可能出现的问题也变得更简单。通过研究浏览器的开发者工具,并使用 IPython 进行调试,你可以操作变量,测试可能有效的方法。

在找到所有数据的基础上,要确保正确地格式化脚本。我们想要创建函数,更好地抽象数据提取。相对于从初始页面直接解析 URL,应该简化代码,直接加载页面。通过在浏览器中反复试错发现,可以移除 iframe URL 中的长查询字符串(即,?scroll=auto&cols=4&format=embed&eh=...),并且仍然使用来自社交媒体的嵌入内容加载整个页面。让我们看一下整理和简化后的脚本:

from selenium.common.exceptions import NoSuchElementException, \
  WebDriverException
from selenium import webdriver


def find_text_element(html_element, element_css): ➊
  try:
    return html_element.find_element_by_css_selector(element_css).text ➋
  except NoSuchElementException:
    pass
  return None


def find_attr_element(html_element, element_css, attr): ➌
  try:
    return html_element.find_element_by_css_selector(
      element_css).get_attribute(attr) ➍
  except NoSuchElementException:
    pass
  return None


def get_browser():
  browser = webdriver.Firefox()
  return browser


def main():
  browser = get_browser()
  browser.get('http://apps.twinesocial.com/fairphone')

  all_data = []
  browser.implicitly_wait(10) ➎
  try:
    all_bubbles = browser.find_elements_by_css_selector(
      'div.twine-item-border')
  except WebDriverException:
    browser.implicitly_wait(5)
    all_bubbles = browser.find_elements_by_css_selector(
      'div.twine-item-border')
  for elem in all_bubbles:
    elem_dict = {}
    content = elem.find_element_by_css_selector('div.content')
    elem_dict['full_name'] = find_text_element(
      content, 'div.fullname')
    elem_dict['short_name'] = find_attr_element(
      content, 'div.name', 'innerHTML')
    elem_dict['text_content'] = find_text_element(
      content, 'div.twine-description')
    elem_dict['timestamp'] = find_attr_element(
      elem, 'div.when a abbr.timeago', 'title') ➏
    elem_dict['original_link'] = find_attr_element(
      elem, 'div.when a', 'data-href')
    elem_dict['picture'] = find_attr_element(
      content, 'div.picture img', 'src')
    all_data.append(elem_dict)
  browser.quit() ➐
  return all_data ➑

if __name__ == '__main__':
  all_data = main()
  print all_data

❶ 创建一个函数,接受 HTML 元素和 CSS 选择器,返回文本元素。在上一个代码实例中,需要一次又一次地重复代码;现在我们想要创建一个函数,这样可以重复使用它,而不需要在脚本中重新编写代码。

❷ 使用抽象函数变量,返回 HTML 元素的文本。如果没有找到匹配,返回 None。

❸ 创建一个函数来找到和返回属性,类似于我们的文本元素函数。这需要 HTML 元素、CSS 选择器,以及我们想要从选择器中拉取的属性,并且为这个选择器返回值或者 None。

❹ 使用抽象函数变量找到 HTML 元素,并且返回属性。

❺ 使用 Seleniumbrowser 类的 implicitly_wait 方法,它接受一个希望浏览器在执行下一行代码前隐式等待的秒数为参数。如果不确定页面是否会立即加载完成,这是个很棒的方法。关于隐式和显式等待有很多很棒的 Selenium 文档(http://selenium-python.readthedocs.io/en/latest/waits.html)。

❻ 传递 CSS 选择器,来获取 when div 中一个锚标签的 abbr 元素的 title 属性,以获取时间戳数据。

❼ 抓取数据结束后,使用 quit 方法关闭浏览器。

❽ 返回收集的数据。 __name__ == '__main__' 代码块允许从命令行执行代码时打印数据,或者我们可以导入函数到 IPython 中,并且运行 main 函数返回数据。

尝试从命令行中运行脚本,或者将其导入到 IPython,之后运行 main 函数。这次数据看起来更加完整了吗?你还会发现添加了另外一个 try...except 代码块。我们注意到,有些时候 Selenium 使用的交互会与页面上的 JavaScript 冲突,使 Selenium 抛出一个 WebDriverException 异常。允许页面加载更长的时间,再一次尝试后,可以解决这个问题。

如果在浏览器中访问 URL,你可以看到,随着下拉页面能够加载更多的数据。有了 Selenium,我们同样可以做这件事!让我们看一下 Selenium 可以做的其他漂亮的事。可以尝试在 Google 中搜索 Python 网页抓取库,并且使用 Selenium 与搜索结果交互:

from selenium import webdriver
from time import sleep


browser = webdriver.Firefox()
browser.get('http://google.com')

inputs = browser.find_elements_by_css_selector('form input') ➊
for i in inputs:
  if i.is_displayed(): ➋
    search_bar = i ➌
    break

search_bar.send_keys('web scraping with python') ➍

search_button = browser.find_element_by_css_selector('form button')
search_button.click() ➎

browser.implicitly_wait(10)
results = browser.find_elements_by_css_selector('div h3 a') ➏

for r in results:
  action = webdriver.ActionChains(browser) ➐
  action.move_to_element(r) ➑
  action.perform() ➒
  sleep(2)

browser.quit()

❶ 需要找到一个输入。Google 和其他的站点一样,在页面的很多地方都有输入框,但是通常来说,只有一个大的可见搜索框。这行代码定位所有的输入表单,这样我们有了一个好的起点。

❷ 这行代码遍历每一个输入,看它们是隐藏的还是显示的。如果 is_displayed 返回 True,那么有了一个可见的元素。反之,这个循环会继续遍历。

❸ 找到一个显示出来的输入时,将它赋值给变量 search_bar,终止循环。这会找到第一个可见的输入,这可能是我们想找的那一个。

❹ 这行代码通过使用 send_keys 方法发送键和字符串到选定的元素(在这个例子中,它发送键到搜索框)。这类似于在键盘上输入,但是是用 Python !

❺ Selenium 还可以 click 页面上可见的元素。这行代码告诉 Selenium 点击搜索表单的提交按钮,来查看搜索结果。

❻ 为了查看所有的搜索结果,这行代码选择 div 中有链接的标题元素,这是谷歌搜索结果页面的结构。

❼ 这段代码遍历每一个结果,利用 Selenium 的 ActionChains 定义一系列的操作,并告诉浏览器执行这些操作。

❽ 这行代码使用 ActionChain 的 move_to_element 方法,传递给这个方法想要浏览器访问的元素。

❾ 这行代码调用 perform,这意味着浏览器会高亮每一个搜索结果。我们使用一个 sleep 函数,这告诉 Python 在执行下一行代码前等待特定的秒数(这里是 2),这样,浏览器不会执行得过快,以免你失去很多乐趣。

喔!现在我们可以找到一个站点,填充一个表单,提交它,并且使用 Selenium ActionChains 来遍历结果。正如你看到的,ActionChains 是在浏览器中执行一系列操作的有效方式。你可以在 Selenium 的 Python 附带文档(http://selenium-python.readthedocs.org/)中探索很多很棒的特性,包括显式的等待(http://selenium-python.readthedocs.io/waits.html#explicit-waits,浏览器可以等待,直到一个特定的元素被加载,而不只是整个页面加载完成)、处理警告(http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.alert)和保存截图(http://selenium-python.readthedocs.io/api.html#selenium.webdriver.remote.webdriver.WebDriver.save_screenshot),这些对于调试来说很有用处。

现在已经看到 Selenium 的一些能力,你能不能重写我们已经为#WeAreFairphone 站点编写的代码,并且遍历前 100 个记录? [ 提示:如果你不想使用 ActionChains 遍历每一个元素,你总是可以使用 JavaScript ! Selenium 驱动器的 execute_script 方法允许你执行 JavaScript,就像在浏览器控制台中执行 JavaScript 一样。你可以使用 JavaScript 的 scroll 方法(https://developer.mozilla.org/en-US/docs/Web/API/Window/scroll)。Selenium 元素对象同样有一个 location 属性,它会返回页面上元素的 x 和 y 坐标值。]

我们已经学习了如何利用 Selenium 操作和使用浏览器来进行网页抓取,但是,还没有结束!让我们看一下如何使用 Selenium 和无头浏览器。

Selenium和无头浏览器

最流行的无头浏览器工具之一是 PhantomJS(http://phantomjs.org/)。如果你是一个熟练的 JavaScript 开发者,可以直接在 PhantomJS 中构建抓取器。然而,如果你想要使用 Python 尝试一下,可以使用 Selenium 和 PhantomJS。PhantomJS 同 GhostDriver(https://github.com/detro/ghostdriver)一起工作,打开 Web 页面并导航。

为什么使用无头浏览器?无头浏览器(http://en.wikipedia.org/wiki/Headless_browser)可以在服务器上运行。相对于普通的浏览器,它们可以更快地运行和解析页面,并且可以在更多的平台上使用。如果最终想要在服务器上运行基于浏览器的网页抓取脚本,你会想要使用无头浏览器。在 10 分钟之内就可安装并运行一个无头浏览器,而大多数其他浏览器需要更长的时间来正确加载和运行(取决于你使用的功能和部署的方式)。

12.1.2 使用Ghost.py进行屏幕读取

Ghost.py(http://jeanphix.me/Ghost.py/)是一个用于屏幕读取的 WebKit 实现,用来直接与 Qt WebKit(http://doc.qt.io/qt-5/qtwebkit-index.html)交互。Qt WebKit 是一个基于 Qt(https://en.wikipedia.org/wiki/Qt_(software))的 WebKit 实现,而 Qt 是一个用 C++ 实现的跨平台的应用开发框架。

为了开始使用 Ghost.py,你首先需要安装一些很有效的库。如果你能够安装 PySide(https://pypi.python.org/pypi/PySide),那效果会是最好的,这会允许 Python 同 Qt 通信,给 Python 访问更广泛程序和交互的能力。这个过程会花一些时间,所以在开始运行安装之后,尽情地去给自己做一个三明治吧 2

2如果你在安装 PySide 时碰到了问题,查看与操作系统相关的项目文档。你可以选择安装 PyQt(http://pyqt.sourceforge.net/Docs/PyQt5/installation.html)。你也可以通过 GitHub 上的安装文档(https://github.com/jeanphix/Ghost.py#installation)检查更新。

pip install pyside
pip install ghost.py --pre

使用 Ghost.py 搜索 Python 主页(http://python.org),找到新的抓取文档。开始一个新的 Ghost.py 实例非常简单:

from ghost import Ghost


ghost = Ghost() ➊
with ghost.start() as session:
  page, extra_resources = session.open('http://python.org') ➋

  print page
  print page.url
  print page.headers
  print page.http_status
  print page.content ➌

  print extra_resources

  for r in extra_resources:
    print r.url ➍

❶ 这行代码调用 Ghost 类的会话对象,实例化一个 Ghost 对象来同页面交互。

❷ Ghost 类的 open 方法返回两个对象,所以这行代码在两个独立的变量中捕获这些对象。第一个对象是用来同 HTML 元素交互的页面对象。第二个对象是页面加载的其他资源列表(你在网络标签中看到的列表)。

❸ 页面对象有很多属性,比如头部、内容、链接和页面上的内容。这行代码打印页面的内容。

❹ 这行代码遍历页面的其他资源,并且打印它们,来看是否有用。有时,这些 URL 是 API 调用,可以利用它们简化数据的访问。

Ghost.py 让我们能够洞察页面使用的资源(在第一次使用 open 方法打开页面时,通过一个元组给出)和真实页面上的许多特性。同样可以使用 .content 属性来加载页面的内容,这样如果想要使用其中一个页面解析器解析它,比如 LXML,我们可以做得到,并且仍然使用 Ghost.py 进行交互。

 当前,Ghost.py 的大多数能力在于执行页面上的 JavaScript 代码(不是 jQuery),所以你可能需要打开 Mozilla 开发者网络的 JavaScript 指南(https://developer.mozilla.org/en-US/docs/Web/JavaScript)。这会帮助你更加容易地搜索和找到同 Ghost.py 一起使用的 JavaScript。

由于对于在 Python 主页中搜索抓取库感兴趣,让我们看一下是否可以定位输入框:

print page.content.contains('input') ➊

result, resources = session.evaluate(
  'document.getElementsByTagName("input");') ➋

print result.keys()
print result.get('length') ➌
print resources

❶ 测试页面上是否存在一个 input 标签(大多数的搜索框是简单的输入对象)。这会返回一个布尔值。

❷ 使用一些简单的 JavaScript 来找到页面上所有以“input”为标签名称的元素。

❸ 打印来看响应中的 JavaScript 数组的长度。

根据 JavaScript 结果,在页面上,只有两个输入。为了确定使用哪一个,看一下第一个是否合适。

result, resources = session.evaluate(
  'document.getElementsByTagName("input")[0].getAttribute("id");') ➊

print result

❶ 索引结果列表,获取 id 属性。JavaScript 直接给出了元素的 CSS 属性,所以这是一个查看选择元素的相关 CSS 的有用方式。

类似于在 Python 中索引结果,也可以在 JavaScript 中索引它们。我们想要第一个输入元素,之后抓取输入的 CSS id。

 甚至可以编写一个 JavaScript for 循环(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for)来遍历 getElementsByTagName 函数返回的列表,通过这种方式来检查属性。如果你希望在浏览器中尝试 JavaScript,可以使用控制台完成这件事(见图 11-12)。

通过 id 的名称(id-search-field),已经定位了搜索字段元素,现在发送一些数据到这个字段:

result, resources = ghost.set_field_value("input", "scraping")

这段代码使用 set_field_value 方法,它需要一个选择器(这里就是 "input"),并且发送给它一个字符串("scraping")。Ghost.py 同样有一个 fill 方法(http://jeanphix.me/Ghost.py/#form),它会发送一个值的字典,来填充一系列匹配的表单字段。这在有多个字段要填充时很有用处。现在填充了检索词;让我们看一下是否能够提交查询。我们看到这在一个表单中,所以可以直接尝试进行一次表单提交:

page, resources = session.fire("form", "submit", expect_loading=True) ➊

print page.url

❶ 这行代码调用 Ghost.py 的 fire 方法,这会触发一个 JavaScript 事件。我们想给表单元素发送一个信号,以提交事件,这样它会提交搜索,并导航至下一页。设置 expect_loading 为 True,这样 Ghost.py 知道我们在等待页面加载。

这有效吗?在测试中,当运行这段代码时,收到了超时响应。我们会在本章后文中讨论超时,但是这意味着 Ghost.py 停止等待响应,因为这花费了太长的时间。当你处理抓取器提交数据任务时,找到一个合适的超时时间,对于保证脚本继续工作是必需的。让我们尝试一个不同的提交方式。Ghost.py 可以同页面元素交互并且点击,让我们尝试一下。

result, resources = session.click('button[id=submit]') ➊

print result

for r in resources:
  print r.url ➋

❶ Ghost.py 的 click 方法使用 JavaScript 选择器点击对象。这行代码点击 id="submit" 的按钮。

❷ 对于大多数通过 Ghost.py 的交互,你会收到一个结果和一个资源列表。这行代码查看代码交互返回的资源。

嗯——点击提交按钮,我们得到了一个看起来像是控制台的 URL。让我们看一下是否可以看到 Qt WebKit 看到的内容。类似于 Selenium 的 save_screenshot 方法,Ghost.py 允许我们查看页面。

 在使用无头部或不能脱离代码使用的 WebKit 浏览器时,有时页面表现得会与在普通浏览器中有所不同。当使用 Ghost.py 或 PhantomJS 时,你会想要利用屏幕截图来“查看”无头或 kit 浏览器正在使用的页面。

可以使用 Ghost.py 的 show 方法来“查看”该页面:

session.show()

你会看到打开了一个新窗口,展示出同抓取器所看到的相同的站点。它应该类似于图 12-3。

图 12-3:Ghost 页面

喔!我们正在页面的中间。尝试向上滚动,从另外的角度看一下。

session.evaluate('window.scrollTo(0, 0);')

session.show()

现在它应该看起来类似于图 12-4。

图 12-4:Ghost 页面顶端

这个视图帮助我们理解错误。页面并没有像在普通浏览器中那样完整地打开,搜索和提交输入不可读。一个解决方案是使用更大的视窗重新打开页面,或者为提交设置一个更长的超时时间。

 正如你可以在文档(http://ghost-py.readthedocs.io/en/latest/index.html)中看到的那样,我们创建的第一个 Ghost 对象可以接受类似 viewport_size 和 wait_timeout 的参数。如果你希望重启浏览器,设置一个更大的视窗或一个更长的超时时间,这些都是合理的修正。

现在,看看是否可以使用一些 JavaScript 来将它提交:

result, resources = session.evaluate(
  'document.getElementsByTagName("input")[0].value = "scraping";') ➊
result, resources = session.evaluate(
  'document.getElementsByTagName("form")[0].submit.click()') ➋

❶ 完全使用 JavaScript 设置输入值为“scraping”。

❷ 使用 JavaScript 函数调用表单的提交元素,并主动点击它。

现在如果再一次运行 show,你会看到图 12-5 所示的页面。

图 12-5:Ghost 搜索

我们使用 Qt 浏览器成功地执行了搜索。一些功能还没有像 Selenium 一样流畅,但是 Ghost.py 仍然是一个相当年轻的项目。

 你可以通过评估版本号来评估一个项目的年龄。在编写本书的时候,Ghost. py 仍然低于 1.0 版本(事实上,本书可能只能兼容 0.2 发布版)。它可能会在未来的几年里有大量的改变,但是这是一个非常有趣的项目。我们鼓励你通过向作者提交想法以及研究和修复 bug 来帮助它。

现在,我们已经学习了 Python 中几种与浏览器交互的不同方式,让我们做一些爬取!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文