前言,本篇文章是我几年前开始陆陆续续记录最初学爬虫时的笔记,现在发出来(水一篇文章)。较为基础,有些地方也比较简陋,仅供参考。如有纰漏之处,欢迎指正。

一、基本介绍

1 爬虫的基本流程

模拟浏览器发送请求->下载网页代码->只提取有用的数据->存放于数据库或文件中

对每一个步骤进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#1、发起请求
使用http库向目标站点发起请求,即发送一个Request
Request包含:请求头、请求体等

#2、获取响应内容
如果服务器能正常响应,则会得到一个Response
Response可能是:html,xml,json,图片,视频或者加密格式

#3、解析内容
解析html数据:正则表达式(re),第三方解析库如Beautifulsoup,pyquery等
解析json数据:json模块
解析二进制数据:以b的方式写入文件

#4、保存数据
数据库
文件

2 robots协议

Robots协议(也称为爬虫协议、机器人协议等)的全称是“网络爬虫排除标准”(Robots Exclusion Protocol),网站通过Robots协议告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。因为一些系统中的URL是大小写敏感的,所以robots.txt的文件名应统一为小写。robots.txt应放置于网站的根目录下。robots.txt协议并不是一个规范,而只是约定俗成的,所以并不能保证网站的隐私。

Robots协议的基本语法:

1
2
3
4
#*代表所有,/代表根目录
User-agent:* #user-agent代表来源
Allow:/ #代表运行爬取的内容
Disallow:/ #代表不可爬取的目录,如果是/后面没有写内容,便是其对应的访问者不可爬取所有内容

网站管理员可以在网站域名的根目录下放一个robots.txt 文本文件,里面可以指定不同的网络爬虫能访问的页面和禁止访问的页面,指定的页面由正则表达式表示。网络爬虫在采集这个网站之前,首先获取到这个文件,然后解析到其中的规则,然后根据规则来采集网站的数据。如果一个网站不提供Robots协议,是说明这个网站对应所有爬虫没有限制。

自有搜索引擎之日起,Robots协议已是一种目前为止最有效的方式,用自律维持着网站与搜索引擎之间的平衡,让两者之间的利益不致过度倾斜。它就像一个钟摆,让互联网上的搜索与被搜索和谐相处。

二、request库

使用requests可以模拟浏览器的请求,比起之前用到的urllib,requests模块的api更加便捷(本质就是封装了urllib3)

1 安装模块

1
pip install requests

它支持各种请求方式,常用的就是requests.get()和requests.post()

2 发送GET请求

发送get请求很简单,传入url即可完成。

1
2
3
import requests # 导入模块
# 使用get方法请求url,会得到一个response对象
response = requests.get('https://www.baidu.com/')

2.1 请求地址携带参数

如果想要在请求地址中添加参数(问号后的参数),比如www.baidu.com/?key=val,可以使用params

1
2
3
payload = {'key1': 'value1', 'key2': 'value2'}
response = requests.get('https://www.baidu.com/', params=payload)
# https://www.baidu.com/?key2=value2&key1=value1

也可以传递一个列表

1
2
3
4
5
payload = {'key1': 'value1', 'key2': ['value2', 'value3']}

response = requests.get('https://www.baidu.com/', params=payload)
print(response.url)
# https://www.baidu.com/?key1=value1&key2=value2&key2=value3

推荐使用上述方式,如果不想用params,可以直接把参数就写在url字符串里,但是如果是中文,可能会出现编码问题。

1
2
3
4
5
6
# 我们想访问的是:'https://www.baidu.com/?wd=你好'
# 可能会被转码成:'https://www.baidu.com/s?wd=%E4%BD%A0%E5%A5%BD'
# params帮我们转码,而如果不使用,也可以自己转码
res = urlencode({'wd':'你好'},encoding='utf-8')
print(res)
# 不推荐这种方法

2.2 请求头中携带参数

在请求头中也可以添加参数,比如添加headers、cookies等信息

1
2
3
4
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36'
}
response = requests.get('https://www.baidu.com/',headers=headers)

2.3 携带cookies信息

其实在2.2中可以把cookies添加在headers里面(不推荐这种方式),第二种方式是使用cookies参数,本质上还是拼在headers中。

1
response = requests.get('https://www.baidu.com/',cookies={"k1":"v1","k2":"v2"})

3 发送POST请求

POST和GET请求基本完全一样,但是多出一个data参数用来存放请求体数据。

1
2
3
4
payload = {'key1': 'value1', 'key2': 'value2'}
r = requests.post("https://httpbin.org/post", data=payload)
# 当请求发出时,data将自动进行形式编码,默认的请求头:application/x-www-form-urlencoed
print(r.text)

data参数还可以为每个键具有多个值,当表单中有多个元素使用相同的键时,这一点特别有用:

1
2
3
4
payload_tuples = [('key1', 'value1'), ('key1', 'value2')]
r1 = requests.post('https://httpbin.org/post', data=payload_tuples)
payload_dict = {'key1': ['value1', 'value2']}
r2 = requests.post('https://httpbin.org/post', data=payload_dict)

对于这些form-data的数据可以直接传,但如果是其他格式的数据,比如json,需要自己转换

1
2
3
4
import json
url = 'https://api.github.com/some/endpoint'
payload = {'some': 'data'}
r = requests.post(url, data=json.dumps(payload))

在2.4.2版本中,request模块添加了json参数,自动编码json格式的数据

1
2
3
4
5
url = 'https://api.github.com/some/endpoint'
payload = {'some': 'data'}
r = requests.post(url, json=payload)
# 如果传文件则json参数会被忽略
# 使用json参数会将headers中的Content-Type更改为application/json。

使用session发送请求可以帮我们自动处理cookies

1
2
3
4
5
session=requests.session()
# 使用session来发请求
r=session.post('https://github.com/login',data={'some': 'data'})
# 假设post请求登录成功了,下次再使用get请求就不用自己处理cookies参数了,session对象会自动处理
r=session.get('https://github.com/login')

4 response响应对象

我们发送get和post之后,会返回一个response对象,我们可以从这个对象中得到我们需要的所有信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
respone=requests.get('https://www.baidu.com/')
# respone属性
print(respone.text) # 转码后的响应文本
print(respone.content) # 二进制文本
print(respone.status_code) # 响应状态码(1xx,2xx...)
print(respone.headers) # 响应头
print(respone.cookies) # cookie对象
print(respone.cookies.get_dict()) # cookie转换为字典
print(respone.cookies.items()) # cookie字典的k和v
print(respone.url) # 请求时的url
print(respone.history) # 重定向之前的url地址列表
print(respone.encoding) # 响应的编码方式
response.iter_content() # 如果是大文件,视频之类的,可以for循环一点点取出来
# for line in response.iter_content():
# pass

# 关于response.encoding
# 1、encoding是从http中的header中的charset字段中提取的编码方式,若header中没有charset字段则默认为ISO-8859-1编码模式
# 2、apparent_encoding会从网页的内容中分析网页编码的方式,所以apparent_encoding比encoding更加准确。当网页出现乱码时可以把apparent_encoding的编码格式赋值给encoding
# 3、但是apparent_encoding分析编码是比较耗时的, 在爬取大量网页时, 如果事先确定知道网页的编码了, 可以自己手动设置`r.encoding="编码"`, 这样性能会提升
# 综上所述,如果response.text响应文本出现乱码问题:
response.encoding='gbk' # 解决方式一:可以自己指定编码格式
response.encoding = response.apparent_encoding # 解决方式二、如果没找到编码格式,使用这种方式

