- 本书赞誉
- 前言
- 目标读者
- 不适合阅读本书的读者
- 本书结构
- 什么是数据处理
- 遇到困难怎么办
- 排版约定
- 使用代码示例
- 致谢
- 第 1 章 Python 简介
- 第 2 章 Python 基础
- 第 3 章 供机器读取的数据
- 第 4 章 处理 Excel 文件
- 第 5 章 处理 PDF 文件 以及用 Python 解决问题
- 第 6 章 数据获取与存储
- 第 7 章 数据清洗:研究、匹配与格式化
- 第 8 章 数据清洗:标准化和脚本化
- 第 9 章 数据探索和分析
- 第 10 章 展示数据
- 第 11 章 网页抓取:获取并存储网络数据
- 第 12 章 高级网页抓取:屏幕抓取器与爬虫
- 第 13 章 应用编程接口
- 第 14 章 自动化和规模化
- 第 15 章 结论
- 附录 A 编程语言对比
- 附录 B 初学者的 Python 学习资源
- 附录 C 学习命令行
- 附录 D 高级 Python 设置
- 附录 E Python 陷阱
- 附录 F IPython 指南
- 附录 G 使用亚马逊网络服务
- 关于作者
- 关于封面
12.1 基于浏览器的解析
有时,站点使用大量的 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论