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)
以爬取“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
我们使用aiohttp
将上面的代码修改为异步 I/O 的版本。为了以异步 I/O 的方式实现网络资源的获取和写文件操作,我们首先得安装三方库aiohttp
和aiofile
,命令如下所示。
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 的优势将更为明显,有兴趣的读者可以自行试验。
根据权威机构发布的全球互联网可访问性审计报告,全球约有四分之三的网站其内容或部分内容是通过JavaScript动态生成的,这就意味着在浏览器窗口中“查看网页源代码”时无法在HTML代码中找到这些内容,也就是说我们之前用的抓取数据的方式无法正常运转了。解决这样的问题基本上有两种方案,一是获取提供动态内容的数据接口,这种方式也适用于抓取手机 App 的数据;另一种是通过自动化测试工具 Selenium 运行浏览器获取渲染后的动态内容。对于第一种方案,我们可以使用浏览器的“开发者工具”或者更为专业的抓包工具(如:Charles、Fiddler、Wireshark等)来获取到数据接口,后续的操作跟上一个章节中讲解的获取“360图片”网站的数据是一样的,这里我们不再进行赘述。这一章我们重点讲解如何使用自动化测试工具 Selenium 来获取网站的动态内容。
Selenium 是一个自动化测试工具,利用它可以驱动浏览器执行特定的行为,最终帮助爬虫开发者获取到网页的动态内容。简单的说,只要我们在浏览器窗口中能够看到的内容,都可以使用 Selenium 获取到,对于那些使用了 JavaScript 动态渲染技术的网站,Selenium 会是一个重要的选择。下面,我们还是以 Chrome 浏览器为例,来讲解 Selenium 的用法,大家需要先安装 Chrome 浏览器并下载它的驱动。Chrome 浏览器的驱动程序可以在ChromeDriver官网进行下载,驱动的版本要跟浏览器的版本对应,如果没有完全对应的版本,就选择版本代号最为接近的版本。
我们可以先通过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
解决这个问题的办法有三种:
将下载的 ChromeDriver 放到已有的 PATH 环境变量下,建议直接跟 Python 解释器放在同一个目录,因为之前安装 Python 的时候我们已经将 Python 解释器的路径放到 PATH 环境变量中了。
将 ChromeDriver 放到项目虚拟环境下的 bin
文件夹中(Windows 系统对应的目录是 Scripts
),这样 ChromeDriver 就跟虚拟环境下的 Python 解释器在同一个位置,肯定是能够找到的。
修改上面的代码,在创建 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_element
和find_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_element
或find_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 弹窗 |
对于使用瀑布式加载的页面,如果希望在浏览器窗口中加载更多的内容,可以通过浏览器对象的execute_scripts
方法执行 JavaScript 代码来实现。对于一些高级的爬取操作,也很有可能会用到类似的操作,如果你的爬虫代码需要 JavaScript 的支持,建议先对 JavaScript 进行适当的了解,尤其是 JavaScript 中的 BOM 和 DOM 操作。我们在上面的代码中截屏之前加入下面的代码,这样就可以利用 JavaScript 将网页滚到最下方。
# 执行JavaScript代码
browser.execute_script('document.documentElement.scrollTop = document.documentElement.scrollHeight')
有一些网站专门针对 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)
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 |
查找单个元素 / 查找一系列元素 |
表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)
运行上面的代码,检查指定的目录下是否下载了根据关键词搜索到的图片。
当你写了很多个爬虫程序之后,你会发现每次写爬虫程序时,都需要将页面获取、页面解析、爬虫调度、异常处理、反爬应对这些代码从头至尾实现一遍,这里面有很多工作其实都是简单乏味的重复劳动。那么,有没有什么办法可以提升我们编写爬虫代码的效率呢?答案是肯定的,那就是利用爬虫框架,而在所有的爬虫框架中,Scrapy 应该是最流行、最强大的框架。
Scrapy 是基于 Python 的一个非常流行的网络爬虫框架,可以用来抓取 Web 站点并从页面中提取结构化的数据。下图展示了 Scrapy 的基本架构,其中包含了主要组件和系统的数据处理流程(图中带数字的红色箭头)。
我们先来说说 Scrapy 中的组件。
Scrapy 的整个数据处理流程由引擎进行控制,通常的运转流程包括以下的步骤:
引擎询问蜘蛛需要处理哪个网站,并让蜘蛛将第一个需要处理的 URL 交给它。
引擎让调度器将需要处理的 URL 放在队列中。
引擎从调度那获取接下来进行爬取的页面。
调度将下一个爬取的 URL 返回给引擎,引擎将它通过下载中间件发送到下载器。
当网页被下载器下载完成以后,响应内容通过下载中间件被发送到引擎;如果下载失败了,引擎会通知调度器记录这个 URL,待会再重新下载。
引擎收到下载器的响应并将它通过蜘蛛中间件发送到蜘蛛进行处理。
蜘蛛处理响应并返回爬取到的数据条目,此外还要将需要跟进的新的 URL 发送给引擎。
引擎将抓取到的数据条目送入数据管道,把新的 URL 发送给调度器放入队列中。
上述操作中的第2步到第8步会一直重复直到调度器中没有需要请求的 URL,爬虫就停止工作。
可以使用 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 电影标题、评分和金句的爬虫。
在items.py
的Item
类中定义字段,这些字段用来保存数据,方便后续的操作。
import scrapy
class DoubanItem(scrapy.Item):
title = scrapy.Field()
score = scrapy.Field()
motto = scrapy.Field()
修改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 或正则表达式进行页面解析,对应的方法分别是xpath
和re
。
如果还要生成后续爬取的请求,我们可以用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
如果希望完成爬虫数据的持久化,可以在数据管道中处理蜘蛛程序产生的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_item
和close_spider
都是回调方法(钩子函数), 简单的说就是 Scrapy 框架会自动去调用的方法。当蜘蛛程序产生一个Item
对象交给引擎时,引擎会将该Item
对象交给数据管道,这时我们配置好的数据管道的parse_item
方法就会被执行,所以我们可以在该方法中获取数据并完成数据的持久化操作。另一个方法close_spider
是在爬虫结束运行前会自动执行的方法,在上面的代码中,我们在这个地方进行了保存 Excel 文件的操作,相信这段代码大家是很容易读懂的。
总而言之,数据管道可以帮助我们完成以下操作:
修改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
选项是一个字典,可以配置多个处理数据的管道,后面的数字代表了执行的优先级,数字小的先执行。
原文链接:JavaScript 中的 BOM 和 DOM 操作
1.BOM:
浏览器对象模型 Browser Object Model
js代码操作浏览器
2.DOM:
文档对象模型 Document Object Model
js代码操作标签
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')
(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 的协议信息
(1).警告框
alert('难道30岁再学python?')
(2).确认框
confirm('你确定明天开始学python了嘛') // 用户点击可以确认返回true,点击取消返回false
(3).提示框
prompt('已为您调好闹钟','17:00') // 用户点击可以确认返回17:00,点击取消返回null,更改提示框内容确认返回更改内容
(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()
(1).DOM:文档对象模型,一种处理HTML和XML文件的标准API:
DOM+树:将文档作为一个树形结构,如HTML为根节点,那么树的每个结点表示了一个HTML的标签或标签内的文本项
(2).DOM操作:让JavaScript可以对文档(页面)中的标签、属性、内容等进行 访增删改 操作。
(3).DOM操作分为两步:查找标签,操作标签
(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 // 找哥哥标签同级别上面第一个
操作标签:创建标签,添加/更改标签属性,标签位置增·删·改,标签添加文本
以下用插入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 = '一起学习python!
' // 不识别html标签
b.innerHTML:
pEle.innerHTML = '一起学习python!
' // 识别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标签上面
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') // 有则删除无则添加
let pEle = document.getElementsByTagName('p')[0] // 获取标签,注意返回是数组要加索引
pEle.style.color = 'red' // 更改样式red
pEle.style.fontSize = '28px'
pEle.style.backgroundColor = 'yellow'
目的:为了获取用户输入的数据
(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],第一个才是文件数据