5 SSL证书验证

https网站有的要SSL证书验证,如果没有则会报错。可以请求忽略对SSL证书的验证,返回200

1
requests.get('https://kennethreitz.org', verify=False)

但是会报警告,如果想去掉警告

1
2
3
4
5
import requests
from requests.packages import urllib3
urllib3.disable_warnings() #关闭警告
respone=requests.get('https://www.12306.cn',verify=False)
print(respone.status_code)

有些网站必须有证书才可以访问,那就只能带上

1
2
3
4
5
6
7
8
9
#很多网站都是https,但是不用证书也可以访问,大多数情况都是可以携带也可以不携带证书
#知乎/百度等都是可带可不带
#有硬性要求的,则必须带,比如对于定向的用户,拿到证书后才有权限访问某个特定网站
#cert参数指定一个本地证书作为客户端证书使用,是一个元组:(私钥和证书)的两个文件路径
import requests
respone=requests.get('https://www.12306.cn',
cert=('/path/server.crt',
'/path/key'))
print(respone.status_code)

6 使用代理

如果你需要使用代理,你可以在任何请求方法上使用代理参数。

1
2
3
4
5
6
7
8
9
10
# HTTP代理
import requests

proxies = {
'http':'http://user:123@localhost:9743',
'http': 'http://10.10.1.10:3128',
'https': 'http://10.10.1.10:1080',
}

requests.get('http://example.org', proxies=proxies)

2.10.0版本后,支持SOCKS代理。这是一个可选特性,需要在使用之前安装额外的第三方库。

1
pip install requests[socks]

一旦你安装了这些依赖项,使用SOCKS代理就像使用HTTP代理一样简单

1
2
3
4
proxies = {
'http': 'socks5://user:pass@host:port',
'https': 'socks5://user:pass@host:port'
}

7 超时设置

1
2
3
4
5
6
7
#两种超时:float or tuple
#timeout=0.1 #代表接收数据的超时时间
#timeout=(0.1,0.2)#0.1代表链接超时 0.2代表接收数据的超时时间

import requests
respone=requests.get('https://www.baidu.com',
timeout=0.0001)

8 认证设置

1
2
# 许多需要身份验证的web服务接受HTTP基本身份验证。这是最简单的一种,但是通常网站都不会用默认的加密设置
requests.get('https://api.github.com/user', auth=('user', 'pass'))

一般的网站都不用默认的加密方式,都是自己写的认证方法

9 异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 异常处理
import requests
from requests.exceptions import * #可以查看requests.exceptions获取异常类型

try:
r=requests.get('http://www.baidu.com',timeout=0.00001)
except ReadTimeout:
print('===:')
# except ConnectionError: #网络不通
# print('-----')
# except Timeout:
# print('aaaaa')

except RequestException:
print('Error')

细分出的一些异常,但一般用不到,直接用Exception就可以。

10 文件上传

上传文件是post请求。

1
2
3
4
import requests
files={'file':open('a.jpg','rb')}
respone=requests.post('http://httpbin.org/post',files=files)
print(respone.status_code)

三、selenium模块

相关链接:中文文档官方Github英文文档(非官方)

selenium是一个模拟浏览器操作的模块,可以解决requests模块无法直接执行js代码的问题。selenium可以完全模拟浏览器的操作,获取网页渲染之后(js执行后)的结果,支持多种浏览器。

1 安装

1
pip install selenium

2 安装浏览器驱动

selenium的本质是驱动浏览器操作,需要安装对应的浏览器驱动。这里使用Chrome驱动,注意驱动要和浏览器的版本对应一致。

查看浏览器版本的方法,谷歌浏览器右上角—>帮助—>关于Google Chrome,查看版本号为95.0.4638.69

官方下载,打不开可以用镜像下载

下载完成后解压是对应平台的可执行文件,windows是exe文件,把它放在环境变量里配置即可,为了方便推荐放在python安装目录的script文件夹下。

3 快速开始

下面是一个简单的例子,如果已经安装好了模块和驱动,执行下面代码可以操作浏览器访问网址。

1
2
3
4
5
6
7
import time
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("http://www.python.org")
print(driver.current_url) # 当前的url
time.sleep(3) # 等待3秒观看效果
driver.close()

我们真正想要的是与页面做交互。 更具体地说,对于一个页面中的HTML元素,首先我们要找到它,WebDriver 提供了大量的方法帮助你去查找元素(下一节),例如:已知一个输入框元素定义如下

1
<input type="text" name="passwd" id="passwd-id" />

你可以通过下面的方法查找

1
2
3
element = driver.find_element_by_id("passwd-id")
element = driver.find_element_by_name("passwd")
element = driver.find_element_by_xpath("//input[@id='passwd-id']")

拿到了输入框,可以继续操作浏览器输入一些数据,还可以通过”Keys”类来模式输入方向键

1
element.send_keys(" and some", Keys.ARROW_DOWN) # ARROW_DOWN下箭头

你的输入会添加在原有的文本之后,因此你可以使用 clear 方法去清除元素的内容

1
element.clear()

除此之外,你还可以操作下拉菜单,填写表格等等更多操作,下面分别介绍。

4 查找元素

在一个页面中有很多不同的方法可以定位一个元素。在你的项目中,你可以选择最合适的方法去查找元素。Selenium提供了下列方法

1
2
3
4
5
6
7
8
find_element_by_id  # 通过id查找元素,页面中第一个该 id 元素 会被匹配并返回,找不到报错
find_element_by_name # 通过Name查找元素,页面中第一个该 name 元素 会被匹配并返回,找不到报错
find_element_by_xpath # 通过XPath查找元素
find_element_by_link_text # 通过a标签的href获取
find_element_by_partial_link_text # 通过a标签的href获取,模糊匹配
find_element_by_tag_name # 通过标签名查找元素,比如h1标签,p标签
find_element_by_class_name # 通过属性名class查找元素
find_element_by_css_selector # 通过css选择器查找元素

以上都是最多找到一个,如果想要查找全部,使用elements,其它完全相同

1
2
3
4
5
6
7
find_elements_by_name
find_elements_by_xpath
find_elements_by_link_text
find_elements_by_partial_link_text
find_elements_by_tag_name
find_elements_by_class_name
find_elements_by_css_selector

另外以上的查找器还可以写成这种形式

1
find_element(By.ID,'kw')

5 获取标签内容

查找到元素之后,后续还需要获取标签内容。

1
2
3
4
5
6
7
8
9
10
11
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("http://www.python.org")
tag = driver.find_element_by_id("mainnav") # 找到元素tag

print(tag.get_attribute('class')) # 获取标签属性
print(tag.text) # 标签内的文本内容
print(tag.id) # 标签的id,基本没用
print(tag.location) # 标签的x,y坐标,用于定位
print(tag.tag_name) # 标签名,基本没用
print(tag.size) # 标签大小,宽多少,高多少

6 等待元素被加载

现在的大多数的Web应用程序是使用Ajax,当一个页面被加载到浏览器时, 该页面内的元素可以在不同的时间点被加载,如果提前获取某元素可能还没有加载,使用 waits, 我们可以解决这个问题。

一共两种类型的waits隐式和显式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 隐式等待
# 如果某些元素不是立即可用的,隐式等待是告诉WebDriver去等待一定的时间后去查找元素。 默认等待时间是0秒
driver.implicitly_wait(10)
# 显式等待
# 在代码中定义的,等待一定条件发生后,再进一步执行你的代码
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

