Python|并发编程|爬虫|单线程|多线程|异步I/O|360图片|Selenium及JavaScript|Scrapy框架|BOM 和 DOM 操作简介|语言基础50课:学习(12)

系列目录

Python|Git remote|hosts|PyCharm常用快捷键|变量转换|命名|类型|运算符|分支|调整tab|循环|语言基础50课:学习记录(1)-项目简介及变量、条件及循环
Python|list|切片|列表的运算符、比较及遍历|生成式|元素位置和次数|元素排序和反转|sort() 方法|嵌套的列表|语言基础50课:学习记录(2)-常用数据结构之列表
Python|元组|字符串|语言基础50课:学习记录(3)-常用数据结构之元组及字符串相关
Python|集合|运算|哈希码|语言基础50课:学习记录(4)-常用数据结构之集合
Python|字典|函数和模块|应用及进阶|分数符号(Latex)|String库|operator库|处理数据三步骤|语言基础50课:学习记录(5)-常用数据结构之字典、函数和模块应用及进阶
Python|装饰器|执行时间|递归|动态属性|静态方法和类|继承和多态|isinstance类型判断|溢出|“魔法”方法|语言基础50课:学习记录(6)-函数的高级应用、面向对象编程、进阶及应用
Python|base64|collections|hashlib|heapq|itertools|random|os.path|uuid|文件|异常|JSON|API|CSV|语言基础50课:学习7
Python|xlwt|xlrd|调整单元格样式(背景,字体,对齐、虚线边框、列宽行高、添加公式)|xlutils|openpyxl|只读与只写|图表|语言基础50课:学习(8)
Python|python-docx|python-pptx|Pillow|smtplib|螺丝帽短信网关|正则表达式的应用|语言基础50课:学习(9)
Python|http|Chrome Developer Tools|Postman|HTTPie|builtwith库|python-whois库|爬虫及解析|语言基础50课:学习(10)
Python|线程和进程|阻塞|非阻塞|同步|异步|生成器和协程|资源竞争|进程间通信|aiohttp库|daemon属性值详解|语言基础50课:学习(11)
Python|并发编程|爬虫|单线程|多线程|异步I/O|360图片|Selenium及JavaScript|Scrapy框架|BOM 和 DOM 操作简介|语言基础50课:学习(12)
Python|MySQL概述|Windows-Linux-macOS安装|MySQL 基本命令|获取帮助|SQL注释|语言基础50课:学习(13)
Python|SQL详解之DDL|DML|DQL|DCL|索引|视图、函数和过程|JSON类型|窗口函数|接入MySQL|清屏|正则表达式|executemany|语言基础50课:学习(14)

原项目地址