driver = webdriver.Firefox()
driver.get("http://somedomain/url_that_delays_loading")
try:
element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, "myDynamicElement"))
)
finally:
driver.quit()

推荐隐式等待,简单。

7 无界面浏览器

谷歌浏览器支持无界面使用。添加--headless参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
from selenium.webdriver.chrome.options import Options

# 添加配置选项
chrome_options = Options()
chrome_options.add_argument('window-size=1920x3000') #指定浏览器分辨率
chrome_options.add_argument('--disable-gpu') #谷歌文档提到需要加上这个属性来规避bug
chrome_options.add_argument('--hide-scrollbars') #隐藏滚动条, 应对一些特殊页面
chrome_options.add_argument('blink-settings=imagesEnabled=false') #不加载图片, 提升速度
chrome_options.add_argument('--headless') #浏览器不提供可视化页面. linux下如果系统不支持可视化不加这条会启动失败
chrome_options.binary_location = r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" #手动指定使用的浏览器位置

# 配置好之后使用
driver=webdriver.Chrome(chrome_options=chrome_options)

8 操控浏览器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
driver = webdriver.Chrome()
driver.get("http://www.python.org")
# 点击动作
tag = find_element_by_id('btn')
tag.click()
# 添加内容
tag.send_keys('123')
# 清空内容
tag.clear()
# 执行js代码
driver.execute_script('alert("hello world")') # 里面写js代码
# 前进:按下浏览器的前进键
driver.forward()
# 后退:按下浏览器的后退键
driver.back()
# 刷新:刷新当前页面(F5)
driver.refresh()
# 获取窗口标题
driver.title
# 获取cookie
driver.get_cookies()
# 删除cookie
browser.delete_all_cookies()
# 创建新窗口(执行js代码)
browser.execute_script('window.open()')
# 获取所有的选项卡,返回一个列表,是所有的标签页
driver.window_handles
# 切换标签页
browser.switch_to_window(browser.window_handles[1])
# 关闭窗口
browser.close()
# 模拟键盘按键
from selenium.webdriver.common.keys import Keys # 导入
tag.send_keys(Keys.ENTER) # 回车键

9 动作链

当调用动作链ActionChains时,不会立即执行,而是将操作添加到队列里,当调用perform方法时,队列中的事件依次执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
from selenium.webdriver import ActionChains
driver = webdriver.Chrome()
driver.get('https://www.baidu.com/')

menu = driver.find_element_by_css_selector(".nav")
hidden_submenu = driver.find_element_by_css_selector(".nav #submenu1")
# 方式一:链式操作
ActionChains(driver).move_to_element(menu).click(hidden_submenu).perform()
# 方式二:分步操作
actions = ActionChains(driver)
actions.move_to_element(menu)
actions.click(hidden_submenu)
actions.perform()

所有的方法总结:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# on_element意思是某个标签或者元素(比如按钮,输入框input等等)
click(on_element=None) # 单击鼠标左键
click_and_hold(on_element=None) # 点击鼠标左键,不松开
context_click(on_element=None) # 点击鼠标右键
double_click(on_element=None) # 双击鼠标左键
drag_and_drop(source, target) # 拖拽到某个元素然后松开
drag_and_drop_by_offset(source, xoffset, yoffset) # 拖拽到某个坐标然后松开
key_down(value, element=None) # 按下某个键盘上的键
key_up(value, element=None) # 松开某个键
move_by_offset(xoffset, yoffset) # 鼠标从当前位置移动到某个坐标
move_to_element(to_element) # 鼠标移动到某个元素
move_to_element_with_offset(to_element, xoffset, yoffset) # 移动到距某个元素(左上角坐标)多少距离的位置
perform() # 执行链中的所有动作
release(on_element=None) # 在某个元素位置松开鼠标左键
send_keys(*keys_to_send) # 发送某个键到当前焦点的元素
send_keys_to_element(element, *keys_to_send) # 发送某个键到指定元素
release(self, on_element=None) # 释放动作链

四、BeautifulSoup

Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库。它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式。Beautiful Soup会帮你节省数小时甚至数天的工作时间。

中文文档

1 安装模块

1
2
$ apt-get install Python-bs4 # Debain或ubuntu
pip install beautifulsoup4 # pip

2 安装解析器

Beautiful Soup支持Python标准库中的HTML解析器,还支持一些第三方的解析器,其中一个是lxml。根据操作系统不同,可以选择下列方法来安装lxml:

1
2
$ apt-get install Python-lxml # Debain或ubuntu
$ pip install lxml

另一个可供选择的解析器是纯Python实现的html5lib,html5lib的解析方式与浏览器相同,可以选择下列方法来安装html5lib:

1
2
$ apt-get install Python-html5lib
$ pip install html5lib

下表列出了主要的解析器,以及它们的优缺点:

解析器使用方法优势劣势
Python标准库BeautifulSoup(markup, "html.parser")Python的内置标准库执行速度适中文档容错能力强Python 2.7.3 or 3.2.2)前 的版本中文档容错能力差
lxml HTML 解析器BeautifulSoup(markup, "lxml")速度快文档容错能力强需要安装C语言库
lxml XML 解析器BeautifulSoup(markup, ["lxml", "xml"])``BeautifulSoup(markup, "xml")速度快唯一支持XML的解析器需要安装C语言库
html5libBeautifulSoup(markup, "html5lib")最好的容错性以浏览器的方式解析文档生成HTML5格式的文档速度慢不依赖外部扩展

3 获得文档对象

将一段文档传入BeautifulSoup 的构造方法,就能得到一个文档的对象,也可以传入一段字符串或一个文件句柄。

1
2
3
4
5
6
from bs4 import BeautifulSoup
html_doc = "<!DOCTYPE html><html><head>........" # 一段html文档
# 第一个参数为文档内容,第二个参数是解析器(如果不指定解析器则自动选择)
soup=BeautifulSoup(html_doc,'lxml') # 实例化得到对象,第一个参数是要解析的文档,第二个是解析器。解析具有容错功能
res=soup.prettify() #处理好缩进,结构化显示
print(res)

Beautiful Soup将复杂HTML文档转换成一个复杂的树形结构,每个节点都是Python对象。

所有对象可以归纳为4种TagNavigableStringBeautifulSoupComment

下面的一段html代码被多次用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

# 容错处理:文档的容错能力指的是在html代码不完整的情况下,使用该模块可以识别该错误。使用BeautifulSoup解析上述代码,能够得到一个 BeautifulSoup 的对象,并能按照标准的缩进格式的结构输出

3.1 tag

Tag 对象与XML或HTML原生文档中的tag(标签)相同:

1
2
3
4
5
soup = BeautifulSoup(html_doc,'lxml')
tag = soup.p # 取出一个p标签
print(tag)

# <p class="title"><b>The Dormouse's story</b></p>

tag中最重要的属性是nameattributes

Name
1
2
3
4
print(tag.name) # p
tag.name='div' # 将p修改为div
print(tag)
# 如果改变了tag的name,那将影响所有通过当前Beautiful Soup对象生成的HTML文档,源文档html_doc也会变
Attributes

一个tag可能有很多个属性。tag的属性的操作方法与字典相同:

1
2
3
print(tag['class']) # 中括号取
print(tag.attrs) # 点取属性
print(tag.get('class')) # get方法取

再说一次,tag的属性操作方法与字典一样。意味着tag的属性可以被添加、删除或修改。

1
2
3
tag['class'] = 'abc' # 赋值
del tag['class'] # 删除键
print(tag)

关于HTML5中的多值属性,比如标签中的class可以有多值,bs4会将其返回为列表类型。

1
2
3
4
5
6
7
css_soup = BeautifulSoup('<p class="body strikeout"></p>')
css_soup.p['class']
# ["body", "strikeout"]

css_soup = BeautifulSoup('<p class="body"></p>')
css_soup.p['class']
# ["body"]

如果某个属性看起来好像有多个值,但在任何版本的HTML定义中都没有被定义为多值属性,那么Beautiful Soup会将这个属性作为字符串返回

1
2
3
id_soup = BeautifulSoup('<p id="my id"></p>')
id_soup.p['id']
# 'my id'

在赋值时,多值属性会被转换回字符串

1
2
3
4
5
6
rel_soup = BeautifulSoup('<p>Back to the <a rel="index">homepage</a></p>')
rel_soup.a['rel']
# ['index']
rel_soup.a['rel'] = ['index', 'contents']
print(rel_soup.p)
# <p>Back to the <a rel="index contents">homepage</a></p>

3.2 NavigableString

字符串常被包含在tag内,bs4使用NavigableString(可以遍历的字符串)类来包装tag中的字符串(标签包裹的字符串)。

1
2
print(tag.string) # string直接获取内部字符串
# The Dormouse's story

tag中包含的字符串不能编辑,但是可以被替换成其它的字符串。

1
2
3
tag.string.replace_with("No longer bold") # 用replace_with方法替换
print(tag)
# <blockquote>No longer bold</blockquote>

NavigableString对象支持遍历文档树和搜索文档树中定义的大部分属性。

3.3 BeautifulSoup

BeautifulSoup对象表示的是一个文档的全部内容。大部分时候,可以把它当作 Tag 对象,它支持遍历文档树和搜索文档树中描述的大部分的方法。但是它并不是真正的HTML或XML的tag,所以没有name和attribute属性

3.4 Comment

上面三种对象几乎覆盖了html和xml中的所有内容,但是有些内容不包括,比如文档的注释部分,Comment(注释及特殊字符串)用于处理这些内容。实际上它是一个特殊类型的NavigableString对象

1
2
3
4
5
markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
soup = BeautifulSoup(markup)
comment = soup.b.string
type(comment)
# <class 'bs4.element.Comment'>

4 遍历文档树

类似于js进行DOM操作一样,一个tag标签可能包含多个字符串或其它的tag,这些都是它的子节点。bs4提供了许多操作和遍历子节点的属性,但如果存在多个相同的标签则只返回第一个。

4.1 子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 根据名字获取tag
print(tag.p) #存在多个相同的标签则只返回第一个
print(tag.a) #存在多个相同的标签则只返回第一个
# 获取tag的名字
print(tag.p.name)
# 获取标签的属性
print(tag.p.attrs)
# 小窍门:在文档树中多次调用这个方法进行嵌套选择(类似于链式操作)
print(tag.body.b) # body下的b标签(存在多个则返回一个,没有则返回None)
# 想要获得所有(不止一个)的tag,使用find_all方法
print(tag.find_all('a'))
# 获取标签包裹的内容
print(tag.p.string) # p下的文本只有一个时,取到,否则为None
print(tag.p.strings) #拿到一个生成器对象, 取到p下所有的文本内容
print(tag.p.text) # 取到p下所有的文本内容,直接用这个获取子孙内容
for line in tag.stripped_strings: #去掉空白
print(line)

# contents属性可以将tag的子节点以列表的方式输出。字符串没有contents属性,因为字符串没有子节点
print(tag) # <p class="title"><b>The Dormouse's story</b></p>
print(tag.contents) # [<b>The Dormouse's story</b>]
print(tag.contents[0].contents) # ["The Dormouse's story"]
# 通过tag的children属性获得生成器,可以对tag的子节点进行循环
for t in tag.children:
print(t) # <b>The Dormouse's story</b>

# contents和children都是获取子节点,descendants是一个生成器,遍历可以获取所有子孙节点
for child in tag.descendants:
print(child)
# 输出结果:
# <b>The Dormouse's story</b>
# The Dormouse's story

# 如果tag只有一个NavigableString类型子节点(或者仅有一个子节点),那么这个tag可以使用string:
print(tag.string)
# 如果tag包含了多个子节点,tag就无法确定string方法应该调用哪个子节点的内容,string的输出结果是 None
# 此时可以使用strings 拿到一个生成器对象,for循环取到tag下所有的文本内容
# 输出的字符串中可能包含了很多空格或空行,使用stripped_strings可以去除多余空白内容
for string in soup.strings:
print(string)
for string in soup.stripped_strings:
print(string)

####4.2 父节点

1
2
3
4
# parent获取父节点,文档的顶层(比如html)的父节点是BeautifulSoup对象;BeautifulSoup对象的parent是None:
print(tag.parent)
# parents找到所有的祖先节点
print(tag.parents)

4.3 兄弟节点

1
2
3
4
5
# next_sibling下一个兄弟
print(tag.next_sibling)
# previous_sibling上一个兄弟
print(tag.previous_sibling)
# 同理可以通过next_siblings和previous_siblings属性可以对当前节点的兄弟节点迭代输出

5 搜索文档树

Beautiful Soup定义了很多搜索方法,这里主要是两个,find和find_all,其它方法的参数和用法类似。

5.1 五个过滤器

在介绍方法之前,首先了解一下过滤器,这些过滤器贯穿整个搜索的API。一共有五种过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 
#1、字符串:最简单的过滤器是字符串,在搜索方法中传入一个字符串参数,会查找与字符串完整匹配的内容,比如查找b标签
print(soup.find_all('b'))

#2、通过正则表达式匹配内容
import re
print(soup.find_all(re.compile('^b'))) #找出b开头的标签,结果有body和b标签

#3、列表:如果传入列表参数,Beautiful Soup会将与列表中任一元素匹配的内容返回,下面代码找到文档中所有<a>标签和<b>标签:
print(soup.find_all(['a','b']))

#4、True:可以匹配任何值,下面代码查找到所有的tag,但是不会返回字符串节点
print(soup.find_all(True))
for tag in soup.find_all(True):
print(tag.name)

#5、方法:如果没有合适过滤器,那么还可以定义一个方法,方法只接受一个元素参数 ,如果这个方法返回 True 表示当前元素匹配并且被找到,如果不是则反回 False
def has_class_but_no_id(tag):
return tag.has_attr('class') and not tag.has_attr('id')

print(soup.find_all(has_class_but_no_id))

5.2 find_all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def find_all( name , attrs , recursive , text , **kwargs )
# 参数说明:
# 1.name参数可以查找所有名字为name的tag,字符串对象会被自动忽略掉
# name参数的值可以使任一类型的过滤器,字符串,正则表达式,列表,方法,True
soup.find_all("title")
# 2.按CSS搜索,但标识CSS类名的关键字`class`在Python中是保留字,所以使用`class_`加下划线代替
soup.find_all("a", class_="sister") # 完全匹配
soup.find_all(class_=re.compile("itl")) # 使用re模糊匹配
# 3.attrs参数,字典,class_也属于attr参数,除了class也可以查找自定义的标签属性
soup.find_all('p',attrs={'class':'story'})
# 4.keyword参数,如果一个指定名字的参数不是搜索内置的参数名,搜索时会把该参数当作指定名字tag的属性来搜索,比如搜索href
soup.find_all(href=re.compile("elsie"))
# 5.text,搜索文档中的字符串内容
soup.find_all(text='Elsie')
# 6.limit 限制搜索的数量,比如设置为2表示最多搜索2个
soup.find_all(string='Elsie',limit=2)
# 7.recursive,调用find_all方法时,会检索当前tag的所有子孙节点,如果只想搜索tag的直接子节点,可以使用参数recursive=False
soup.html.find_all('a',recursive=False)