Python-Core-50-Courses(https://hub.fastgit.org/jackfrued/Python-Core-50-Courses.git)

第37课:并发编程在爬虫中的应用

以爬取“360图片”网站的图片并保存到本地为例,为大家分别展示使用单线程、多线程和异步 I/O 编程的爬虫程序有什么区别,同时也对它们的执行效率进行简单的对比。

“360图片”网站的页面使用了 Ajax 技术,这是很多网站都会使用的一种异步加载数据和局部刷新页面的技术。简单的说,页面上的图片都是通过 JavaScript 代码异步获取 JSON 数据并动态渲染生成的,而且整个页面还使用了瀑布式加载(一边向下滚动,一边加载更多的图片)。我们在浏览器的“开发者工具”中可以找到提供动态内容的数据接口,如下图所示,我们需要的图片信息就在服务器返回的 JSON 数据中。
在这里插入图片描述
例如,要获取“美女”频道的图片,我们可以请求如下所示的URL,其中参数ch表示请求的频道,=后面的参数值beauty就代表了“美女”频道,参数sn相当于是页码,0表示第一页(共30张图片),30表示第二页,60表示第三页,以此类推。
在这里插入图片描述

https://image.so.com/zjl?ch=beauty&sn=0

单线程版本

通过上面的 URL 下载“美女”频道共90张图片。

"""
example04.py - 单线程版本爬虫
"""
import os

import requests


def download_picture(url):
    filename = url[url.rfind('/') + 1:]
    resp = requests.get(url)
    if resp.status_code == 200:
        with open(f'images/beauty/{filename}', 'wb') as file:  #图片的字节数据写入文件完成下载
            file.write(resp.content)


def main():
    if not os.path.exists('images/beauty'):
        os.makedirs('images/beauty')
    for page in range(3):
        resp = requests.get(f'https://image.so.com/zjl?ch=beauty&sn={page * 30}')
        if resp.status_code == 200:
            pic_dict_list = resp.json()['list'] #resp.json()是格式化resp,其中的list是图片相关信息
            for pic_dict in pic_dict_list:
                download_picture(pic_dict['qhimg_url'])

if __name__ == '__main__':
    main()

在 macOS 或 Linux 系统上,我们可以使用time命令来了解上面代码的执行时间以及 CPU 的利用率,如下所示。

time python3 example04.py

下面是单线程爬虫代码在原作者的电脑上执行的结果。

python3 example04.py  2.36s user 0.39s system 12% cpu 21.578 total

这里我们只需要关注代码的总耗时为21.578秒,CPU 利用率为12%

多线程版本

我们使用之前讲到过的线程池技术,将上面的代码修改为多线程版本。

"""
example05.py - 多线程版本爬虫
"""
import os
from concurrent.futures import ThreadPoolExecutor

import requests


def download_picture(url):
    filename = url[url.rfind('/') + 1:]
    resp = requests.get(url)
    if resp.status_code == 200:
        with open(f'images/beauty/{filename}', 'wb') as file:
            file.write(resp.content)


def main():
    if not os.path.exists('images/beauty'):
        os.makedirs('images/beauty')
    with ThreadPoolExecutor(max_workers=16) as pool:
        for page in range(3):
            resp = requests.get(f'https://image.so.com/zjl?ch=beauty&sn={page * 30}')
            if resp.status_code == 200:
                pic_dict_list = resp.json()['list']
                for pic_dict in pic_dict_list:
                    pool.submit(download_picture, pic_dict['qhimg_url'])


if __name__ == '__main__':
    main()

执行如下所示的命令。

time python3 example05.py

代码的执行结果如下所示:

python3 example05.py  2.65s user 0.40s system 95% cpu 3.193 total

异步I/O版本

我们使用aiohttp将上面的代码修改为异步 I/O 的版本。为了以异步 I/O 的方式实现网络资源的获取和写文件操作,我们首先得安装三方库aiohttpaiofile,命令如下所示。

pip install aiohttp aiofile

aiohttp 的用法在之前的课程中已经做过简要介绍,aiofile模块中的async_open函数跟 Python 内置函数open的用法大致相同,只不过它支持异步操作。下面是异步 I/O 版本的爬虫代码。

"""
example06.py - 异步I/O版本爬虫
"""
import asyncio
import json
import os

import aiofile
import aiohttp


async def download_picture(session, url):
    filename = url[url.rfind('/') + 1:]
    async with session.get(url, ssl=False) as resp:
        if resp.status == 200:
            data = await resp.read()
            async with aiofile.async_open(f'images/beauty/{filename}', 'wb') as file:
                await file.write(data)


async def fetch_json():
    async with aiohttp.ClientSession() as session:
        for page in range(3):
            async with session.get(
                url=f'https://image.so.com/zjl?ch=beauty&sn={page * 30}',
                ssl=False
            ) as resp:
                if resp.status == 200:
                    json_str = await resp.text()
                    result = json.loads(json_str)
                    for pic_dict in result['list']:
                        await download_picture(session, pic_dict['qhimg_url'])


def main():
    if not os.path.exists('images/beauty'):
        os.makedirs('images/beauty')
    loop = asyncio.get_event_loop()
    loop.run_until_complete(fetch_json())
    loop.close()


if __name__ == '__main__':
    main()

执行如下所示的命令。

time python3 example06.py

代码的执行结果如下所示:

python3 example06.py  0.82s user 0.21s system 27% cpu 3.782 total

总结

通过上面三段代码执行结果的比较,我们可以得出一个结论,使用多线程和异步 I/O 都可以改善爬虫程序的性能,因为我们不用将时间浪费在因 I/O 操作造成的等待和阻塞上,而time命令的执行结果也告诉我们,单线程的代码 CPU 利用率仅仅只有12%,而多线程版本的 CPU 利用率则高达95%;单线程版本的爬虫执行时间约21秒,而多线程和异步 I/O 的版本仅执行了3秒钟。另外,在运行时间差别不大的情况下,多线程的代码比异步 I/O 的代码耗费了更多的 CPU 资源,这是因为多线程的调度和切换也需要花费 CPU 时间。至此,三种方式在 I/O 密集型任务上的优劣已经一目了然,当然这只是在我的电脑上跑出来的结果。如果网络状况不是很理想或者目标网站响应很慢,那么使用多线程和异步 I/O 的优势将更为明显,有兴趣的读者可以自行试验。

第38课:抓取网页动态内容

根据权威机构发布的全球互联网可访问性审计报告,全球约有四分之三的网站其内容或部分内容是通过JavaScript动态生成的,这就意味着在浏览器窗口中“查看网页源代码”时无法在HTML代码中找到这些内容,也就是说我们之前用的抓取数据的方式无法正常运转了。解决这样的问题基本上有两种方案,一是获取提供动态内容的数据接口,这种方式也适用于抓取手机 App 的数据;另一种是通过自动化测试工具 Selenium 运行浏览器获取渲染后的动态内容。对于第一种方案,我们可以使用浏览器的“开发者工具”或者更为专业的抓包工具(如:Charles、Fiddler、Wireshark等)来获取到数据接口,后续的操作跟上一个章节中讲解的获取“360图片”网站的数据是一样的,这里我们不再进行赘述。这一章我们重点讲解如何使用自动化测试工具 Selenium 来获取网站的动态内容。

Selenium 介绍

Selenium 是一个自动化测试工具,利用它可以驱动浏览器执行特定的行为,最终帮助爬虫开发者获取到网页的动态内容。简单的说,只要我们在浏览器窗口中能够看到的内容,都可以使用 Selenium 获取到,对于那些使用了 JavaScript 动态渲染技术的网站,Selenium 会是一个重要的选择。下面,我们还是以 Chrome 浏览器为例,来讲解 Selenium 的用法,大家需要先安装 Chrome 浏览器并下载它的驱动。Chrome 浏览器的驱动程序可以在ChromeDriver官网进行下载,驱动的版本要跟浏览器的版本对应,如果没有完全对应的版本,就选择版本代号最为接近的版本。

使用Selenium

我们可以先通过pip来安装 Selenium,命令如下所示。

pip install selenium
加载页面

接下来,我们通过下面的代码驱动 Chrome 浏览器打开百度。

from selenium import webdriver

# 创建Chrome浏览器对象
browser = webdriver.Chrome()
# 加载指定的页面
browser.get('https://www.baidu.com/')

如果不愿意使用 Chrome 浏览器,也可以修改上面的代码操控其他浏览器,只需创建对应的浏览器对象(如 Firefox、Safari 等)即可。运行上面的程序,如果看到如下所示的错误提示,那是说明我们还没有将 Chrome 浏览器的驱动添加到 PATH 环境变量中,也没有在程序中指定 Chrome 浏览器驱动所在的位置。

selenium.common.exceptions.WebDriverException: Message: 'chromedriver' executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home

解决这个问题的办法有三种:

  1. 将下载的 ChromeDriver 放到已有的 PATH 环境变量下,建议直接跟 Python 解释器放在同一个目录,因为之前安装 Python 的时候我们已经将 Python 解释器的路径放到 PATH 环境变量中了。

  2. 将 ChromeDriver 放到项目虚拟环境下的 bin 文件夹中(Windows 系统对应的目录是 Scripts),这样 ChromeDriver 就跟虚拟环境下的 Python 解释器在同一个位置,肯定是能够找到的。

  3. 修改上面的代码,在创建 Chrome 对象时,通过service参数配置Service对象,并通过创建Service对象的executable_path参数指定 ChromeDriver 所在的位置,如下所示:

    from selenium import webdriver
    from selenium.webdriver.chrome.service import Service
    
    browser = webdriver.Chrome() #若已配置好chrome的路径,可用默认参数
    #browser = webdriver.Chrome(service=Service(executable_path='venv/bin/chromedriver'))
    browser.get('https://www.baidu.com/')
    
查找元素和模拟用户行为

接下来,我们可以尝试模拟用户在百度首页的文本框输入搜索关键字并点击“百度一下”按钮。在完成页面加载后,可以通过Chrome对象的find_elementfind_elements方法来获取页面元素,Selenium 支持多种获取元素的方式,包括:CSS 选择器、XPath、元素名字(标签名)、元素 ID、类名等,前者可以获取单个页面元素(WebElement对象),后者可以获取多个页面元素构成的列表。获取到WebElement对象以后,可以通过send_keys来模拟用户输入行为,可以通过click来模拟用户点击操作,代码如下所示。

from selenium import webdriver
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
browser.get('https://www.baidu.com/')
# 通过元素ID获取元素
kw_input = browser.find_element(By.ID, 'kw')
# 模拟用户输入行为
kw_input.send_keys('Python')
# 通过CSS选择器获取元素
su_button = browser.find_element(By.CSS_SELECTOR, '#su')
# 模拟用户点击行为
su_button.click()

如果要执行一个系列动作,例如模拟拖拽操作,可以创建ActionChains对象,有兴趣的读者可以自行研究。
参考网址:selenium鼠标操作 ActionChains对象用法

隐式等待和显式等待

这里还有一个细节需要大家知道,网页上的元素可能是动态生成的,在我们使用find_elementfind_elements方法获取的时候,可能还没有完成渲染,这时会引发NoSuchElementException错误。为了解决这个问题,我们可以使用隐式等待的方式,通过设置等待时间让浏览器完成对页面元素的渲染。除此之外,我们还可以使用显示等待,通过创建WebDriverWait对象,并设置等待时间和条件,当条件没有满足时,我们可以先等待再尝试进行后续的操作,具体的代码如下所示。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait

browser = webdriver.Chrome()
# 设置浏览器窗口大小
browser.set_window_size(1200, 800)
browser.get('https://www.baidu.com/')
# 设置隐式等待时间为10秒
browser.implicitly_wait(10)
kw_input = browser.find_element(By.ID, 'kw')
kw_input.send_keys('Python')
su_button = browser.find_element(By.CSS_SELECTOR, '#su')
su_button.click()
# 创建显示等待对象
wait_obj = WebDriverWait(browser, 10)
# 设置等待条件(等搜索结果的div出现)
wait_obj.until(
    expected_conditions.presence_of_element_located(
        (By.CSS_SELECTOR, '#content_left')
    )
)
# 截屏
browser.get_screenshot_as_file('python_result.png')

上面设置的等待条件presence_of_element_located表示等待指定元素出现,下面的表格列出了常用的等待条件及其含义。

等待条件具体含义
title_is / title_contains标题是指定的内容 / 标题包含指定的内容
visibility_of元素可见
presence_of_element_located定位的元素加载完成
visibility_of_element_located定位的元素变得可见
invisibility_of_element_located定位的元素变得不可见
presence_of_all_elements_located定位的所有元素加载完成
text_to_be_present_in_element元素包含指定的内容
text_to_be_present_in_element_value元素的value属性包含指定的内容
frame_to_be_available_and_switch_to_it载入并切换到指定的内部窗口
element_to_be_clickable元素可点击
element_to_be_selected元素被选中
element_located_to_be_selected定位的元素被选中
alert_is_present出现 Alert 弹窗
执行JavaScript代码

对于使用瀑布式加载的页面,如果希望在浏览器窗口中加载更多的内容,可以通过浏览器对象的execute_scripts方法执行 JavaScript 代码来实现。对于一些高级的爬取操作,也很有可能会用到类似的操作,如果你的爬虫代码需要 JavaScript 的支持,建议先对 JavaScript 进行适当的了解,尤其是 JavaScript 中的 BOM 和 DOM 操作。我们在上面的代码中截屏之前加入下面的代码,这样就可以利用 JavaScript 将网页滚到最下方。

# 执行JavaScript代码
browser.execute_script('document.documentElement.scrollTop = document.documentElement.scrollHeight')
Selenium反爬的破解

有一些网站专门针对 Selenium 设置了反爬措施,因为使用 Selenium 驱动的浏览器,在控制台中可以看到如下所示的webdriver属性值为true,如果要绕过这项检查,可以在加载页面之前,先通过执行 JavaScript 代码将其修改为undefined
另一方面,我们还可以将浏览器窗口上的“Chrome正受到自动测试软件的控制”隐藏掉,完整的代码如下所示。

# 创建Chrome参数对象
options = webdriver.ChromeOptions()
# 添加试验性参数
options.add_experimental_option('excludeSwitches', ['enable-automation'])
options.add_experimental_option('useAutomationExtension', False)
# 创建Chrome浏览器对象并传入参数
browser = webdriver.Chrome(options=options)
# 执行Chrome开发者协议命令(在加载页面时执行指定的JavaScript代码)
browser.execute_cdp_cmd(
    'Page.addScriptToEvaluateOnNewDocument',
    {'source': 'Object.defineProperty(navigator, "webdriver", {get: () => undefined})'}
)
browser.set_window_size(1200, 800)
browser.get('https://www.baidu.com/')
无头浏览器

很多时候,我们在爬取数据时并不需要看到浏览器窗口,只要有 Chrome 浏览器以及对应的驱动程序,我们的爬虫就能够运转起来。如果不想看到浏览器窗口,我们可以通过下面的方式设置使用无头浏览器。

options = webdriver.ChromeOptions()
options.add_argument('--headless')
browser = webdriver.Chrome(options=options)

API参考

Selenium 相关的知识还有很多,我们在此就不一一赘述了,下面为大家罗列一些浏览器对象和WebElement对象常用的属性和方法。具体的内容大家还可以参考 Selenium 官方文档的中文翻译

浏览器对象

表1. 常用属性

属性名描述
current_url当前页面的URL
current_window_handle当前窗口的句柄(引用)
name浏览器的名称
orientation当前设备的方向(横屏、竖屏)
page_source当前页面的源代码(包括动态内容)
title当前页面的标题
window_handles浏览器打开的所有窗口的句柄

表2. 常用方法

方法名描述
back / forward在浏览历史记录中后退/前进
close / quit关闭当前浏览器窗口 / 退出浏览器实例
get加载指定 URL 的页面到浏览器中
maximize_window将浏览器窗口最大化
refresh刷新当前页面
set_page_load_timeout设置页面加载超时时间
set_script_timeout设置 JavaScript 执行超时时间
implicit_wait设置等待元素被找到或目标指令完成
get_cookie / get_cookies获取指定的Cookie / 获取所有Cookie
add_cookie添加 Cookie 信息
delete_cookie / delete_all_cookies删除指定的 Cookie / 删除所有 Cookie
find_element / find_elements查找单个元素 / 查找一系列元素
WebElement对象

表1. WebElement常用属性

属性名描述
location元素的位置
size元素的尺寸
text元素的文本内容
id元素的 ID
tag_name元素的标签名

表2. 常用方法

方法名描述
clear清空文本框或文本域中的内容
click点击元素
get_attribute获取元素的属性值
is_displayed判断元素对于用户是否可见
is_enabled判断元素是否处于可用状态
is_selected判断元素(单选框和复选框)是否被选中
send_keys模拟输入文本
submit提交表单
value_of_css_property获取指定的CSS属性值
find_element / find_elements获取单个子元素 / 获取一系列子元素
screenshot为元素生成快照

简单案例

下面的例子演示了如何使用 Selenium 从“360图片”网站搜索和下载图片。

import os
import time
from concurrent.futures import ThreadPoolExecutor

import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

DOWNLOAD_PATH = 'images/'


def download_picture(picture_url: str):
    """
    下载保存图片
    :param picture_url: 图片的URL
    """
    filename = picture_url[picture_url.rfind('/') + 1:]
    resp = requests.get(picture_url)
    with open(os.path.join(DOWNLOAD_PATH, filename), 'wb') as file:
        file.write(resp.content)


if not os.path.exists(DOWNLOAD_PATH):
    os.makedirs(DOWNLOAD_PATH)
browser = webdriver.Chrome()
browser.get('https://image.so.com/z?ch=beauty')
browser.implicitly_wait(10)
kw_input = browser.find_element(By.CSS_SELECTOR, 'input[name=q]')
kw_input.send_keys('苍老师')
kw_input.send_keys(Keys.ENTER)
for _ in range(10):
    browser.execute_script(
        'document.documentElement.scrollTop = document.documentElement.scrollHeight'
    )
    time.sleep(1)
imgs = browser.find_elements(By.CSS_SELECTOR, 'div.waterfall img')
with ThreadPoolExecutor(max_workers=32) as pool:
    for img in imgs:
        pic_url = img.get_attribute('src')
        pool.submit(download_picture, pic_url)

运行上面的代码,检查指定的目录下是否下载了根据关键词搜索到的图片。

第39课:爬虫框架Scrapy简介

当你写了很多个爬虫程序之后,你会发现每次写爬虫程序时,都需要将页面获取、页面解析、爬虫调度、异常处理、反爬应对这些代码从头至尾实现一遍,这里面有很多工作其实都是简单乏味的重复劳动。那么,有没有什么办法可以提升我们编写爬虫代码的效率呢?答案是肯定的,那就是利用爬虫框架,而在所有的爬虫框架中,Scrapy 应该是最流行、最强大的框架。

Scrapy 概述

Scrapy 是基于 Python 的一个非常流行的网络爬虫框架,可以用来抓取 Web 站点并从页面中提取结构化的数据。下图展示了 Scrapy 的基本架构,其中包含了主要组件和系统的数据处理流程(图中带数字的红色箭头)。

Scrapy的组件

我们先来说说 Scrapy 中的组件。

  1. Scrapy 引擎(Engine):用来控制整个系统的数据处理流程。
  2. 调度器(Scheduler):调度器从引擎接受请求并排序列入队列,并在引擎发出请求后返还给它们。
  3. 下载器(Downloader):下载器的主要职责是抓取网页并将网页内容返还给蜘蛛(Spiders)。
  4. 蜘蛛程序(Spiders):蜘蛛是用户自定义的用来解析网页并抓取特定URL的类,每个蜘蛛都能处理一个域名或一组域名,简单的说就是用来定义特定网站的抓取和解析规则的模块。
  5. 数据管道(Item Pipeline):管道的主要责任是负责处理有蜘蛛从网页中抽取的数据条目,它的主要任务是清理、验证和存储数据。当页面被蜘蛛解析后,将被发送到数据管道,并经过几个特定的次序处理数据。每个数据管道组件都是一个 Python 类,它们获取了数据条目并执行对数据条目进行处理的方法,同时还需要确定是否需要在数据管道中继续执行下一步或是直接丢弃掉不处理。数据管道通常执行的任务有:清理 HTML 数据、验证解析到的数据(检查条目是否包含必要的字段)、检查是不是重复数据(如果重复就丢弃)、将解析到的数据存储到数据库(关系型数据库或 NoSQL 数据库)中。
  6. 中间件(Middlewares):中间件是介于引擎和其他组件之间的一个钩子框架,主要是为了提供自定义的代码来拓展 Scrapy 的功能,包括下载器中间件和蜘蛛中间件。
数据处理流程

Scrapy 的整个数据处理流程由引擎进行控制,通常的运转流程包括以下的步骤:

  1. 引擎询问蜘蛛需要处理哪个网站,并让蜘蛛将第一个需要处理的 URL 交给它。

  2. 引擎让调度器将需要处理的 URL 放在队列中。

  3. 引擎从调度那获取接下来进行爬取的页面。

  4. 调度将下一个爬取的 URL 返回给引擎,引擎将它通过下载中间件发送到下载器。

  5. 当网页被下载器下载完成以后,响应内容通过下载中间件被发送到引擎;如果下载失败了,引擎会通知调度器记录这个 URL,待会再重新下载。

  6. 引擎收到下载器的响应并将它通过蜘蛛中间件发送到蜘蛛进行处理。

  7. 蜘蛛处理响应并返回爬取到的数据条目,此外还要将需要跟进的新的 URL 发送给引擎。

  8. 引擎将抓取到的数据条目送入数据管道,把新的 URL 发送给调度器放入队列中。

上述操作中的第2步到第8步会一直重复直到调度器中没有需要请求的 URL,爬虫就停止工作。

安装和使用Scrapy

可以使用 Python 的包管理工具pip来安装 Scrapy。

pip install scrapy

在命令行中使用scrapy命令创建名为demo的项目。

scrapy startproject demo

项目的目录结构如下图所示。

demo
|____ demo
|________ spiders
|____________ __init__.py
|________ __init__.py
|________ items.py
|________ middlewares.py
|________ pipelines.py
|________ settings.py
|____ scrapy.cfg

切换到demo 目录,用下面的命令创建名为douban的蜘蛛程序。

scrapy genspider douban movie.douban.com
一个简单的例子

接下来,我们实现一个爬取豆瓣电影 Top250 电影标题、评分和金句的爬虫。

  1. items.pyItem类中定义字段,这些字段用来保存数据,方便后续的操作。

    import scrapy
    
    
    class DoubanItem(scrapy.Item):
        title = scrapy.Field()
        score = scrapy.Field()
        motto = scrapy.Field()
    
  2. 修改spiders文件夹中名为douban.py 的文件,它是蜘蛛程序的核心,需要我们添加解析页面的代码。在这里,我们可以通过对Response对象的解析,获取电影的信息,代码如下所示。

    import scrapy
    from scrapy import Selector, Request
    from scrapy.http import HtmlResponse
    
    from demo.items import MovieItem
    
    
    class DoubanSpider(scrapy.Spider):
        name = 'douban'
        allowed_domains = ['movie.douban.com']
        start_urls = ['https://movie.douban.com/top250?start=0&filter=']
    
        def parse(self, response: HtmlResponse):
            sel = Selector(response)
            movie_items = sel.css('#content > div > div.article > ol > li')
            for movie_sel in movie_items:
                item = MovieItem()
                item['title'] = movie_sel.css('.title::text').extract_first()
                item['score'] = movie_sel.css('.rating_num::text').extract_first()
                item['motto'] = movie_sel.css('.inq::text').extract_first()
                yield item
    

    通过上面的代码不难看出,我们可以使用 CSS 选择器进行页面解析。当然,如果你愿意也可以使用 XPath 或正则表达式进行页面解析,对应的方法分别是xpathre

    如果还要生成后续爬取的请求,我们可以用yield产出Request对象。Request对象有两个非常重要的属性,一个是url,它代表了要请求的地址;一个是callback,它代表了获得响应之后要执行的回调函数。我们可以将上面的代码稍作修改。

    import scrapy
    from scrapy import Selector, Request
    from scrapy.http import HtmlResponse
    
    from demo.items import MovieItem
    
    
    class DoubanSpider(scrapy.Spider):
        name = 'douban'
        allowed_domains = ['movie.douban.com']
        start_urls = ['https://movie.douban.com/top250?start=0&filter=']
    
        def parse(self, response: HtmlResponse):
            sel = Selector(response)
            movie_items = sel.css('#content > div > div.article > ol > li')
            for movie_sel in movie_items:
                item = MovieItem()
                item['title'] = movie_sel.css('.title::text').extract_first()
                item['score'] = movie_sel.css('.rating_num::text').extract_first()
                item['motto'] = movie_sel.css('.inq::text').extract_first()
                yield item
    
            hrefs = sel.css('#content > div > div.article > div.paginator > a::attr("href")')
            for href in hrefs:
                full_url = response.urljoin(href.extract())
                yield Request(url=full_url)
    

    到这里,我们已经可以通过下面的命令让爬虫运转起来。

    scrapy crawl movie
    

    可以在控制台看到爬取到的数据,如果想将这些数据保存到文件中,可以通过-o参数来指定文件名,Scrapy 支持我们将爬取到的数据导出成 JSON、CSV、XML 等格式。

    scrapy crawl moive -o result.json
    

    不知大家是否注意到,通过运行爬虫获得的 JSON 文件中有275条数据,那是因为首页被重复爬取了。要解决这个问题,可以对上面的代码稍作调整,不在parse方法中解析获取新页面的 URL,而是通过start_requests方法提前准备好待爬取页面的 URL,调整后的代码如下所示。

    import scrapy
    from scrapy import Selector, Request
    from scrapy.http import HtmlResponse
    
    from demo.items import MovieItem
    
    
    class DoubanSpider(scrapy.Spider):
        name = 'douban'
        allowed_domains = ['movie.douban.com']
    
        def start_requests(self):
            for page in range(10):
                yield Request(url=f'https://movie.douban.com/top250?start={page * 25}')
    
        def parse(self, response: HtmlResponse):
            sel = Selector(response)
            movie_items = sel.css('#content > div > div.article > ol > li')
            for movie_sel in movie_items:
                item = MovieItem()
                item['title'] = movie_sel.css('.title::text').extract_first()
                item['score'] = movie_sel.css('.rating_num::text').extract_first()
                item['motto'] = movie_sel.css('.inq::text').extract_first()
                yield item
    
  3. 如果希望完成爬虫数据的持久化,可以在数据管道中处理蜘蛛程序产生的Item对象。例如,我们可以通过前面讲到的openpyxl操作 Excel 文件,将数据写入 Excel 文件中,代码如下所示。

    import openpyxl
    
    from demo.items import MovieItem
    
    
    class MovieItemPipeline:
    
        def __init__(self):
            self.wb = openpyxl.Workbook()
            self.sheet = self.wb.active
            self.sheet.title = 'Top250'
            self.sheet.append(('名称', '评分', '名言'))
    
        def process_item(self, item: MovieItem, spider):
            self.sheet.append((item['title'], item['score'], item['motto']))
            return item
    
        def close_spider(self, spider):
            self.wb.save('豆瓣电影数据.xlsx')
    

    上面的process_itemclose_spider都是回调方法(钩子函数), 简单的说就是 Scrapy 框架会自动去调用的方法。当蜘蛛程序产生一个Item对象交给引擎时,引擎会将该Item对象交给数据管道,这时我们配置好的数据管道的parse_item方法就会被执行,所以我们可以在该方法中获取数据并完成数据的持久化操作。另一个方法close_spider是在爬虫结束运行前会自动执行的方法,在上面的代码中,我们在这个地方进行了保存 Excel 文件的操作,相信这段代码大家是很容易读懂的。

    总而言之,数据管道可以帮助我们完成以下操作:

    • 清理 HTML 数据,验证爬取的数据。
    • 丢弃重复的不必要的内容。
    • 将爬取的结果进行持久化操作。
  4. 修改settings.py文件对项目进行配置,主要需要修改以下几个配置。

    # 用户浏览器
    USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36'
    
    # 并发请求数量
    CONCURRENT_REQUESTS = 4
    
    # 下载延迟
    DOWNLOAD_DELAY = 3
    # 随机化下载延迟
    RANDOMIZE_DOWNLOAD_DELAY = True
    
    # 是否遵守爬虫协议
    ROBOTSTXT_OBEY = True
    
    # 配置数据管道
    ITEM_PIPELINES = {
       'demo.pipelines.MovieItemPipeline': 300,
    }
    

    说明:上面配置文件中的ITEM_PIPELINES选项是一个字典,可以配置多个处理数据的管道,后面的数字代表了执行的优先级,数字小的先执行。

TIPS:JavaScript 中的 BOM 和 DOM 操作

原文链接:JavaScript 中的 BOM 和 DOM 操作

BOM与DOM操作基本概念

1.BOM:
浏览器对象模型 Browser Object Model
js代码操作浏览器
2.DOM:
文档对象模型 Document Object Model
js代码操作标签

BOM操作 (window对象,window子对象,弹出框,计算器)

1.window对象
window对象指代的就是浏览器窗口,可以在浏览器控制台console进行窗口控制,控制参数:
    window.innerHeight      //浏览器窗口的高度
    window.innerWidth       //浏览器窗口的宽度
    window.open()           //新建窗口打开页面(三个参数),
                            //第一个参数为url  第二个参数写空 第三个参数写新建的窗口的大小位置
    window.close()          //关闭当前页面
    补充open例子:window.open('https://www.baidu.com/','','height=400px,width=400px,top=400px,left=400px')
2.window子对象
(1).navigator对象(浏览器对象)
    window.navigator.appName        //返回浏览器的名称
    window.navigator.appVersion     //返回浏览器的平台和版本信息
    window.navigator.userAgent      //判断是否是一个浏览器,用户代理头的字符串表示
    window.navigator.platform       //返回运行浏览器的操作系统平台
    window.navigator.appCodeName    //返回浏览器的代码名称的字符串
    window.navigator.cookieEnabled  //指明浏览器中是否启用 cookie 的布尔值

(2).history对象
    window.history.back()           //回退到上一页
    window.history..forward()       //前进到下一页

(3).window子对象之location对象
    window.location                 //包含有关当前URL端口等等资源全部的信息
    window.location.href	        //获取当前页面的url
    window.location.href = url      //跳转到指定的url
    window.location.reload()        //重新加载当前页面(刷新)
    window.location.hostname        //获取当前 URL 的主机名
    window.location.pathname        //获取当前 URL 的路径
    window.location.protocol        //获取当前 URL 的协议信息
3.弹出框
(1).警告框
    alert('难道30岁再学python?')
(2).确认框
    confirm('你确定明天开始学python了嘛') // 用户点击可以确认返回true,点击取消返回false
(3).提示框
    prompt('已为您调好闹钟','17:00')  // 用户点击可以确认返回17:00,点击取消返回null,更改提示框内容确认返回更改内容
4.计时器
(1).过一段时间之后触发(一次):
    setTimeout(代码块,3000);    // 毫秒为单位 3秒之后自动执行代码块(可以是函数)

(2).每隔一段时间触发一次(循环/无数次):
    setInterval(代码块,3000);    // 每隔3秒执行一次代码块

(3).清除定时器:(clear-代替set-)
    无论是一次还是无数次,清除都要指定变量给定时器,利用清除变量来清除定时器
    a.清除一次:
        let t = setTimeout(代码块,3000);
        clearTimeout(t)
    b.清除无数次:
        let t = setInterval(代码块,3000);
        clearInterval(t)

(4).自定义定时器
    利用定时一次和循环无数次,制定一个某个时刻n次的定时器:
        function func1() {
            alert(123)
        }
        function show(){
            let t = setInterval(func2,3000);    // 每隔3秒执行一次
            function inner(){
                clearInterval(t)                // 清除定时器
            }
            setTimeout(inner,9000)              // 9秒中之后触发
        }
        show()

DOM操作(查标签,操作标签,操作class与css,获取值)

1.DOM操作概念(临时有效,刷新即无):
(1).DOM:文档对象模型,一种处理HTML和XML文件的标准API:
    DOM+树:将文档作为一个树形结构,如HTML为根节点,那么树的每个结点表示了一个HTML的标签或标签内的文本项
(2).DOM操作:让JavaScript可以对文档(页面)中的标签、属性、内容等进行 访增删改 操作。
(3).DOM操作分为两步:查找标签,操作标签
2.Dom查找标签:关键字document起手
(1).直接查找
    a.id查找
        document.getElementById('d1')               // 返回id为d1的标签
    b.类查找
        document.getElementsByClassName('c1')       // 返回id为c1的数组(注意)
    c.标签查找
        document.getElementsByTagName('div')        // 返回标签为div的数组(注意)
    d.补充:
        在用变量名指代标签对象时,变量名应书写成xxxEle

(2).间接查找
    基于一个标签通过父,儿子,哥哥,弟弟等方式查找标签,用关键字document
    a.找父标签(最高找到HTML)
        let pEle = document.getElementsByClassName('c1')[0]  // 基标签,注意是否需要索引取值
        pEle.parentElement
    b.找儿子标签
        let divEle = document.getElementById('d1')      // 基标签
        divEle.children                                 // 获取所有的子标签
        divEle.firstElementChild                        // 获取大儿子标签
        divEle.lastElementChild                         // 获取小儿子标签
    c.找哥哥弟弟标签(同级)
        let divEle = document.getElementById('d1')      // 基标签
        divEle.nextElementSibling                       // 找弟弟标签,同级别下面第一个
        divEle.previousElementSibling                   // 找哥哥标签同级别上面第一个
3.Dom操作标签:关键字document起手
操作标签:创建标签,添加/更改标签属性,标签位置增·删·改,标签添加文本
以下用插入img标签,插入div内部演示:
(1).创建标签:createElement()
    let imgEle = document.createElement('img')   // 通常指定标签名方便操作

(2).添加/更改标签属性:
    a.添加属性1:用点.方式只能添加默认属性
                如添加图片路径属性:imgEle.src = 'python,jpg'
    b.添加属性2:setAttribute():可以添加自定义属性和默认属性
                如添加图片自定义password属性:imgEle.setAttribute('password','123')
                    添加图片默认的title属性:imgEle.setAttribute('title','一张图片')
    c.获取属性:getAttribute()
    d.移除属性:removeAttribute()

(3).指定位置插入,删除标签,改标签
    let divEle = document.getElementById('d1')
    a.插入1:指定标签内尾部追加:
                divEle.appendChild(imgEle)
    b.插入2:指定标签内中的某个标签前/后:
                let pEle = document.getElementById('d2')
                divEle.insertBefore(imgEle,pEle)          // 在div里面,pEle标签前面插入imgEle标签
                divEle.insertafter(imgEle,pEle)
    c.删除:removeChild()
    d.替换:replaceChild()

(4).添加标签内部文本innerText与innerHTML
    创建p标签,并给p标签添加文本
    let pEle = document.createElement('p')
    a.innerText:
        pEle.innerText = '<h1>一起学习python!</h1>'     // 不识别html标签
    b.innerHTML:
        pEle.innerHTML = '<h1>一起学习python!</h1>'     // 识别html标签
    c.补充:获取表签内部文本:
        pEle.innerText

(5).案例1:
    通过DOM操作动态创建img标签,标签加属性,标签添加到div内部中
        let imgEle = document.createElement('img')      // 创建标签
        imgEle.src = 'python.png'                       // 给标签设置默认的属性
        imgEle.setAttribute('password','123')           // 自定义的属性
        let divEle = document.getElementById('d1')
        divEle.appendChild(imgEle)                      // 添加到div内部(尾部追加)
(6).案例2
    过DOM操作动态创建a标签,标签加属性,设置标签内部文本,添加到div内部中并在p标签上面
        let aEle = document.createElement('a')          // 创建标签
        aEle.href = 'https://www.baidu.com/'            // 给标签设置属性
        aEle.innerText = '百度一下!'                     // 设置标签内部文本
        let divEle = document.getElementById('d1')
        let pEle = document.getElementById('d2')
        divEle.insertBefore(aEle,pEle)                   // 添加到div内部中并在p标签上面
4.Dom操作类class选择器,更改属性(用classList起手)
let divEle = document.getElementById('d1')      // 获取标签
divEle.classList                                // 获取标签所有的类属性,返回数组[属性,值]
divEle.classList.add('bg_red')                  // 添加类属性bg_red
divEle.classList.remove('bg_red')               // 移除某个类属性
divEle.classList.contains('c1')                 // 验证是否包含c1类属性,返回true/false
divEle.classList.toggle('bg_red')               // 有则删除无则添加
5.Dom操作标签样式css,更改样式(用style起手)
let pEle = document.getElementsByTagName('p')[0]        // 获取标签,注意返回是数组要加索引
pEle.style.color = 'red'                                // 更改样式red
pEle.style.fontSize = '28px'
pEle.style.backgroundColor = 'yellow'
6.Dom获取值操作:
目的:为了获取用户输入的数据
(1).获取输入文本数据
    let seEle = document.getElementById('d2')       // 获取标签
    seEle.value                                     // 获取标签的value值
    seEle.value = ''                                // 将标签的value值清空
    seEle.value = 'df'                              // 将标签的value值更改df
(2).获取上传文件数据
    let fileEle = document.getElementById('d3')     // 获取标签
    fileEle.value                                   // 无法获取到文件数据,只拿到路径
    fileEle.files[0]                                // 获取文件数据,要加索引[0],第一个才是文件数据
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

打酱油的工程师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值