5.3 find

1
2
3
4
5
6
7
def find( name , attrs , recursive , string , **kwargs)
# 只搜索一个结果,比如文档中只有一个<body>标签,用find_all就不合适
# 使用find_all方法并设置limit=1参数不如直接使用find方法,下面两行代码是等价的
soup.find_all('title', limit=1)
soup.find('title')
# 唯一的区别是find_all方法的返回结果是值包含一个元素的列表,而find方法直接返回结果
# find_all方法没有找到目标是返回空列表,find方法找不到目标时,返回None

5.4 get_text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 如果只想得到tag中包含的文本内容,那么可以调用 get_text()方法。
# 这个方法获取到tag中包含的所有文版内容包括子孙tag中的内容,并将结果作为Unicode字符串返回
markup = '<a href="http://example.com/">\nI linked to <i>example.com</i>\n</a>'
soup = BeautifulSoup(markup)
soup.get_text()
soup.i.get_text()

# 可以通过参数指定tag的文本内容的分隔符:
soup.get_text("|")

# 还可以去除获得文本内容的前后空白:
soup.get_text("|", strip=True)

# 或者使用 .stripped_strings 生成器,获得文本列表后手动处理列表:
[text for text in soup.stripped_strings]

5.5 CSS选择器

可以直接使用select方法,对于熟悉CSS选择器语法的人来说这是个非常方便的方法。

1
2
3
4
5
6
# 在 Tag 或 BeautifulSoup 对象的 .select() 方法中传入字符串参数, 即可使用CSS选择器的语法找到tag
soup.select("body a")
soup.select("head > title")
soup.select("p > a")
soup.select("#link1 ~ .sister")
soup.select("#link1")

关于更多,比如修改文档树,请查阅官方文档

五、xpath

xpath是是一门在XML文档中查找信息的语言,与css选择一样可以查找标签,并不是只有selenium可以用,只要是支持xpath选择都可以使用这种语法。

能看懂就可以,最核心的选择器是///,常用的有@(属性选择)text(选择文本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
doc='''
<html>
<head>
<base href='http://example.com/' />
<title>Example website</title>
</head>
<body>
<div id='images'>
<a href='image1.html'>Name: My image 1 <br /><img src='image1_thumb.jpg' /></a>
<a href='image2.html'>Name: My image 2 <br /><img src='image2_thumb.jpg' /></a>
<a href='image3.html'>Name: My image 3 <br /><img src='image3_thumb.jpg' /></a>
<a href='image4.html'>Name: My image 4 <br /><img src='image4_thumb.jpg' /></a>
<a href='image5.html' class='li li-item' name='items'>Name: My image 5 <br /><img src='image5_thumb.jpg' /></a>
<a href='image6.html' name='items'><span><h5>test</h5></span>Name: My image 6 <br /><img src='image6_thumb.jpg' /></a>
</div>
</body>
</html>
'''
from lxml import etree

html=etree.HTML(doc)
# html=etree.parse('search.html',etree.HTMLParser())
# 1 所有节点
# a=html.xpath('//*')
# 2 指定节点(结果为列表)
# a=html.xpath('//head')
# 3 子节点,子孙节点
# a=html.xpath('//div/a')
# a=html.xpath('//body/a') #无数据
# a=html.xpath('//body//a')
# 4 父节点
# a=html.xpath('//body//a[@href="image1.html"]/..')
# a=html.xpath('//body//a[1]/..')
# 也可以这样
# a=html.xpath('//body//a[1]/parent::*')
# 5 属性匹配
# a=html.xpath('//body//a[@href="image1.html"]')

# 6 文本获取
# a=html.xpath('//body//a[@href="image1.html"]/text()')

# 7 属性获取
# a=html.xpath('//body//a/@href')
# # 注意从1 开始取(不是从0)
# a=html.xpath('//body//a[1]/@href')
# 8 属性多值匹配
# a 标签有多个class类,直接匹配就不可以了,需要用contains
# a=html.xpath('//body//a[@class="li"]')
# a=html.xpath('//body//a[contains(@class,"li")]')
# a=html.xpath('//body//a[contains(@class,"li")]/text()')
# 9 多属性匹配
# a=html.xpath('//body//a[contains(@class,"li") or @name="items"]')
# a=html.xpath('//body//a[contains(@class,"li") and @name="items"]/text()')
# # a=html.xpath('//body//a[contains(@class,"li")]/text()')
# 10 按序选择
# a=html.xpath('//a[2]/text()')
# a=html.xpath('//a[2]/@href')
# 取最后一个
# a=html.xpath('//a[last()]/@href')
# 位置小于3的
# a=html.xpath('//a[position()<3]/@href')
# 倒数第二个
# a=html.xpath('//a[last()-2]/@href')
# 11 节点轴选择
# ancestor:祖先节点
# 使用了* 获取所有祖先节点
# a=html.xpath('//a/ancestor::*')
# # 获取祖先节点中的div
# a=html.xpath('//a/ancestor::div')
# attribute:属性值
# a=html.xpath('//a[1]/attribute::*')
# child:直接子节点
# a=html.xpath('//a[1]/child::*')
# descendant:所有子孙节点
# a=html.xpath('//a[6]/descendant::*')
# following:当前节点之后所有节点
# a=html.xpath('//a[1]/following::*')
# a=html.xpath('//a[1]/following::*[1]/@href')
# following-sibling:当前节点之后同级节点
# a=html.xpath('//a[1]/following-sibling::*')
# a=html.xpath('//a[1]/following-sibling::a')
# a=html.xpath('//a[1]/following-sibling::*[2]')
# a=html.xpath('//a[1]/following-sibling::*[2]/@href')

# print(a)

六、Scrapy

1 介绍

Scrapy一个开源和协作的框架,其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的,使用它可以以快速、简单、可扩展的方式从网站中提取所需的数据。但目前Scrapy的用途十分广泛,可用于如数据挖掘、监测和自动化测试等领域,也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。

Scrapy 是基于twisted框架开发而来,twisted是一个流行的事件驱动的python网络框架。因此Scrapy使用了一种非阻塞(又名异步)的代码来实现并发。

2 安装

相关链接:官方网站官方文档中文文档2.5(机翻)中文文档1.0(易阅读)

安装:

1
2
3
4
5
6
7
8
9
10
11
pip install scrapy # mac和linux,windows如果安装失败,参考下面

# 如果windows安装不上
1、pip3 install wheel
#安装后,便支持通过wheel文件安装软件,wheel文件官网:https://www.lfd.uci.edu/~gohlke/pythonlibs
2、pip3 install lxml
3、pip3 install pyopenssl
4、下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/
5、下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
6、执行pip3 install 下载目录\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
7、pip3 install scrapy

命令行输入scrapy如果有命令,说明安装成功。

3 结构体系概述

下图显示了Scrapy架构及其组件的概述,以及系统内部发生的数据流的概要(以红色箭头显示)

image

红色箭头为数据流,由执行引擎(Engine)控制

3.1 组件介绍

一共有五个组件,两个中间件。

引擎(EGINE)

引擎负责控制系统所有组件之间的数据流,并在某些动作发生时触发事件。

调度器(SCHEDULER)

用来接受引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回。可以想像成一个URL的优先级队列,由它来决定下一个要抓取的网址是什么,同时去除重复的网址。

下载器(DOWLOADER)

用于下载网页内容,并将网页内容返回给EGINE,下载器是建立在twisted这个高效的异步模型上的。

爬虫(SPIDERS)

SPIDERS是开发人员自定义的类,用来解析responses,并且提取items,或者发送新的请求。

项目管道(ITEM PIPLINES)

在items被提取后负责处理它们,主要包括清理、验证、持久化(比如存到数据库)等操作。

下载器中间件(Downloader Middlewares)

位于EGINE和DOWLOADER之间,主要用来处理从EGINE传到DOWLOADER的请求request,已经从DOWNLOADER传到EGINE的响应response,你可用该中间件做以下几件事:

  • 将请求发送给下载器之前处理该请求
  • 修改下载器返回的response,然后再将修改后的返回给SPIDERS
  • 发送一个新的请求,而不是将接收到的响应传递给SPIDERS
  • 在不获取网页的情况下将响应传递给SPIDERS
  • 悄悄地放弃一些请求

爬虫中间件(Spider Middlewares)

位于EGINE和SPIDERS之间,主要工作是处理SPIDERS的输入(即responses)和输出(即requests)。

3.2 流程介绍

1)首先spiders从入口url开始执行,将request请求对象发送给engine

2)engine将request请求放到scheduler中等待调度

3)scheduler使用栈和队列两种调度结构,将request对象发给下载器downloader

4)downloader收到request对象后向目标url发请求

5)downloader获取response响应对象,将它返回给engine

6)engine收到响应对象,将其返回给spider中,默认由spider中定义的解析器进行数据解析

7)如果解析后需要存储,就将items对象发给engine,由engine将它送到管道pipline进行数据持久化

8)如果解析后还需要继续发请求,spider会继续封装request对象,把它送到engine,转1)

4 创建项目、运行爬虫

4.1 创建项目

命令行输入

1
scrapy startproject 项目名

会在当前目录下新建一个scrapy项目文件夹

4.2 创建爬虫

进入项目根路径,命令行输入

1
2
3
cd 项目名/
scrapy genspider 爬虫名 url # 创建一个爬虫,和指定的url
# 举例:scrapy genspider baidu www.baidu.com

4.3 运行爬虫

1
2
3
scrapy crawl 爬虫名 [--nolog]
# 举例:scrapy crawl baidu --nolog
# --nolog可选,表示不打印日志信息

如果想在pycharm右键运行,可以在项目根目录下创建一个py文件,复制下面代码

1
2
3
4
from scrapy.cmdline import execute
execute(['scrapy','crawl','爬虫名']) # 相当于命令行执行

# execute(['scrapy','crawl','baidu','--nolog'])

4.3 目录结构

scrapy的目录结构如图

scrapy项目目录结构

1
2
3
4
5
6
7
8
9
10
11
12
crawl01/ # / 根路径
crawl01/ # 目录名
spiders/ # 你的爬虫文件都放在里面
__init__.py
baidu.py # 主要爬虫逻辑都在这里写
__init__.py
items.py # 项目项目定义文件,写一个个的类
middlewares.py # 中间件(两大中间件都写在这里)
pipelines.py # 管道,用于数据持久化等
run.py # 我们自己创建的,用于pycharm右键执行
settings.py # 配置文件
scrapy.cfg # 项目上线部署配置文件

5 简单示例

我们以小不点搜索网为例:https://www.xiaoso.net/

1
2
3
4
5
6
7
8
9
import scrapy

class XiaosoSpider(scrapy.Spider):
name = 'xiaoso'
allowed_domains = ['www.xiaoso.net']
start_urls = ['https://www.xiaoso.net/']

def parse(self, response):
print(response.text) # 打印网站文本

然后运行爬虫,可以看到文本内容。

6 在Scrapy中解析数据

可以发现发送请求后,返回时会默认调用parse方法,response就是返回对象,里面包含我们需要的数据。我们需要对其进行解析。

6.1 使用第三方解析

首先,可以使用第三方的解析,比如BeautifulSoup

1
2
3
4
5
6
7
8
9
10
11
12
13
from bs4 import BeautifulSoup
class XiaosoSpider(scrapy.Spider):
name = 'xiaoso'
allowed_domains = ['www.xiaoso.net']
start_urls = ['https://www.xiaoso.net/']

def parse(self, response):
soup = BeautifulSoup(response.text,'lxml')
box_list = soup.find_all(class_='box_line hot')
for box in box_list:
link = box.find('a').get('href') # 新闻链接
title = box.find('a').text # 新闻标题
print(link,title)

6.2 使用自带解析器

scrapy有两个解析器css和xpath

1
2
3
4
5
6
7
box_list = response.css('.box_line') # css解析
box_list = response.css('.box_line::text').extract() # 获取文本内容


box_list = response.xpath('//div[contains(@class,"box_line hot")]//a/@href').extract() # xpath解析
# extract() 用于提取真实的原文数据,返回一个列表
# extract_first() 可以取出一个

7 scrapy持久化存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 方案一:使用命令,支持的格式有('json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle')
def parse(self, response):
li = []
data_list = response.xpath('//div[contains(@class,"box_line hot")]//a')
for data in data_list:
title = data.xpath('text()').extract_first()
url = data.xpath('@href').extract_first()
print(title,url)
li.append({'title':title,'url':url})
return li # 要有返回值,格式为列表内嵌套字典[{}]
# 然后使用命令:
scrapy crawl xiaoso -o items.json


# 方案二:高级用法,使用 pipline item存储
# 1.首先去items.py中定义一个类:
class XiaosoItem(scrapy.Item):
title = scrapy.Field() # 类似于django orm 但是只有一个Field字段
url = scrapy.Field()

# 2.在spider中导入:
class XiaosoSpider(scrapy.Spider):
def parse(self, response):
data_list = response.xpath('//div[contains(@class,"box_line hot")]//a')
for data in data_list:
item = XiaosoItem() # 实例化得到对象
title = data.xpath('text()').extract_first()
url = data.xpath('@href').extract_first()
item['title'] = title # 把数据放进去
item['url'] = url # 把数据放进去
yield item # 用yield,item会进入管道;不用return,是因为return会中断for循环

# 3.在管道文件pipelines.py中编写方法(用于将数据存储为文件)
class XiaosoPipeline:
def open_spider(self,spider):
self.file = open('item.txt','w',encoding='utf-8')

def process_item(self, item, spider):
self.file.write(item['title']+'\n')
self.file.write(item['url']+'\n')
return item # 这里一定要return
def close_spider(self,spider):
self.file.close()
# 4.在配置文件中配置我们写的管道
ITEM_PIPELINES = {
'crawl01.pipelines.XiaosoPipeline': 300, # 后面的数字是优先级,数字越小越优先
# 可以写多个管道
}
# 5.运行,当前目录就会有item.txt文件

管道文件是一个独立的Python类,在内部需要定义三个方法(四个,有一个不常用)

1
2
3
4
5
6
7
8
9
10
11
12
# 每个item pipeline组件都需要调用该方法,这个方法必须返回一个具有数据的dict,或是 Item (或任何继承类)对象, 或是抛出 DropItem 异常,被丢弃的item将不会被之后的pipeline组件所处理。
# 必须定义这个方法
def process_item(self, item, spider)
参数:
- item (Item 对象或者一个dict) – 被爬取的item
- spider (Spider 对象) – 爬取该item的spider
# 可选方法,当spider被开启时,这个方法被调用。
def open_spider(self, spider)
- spider (Spider 对象) – 被开启的spider
# 可选方法,当spider被关闭时,这个方法被调用。
def close_spider(self, spider)
- spider (Spider 对象) – 被关闭的spider

一般情况下,我们不会在process_item打开文件,因为每次循环都会执行该方法,循坏几次就打开几次文件,会导致文件内容被覆盖,只在这里写数据。打开和关闭文件的操作由另外两个方法执行。搭配open_spider(打开时调用,在这里打开文件)close_spider(关闭时调用,在这里关闭文件)。

可以写入文件,也可以写入数据库,比如mysql(使用pymysql模块),或者是MongoDB、redis等等都可以。

8 配置文件

配置文件有很多,常用的如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 是否遵守robots.txt协议
ROBOTSTXT_OBEY = False
# 管道配置
ITEM_PIPELINES = {
'crawl01.pipelines.XiaosoPipeline': 300, # 后面的数字是优先级,数字越小越优先;习惯设定在0-1000范围内
...
}
# 请求头的user-agent配置
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36'
# 日志的最低级别,默认: 'DEBUG'
# 可选的级别有: CRITICAL、ERROR、WARNING、INFO、DEBUG
LOG_LEVEL = 'ERROR'

# 下载器在下载同一个网站下一个页面前需要等待的时间。该选项可以用来限制爬取速度, 减轻服务器压力。
DOWNLOAD_DELAY = 3
# 如果启用,Scrapy 将在从同一网站获取请求时等待随机时间。介于0.5*DOWNLOAD_DELAY和1.5*DOWNLOAD_DELAY之间
RANDOMIZE_DOWNLOAD_DELAY = False # 默认启用

还有一些配置,见10.提高爬取效率的配置

中间件配置见11.中间件

9 request和response

整个流程为:Request对象生成一个spider,到达downloader之后,向url执行请求并返回一个Response对象,该对象返回发出请求的spider。

9.1 request对象

1
2
3
4
5
6
7
8
# 第一种导入方式from scrapy.http.request import Request
# 第二种导入方式from scrapy import Request

class Request(object_ref):
# 参数
def __init__(self, url, callback=None, method='GET', headers=None, body=None,
cookies=None, meta=None, encoding='utf-8', priority=0,
dont_filter=False, errback=None, flags=None, cb_kwargs=None):

一个request对象代表了一个HTTP请求,请求结束后,返回response对象。

参数说明:

  • url(string 必须,它是发送http请求的url
  • callback(callable 回调函数,默认为None,一旦响应回来后,就调用callback指定的函数,并将response对象作为第一个参数传递另如果没有指定callback函数,默认会调用parse()方法
  • method(string 请求方法,默认为GET请求
  • headers(dict) 请求头
  • body(str or unicode) 请求体
  • errback(callable) 如果在处理请求时引发任何异常,将调用该函数。这包括页面失败与404 HTTP错误等。它接收一个Twisted Failure实例作为第一个参数。
  • encoding(string) 编码格式,默认为utf8
  • cookies (dict or list) 请求携带的cookie
  • meta(dict) 可以包含任意数据的字典,有一些特殊的key可以被scrapy识别,也可以将一些自定义的数据传递给response对象。

meta中有一些字段可以被scrapy识别并解析成特殊意义,比如代理、超时时间等等。详见这里

关于回调函数的使用场景:

1
2
3
4
5
6
7
8
9
# 运行scrapy时,发送的request请求回调到parse_page1
def parse_page1(self, response):
# 在这里进行解析后,还需要再发请求
return scrapy.Request("http://www.example.com/some_page.html",
callback=self.parse_page2)
# 第二次请求返回后,由parse_page2解析
def parse_page2解析(self, response):
# this would log http://www.example.com/some_page.html
self.logger.info("Visited %s", response.url)

9.2 response对象

1
2
3
4
5
# 导入方法from scrapy.http.response import Response

class Response(object_ref):
def __init__(self,url,status=200,headers=None,body=b"",flags=None,
request=None,certificate=None,ip_address=None,protocol=None,):

参数说明:

  • url(string 必须,它是响应的url
  • status(int) 响应状态码,默认为200
  • **headers (dict) ** 响应头
  • body(byte) 响应体的二进制形式,如果想要字符串形式,使用方法Response.text
  • request(scrapy.http.Request object) 生成此响应的请求对象
  • certificate(twisted.internet.ssl.Certificate object) 服务器SSL证书的对象
  • ip_address((ipaddress.IPv4Address or ipaddress.IPv6Address) 服务器的ip地址
  • protocol(str) 用于下载响应的协议。例如:“ HTTP/1.0 “, “ HTTP/1.1 “, “ h2 “

关于response.meta的使用

1
2
3
4
5
6
7
8
9
# 为了后续操作,可以在请求时传给response一些字段或者对象
# 在request.meta 中定义
def parse(self, response):
...
data = {.....}
Request(url,callback=self.parse_02,meta={"data":data}) # 把数据放到meta

def parse_02(self, response):
data = response.meta.get("data") # 从meta取出data

10 提高爬取效率的配置

1
2
3
4
5
6
7
8
9
10
11
在配置文件中进行相关的配置即可:(默认还有一套setting)
# 1 增加并发:
默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改CONCURRENT_REQUESTS = 100值为100,并发设置成了为100
# 2 降低日志级别:
在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:LOG_LEVEL = ‘INFO’
# 3 禁止cookie:
如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:COOKIES_ENABLED = False
# 4 禁止重试:
对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:RETRY_ENABLED = False
# 5 减少下载超时:
如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:DOWNLOAD_TIMEOUT = 10 超时时间为10s

11 中间件

在创建项目的时候,目录结构中会自动生成middlewares.py,两大中间件都在这里写。

要想中间件生效,需要在配置文件中进行配置。

1
2
3
4
5
6
7
8
9
10
11
# Enable or disable spider middlewares
# 爬虫中间件(很少使用)
SPIDER_MIDDLEWARES = {
'crawl01.middlewares.Crawl01SpiderMiddleware': 543,# 数字代表优先级,越小越优先
}

# Enable or disable downloader middlewares
# 下载中间件(常用)
DOWNLOADER_MIDDLEWARES = {
'crawl01.middlewares.Crawl01DownloaderMiddleware': 543,
}

每个中间件都是一个类,它定义了下面定义的一个或多个方法。主要介绍下载中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Crawl01DownloaderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.

def process_request(self, request, spider):
# Called for each request that goes through the downloader middleware.
# 每次请求通过时,执行该方法

# Must either: # 返回值必须是其中一个
# - return None: continue processing this request.
# If it returns None, Scrapy will continue processing this request, executing all other middlewares until, finally, the appropriate downloader handler is called the request performed (and its response downloaded).
# - or return a Response object.
# If it returns a Response object, Scrapy won’t bother calling any other process_request() or process_exception() methods, or the appropriate download function; it’ll return that response. The process_response() methods of installed middleware is always called on every response.
# - or return a Request object.
# If it returns a Request object, Scrapy will stop calling process_request methods and reschedule the returned request. Once the newly returned request is performed, the appropriate middleware chain will be called on the downloaded response.
# - or raise IgnoreRequest:process_exception() methods of installed downloader middleware will be called.
# If it raises an IgnoreRequest exception, the process_exception() methods of installed downloader middleware will be called. If none of them handle the exception, the errback function of the request (Request.errback) is called. If no code handles the raised exception, it is ignored and not logged (unlike other exceptions).
return None

def process_response(self, request, response, spider):
# Called with the response returned from the downloader.
# 每次响应通过时,执行该方法

# Must either; # 返回值必须是其中一个
# - return a Response object.
# If it returns a Response (it could be the same given response, or a brand-new one), that response will continue to be processed with the process_response() of the next middleware in the chain.
# - return a Request object.
# If it returns a Request object, the middleware chain is halted and the returned request is rescheduled to be downloaded in the future. This is the same behavior as if a request is returned from process_request().
# - or raise IgnoreRequest.
# If it raises an IgnoreRequest exception, the errback function of the request (Request.errback) is called. If no code handles the raised exception, it is ignored and not logged (unlike other exceptions).
return response

def process_exception(self, request, exception, spider):
# Called when a download handler or a process_request() (from other downloader middleware) raises an exception.
# 当其他中间件引发异常时,调用此方法

# Must either: # 返回值必须是其中一个
# - return None: continue processing this exception.
# If it returns None, Scrapy will continue processing this exception, executing any other process_exception() methods of installed middleware, until no middleware is left and the default exception handling kicks in.
# - return a Response object: stops process_exception() chain.
# If it returns a Response object, the process_response() method chain of installed middleware is started, and Scrapy won’t bother calling any other process_exception() methods of middleware.
# - return a Request object: stops process_exception() chain.
# If it returns a Request object, the returned request is rescheduled to be downloaded in the future. This stops the execution of process_exception() methods of the middleware the same as returning a response would.
pass

我们可以在中间件中做很多事情,比如更换cookie,更换请求头,更换请求体,加代理(在request.meta中添加proxy字段),或者是更换请求url等等。在响应返回之前,在中间件也可以对响应对象操作,比如更换响应头,获取响应cookie等等。

12 去重规则

1
2
3
4
5
6
7
8
from scrapy.dupefilters import BaseDupeFilter
# 自定义去重规则,需要继承BaseDupeFilter
class MyDupeFilter(BaseDupeFilter):
# 重写request_seen方法
def request_seen(self, request):
# 可以使用布隆过滤(数据量大的情况,有概率不准确),或者是哈希(准确,占用内存多)
# 思路:维护一个布隆过滤器,请求来的时候,判断url是否已经存在,如果存在返回True,
# 如果不存在就将这个url做处理,然后添加进去

13 分布式爬虫

假如有三台机器运行scrapy项目,它们分别维护三个去重队列,还有三个任务队列,在实现分布式时,可能会出现数据重复的情况。

解决办法是让他们都从一个公共的共享队列取东西,比如专门一个主机redis:

1
2
3
# 维护一个共享的主机,运行redis,只有一个队列。
# 重写scheduler,使其无论去重还是任务都去共享队列
# 利用redis的集合类型,重写scheduler内的去重规则

使用scrapy-redis模块做分布式爬虫,github地址

安装:

1
pip install scrapy-redis

要使用该模块,需要做一下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# 1.以前继承Spider,现在继承RedisSpider
from scrapy_redis.spiders import RedisSpider
class XiaosoSpider(RedisSpider): # 改为继承RedisSpider
# 这里不能写start_urls = ['https:/www.baidu.com/'],因为要统一调度
# 而是需要写一个redis_key,共用一个起始地址
redis_key = 'myspider:start_urls'
# 这个就是redis中的key值,我们后续通过这个key去获取请求的url地址







# 2.在settings中配置
# Enables scheduling storing requests queue in redis.
SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 使用scrapy_redis的调度器

# Ensure all spiders share same duplicates filter through redis.
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"# 使用scrapy_redis的去重

# Enables stats shared based on Redis
STATS_CLASS = "scrapy_redis.stats.RedisStatsCollector"

# Default requests serializer is pickle, but it can be changed to any module
# with loads and dumps functions. Note that pickle is not compatible between
# python versions.
# Caveat: In python 3.x, the serializer must return strings keys and support
# bytes as values. Because of this reason the json or msgpack module will not
# work by default. In python 2.x there is no such issue and you can use
# 'json' or 'msgpack' as serializers.
#SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat"

# Don't cleanup redis queues, allows to pause/resume crawls.
#SCHEDULER_PERSIST = True

# Schedule requests using a priority queue. (default)
#SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'

# Alternative queues.
#SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'
#SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue'

# Max idle time to prevent the spider from being closed when distributed crawling.
# This only works if queue class is SpiderQueue or SpiderStack,
# and may also block the same time when your spider start at the first time (because the queue is empty).
#SCHEDULER_IDLE_BEFORE_CLOSE = 10

# Maximum idle time before close spider.
# When the number of idle seconds is greater than MAX_IDLE_TIME_BEFORE_CLOSE, the crawler will close.
# If 0, the crawler will DontClose forever to wait for the next request.
# If negative number, the crawler will immediately close when the queue is empty, just like Scrapy.
#MAX_IDLE_TIME_BEFORE_CLOSE = 0

# Store scraped item in redis for post-processing.
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 300
# 管道配置成scrapy_redis的,所有的数据会在redis存放一份,可以不配置
# 不配置也行,用自己的,存放在本地(或者mysql)
}

# The item pipeline serializes and stores the items in this redis key.
#REDIS_ITEMS_KEY = '%(spider)s:items'

# The items serializer is by default ScrapyJSONEncoder. You can use any
# importable path to a callable object.
#REDIS_ITEMS_SERIALIZER = 'json.dumps'

# Specify the host and port to use when connecting to Redis (optional).
# 指定连接到Redis时使用的主机和端口
REDIS_HOST = 'localhost'
REDIS_PORT = 6379

# Specify the full Redis URL for connecting (optional).
# If set, this takes precedence over the REDIS_HOST and REDIS_PORT settings.
# 完整的redis连接地址,如果配置了,会优先使用
#REDIS_URL = 'redis://user:pass@hostname:9001'

# Custom redis client parameters (i.e.: socket timeout, etc.)
#REDIS_PARAMS = {}
# Use custom redis client class.
#REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient'

# If True, it uses redis' ``SPOP`` operation. You have to use the ``SADD``
# command to add URLs to the redis queue. This could be useful if you
# want to avoid duplicates in your start urls list and the order of
# processing does not matter.
#REDIS_START_URLS_AS_SET = False

# If True, it uses redis ``zrevrange`` and ``zremrangebyrank`` operation. You have to use the ``zadd``
# command to add URLS and Scores to redis queue. This could be useful if you
# want to use priority and avoid duplicates in your start urls list.
#REDIS_START_URLS_AS_ZSET = False

# Default start urls key for RedisSpider and RedisCrawlSpider.
#REDIS_START_URLS_KEY = '%(name)s:start_urls'

# Use other encoding than utf-8 for redis.
# 如果要让redis使用除utf-8其他的编码,配置这个参数
#REDIS_ENCODING = 'latin1'

我们配置了一个myspider:start_urls,所以:

1
2
# 现在要让爬虫运行起来,需要去redis中以myspider:start_urls为key,插入一个起始url地址
# 在redis-cli中执行命令:lpush myspider:start_urls https://www.www.baidu.com/