Python 爬虫从入门到入狱
前戏
什么是网络爬虫
- 通俗理解:爬虫是一个模拟人类请求网站行为的程序。可以自动请求网页、并数据抓取下来,然后使用一定的规则提取有价值的数据。
- 专业介绍:百度百科。
安装第三方库
- pip install requests
- pip install beautifulsoup4
- pip install pyquery
- pip install lxml
- pip install selenium
第一章 urllib
1、urllib 的简单使用
# 导入urllib库(该库不需要安装)
import urllib.request
# 请求百度,并接收响应
response = urllib.request.urlopen("http://www.baidu.com/")
# 打印页面
print(response.read().decode('utf-8'))
2、urllib 用法讲解
# urllib 用法讲解
# urlopen : urllib.request.urlopen('网址','数据','超时设置')
import urllib.request
import urllib.parse
import urllib.error
"""
A:
response = urllib.request.urlopen('http://www.baidu.com/')
print(response.read().decode('utf-8'))
B:
data = urllib.parse.urlencode({'word': 'hello'}).encode('utf-8')
response = urllib.request.urlopen("http://httpbin.org/post", data = data)
print(response.read())
C:
response = urllib.request.urlopen("http://httpbin.org/get",timeout=1)
print(response.read())
"""
try:
response = urllib.request.urlopen("http://httpbin.org/get", timeout=0.1)
except urllib.error.URLError as e:
if isinstance(e.reason.socket.timeout):
print(response.read())
3、urllib 响应
# urllib 响应
import urllib.request
response = urllib.request.urlopen("http://www.baidu.com/")
# 打印响应类型
print(type(response))
# 打印状态码
print(response.status)
# 打印响应头
print(response.getheaders())
4、urllib request
# Request 详解
import urllib.request
from urllib import parse
"""
A:
request = urllib.request.Request('http://www.baidu.com')
response = urllib.request.urlopen(request)
print(response.read().decode('utf-8'))
B:
url = "http://httpbin.org/post"
# 指定请求头
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
"Host": "api.github.com"
}
# 请求数据
dict = {
"name":"Germey"
}
data = bytes(parse.urlencode(dict),encoding='utf-8')
request = urllib.request.Request(url=url,data=data,headers=headers,method='POST')
response = urllib.request.urlopen(request)
print(response.read().decode('utf-8'))
"""
url = "http://httpbin.org/post"
# 请求数据
dict = {
"name":"Germey"
}
data = bytes(parse.urlencode(dict),encoding='utf-8')
request = urllib.request.Request(url=url,data=data,method='POST')
request.add_header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36")
response = urllib.request.urlopen(request)
print(response.read().decode('utf-8'))
5、urllib 代理
# handler(代理)
import urllib.request
proxy_header = urllib.request.ProxyHandler({
"http":"http://xxx.xxx.xxx.xxx:xxxx",
"https":"https://xxx.xxx.xxx.xxx:xxxx"
})
opener = urllib.request.build_opener(proxy_header)
response = opener.open('http://www.baidu.com')
print(response.read().decode('utf-8'))
6、urllib cookie
# cookie
import http.cookiejar
import urllib.request
"""
A: http.cookiejar 简单使用
cookie = http.cookiejar.CookieJar()
handir = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handir)
response = opener.open('http://www.baidu.com')
print(response.read().decode('utf-8'))
B:MozillaCookieJar 将网站的cookie存储在本地文件中
filename = "utils/cookie.txt"
cookie = http.cookiejar.MozillaCookieJar(filename)
handir = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handir)
response = opener.open('http://www.baidu.com')
cookie.save(ignore_discard=True,ignore_expires=True)
C: LWPCookieJar 将网站的cookie存储在本地文件中
filename = "utils/cookie01.txt"
cookie = http.cookiejar.LWPCookieJar(filename)
handir = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handir)
response = opener.open('http://www.baidu.com')
cookie.save(ignore_discard=True,ignore_expires=True)
D: 使用文件中的cookie
"""
cookie = http.cookiejar.LWPCookieJar()
cookie.load('utils/cookie01.txt',ignore_discard=True,ignore_expires=True)
handir = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handir)
response = opener.open('http://www.baidu.com')
print(response.read().decode('utf-8'))
7、urllib 异常处理
# 异常处理
import urllib.request
from urllib import error
"""
A: urllib error 简单使用
try:
response = urllib.request.urlopen('http://www.baidu.com')
except error.URLError as e:
print(e.reason)
B:
try:
response = urllib.request.urlopen('http://www.baidu.com/')
print(response.read().decode('utf-8'))
except error.URLError as e:
print(e.reason)
else:
print("*************")
C: timeout
try:
response = urllib.request.urlopen('http://www.baidu.com',timeout=0.01)
except error.URLError as e:
print(e.reason)
"""
# 一个不存在的连接
try:
response = urllib.request.urlopen("http://www.abcdhaha2.com/")
html = response.read().decode('utf-8')
print(html)
except error.URLError as e:
print(e.reason)
8、urllib URL解析
from urllib.parse import urlparse
from urllib.parse import urlunparse
from urllib.parse import urljoin
from urllib.parse import urlencode
# 语法:urlparse("网址",scheme='http|https', allow_fragments=True)
# A
resuit = urlparse('https://www.baidu.com/index.html;user?id=5#comment')
print(type(resuit))
print(resuit)
# B
resuit = urlparse('www.baidu.com/index.html;user?id=5#comment', scheme="https")
print(resuit)
# C
resuit = urlparse('https://www.baidu.com/index.html;user?id=5#comment', allow_fragments=True)
print(resuit)
# D
resuit = urlparse('https://www.baidu.com/index.html;user?id=5#comment', allow_fragments=False)
print(resuit)
# E
resuit = urlparse('https://www.baidu.com/index.html#comment', allow_fragments=False)
print(resuit)
# F (urlunparse)
data = ["http", "www.baidu.com", "index.html", "user", "a=6", "comment"]
print(urlunparse(data))
# G (urljoin)
# 语法 : urljoin("网址","要添加的后缀")
print(urljoin("https://www.cnblogs.com/xingxingnbsp/p/xxxxxxxxx.html", "12129466.html"))
# H (urlencode)
params = {
'name': 'hello_urllib',
'age': 18
}
base_url = 'http://www.baidu.com?'
url = base_url + urlencode(params)
print(url)
第二章 requests
1、requests 基本使用
import requests
response = requests.get("http://www.baidu.com")
print(type(response)) # 打印响应类型
print(response.status_code) # 打印状态码
print(type(response.text)) # 打印响应内容类型
print(response.text) # 打印响应内容
print(response.cookies) # 打印响应cookie
2、requests 请求方式
requests.get('网址')
requests.post('网址')
requests.put('网址')
requests.patch('网址')
requests.delete('网址')
requests.head('网址')
requests.options('网址')
3、requests 基本get请求
import requests
"""
A:
response = requests.get('http://www.baidu.com')
print(response.text)
B:
response = requests.get('http://httpbin.org/get?name=hello&age=22')
print(response.text)
"""
data = {
"name":"hello",
"age":22
}
response = requests.get('http://httpbin.org/get',params=data)
print(response.text)
4、requests 解析json
import requests
response = requests.get('https://api.jinse.com/v6/www/information/list?catelogue_key=news&limit=23&information_id=18762945&flag=down&version=9.9.9&_source=www')
print(type(response))
print(response.json())
print(type(response.json()))
5、requests 获取二进制数据
import requests
"""
A:
response = requests.get('https://images.pexels.com/photos/3393793/pexels-photo-3393793.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500')
print(type(response.text))
print(type(response.content))
print(response.text)
print(response.content)
"""
response = requests.get('https://images.pexels.com/photos/3393793/pexels-photo-3393793.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500')
with open('images/image.png','wb') as f:
f.write(response.content)
f.close()
6、requests 添加headers
import requests
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36"
}
response = requests.get("http://www.baidu.com",headers=headers)
print(response.text)
7、requests 基本的post请求
import requests
"""
A:
data = {
"name":"hello",
"age":22
}
response = requests.post("http://httpbin.org/post",data=data)
print(response.text)
"""
data = {
"name":"hello",
"age":22
}
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36"
}
response = requests.post("http://httpbin.org/post",data=data,headers=headers)
print(response.text)
8、requests 响应
import requests
response = requests.get('http://www.baidu.com')
print(type(response.status_code),response.status_code) # 打印响应 状态码类型 和 状态码
print(type(response.headers),response.headers) # 打印响应 头类型 和 响应头
print(type(response.cookies),response.cookies) # 打印响应 cookies类型 和 cookies
print(type(response.url),response.url) # 打印响应 URL类型 和 URL
print(type(response.history),response.history) # 打印历史记录
9、requests 状态码判断
import requests
"""
A:
response = requests.get('http://www.baidu.com')
# 这里使用了python三元表达式
exit() if not response.status_code == requests.codes.ok else print('request successfully')
B:
response = requests.get('http://www.baidu.com')
# 这里使用了python三元表达式
exit() if not response.status_code == 200 else print('request successfully')
"""
response = requests.get('http://www.baidu.com')
if not response.status_code == 200:
exit()
else:
print('request successfully')
# 以上三种方式表达的意思是一样的
10、requests 高级操作
import requests
# A: 上传文件 ----------------------------------------------------------------
files = {
"files":open('images/image.png','rb')
}
response = requests.post('http://www.baidu.com',files=files)
print(response.text)
# B:获取cookie -------------------------------------------------------------
response = requests.get('http://www.baidu.com')
print(response.cookies)
for key,value in response.cookies.items():
print(key + "=" + value)
# C: 会话维持 --------------------------------------------------------------
requests.get('http://httpbin.org/cookie/set/number/123456789')
response = requests.get('http://httpbin.org/cookkie')
print(response.text)
s = requests.session()
s.get('http://httpbin.org/cookie/set/number/123456789')
response = s.get('http://httpbin.org/cookkie')
print(response.text)
# D: 代理设置 --------------------------------------------------------------
# 方式一:
proxies = {
'http':'http://ip:port',
'https':'https://ip:port'
}
response = requests.get('http://www.baidu.com',proxies=proxies)
print(response.status_code)
# 方式二:
proxies = {
'http':'http://user:password@ip:port/',
'https':'https://user:password@ip:port/'
}
response = requests.get('http://www.baidu.com',proxies=proxies)
print(response.status_code)
# 方式三:
proxies = {
'http':'socks5://ip:port',
'https':'socks5://ip:port'
}
response = requests.get('http://www.baidu.com',proxies=proxies)
print(response.status_code)
# E: 证书认证 ----------------------------------------------------------------
response = requests.get('http://www.12306.cn')
print(response.status_code)
response = requests.get('http://www.12306.cn',verify=False)
print(response.status_code)
# 注意这里的路径 'path/server.crt','path/key' 该成自己的
response = requests.get('http://www.12306.cn',cert=('path/server.crt','path/key'))
print(response.status_code)
# F:超时设置 ----------------------------------------------------------------
from requests.exceptions import ReadTimeout
try:
response = requests.get('http://www.taobao.com', timeout=0.1)
print(response.status_code)
except ReadTimeout:
print("Timeout")
# G: 认证管理 ----------------------------------------------------------------
from requests.auth import HTTPBasicAuth
response = requests.get('http://www.taobao.com', auth=HTTPBasicAuth('user','123'))
print(response.status_code)
response = requests.get('http://www.taobao.com', auth=('user','123'))
print(response.status_code)
# H: 异常处理 ----------------------------------------------------------------
from requests.exceptions import ReadTimeout,ConnectionError,HTTPError,RequestException
try:
response = requests.get('http://www.taobao.com', timeout=0.1)
print(response.status_code)
except ReadTimeout:
print("Timeout")
except HTTPError:
print("HTTPError")
except ConnectionError:
print("ConnectionError")
except RequestException:
print("Error")
第三章 aiohttp
https://blog.csdn.net/weixin_45459224/article/details/105967518
第四章 BeautifulSoup
1、BeautifulSoup 基本用法
from bs4 import BeautifulSoup
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div>
<h2>这是一个列表</h2>
<ul>
<li>选项1</li>
<li>选项2</li>
<li>选项3</li>
<li>选项4</li>
<li>选项5</li>
<li>选项6</li>
<li>选项7</li>
<li>选项8</li>
<li>选项9</li>
</ul>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html,'lxml')
print(soup.prettify())
print(soup.title.string)
2、BeautifulSoup 标签选择器(只拿一次)
from bs4 import BeautifulSoup
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div>
<h2>这是一个列表</h2>
<ul>
<li>选项1</li>
<li>选项2</li>
<li>选项3</li>
<li>选项4</li>
<li>选项5</li>
<li>选项6</li>
<li>选项7</li>
<li>选项8</li>
<li>选项9</li>
</ul>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html,'lxml')
print(soup.title)
print(type(soup.title))
print(soup.head)
print(soup.li)
3、BeautifulSoup 获取标签名称
from bs4 import BeautifulSoup
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
</body>
</html>
"""
soup = BeautifulSoup(html,'lxml')
print(soup.title.name)
4、BeautifulSoup 获取标签属性
from bs4 import BeautifulSoup
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<p class="font-p"></p>
<a href="http://www.baidu.com">百度一下 你就知道</a>
</body>
</html>
"""
soup = BeautifulSoup(html,'lxml')
print(soup.p.attrs)
print(soup.p.attrs["class"])
print(soup.a.attrs["href"])
5、BeautifulSoup 获取内容
from bs4 import BeautifulSoup
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
div
<a href="http://www.baidu.com">百度一下 你就知道</a>
</body>
</html>
"""
soup = BeautifulSoup(html,'lxml')
print(soup.p.string)
print(soup.a.string)
6、BeautifulSoup 嵌套选择
from bs4 import BeautifulSoup
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div>
<h2>这是一个列表</h2>
<ul>
<li>选项1</li>
</ul>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html,'lxml')
print(soup.ul.li.string)
7、BeautifulSoup 子节点和孙节点
# 子节点和孙节点
from bs4 import BeautifulSoup
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div>
<h2>这是一个列表</h2>
<ul><li>选项1</li><li>选项2</li><li><a href="http://www.baidu.com">百度一下 你就知道</a></li></ul>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html, 'lxml')
print(soup.ul.contents) # 选择所有子节点 返回值为列表类型
print(soup.ul.childern) # 选择单个子节点
print(soup.ul.descendants) # 获取所有子孙节点
for i,child in enumerate(soup.ul.descendants):
print(i,child)
8、BeautifulSoup 父节点和祖先节点
from bs4 import BeautifulSoup
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<ol>
<li><a href="http://www.baidu.com">百度一下 你就知道</a></li>
</ol>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html,'lxml')
print(soup.a.parent) # 选择父节点
print(type(soup.a.parents)) # 选择所有父节点
print(list(enumerate(soup.a.parents)))
9、BeautifulSoup 兄弟节点
from bs4 import BeautifulSoup
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div>
<h1>我是一个大大的H1</h1>
<h2>我是一个大大的H2</h2>
<p>我是一个简单的p标签</p>
<h3>我是一个大大的H3</h3>
<h4>我是一个大大的H4</h4>
</div>
</body>
</html>
"""
html = html.replace('\n','').replace(' ','') # 去掉html代码的 "\n" 和 空格
soup = BeautifulSoup(html, 'lxml')
print(list(enumerate(soup.p.next_siblings))) # 获取当前加点下所有的兄弟节点
print(list(enumerate(soup.p.previous_siblings))) # 获取当前加点上所有的兄弟节点
10、BeautifulSoup 标准选择器(重点)
from bs4 import BeautifulSoup
# 标准选择器(重点 建议反复观看)
# 语法:find_all(name,attrs,recursive,text,**kwargs)
"""
find 返回符合条件的单个元素 find_all 返回所有符合条件的所有元素
1. find_parent() # 返回直接父节点
2. find_parents() # 获取所有祖先节点
3. find_next_sibling() # 返回当前节点后边一个兄弟节点
4. find_next_siblings() # 返回当前节点后边所有兄弟节点
5. find_all_next() # 返回当前节点后所有符合条件的节点
6. find_next() # 返回当前节点后第一个符合条件的节点
7. find_all_previous() # 返回当前节点后所有符合条件的节点
8. find_previous() # 返回当前节点后第一个符合条件的节点
"""
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div id="divid">
<h2>这是一个列表</h2>
<ul id="list-1">
<li class="zhangsan">选项1</li>
<li class="zhangsan">选项2</li>
<li class="zhangsan">选项3</li>
</ul>
<ul id="list-2">
<li class="lisi">选项1</li>
<li class="lisi">选项2</li>
<li class="lisi">选项3</li>
</ul>
</div>
</body>
</html>
"""
# A:name --------------------------------------------------------------
soup = BeautifulSoup(html, 'lxml')
print(soup.find_all('ul')) # 获取所有ul标签 返回列表类型
print(type(soup.find_all('ul')[0])) # 获取类型
for ul in soup.find_all('ul'):
print(ul.find_all('li'))
# B:attrs -------------------------------------------------------------
# 方式一:
soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(attrs={"id":"list-1"})) # 获取 id 为 list-1 的所有元素
print(soup.find_all(attrs={"class":"lisi"})) # 获取 class 为 lisi 的所有元素
# 方式二:
print(soup.find_all(id = "list-1")) # 获取 id 为 list-1 的所有元素
print(soup.find_all(class_ = "lisi")) # 获取 class 为 lisi 的所有元素
# 以上两种方式执行结果是一样的
# C:text --------------------------------------------------------------
soup = BeautifulSoup(html, 'lxml')
print(soup.find_all(text = "选项1"))
# D:css选择器(***) -----------------------------------------------------
# 1:
soup = BeautifulSoup(html, 'lxml')
print(soup.select('#list-2')) # ID 选择器
print(soup.select('.zhangsan')) # class 选择器
print(soup.select('ul li')) # 标签选择器
print(soup.select('#divid h2')) # ID 和 标签 共同使用
# 2:
soup = BeautifulSoup(html, 'lxml')
for ul in soup.select('ul'):
print(ul.select('li'))
# 3:属性选择器
soup = BeautifulSoup(html, 'lxml')
for ul in soup.select('ul'):
print(ul.get('id'))
print(ul['id'])
# 4:获取内容
soup = BeautifulSoup(html, 'lxml')
for li in soup.select('li'):
print(li.get_text())
第五章 pyquery
1、pyquery 初始化
# 初始化
from pyquery import PyQuery
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div id="divid">
<h2>这是一个列表</h2>
<ul id="list-1">
<li class="zhangsan">选项1</li>
<li class="zhangsan">选项2</li>
<li class="zhangsan">选项3</li>
</ul>
<ul id="list-2">
<li class="lisi">选项1</li>
<li class="lisi">选项2</li>
<li class="lisi">选项3</li>
</ul>
</div>
</body>
</html>
"""
# A: 字符串初始化 -------------------------------------------------------------------------------------------------------
doc = PyQuery(html)
print(doc('li'))
# B: URL初始化 ----------------------------------------------------------------------------------------------------------
doc = PyQuery(url="http://www.baidu.com")
print(doc('head'))
# C: 文件初始化(在同级目录下创建index.html 代码和上边的一样) ---------------------------------------------------------------
# 这种方法会报错 :UnicodeDecodeError: 'gbk' codec can't decode byte 0x80 in position 187: illegal multibyte sequence
# 解决方法去掉html文件中的中文字符,这种解决方式不推荐(有待研究)
# doc = PyQuery(filename='index.html')
# print(doc('li'))
# 可以改成这种方法(但是,总感觉有问题)
with open("index.html","r",encoding="utf-8")as f:
doc = f.read()
result = PyQuery(doc)
print(result('li'))
2、pyquery 基本CSS选择器
# 基本CSS选择器
from pyquery import PyQuery
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div id="divid">
<h2>这是一个列表</h2>
<ul id="list-1">
<li class="zhangsan">选项1</li>
<li class="zhangsan">选项2</li>
<li class="zhangsan">选项3</li>
</ul>
<ul id="list-2">
<li class="lisi">选项1</li>
<li class="lisi">选项2</li>
<li class="lisi">选项3</li>
</ul>
</div>
</body>
</html>
"""
doc = PyQuery(html)
print(doc('#divid #list-1 li'))
3、pyquery 查找元素-子元素
# 子元素
from pyquery import PyQuery
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div id="divid">
<h2>这是一个列表</h2>
<ul id="list-1">
<li class="zhangsan">选项1</li>
<li class="zhangsan">选项2</li>
<li class="zhangsan">选项3</li>
</ul>
<ul id="list-2">
<li class="lisi">选项1</li>
<li class="lisi">选项2</li>
<li class="lisi">选项3</li>
</ul>
</div>
</body>
</html>
"""
doc = PyQuery(html)
items = doc('#list-1')
print(type(items))
print(items)
li_list = items.find('li')
print(type(li_list))
print(li_list)
4、pyquery 查找元素-父元素
# 父元素
from pyquery import PyQuery
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div id="divid">
<h2>这是一个列表</h2>
<ul id="list-1">
<li class="zhangsan">选项1</li>
<li class="zhangsan">选项2</li>
<li class="zhangsan">选项3</li>
</ul>
<ul id="list-2">
<li class="lisi">选项1</li>
<li class="lisi">选项2</li>
<li class="lisi">选项3</li>
</ul>
</div>
</body>
</html>
"""
doc = PyQuery(html)
items = doc('#list-1')
container = items.parent()
print(type(container))
print(container)
parents = items.parents()
print(type(parents))
print(parents)
5、pyquery 查找元素-兄弟元素
# 兄弟元素
from pyquery import PyQuery
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div id="divid">
<h2>这是一个列表</h2>
<ul id="list-1">
<li class="zhangsan">选项1</li>
<li class="zhangsan">选项2</li>
<li class="zhangsan">选项3</li>
</ul>
<ul id="list-2">
<li class="lisi">选项1</li>
<li class="lisi">选项2</li>
<li class="lisi">选项3</li>
</ul>
</div>
</body>
</html>
"""
doc = PyQuery(html)
lis = doc('#list-1 .zhangsan')
print(lis.siblings())
print(lis.siblings('.zhangsan'))
6、pyquery 查找元素-遍历
# 遍历
from pyquery import PyQuery
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div id="divid">
<h2>这是一个列表</h2>
<ul id="list-1">
<li class="zhangsan">选项1</li>
<li class="zhangsan">选项2</li>
<li class="zhangsan">选项3</li>
</ul>
<ul id="list-2">
<li class="lisi">选项1</li>
<li class="lisi">选项2</li>
<li class="lisi">选项3</li>
</ul>
</div>
</body>
</html>
"""
doc = PyQuery(html)
lis = doc('#list-2 .lisi')
print(lis)
li_list = doc('.lisi').items()
print(type(li_list))
for li in li_list:
print(li)
7、pyquery 查找元素-获取标签属性
# 获取信息(获取属性)
from pyquery import PyQuery
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div id="divid">
<a href="http://www.baidu.com">百度一下 你就知道</a>
</div>
</body>
</html>
"""
doc = PyQuery(html)
a = doc('#divid a')
print(a)
print(a.attr('href'))
print(a.attr.href)
8、pyquery 查找元素-获取文本
# 获取文本
from pyquery import PyQuery
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div id="divid">
<a href="http://www.baidu.com">百度一下 你就知道</a>
</div>
</body>
</html>
"""
doc = PyQuery(html)
a = doc('#divid a')
print(a)
print(a.text())
9、pyquery 查找元素-获取html
# 获取html
from pyquery import PyQuery
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<div id="divid">
<a href="http://www.baidu.com">百度一下 你就知道</a>
</div>
</body>
</html>
"""
doc = PyQuery(html)
div = doc('#divid')
print(div)
print(div.html())
10、pyquery DOM操作
# DOM 操作
from pyquery import PyQuery
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup 学习</title>
</head>
<body>
<h1>BeautifulSoup</h1>
div id="divid">
<h2>这是一个列表</h2>
<ul id="list-1">
<li class="zhangsan">选项1</li>
<li class="zhangsan">选项2</li>
<li class="zhangsan">选项3</li>
</ul>
<ul id="list-2">
<li class="lisi">选项1</li>
<li class="lisi">选项1</li>
<li class="lisi">选项1</li>
</ul>
</div>
</body>
</html>
"""
# 1. addClass,removeClass ----------------------------------------------------------------------------------------------
doc = PyQuery(html)
li = doc('.lisi')
print(li)
li.remove_class('lisi')
print(li)
li.add_class('zhangsan')
print(li)
# 2. attr,css ----------------------------------------------------------------------------------------------------------
doc = PyQuery(html)
li = doc('.zhangsan')
print(li)
li.attr('name','link')
print(li)
li.css('font-size','40px')
print(li)
# 3. remove ------------------------------------------------------------------------------------------------------------
doc = PyQuery(html)
div = doc('#divid')
print(div.text())
div = doc.find('h2').remove()
print(div.text())
# 4. 伪类选择器 ---------------------------------------------------------------------------------------------------------
doc = PyQuery(html)
li = doc('.zhangsan:first-child') # 获取列表的第一个选项
print(li)
li = doc('.zhangsan:last-child') # 获取列表的最后一个选项
print(li)
li = doc('.zhangsan:nth-child(2)') # 获取列表的第二个选项
print(li)
li = doc('.zhangsan:gt(0)') # 获取索引大于0的所有选项
print(li)
li = doc('.zhangsan:nth-child(1n)') # 获取第一个之后的所有选项(包括第一个选项)
print(li)
li = doc('.zhangsan:contains(选项3)') # 过去内容为"选项3"的选项
print(li)
第六章 selenium
1、selenium 基本使用
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
"""
项目目标:实现百度搜索
1. 创建浏览器对象 请求百度
2. 元素定位输入框
3. 输入搜索内容
4. 点击回车
"""
# 创建浏览器对象(我用的是谷歌浏览器)
browser = webdriver.Chrome()
try:
# 请求百度
browser.get("http://www.baidu.com")
# 定位输入框
input = browser.find_element_by_id('kw')
# 输入搜索内容
input.send_keys("selenium")
# 点击回车
input.send_keys(Keys.ENTER)
# 打印当前的url地址
print(browser.current_url)
# 打印cookies
print(browser.get_cookies())
# 打印页面
print(browser.page_source)
except Exception as e:
print(e,"=============================")
finally:
browser.close()
"""
有可能会遇到的错误
1. selenium.common.exceptions.WebDriverException: Message: 'chromedriver' executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home
这是由于程序找不到 chromedriver 驱动
解决:
下载 chromedriver ( http://chromedriver.storage.googleapis.com/index.html )
注意版本:版本对照表 ( https://blog.csdn.net/BinGISer/article/details/88559532 )
2. selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 78
这是由于 ChromeDriver 和 Chrome 版本不对应
解决:
删除之前下载的 chromedriver
重新下载 chromedriver ( http://chromedriver.storage.googleapis.com/index.html )
注意版本:版本对照表 ( https://blog.csdn.net/BinGISer/article/details/88559532 )
大功告成
"""
2、selenium 声明浏览器对象
# selenium 声明浏览器
from selenium import webdriver
browser = webdriver.Chrome() # 谷歌浏览器
browser = webdriver.Firefox() # 火狐浏览器
browser = webdriver.Edge() # 微软浏览器
browser = webdriver.PhantomJS() # 无界面浏览器
browser = webdriver.Safari() # Safari浏览器
3、selenium 访问页面
import time
from selenium import webdriver
# 声明浏览器对象
browser = webdriver.Chrome()
# 访问淘宝
browser.get('https://www.taobao.com')
# 将浏览器最大化显示
browser.maximize_window()
# 停止5秒
time.sleep(5)
# 打印响应页面
print(browser.page_source)
# 关闭浏览器
browser.close()
4、selenium 查找元素-单个元素
from selenium import webdriver
# 声明浏览器对象
browser = webdriver.Chrome()
# 访问淘宝
browser.get('https://www.taobao.com')
# 将浏览器最大化显示
browser.maximize_window()
# 定位淘宝搜索框(三种方式都可以)
input_id = browser.find_element_by_id('q')
input_selector = browser.find_element_by_css_selector('#q')
input_xpath = browser.find_element_by_xpath('//*[@id="q"]')
print(input_id)
print(input_selector)
print(input_xpath)
# 关闭浏览器
browser.close()
"""
查找单个元素常用方法:
browser.find_element_by_xpath()
browser.find_element_by_name()
browser.find_element_by_link_text()
browser.find_element_by_partial_link_text()
browser.find_element_by_tag_name()
browser.find_element_by_class_name()
browser.find_element_by_css_selector()
"""
5、selenium 查找元素-多个元素
# 查找元素(单个元素)
from selenium import webdriver
# 声明浏览器对象
browser = webdriver.Chrome()
# 访问淘宝
browser.get('https://www.taobao.com')
# 将浏览器最大化显示
browser.maximize_window()
# 查找 class="J_Cat a-all" 的所有元素
li_list = browser.find_elements_by_css_selector('.J_Cat')
print(li_list)
# 关闭浏览器
browser.close()
"""
查找多个元素常用方法:
browser.find_elements_by_xpath()
browser.find_elements_by_name()
browser.find_elements_by_link_text()
browser.find_elements_by_partial_link_text()
browser.find_elements_by_tag_name()
browser.find_elements_by_class_name()
browser.find_elements_by_css_selector()
"""
6、selenium 元素交互
(1) 鼠标事件
(2) 键盘事件
import time
from selenium import webdriver
# 声明浏览器对象
browser = webdriver.Chrome()
# 请求淘宝
browser.get("https://www.taobao.com")
# 窗口最大化
browser.maximize_window()
# 定位搜索框
input = browser.find_element_by_id('q')
# 输入"内存条"
input.send_keys("内存条")
time.sleep(3)
# 清除搜索框内容
input.clear()
time.sleep(5)
# 输入 "1T硬盘"
input.send_keys("1T硬盘")
# 定位搜索按钮
button = browser.find_element_by_class_name('btn-search')
# 点击搜索按钮
button.click()
time.sleep(10)
# 关闭浏览器
browser.close()
7、selenium 执行javascrapt
# 执行 javascrapt
from selenium import webdriver
browser = webdriver.Chrome()
browser.get("https://www.taobao.com")
# 滚动条拉到最下边
browser.execute_script('window.scrollTo(0,document.body.scrollHeight)')
# 弹窗
browser.execute_script('alert("To Bottom")')
8、selenium 获取元素信息-获取属性
# 获取元素信息(获取属性)
from selenium import webdriver
browser = webdriver.Chrome()
url = "https://www.zhihu.com/"
browser.get(url)
logo = browser.find_element_by_css_selector('.SignFlowHomepage-logo')
print(logo)
print(logo.get_attribute('src'))
browser.close()
9、selenium 获取元素信息-获取文本值
# 获取元素信息(获取文本值)
from selenium import webdriver
browser = webdriver.Chrome()
url = "https://www.zhihu.com/explore"
browser.get(url)
input = browser.find_element_by_id('Popover1-toggle')
input.send_keys('新冠病毒')
print(input.text)
10、selenium 获取元素信息(获取ID,位置,标签名,大小)
# 获取元素信息(获取ID,位置,标签名,大小)
from selenium import webdriver
browser = webdriver.Chrome()
url = "https://www.zhihu.com/explore"
browser.get(url)
input = browser.find_element_by_id('Popover1-toggle')
print(input.id)
print(input.location)
print(input.tag_name)
print(input.size)
browser.close()
11、selenium 获取元素信息-iframe
# 获取元素信息(iframe)
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
browser = webdriver.Chrome()
url = "https://www.runoob.com/try/try.php?filename=tryjquery_hide"
browser.get(url)
browser.switch_to.frame('iframeResult')
button = browser.find_element_by_css_selector('button')
print(button)
try:
logo = browser.find_element_by_class_name('logo')
except NoSuchElementException:
print('NO LOGO')
finally:
browser.switch_to.parent_frame()
logo = browser.find_element_by_class_name('logo')
print(logo)
print(logo.text)
browser.close()
12、selenium 等待
# 等待
"""
显示等待就是有条件的等待
隐式等待就是无条件的等待
隐式等待
当使用了隐式等待执行测试的时候,如果 WebDriver 没有在 DOM 中找到元素,将继续等待,超出设定时间后则抛出找不到元素的异常,
换句话说,当查找元素或元素并没有立即出现的时候,隐式等待将等待一段时间再查找 DOM,默认的时间是 0
显式等待
指定某个条件,然后设置最长等待时间。如果在这个时间还没有找到元素,那么便会抛出异常。
只有该条件触发,才执行后续代码,这个使用更灵活。
主要涉及到selenium.webdriver.support 下的expected_conditions类。
"""
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
browser = webdriver.Chrome()
browser.get('http://www.taobao.com')
browser.maximize_window()
browser.implicitly_wait(10)
wait = WebDriverWait(browser,10)
input = wait.until(EC.presence_of_all_elements_located((By.ID,'q')))
button = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR,'.btn-search')))
print(input)
print(button)
browser.close()
13、selenium 浏览器的前进和后退
# 浏览器的前进和后退
import time
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.baidu.com')
time.sleep(1)
browser.get('https://www.taobao.com')
time.sleep(1)
browser.get('https://www.cnblogs.com/xingxingnbsp/')
time.sleep(1)
browser.back()
time.sleep(2)
browser.forward()
time.sleep(2)
browser.close()
14、selenium Cookies
# cookies
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.zhihu.com/explore')
print(browser.get_cookies())
browser.add_cookie({"name":"name","domain":"www.zhihu.com","value":"germey"})
print(browser.get_cookies())
browser.delete_all_cookies()
print(browser.get_cookies())
browser.close()
15、selenium 选项卡管理(不兼容)
# 选项卡管理
import time
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('https://www.baidu.com')
time.sleep(2)
browser.execute_script('window.open()')
print(browser.window_handles)
browser.switch_to_window(browser.window_handles[1])
browser.get('https://www.taobao.com')
time.sleep(2)
browser.get('https://www.cnblogs.com/xingxingnbsp/')
time.sleep(3)
browser.close()
16、selenium 异常处理
from selenium import webdriver
from selenium.common.exceptions import TimeoutException,NoSuchElementException
browser = webdriver.Chrome()
try:
browser.get('https://www.baidu.com')
except TimeoutException:
print('Time Out')
try:
browser.find_element_by_id('hello')
except NoSuchElementException:
print('No Element')
finally:
browser.close()
第七章 pyppeteer
1、前戏
https://www.zky.name/article/73.html
https://blog.zhangkunzhi.com/2019/05/13/pyppeteer常用方法手册/index.html
(1) 介绍
Puppeteer 是 Google 基于 Node.js 开发的一个工具,有了它我们可以通过 JavaScript 来控制 Chrome 浏览器的一些操作,当然也可以用作网络爬虫上,其 API 极其完善,功能非常强大,Selenium 当然同样可以做到。
而 Pyppeteer 又是什么呢?它实际上是 Puppeteer 的 Python 版本的实现,但它不是 Google 开发的,是一位来自于日本的工程师依据 Puppeteer 的一些功能开发出来的非官方版本。
在 Pyppetter 中,实际上它背后也是有一个类似 Chrome 浏览器的 Chromium 浏览器在执行一些动作进行网页渲染,首先说下 Chrome 浏览器和 Chromium 浏览器的渊源。
- Chromium 是谷歌为了研发 Chrome 而启动的项目,是完全开源的。二者基于相同的源代码构建 Chrome 所有的新功能都会先在Chromium 上实现,待验证稳定后才会移植,因此 Chromium的版本更新频率更高,也会包含很多新的功能,但作为一款独立的浏览器,Chromium的用户群体要小众得多。两款浏览器“同根同源”,它们有着同样的 Logo,但配色不同,Chrome 由蓝绿黄四种颜色组成,而Chromium 由不同深度的蓝色构成。
- Pyppeteer 就是依赖于 Chromium 这个浏览器来运行的。那么有了 Pyppeteer 之后,我们就可以免去那些烦琐的环境配置等问题。如果第一次运行的时候,Chromium 浏览器没有安装,那么程序会帮我们自动安装和配置,就免去了烦琐的环境配置等工作。另外 Pyppeteer 是基于 Python 的新特性 async 实现的,所以它的一些执行也支持异步操作,效率相对于 Selenium 来说也提高了。
(2) 安装
首先就是安装问题了,由于 Pyppeteer 采用了 Python 的 async 机制,所以其运行要求的 Python 版本为 3.5 及以上。
pip install pyppeteer
好了,安装完成之后我们在命令行下测试:
import pyppeteer
如果没有报错,那么就证明安装成功了。
2、基本使用
(1) 示例:打开页面
import asyncio
from pyppeteer import launch
async def main():
start_parm = {
# 由于没有安装 Chromium 这里使用 Chrome 进行代替
"executablePath": r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
# 关闭无头浏览器 默认是无头启动的
"headless": False,
}
browser = await launch(**start_parm)
page = await browser.newPage()
await page.goto('https://www.baidu.com')
await browser.close()
asyncio.run(main())
(2) 示例:打开页面并截屏
import asyncio
from pyppeteer import launch
async def main():
# 浏览器 启动参数
start_parm = {
# 由于没有安装 Chromium 这里使用 Chrome 进行代替
"executablePath": r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
# 关闭无头浏览器 默认是无头启动的
"headless": False,
}
# 创建浏览器对象
browser = await launch(**start_parm)
# 创建一个页面对象, 页面操作在该对象上执行
page = await browser.newPage()
# 页面跳转
await page.goto('https://www.baidu.com')
# 截图保存
await page.screenshot({'path': 'example.png'})
# 关闭浏览器对象
await browser.close()
asyncio.run(main())
launch参数
(3) 启动参数
| 属性 | 数据类型 | 描述 |
|---|---|---|
| executablePath | str | chrome.exe运行的路径 |
| ignorehttpserrrors | bool | 忽略https错误,默认false |
| headless | bool | True 开始无头浏览器 False关闭无头 |
| dumpio | bool | 设置True 解决浏览器多开卡死 |
| args | list | 其他设置 |
args可选属性
| 属性 | 数据类型 | 描述 |
|---|---|---|
| –disable-infobars | - | 关闭自动化提示框 |
| –window-size=1920,1080 | str | 设置浏览器大小吗,1920是宽,1080是宽 |
| –log-level=30 | str | 日志保存等级 |
| –start-maximized | - | 窗口最大化模式 |
| –proxy-server=http://localhost:1080 | str | 设置代理 |
| userDataDir=D:\userData\ | str | 用户文件保存地址 |
第八章 socket
1、socket 发送请求
############################ 方式一 ############################
import socket
# 1. 创建客户端
client = socket.socket()
# 2. 和百度创建连接: 阻塞
client.connect(('www.baidu.com', 80))
# 3. 问百度我要什么
client.sendall(b'GET /s?wd=Python HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
# 4. 等着百度给我的回复
chunk_list = []
while True:
chunk = client.recv(8096)
if not chunk:
break
chunk_list.append(chunk)
body = b''.join(chunk_list)
print(body.decode('utf-8'))
############################ 方式二 ############################
import requests
response = requests.get(url="http://www.baidu.com/s?wd=Python")
print(response.text)
2、像百度发送请求搜索三个关键字
import socket
import requests
# #################### 解决并发:单线程 ####################
# 方式一
key_list = ['alex','db','sb']
for item in key_list:
ret = requests.get('https://www.baidu.com/s?wd=%s' %item)
# 方式二
def get_data(key):
# 方式二
client = socket.socket()
# 百度创建连接: 阻塞
client.connect(('www.baidu.com',80))
# 问百度我要什么?
client.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
# 我等着接收百度给我的回复
chunk_list = []
while True:
chunk = client.recv(8096)
if not chunk:
break
chunk_list.append(chunk)
body = b''.join(chunk_list)
print(body.decode('utf-8'))
key_list = ['alex','db','sb']
for item in key_list:
get_data(item)
# #################### 解决并发:多线程 ####################
import threading
key_list = ['alex','db','sb']
for item in key_list:
t = threading.Thread(target=get_data,args=(item,))
t.start()
# #################### 解决并发:单线程+IO不等待 ####################
# IO请求?
# 数据回来了?
3、前戏
import socket
client = socket.socket()
client.setblocking(False) # 将原来阻塞的位置变成非阻塞(报错)
# 百度创建连接: 阻塞
try:
client.connect(('www.baidu.com',80)) # 执行了但报错了
except BlockingIOError as e:
pass
# 检测到已经连接成功
# 问百度我要什么?
client.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
# 我等着接收百度给我的回复
chunk_list = []
while True:
chunk = client.recv(8096) # 将原来阻塞的位置变成非阻塞(报错)
if not chunk:
break
chunk_list.append(chunk)
body = b''.join(chunk_list)
print(body.decode('utf-8'))
4、单线程的并发
import socket
import select
client1 = socket.socket()
client1.setblocking(False) # 和百度创建连接: 非阻塞
try:
client1.connect(('www.baidu.com', 80))
except BlockingIOError as e:
pass
client2 = socket.socket()
client2.setblocking(False) # 和搜狗创建连接: 非阻塞
try:
client2.connect(('www.sogou.com', 80))
except BlockingIOError as e:
pass
client3 = socket.socket()
client3.setblocking(False) # 和菜鸟教程创建连接: 非阻塞
try:
client3.connect(('www.runoob.com', 80))
except BlockingIOError as e:
pass
socket_list = [client1, client2, client3]
conn_list = [client1, client2, client3]
while True:
rlist, wlist, elist = select.select(socket_list, conn_list, [], 0.005)
# wlist中表示已经连接成功的socket对象
for sk in wlist:
if sk == client1:
sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
elif sk == client2:
sk.sendall(b'GET /web?query=Python HTTP/1.0\r\nhost:www.sogou.com\r\n\r\n')
else:
sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.oldboyedu.com\r\n\r\n')
conn_list.remove(sk)
# rlist检测是否服务端给我返回数据了
for sk in rlist:
chunk_list = []
while True:
try:
chunk = sk.recv(8096)
if not chunk:
break
chunk_list.append(chunk)
except BlockingIOError as e:
break
body = b''.join(chunk_list)
# print(body.decode('utf-8'))
print('------------>', body)
sk.close()
socket_list.remove(sk)
if not socket_list:
break
IO多路复用
IO多路复用作用:检测多个socket是否已经发生变化(是否已经连接成功/是否已经获取数据)(可读/可写)
基于IO多路复用+socket实现并发请求(一个线程100个请求)
IO多路复用
socket非阻塞
基于事件循环实现的异步非阻塞框架:lzl
非阻塞:不等待
异步:执行完某个人物后自动调用我给他的函数。
Python中开源 基于事件循环实现的异步非阻塞框架 Twisted
5、单线程的并发高级版
# by luffycity.com
import socket
import select
class Req(object):
def __init__(self, sk, func):
self.sock = sk
self.func = func
def fileno(self):
return self.sock.fileno()
class Nb(object):
def __init__(self):
self.conn_list = []
self.socket_list = []
def add(self, url, func):
client = socket.socket()
client.setblocking(False) # 非阻塞
try:
client.connect((url, 80))
except BlockingIOError as e:
pass
obj = Req(client, func)
self.conn_list.append(obj)
self.socket_list.append(obj)
def run(self):
while True:
rlist, wlist, elist = select.select(self.socket_list, self.conn_list, [], 0.005)
# wlist中表示已经连接成功的req对象
for sk in wlist:
# 发生变换的req对象
sk.sock.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
self.conn_list.remove(sk)
for sk in rlist:
chunk_list = []
while True:
try:
chunk = sk.sock.recv(8096)
if not chunk:
break
chunk_list.append(chunk)
except BlockingIOError as e:
break
body = b''.join(chunk_list)
# print(body.decode('utf-8'))
sk.func(body)
sk.sock.close()
self.socket_list.remove(sk)
if not self.socket_list:
break
def baidu_repsonse(body):
print('百度下载结果:', body)
def sogou_repsonse(body):
print('搜狗下载结果:', body)
def oldboyedu_repsonse(body):
print('老男孩下载结果:', body)
t1 = Nb()
t1.add('www.baidu.com', baidu_repsonse)
t1.add('www.sogou.com', sogou_repsonse)
t1.add('www.oldboyedu.com', oldboyedu_repsonse)
t1.run()
"""
client1 = socket.socket()
client1.setblocking(False) # 百度创建连接: 非阻塞
try:
client1.connect(('www.baidu.com',80))
except BlockingIOError as e:
pass
client2 = socket.socket()
client2.setblocking(False) # 百度创建连接: 非阻塞
try:
client2.connect(('www.sogou.com',80))
except BlockingIOError as e:
pass
client3 = socket.socket()
client3.setblocking(False) # 百度创建连接: 非阻塞
try:
client3.connect(('www.oldboyedu.com',80))
except BlockingIOError as e:
pass
class Foo(object):
def __init__(self,sk):
self.sk = sk
def fileno(self):
return self.sk.fileno()
# 1. select.select(socket_list,conn_list,[],0.005)
# select监听的 socket_list/conn_list 内部会调用列表中每一个值的fileno方法,获取该返回值并去系统中检测。
#
# 2. 方式一:
# select.select([client1,client2,client3],[client1,client2,client3],[],0.005)
# 3. 方式二:
# select.select([Foo(client1),Foo(client2),(client3)],Foo(client1),Foo(client2),(client3),[],0.005)
socket_list = [Foo(client1),client2,client3] # client1.fileno
conn_list = [client1,client2,client3]
while True:
rlist,wlist,elist = select.select(socket_list,conn_list,[],0.005)
# wlist中表示已经连接成功的socket对象
for sk in wlist:
if sk == client1:
sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
elif sk==client2:
sk.sendall(b'GET /web?query=fdf HTTP/1.0\r\nhost:www.sogou.com\r\n\r\n')
else:
sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.oldboyedu.com\r\n\r\n')
conn_list.remove(sk)
for sk in rlist:
chunk_list = []
while True:
try:
chunk = sk.recv(8096)
if not chunk:
break
chunk_list.append(chunk)
except BlockingIOError as e:
break
body = b''.join(chunk_list)
# print(body.decode('utf-8'))
print('------------>',body)
sk.close()
socket_list.remove(sk)
if not socket_list:
break
"""
6、总结
-
socket默认是否是阻塞的?阻塞体现在哪里?
-
如何让socket编程非阻塞?
-
IO多路复用作用?
检测多个socket是否发生变化。
操作系统检测socket是否发生变化,有三种模式:
select:最多1024个socket;循环去检测。
poll:不限制监听socket个数;循环去检测(水平触发)。
epoll:不限制监听socket个数;回调方式(边缘触发)。
Python模块:
select.select
select.epoll -
提高并发方案:
多进程
多线程
异步非阻塞模块(Twisted) scrapy框架(单线程完成并发) -
什么是异步非阻塞?
非阻塞,不等待。
比如创建socket对某个地址进行connect、获取接收数据recv时默认都会等待(连接成功或接收到数据),才执行后续操作。
如果设置setblocking(False),以上两个过程就不再等待,但是会报BlockingIOError的错误,只要捕获即可。
异步,通知,执行完成之后自动执行回调函数或自动执行某些操作(通知)。
比如做爬虫中向某个地址baidu.com发送请求,当请求执行完成之后自执行回调函数。 -
什么是同步阻塞?
阻塞:等
同步:按照顺序逐步执行
key_list = ['alex','db','sb'] for item in key_list: ret = requests.get('https://www.baidu.com/s?wd=%s' %item) print(ret.text) -
概念
# 之前: # 你写的代码:7000w v = [ [11, 22], # 每个都有一个append方法 [22, 33], # 每个都有一个append方法 [33, 44], # 每个都有一个append方法 ] # 王思聪 for item in v: print(item.append) # 之后: class Foo(object): def __init__(self, data, girl): self.row = data self.girl = girl def append(self, item): self.row.append(item) v = [ Foo([11, 22], '雪梨'), # 每个都有一个append方法 Foo([22, 33], '冰糖'), # 每个都有一个append方法 Foo([33, 44], '糖宝'), # 每个都有一个append方法 ] for item in v: print(item.append) item.girl
第九章 mitmproxy
参考文档:
- 官方网站: https://mitmproxy.org
- 官方文档: https://docs.mitmproxy.org/stable
- 官方总结: https://blog.wolfogre.com/posts/usage-of-mitmproxy
1、安装
(1) 模块安装
# 安装
pip install mitmproxy
# 启动
mitmdump -p 8888
(2) 证书安装
2-1 手机端
- 手机 Wifi 配置代理 192.168.xx.xx 端口 8888
- 手机浏览器访问: mitm.it
- 根据手机系统下载对应的证书
2-2 电脑端
- C:\Users\admin.mitmproxy 路径下 mitmproxy-ca-cert.p12 点击安装
- 电脑网络设置代理127.0.0.1 8888
2、组件
当我们谈论 "mitmproxy" 时 我们通常指这三种工具中的任何一种, 他们只是同一核心代理的不同前端
| 组件 | 说明 |
|---|---|
| mitmproxy | 提供交互式界面(windows系统不可用) |
| mitmdump | 提供了简明的终端输出 |
| mitmweb | 提供基于浏览器的可视化界面 |
mitmproxy:三个组件唯一的区别就是展示的交互界面不同,所以这里主要用 “mitmdump” 来做演示 后面的案例也是使用 “mitmdump” 去演示
mitmproxy: 默认绑定的端口为 127.0.0.1:8080
注意:如果端口被占用,会报错
基本操作的话,就只看 常规代理 方式就可以了
操作模式: https://docs.mitmproxy.org/stable/concepts-modes/
脚本编写: https://docs.mitmproxy.org/stable/addons-scripting/
如何工作: https://docs.mitmproxy.org/stable/concepts-howmitmproxyworks/
(1) mitmproxy
windows 系统不可用 这里不做展示
https://docs.mitmproxy.org/stable/mitmproxytutorial-userinterface/
(2) mitmdump
查看所有命令
mitmdupm --help
查看版本
mitmdupm --version
常用命令
-p 8888 # 指定端口
-s xxx.py # 执行指定脚本
-w outfile # 指定输出文件
-q quiet # 仅匹配脚本过滤后的数据包
"-m post" # 仅匹配 post 请求
带有颜色的print
log,带有输出不同颜色的功能
- info: 白色
- warn: 黄色
- error: 红色
注意这里要使用cmd ,使用powershell 显示出来的颜色效果不完整
(3) mitmweb
启动 mitmweb -p 8888
启动本地代理
3、事件
(1) request
1.1 事件
| 事件 | 说明 |
|---|---|
| request = flow.request | |
| request.url | url |
| request.host | 域名 |
| request.headers | 请求头 |
| request.headers["Usre-Agent"] = "xxxx" | 修改请求头 |
| request.method | 方式:POST、GET、等等 |
| request.scheme | 协议:HTTPS、HTTP |
| request.path | 路径, URL除域名之外的内容 |
| request.query | 返回 MultDictView类型的数据, URL的键值参数 |
| request.query.keys() | 获取所有请求参数的键 |
| request.query.values() | 获取所有请求参数的值 |
| request.query.get('key') | 获取请求参数中的 ”key“ 对应的值 |
| request.query.set_all('key', ["python"]) | 将 ”key“ 参数的值修改为 ”python“ |
1.2 案例
import mitmproxy.http
# 修改请求头
"""
class Demo(object):
def __init__(self):
self.url = "httpbin.org"
def request(self, flow: mitmproxy.http.HTTPFlow):
request = flow.request
if self.url in request.url:
# 修改请求头
request.headers["User-Agent"] = "xxxxxxxxxx"
# 修改URL
flow.request.url = "https://www.baidu.com"
pass
addons = [
Demo()
]
"""
# 3. 修改百度查询参数
class Demo:
def request(self, flow: mitmproxy.http.HTTPFlow):
request = flow.request
if "www.baidu.com" in request.url:
# 获取查询的值
print("正常输入查询参数: ", request.query.get("wd"))
# 获取所有请求参数的键
print(request.query.keys())
# 获取所有请求参数的值
print(request.query.values())
# 修改请求参数
request.query.set_all("wd", ["美女图片"])
print("修改后的查询参数: ", request.query.get("wd"))
addons = [
Demo()
]
(2) response
2.1 事件
| 事件 | 说明 |
|---|---|
| response= flow.response | |
| response.ststus_code | 状态码 |
| response.text | 响应体(文本) |
| response.content | bytes类型 |
| response.headers | 响应头 |
| response.cookies | 响应 Cookies |
| response.set_text() | 修改响应文本 |
| response.get_text() | 响应体(文本) |
| flow.response = flow.response.make(404) | 响应404 |
2.2 案例
import mitmproxy.http
"""
class Demo:
def response(self, flow: mitmproxy.http.HTTPFlow):
response = flow.response
if flow.request.host == "www.baidu.com":
# 重写网页文本
# text = "<h1>兄弟你瘦了<h1>"
# response.set_text(text=text)
# 替换网页文本
# text = response.get_text()
# text = text.replace('谁是世界上最帅的男人', '谁是世界上最帅的男人: 邢兴')
# response.set_text(text=text)
# 异常响应
# flow.response = mitmproxy.http.HTTPResponse.make(401)
flow.response = flow.response.make(404)
addons = [
Demo()
]
"""
class Demo:
def request(self, flow: mitmproxy.http.HTTPFlow):
request = flow.request
if request.query.get("wd") == "python":
flow.response = mitmproxy.http.HTTPResponse.make(
status_code=404,
content="<h1>你妹的 什么都要看, 一天净搜索这些娘希匹玩意!!!<h1>",
headers={"Content-Type": "text/html"}
)
def response(self, flow: mitmproxy.http.HTTPFlow):
request = flow.request
response = flow.response
if request.host == "www.baidu.com":
# 方式一
flow.response = response.make(404)
# 方式二
flow.response = mitmproxy.http.HTTPResponse.make(401)
addons = [
Demo()
]
4、报错解决
网关证书验证错误,解决方法有二:
- 执行 --ssl-insecure
- 下载最新的 cacer.pem (python 安装路径 Lib\site-packages\certifi) 证书
第十章 Scrapy
1、Scrapy 初窥
(1) scrapy 工作流程
来一张图了解一下scrapy工作流程:(这张图是在百度下载的)

(2) scrapy 各部分的功能
| 功能 | 解释 | 说明 |
|---|---|---|
| Scrapy Engine | 引擎 | 负责Spider,Item Pipeline,Downloader,Scheduler 中间的通讯,信号,数据传递等 |
| Scheduler | 调度器 | 负责接收引擎发送过来的 request 请求,并按照一定的方式进行整理队列,入队,当引擎需要时,交还给引擎 |
| Downloader | 下载器 | 负责下载 Scrapy Engine(引擎)发送的所有 request 请求,并将其获取到的 response 交还给 Scrapy Engine(引擎),由 引擎 交给spider 来处理 |
| Spider | 爬虫 | 它负责处理所有response,从中分析提取数据,获取item字段需要的数据, 并将需要跟进的URL提交个 引擎 再次进入 Scheduler(调度器) |
| Item Pipeline | 管道 | 它负责处理Spider中获取到的item,并进行后期处理(详细分析,过滤,存储等)的地方 |
| Downloader Middlewares | 下载中间件 | 你可以当作是一个可以自定义扩展下载功能的组件 |
| Spider Middlewares | Spider中间件 | 可以理解为是一个可以自定义扩展和操作 引擎 和 Spider 之间通信的功能组件 (例如进入Spider 的responses 和从 Spider 出去的 requests) |
(3) scrapy 运行流程
-
引擎: Hi! Spider,你要处理哪一个网站?
-
Spider: 老大要我处理 xxx.xxx.com.
-
引擎: 你把第一个需要处理的URL给我吧。
-
Spider: 给你,第一个URL是 xxx.xxx.com.
-
引擎: Hi! 调度器,我这里有request请求你帮我排序入队一下。
-
调度器: 好的,正在处理你等一下。
-
引擎: Hi! 调度器,你把处理好的request请求给我。
-
调度器: 给你,这是我处理好的request请求。
-
引擎: Hi! 下载器,你你按照老大的 下载中间件 的设置当我下载一下这个request请求。
-
下载器: 好的!给你这是下载好的东西。(如果下载失败:sorry,这个request下载失败了,然后 引擎 告诉 调度器 ,这个request 下载失败了,你记录一下,我们待会再下载)
-
引擎:Hi! Spider,给你,这是下载好的东西,并且已经按照老大的 下载中间件 处理过了,你自己处理一下(注意!这儿responses默认式交给 def parse() 这个函数处理的)
-
Spider: (处理完毕数据之后对于需要跟进的URL),Hi!引擎,我这里有两个结果,这个是我需要跟进的URL,还有这个是我获取到的item数据。
-
引擎: Hi! 管道 我这里有个item你帮我处理一下!调度器!这是需要跟进的URL你帮我吃力一下。然后从第四步开始循环,知道老大去玩需要的全部数据。
-
管道 调度器: 好的,现在就做!
注意!只有当调度器中不存在任何request了,整个程序才会停止(也就是说对弈下载失败的URL,Scrapy也会重新下载)
2、Scrapy 前期准备
(1) 安装scrapy
pip install wheel
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple wheel
pip install scrapy
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple scrapy
(2) 创建项目
# 创建scrapy项目步骤命令
# 创建项目
scrapy startproject 项目名
# 创建爬虫
scrapy genspider 爬虫名 www.jinse.com
# 运行爬虫
scrapy crawl 爬虫名
# 不带日志打印
scrapy crawl 爬虫名 --nolog
(3) 常用命令
语法:
scrapy <command> [options] [args]
常用命令:
bench # 运行快速基准测试
check Check spider contracts
crawl Run a spider
edit Edit spider
fetch Fetch a URL using the Scrapy downloader
genspider Generate new spider using pre-defined templates
list List available spiders
parse Parse URL (using its spider) and print the results
runspider Run a self-contained spider (without creating a project)
settings Get settings values
shell Interactive scraping console
startproject Create new project
version Print Scrapy version
view Open URL in browser, as seen by Scrapy
(4) 项目结构
项目目录

| 文件 | 说明 |
|---|---|
| items.py | 数据的存储容器,在对数据进行有处理的操作时,必须要用,例如写入txt,插入数据库等,没有对数据进行操作时,可以不使用 |
| middlewares.py | 该文件是scrapy的中间件文件,可以设置随机请求头,代理,cookie,会话维持等 |
| pipelines.py | 该文件和主要用来存储数据 |
| settings.py | 该文件是scrapy的设置文件,可以配置数据库连接等 |
| spiders/jinse.py | 该文件就是我们刚才创建的爬虫文件 |
(5) 基本使用
import scrapy
class DemoSpider01Spider(scrapy.Spider):
# 爬虫名
name = 'demo_spider_01'
# 允许的域名-定向爬虫
allowed_domains = ['dig.chouti.com']
# 起始的url
start_urls = ['https://dig.chouti.com/']
# 回调函数
def parse(self, response):
# response 是一个对象
print(response, type(response))
print(response.text)
print(response.encoding)
print(response.body)
# 当前请求是由哪个请求发出:请求中封装(要访问的url, 下次完成之后执行那个函数)
print(response.request)
if __name__ == '__main__':
from scrapy import cmdline
cmdline.execute('scrapy crawl demo_spider_01 --nolog'.split())
(6) 配置
6-1 scrapy配置
# 爬虫名称
BOT_NAME = 'demo_project_01'
# 爬虫应用路径
SPIDER_MODULES = ['demo_project_01.spiders']
NEWSPIDER_MODULE = 'demo_project_01.spiders'
# 客户端 user-agent请求头
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36'
# 禁止爬虫配置
ROBOTSTXT_OBEY = True
# 并发请求数
# CONCURRENT_REQUESTS = 32
# 延迟下载秒数
# DOWNLOAD_DELAY = 3
# 单域名访问并发数,并且延迟下次秒数也应用在每个域名
# CONCURRENT_REQUESTS_PER_DOMAIN = 16
# 单IP访问并发数,如果有值则忽略:CONCURRENT_REQUESTS_PER_DOMAIN,并且延迟下次秒数也应用在每个IP
# CONCURRENT_REQUESTS_PER_IP = 16
# 是否支持cookie,cookiejar进行操作cookie
# COOKIES_ENABLED = False
# COOKIES_DEBUG = True
# Telnet用于查看当前爬虫的信息,操作爬虫等..., 使用telnet ip port ,然后通过命令操作
# TELNETCONSOLE_ENABLED = False
# TELNETCONSOLE_HOST = '127.0.0.1'
# TELNETCONSOLE_PORT = [6023,]
# 默认请求头
# DEFAULT_REQUEST_HEADERS = {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
# }
# 定义pipeline处理请求
# SPIDER_MIDDLEWARES = {
# 'demo_project_01.middlewares.DemoProject01SpiderMiddleware': 543,
# }
# 下载中间件 https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
# DOWNLOADER_MIDDLEWARES = {
# 'demo_project_01.middlewares.DemoProject01DownloaderMiddleware': 543,
# }
# 自定义扩展,基于信号进行调用 https://docs.scrapy.org/en/latest/topics/extensions.html
# EXTENSIONS = {
# 'scrapy.extensions.telnet.TelnetConsole': None,
# }
# 持续化
ITEM_PIPELINES = {
# 'demo_project_01.pipelines.DemoProject01Pipeline': 300,
# 'demo_project_01.pipelines.DBPipeline': 300,
# 'demo_project_01.pipelines.FilePipeline': 301,
}
"""
自动限速算法
from scrapy.contrib.throttle import AutoThrottle
自动限速设置
1. 获取最小延迟 DOWNLOAD_DELAY
2. 获取最大延迟 AUTOTHROTTLE_MAX_DELAY
3. 设置初始下载延迟 AUTOTHROTTLE_START_DELAY
4. 当请求下载完成后,获取其"连接"时间 latency,即:请求连接到接受到响应头之间的时间
5. 用于计算的... AUTOTHROTTLE_TARGET_CONCURRENCY
target_delay = latency / self.target_concurrency
new_delay = (slot.delay + target_delay) / 2.0 # 表示上一次的延迟时间
new_delay = max(target_delay, new_delay)
new_delay = min(max(self.mindelay, new_delay), self.maxdelay)
slot.delay = new_delay
"""
# 开始自动限速 https://docs.scrapy.org/en/latest/topics/autothrottle.html
# AUTOTHROTTLE_ENABLED = True
# 初始下载延迟
# AUTOTHROTTLE_START_DELAY = 5
# 最大下载延迟
# AUTOTHROTTLE_MAX_DELAY = 60
# 平均每秒并发数
# AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# 是否显示
# AUTOTHROTTLE_DEBUG = False
"""
启用缓存
目的用于将已经发送的请求或相应缓存下来,以便以后使用
from scrapy.downloadermiddlewares.httpcache import HttpCacheMiddleware
from scrapy.extensions.httpcache import DummyPolicy
from scrapy.extensions.httpcache import FilesystemCacheStorage
"""
# 是否启用缓存策略 https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
# HTTPCACHE_ENABLED = True
# 缓存策略:所有请求均缓存,下次在请求直接访问原来的缓存即可
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
# 缓存策略:根据Http响应头:Cache-Control、Last-Modified 等进行缓存的策略
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.RFC2616Policy"
# 缓存超时时间
# HTTPCACHE_EXPIRATION_SECS = 0
# 缓存保存路径
# HTTPCACHE_DIR = 'httpcache'
# 缓存忽略的Http状态码
# HTTPCACHE_IGNORE_HTTP_CODES = []
# 缓存存储的插件
# HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
6-2 scrapy-redis配置
# -------------------- scrapy redis 链接 --------------------
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_PARAMS = {}
REDIS_ENCODING = "utf-8"
# -------------------- 去重 --------------------
DUPEFILTER_KEY = 'dupefilter: %(timestamp)s'
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
# -------------------- 调度器 --------------------
# 由 scrapy_redis 的调度器来负责调配
# enqueue_request: 向调度器中添加任务
# next_request:去调度器中获取一个任务
# from scrapy_redis.scheduler import Scheduler
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# -------------------- 规定任务存放的顺序 --------------------
# 优先级
# 正数-广度优先
# 负数-深度优先
DEPTH_PRIORITY = -1
# 先进先出 广度优先
from scrapy_redis.queue import FifoQueue
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'
# 后进先出 深度优先
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue'
# 默认使用优先级队列(默认)
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
# 调度器中请求存放在redis中的key
SCHEDULER_QUEUE_KEY = '%(spider)s:requests'
# 对保存到redis中的数据进行序列化,默认使用pickle
SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat"
# 是否在关闭时候保留原来的调度器和去重记录,True=保留,False=清空
SCHEDULER_PERSIST = False
# 是否在开始之前清空 调度器和去重记录,True=清空,False=不清空
SCHEDULER_FLUSH_ON_START = True
# 去调度器中获取数据时,如果为空,最多等待时间(最后没数据,未获取到)。
# SCHEDULER_IDLE_BEFORE_CLOSE = 10
# 去重规则,在redis中保存时对应的key
SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter'
# 去重规则对应处理的类
SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
3、Scrapy 解析器
(1) CSS 解析器
以a元素来举例说明
| 选择器 | 说明 |
|---|---|
| response.css('a') | 返回的是selector对象 |
| response.css('a').extract() | 返回的是a标签对象 |
| response.css('a::text').extract_first() | 返回的是第一个a标签中文本的值 |
| response.css('a::attr(href)').extract_first() | 返回的是第一个a标签中href属性的值 |
| response.css('a[href*=image]::attr(href)').extract() | 返回所有a标签中href属性包含image的值 |
| response.css('a[href*=image] img::attr(src)').extract() | 返回所有a标签下image标签的src属性 |
| response.css('a::text').re(r'Q\W+') | 使用正则表达式对结果进行筛选 |
表达式说明
| 表达式 | 说明 |
|---|---|
| #box | 选取 id 为 box 的元素 |
| .box | 选取 class 为 box 的元素 |
| p | 选取所有 p 标签 |
| div img | 选取 div 下的 img 元素 |
| div,img | 选取所有 div 元素和所有 img 元素 |
| div#box | 选取 id 为 box 的 div 元素 |
| div > p | 选择父元素为 div 的所有 p 元素 |
| [title~=flower] | 选择 title 属性包含单词 "flower" 的所有元素 |
| a[herf="page/2"] | 选择 href 属性为 "page/2" 的 a 元素 |
| a[herf^="page"] | 选择 href 属性以 "page" 开头的 a 元素 |
| a[herf$=".png"] | 选取 href 属性以 png 结尾的 a 元素 |
基本案例
import scrapy
class DemoSpider01Spider(scrapy.Spider):
# 爬虫名
name = 'demo_spider_01'
# 允许的域名-定向爬虫
allowed_domains = ['dig.chouti.com']
# 起始的url
start_urls = ['https://dig.chouti.com/']
# 回调函数
def parse(self, response):
# response 是一个对象
print(response, type(response))
# print(response.text)
# 去子孙中找 div 并且class=link-con
content_list = response.css('.link-con > .link-item')
for content in content_list:
title = content.css("a.link-title::text").extract_first()
href = content.css("a.link-title::attr(href)").extract_first()
print(title, href)
if __name__ == '__main__':
from scrapy import cmdline
cmdline.execute('scrapy crawl demo_spider_01 --nolog'.split())
(2) Xpath解析器
| 选择器 | 说明 |
|---|---|
| xpath('//a') | 所有a标签(子孙后代) |
| xpath('//a[2]') | 所有a标签,按索引找第二个 |
| xpath('//a[@id]') | 所有a标签,并且含有id属性 |
| xpath('//a[@id="i1"]') | 所有a标签,并且属性id='i1' |
| xpath('//a[@href="link.html"][@id="i1"]') | 所有a标签,属性href="link.html" 而且 id="i1" |
| xpath('//a[contains(@href, "link")]') | 所有a标签,属性href的值包含"link" |
| xpath('//a[starts-with(@href, "link")]') | 所有a标签,属性href的值以"link"开头 |
| xpath('//a[re:test(@id, "i\d+")]') | 所有a标签 属性id的值 符合正则表达式"i\d+"的规则 |
| xpath('//a[re:test(@id, "i\d+")]/text()').extract() | 所有a标签,取text的值 |
| xpath('//a[re:test(@id, "i\d+")]/@href').extract() | 所有a标签,取href的属性值 |
| xpath('/html/body/ul/li/a/@href').extract() | 取所有的值 |
| xpath('//body/ul/li/a/@href').extract_first() | 取第一个值 |
基本案例
import scrapy
class DemoSpider01Spider(scrapy.Spider):
# 爬虫名
name = 'demo_spider_01'
# 允许的域名-定向爬虫
allowed_domains = ['dig.chouti.com']
# 起始的url
start_urls = ['https://dig.chouti.com/']
# 回调函数
def parse(self, response):
# response 是一个对象
print(response, type(response))
content_list = response.xpath('//div[@class="link-con"]/div[contains(@class, "link-item")]')
for content in content_list:
title = content.xpath('.//a[contains(@class, "link-title")]/text()').extract_first()
href = content.xpath('.//a[contains(@class, "link-title")]/@href').extract_first()
print(title, href)
if __name__ == '__main__':
from scrapy import cmdline
cmdline.execute('scrapy crawl demo_spider_01 --nolog'.split())
(3) 案例
import scrapy
from scrapy.http import Request
class DemoSpider01Spider(scrapy.Spider):
# 爬虫名
name = 'demo_spider_01'
# 允许的域名-定向爬虫
allowed_domains = ['www.netbian.com']
# 起始的url
start_urls = ['http://www.netbian.com/meinv/']
# 回调函数
def parse(self, response):
# response 是一个对象
# print(response, type(response))
# 1. 获取图片列表
images_list = response.xpath('//div[@class="list"]/ul/li')
f = open('images.log', mode='a+', encoding='utf-8')
for image in images_list:
# 获取图片
src = image.xpath('.//img/@src').extract_first()
print(src)
f.write(src + '\n')
# 2. 获取下一页
base_url = "http://www.netbian.com"
nextpage = response.xpath('//a[@class="prev"][last()]/@href').extract_first()
if nextpage:
# 如果有下一页 就重新爬取
yield Request(url=base_url + nextpage, callback=self.parse)
else:
print("下载完成")
if __name__ == '__main__':
from scrapy import cmdline
# cmdline.execute('scrapy crawl demo_spider_01'.split())
cmdline.execute('scrapy crawl demo_spider_01 --nolog'.split())
4、Scrapy Item&Pipeline
(1) 基本使用
1-1 先写 pipeline 类
class DemoProject01Pipeline:
def process_item(self, item, spider):
"""
item: 就是爬虫中 yield DemoProject01Item(title=title, src=src) 的对象
spider: 就是执行的爬虫对象 DemoSpider02Spider
"""
print("-------------------")
print(spider, spider.name)
print(item)
return item
1-2 再写 item 类
import scrapy
class DemoProject01Item(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
src = scrapy.Field()
title = scrapy.Field()
1-3 settings 配置
ITEM_PIPELINES = {
'demo_project_01.pipelines.DemoProject01Pipeline': 300,
}
1-4 spider 爬虫
yield 每执行一次,process_item就调用一次
import scrapy
from demo_project_01.items import DemoProject01Item
class DemoSpider02Spider(scrapy.Spider):
name = 'demo_spider_02'
allowed_domains = ['www.netbian.com']
start_urls = ['http://www.netbian.com/meinv/']
def parse(self, response):
images_list = response.xpath('//div[@class="list"]/ul/li')
for image in images_list:
src = image.xpath('.//img/@src').extract_first()
title = image.xpath('.//img/@alt').extract_first()
# yield Item对象 自动执行 process_item
yield DemoProject01Item(title=title, src=src)
(2) 自定义pipeline
from itemadapter import ItemAdapter
"""
源码:
1. 判断 DemoProject01Pipeline 类中是否有 from_crawler 方法
如果有:
obj = DemoProject01Pipeline.from_crawler(...)
如果没有:
obj = DemoProject01Pipeline()
2. obj.open_spider()
3. obj.process_item()
4. obj.close_spider()
"""
class CustomPipeline:
def __init__(self, path):
self.f = None
self.path = path
# 第一次执行
@classmethod
def from_crawler(cls, crawler):
"""
初始化时候,用于创建pipeline对象
:param crawler:
:return:
"""
path = crawler.settings.get('SRC_FILE_PATH')
return cls(path)
# 第二次执行
def open_spider(self, spider):
"""
爬虫开始执行时,调用
:param spider:
:return:
"""
print('爬虫开始执行了')
self.f = open(self.path, 'a+')
# 第三次执行
def process_item(self, item, spider):
"""
item: 就是爬虫中 yield DemoProject01Item(title=title, src=src) 的对象
spider: 就是执行的爬虫对象 DemoSpider02Spider
"""
print("-------------------")
print(spider, spider.name)
print(item)
self.f.write(item["src"] + '\n')
# 当不想让下一个 Pipeline 执行 就返回一个异常
# raise DropItem()
# 交给下一个 Pipeline 的 process_item 方法
return item
# 第四次执行
def close_spider(self, spider):
"""
爬虫关闭时,被调用
:param spider:
:return:
"""
print('爬虫关闭了')
self.f.close()
注意:Pipeline 是所有爬虫公用的,如果想要限制给某个爬虫定制需要使用 spider 参数自己进行处理
class DemoProject01Pipeline:
def process_item(self, item, spider):
if spider.name == "xxx"
print("要执行的方法1")
else:
print("要执行的方法2")
(3) 多个 Pipeline
3-1 pipelines.py
# 写入数据库
class DBPipeline:
def __init__(self, path):
self.f = None
self.path = path
# 第一次执行
@classmethod
def from_crawler(cls, crawler):
"""
初始化时候,用于创建pipeline对象
:param crawler:
:return:
"""
print("DBPipeline.from_crawler")
path = crawler.settings.get('SRC_DB_PATH')
return cls(path)
# 第二次执行
def open_spider(self, spider):
"""
爬虫开始执行时,调用
:param spider:
:return:
"""
print('爬虫开始执行了')
print("DBPipeline.open_spider")
self.f = open(self.path, 'a+')
# 第三次执行
def process_item(self, item, spider):
"""
item: 就是爬虫中 yield DemoProject01Item(title=title, src=src) 的对象
spider: 就是执行的爬虫对象 DemoSpider02Spider
"""
self.f.write(item["src"] + '\n')
return item
# 第四次执行
def close_spider(self, spider):
"""
爬虫关闭时,被调用
:param spider:
:return:
"""
print('爬虫关闭了')
print("DBPipeline.close_spider")
self.f.close()
# 写入文件
class FilePipeline:
def __init__(self, path):
self.f = None
self.path = path
# 第一次执行
@classmethod
def from_crawler(cls, crawler):
"""
初始化时候,用于创建pipeline对象
:param crawler:
:return:
"""
print("FilePipeline.from_crawler")
path = crawler.settings.get('SRC_DB_PATH')
return cls(path)
# 第二次执行
def open_spider(self, spider):
"""
爬虫开始执行时,调用
:param spider:
:return:
"""
print('爬虫开始执行了')
print("FilePipeline.open_spider")
self.f = open(self.path, 'a+')
# 第三次执行
def process_item(self, item, spider):
"""
item: 就是爬虫中 yield DemoProject01Item(title=title, src=src) 的对象
spider: 就是执行的爬虫对象 DemoSpider02Spider
"""
self.f.write(item["src"] + '\n')
return item
# 第四次执行
def close_spider(self, spider):
"""
爬虫关闭时,被调用
:param spider:
:return:
"""
print('爬虫关闭了')
print("FilePipeline.close_spider")
self.f.close()
3-2 items.py
import scrapy
class DemoProject01Item(scrapy.Item):
src = scrapy.Field()
title = scrapy.Field()
3-3 spider.py
import scrapy
from demo_project_01.items import DemoProject01Item
class DemoSpider02Spider(scrapy.Spider):
name = 'demo_spider_02'
allowed_domains = ['www.netbian.com']
start_urls = ['http://www.netbian.com/meinv/']
def parse(self, response):
images_list = response.xpath('//div[@class="list"]/ul/li')
for image in images_list:
src = image.xpath('.//img/@src').extract_first()
title = image.xpath('.//img/@alt').extract_first()
yield DemoProject01Item(title=title, src=src)
3-4 settings.py
ITEM_PIPELINES = {
'demo_project_01.pipelines.DBPipeline': 300,
'demo_project_01.pipelines.FilePipeline': 301,
}
# 配置存储文件
SRC_FILE_PATH = "images.log"
SRC_DB_PATH = "images_db.log"
5、Scrapy 去重
(1) scrapy 自带的去重
from scrapy.dupefilters import RFPDupeFilter
class RFPDupeFilter(BaseDupeFilter):
"""Request Fingerprint duplicates filter"""
def __init__(self, path=None, debug=False):
self.file = None
self.fingerprints = set()
self.logdupes = True
self.debug = debug
self.logger = logging.getLogger(__name__)
if path:
self.file = open(os.path.join(path, 'requests.seen'), 'a+')
self.file.seek(0)
self.fingerprints.update(x.rstrip() for x in self.file)
@classmethod
def from_settings(cls, settings):
debug = settings.getbool('DUPEFILTER_DEBUG')
return cls(job_dir(settings), debug)
def request_seen(self, request):
# url ---> md5值
# fp 就相当于 url 的 MD5值
fp = self.request_fingerprint(request)
if fp in self.fingerprints:
# 已经访问过了
return True
# 未访问过,并且添加进集合中
self.fingerprints.add(fp)
# 如果有文件,就存放进文件
if self.file:
self.file.write(fp + '\n')
(2) scrapy 自定义去重
2-1 dupefilters.py
from scrapy.dupefilters import BaseDupeFilter
from scrapy.utils.request import referer_str, request_fingerprint
from scrapy.http import Request
class TestDupeFilter(BaseDupeFilter):
def __init__(self):
# 访问过的url
self.visted_url = set()
pass
# 初始化
@classmethod
def from_settings(cls, settings):
return cls()
def request_seen(self, request):
"""
去重
:param request:
:return:
"""
fd = request_fingerprint(request=request)
# 如果存在
if fd in self.visted_url:
return True
self.visted_url.add(fd)
def open(self):
"""
爬虫开始
:return:
"""
pass
def close(self, reason):
"""
爬虫结束
:return:
"""
pass
def log(self, request, spider):
"""
日志记录
:param request:
:param spider:
:return:
"""
pass
2-2 settings.py
# 原始的去重规则
# DUPEFILTER_CLASS = 'scrapy.dupefilter.RFPDupeFilter'
# 修改默认的去重规则
DUPEFILTER_CLASS = 'demo_project_01.dupefilters.TestDupeFilter'
2-3 spider中去重
import scrapy
class DemoSpider02Spider(scrapy.Spider):
name = 'demo_spider_02'
allowed_domains = ['www.netbian.com']
start_urls = ['http://www.netbian.com/meinv/']
def parse(self, response):
print(response.request.url)
base_url = "http://www.netbian.com"
nextpage = response.xpath('//a[@class="prev"][last()]/@href').extract_first()
if nextpage:
from scrapy.http import Request
# 不遵循去重规则
yield Request(url=base_url + nextpage, callback=self.parse, dont_filter=True)
# 遵循去重规则 默认
yield Request(url=base_url + nextpage, callback=self.parse, dont_filter=False)
else:
print("下载完成")
6、Scrapy 请求与相应
(1) Request 对象
Request构造器方法的参数列表:
Request(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)
各参数说明:
| 参数 | 必选 | 说明 |
|---|---|---|
| url | Y | 请求页面的url地址,bytes或str类型 |
| callback | N | 页面解析函数,Callback类型,Request请求对象的页面下载完成后,由该参数指定的页面解析函数解析页面,如果未传递该参数,默认调用Spider的parse方法 |
| method | N | HTTP请求的方法,默认为 ‘GET’ |
| headers | N | HTTP请求的头部字典,dict 类型 |
| body | N | HTTP请求的正文,bytes 或 str类型 |
| cookies | N | Cookie信息字典,dict 类型 |
| meta | N | Request 的元数据字典,dict 类型,用于给框架中其他组件传递信息,比如中间件 Item Pipeline。其他组件可以使用Request 对象的 meta 属性访问该元数据字典 (request.meta), 也用于给响应处理函数传递信息。 |
| encoding | N | url 和 body 参数的编码默认为'utf-8'。如果传入的url或body参数是str 类型,就使用该参数进行编码。 |
| priority | N | 请求的优先级,默认值为0,优先级高的请求优先下载 |
| dont_filter | N | 默认情况下(dont_filter=False),对同一个url地址多次提交下载请求,后面的请求会被去重过滤器过滤(避免重复下载)。如果将该参数置为True,可以使请求避免被过滤,强制下载。例如:在多次爬取一个内容随时间而变化的页面时(每次使用相同的url),可以将该参数设置为True。 |
| errback | N | 请求出现异常或出现HTTP错误时(如404页面不存在)的回调函数。 |
(2) Response 对象
Response 对象用来描述一个HTTP响应,Response只是一个基类,根据响应内容的不同有如下子类:
- TextResponse
- HtmlResponse
- XmlResponse
当一个页面下载完成时,下载器依据HTTP响应头部中的Content-Type信息创建某个Response的子类对象。通常一般是HtmlResponse子类。
HtmlResponse 对象的属性及方法:
| 属性 | 说明 |
|---|---|
| url | HTTP 响应的url地址,str 类型 |
| status | HTTP 响应的状态码,int 类型 |
| headers | HTTP 响应的头部,dict 类型。可以调用get或getlist方法对其进行访问 |
| body | HTTP 响应正文,bytes 类型 |
| text | 文本形式的HTTP响应正文,str 类型,它是由 response.body 使用 response.encoding 解码得到的 response.text = response.body.decode(response.encoding) |
| encoding | HTTP 响应正文的编码,它的值可能是从HTTP响应头部或正文中解析出来的。 |
| request | 产生该HTTP 响应的Request对象 |
| meta | 即 response.request.meta, 在构造 Request对象时,可将要传递给响应处理函数的信息通过meta参数传入;响应处理函数处理响应时,通过response.meta 将信息取出 |
| selector | Selector 对象用于在Response 中提取数据 |
| xpath(query) | 使用XPath选择器在Response中提取数据;它是 response.selector.xpath 方法的快捷方式 |
| css(query) | 使用 CSS选择器在Response中提取数据;它是 response.selector.css方法的快捷方式 |
| urljoin (url) | 用于构造绝对 url 。当传入的url参数是一个相对地址时,根据response.url 计算出相应的绝对 url。 |
7、Scrapy cookie
(1) 获取 cookie
1-1 方式一:
import scrapy
from scrapy.http import Request
from scrapy.http.cookies import CookieJar
class DemoSpider02Spider(scrapy.Spider):
# 爬虫名
name = 'demo_spider_02'
# 允许的域名-定向爬虫
allowed_domains = ['dig.chouti.com']
# 起始的url
start_urls = ['https://dig.chouti.com/']
def parse(self, response):
"""
第一次访问抽屉返回的内容
:param response:
:return:
"""
# 去响应头中获取cookie
cookie_dict = {}
# 去响应头中获取cookie,cookie保存在 cookie_jar 对象
cookie_jar = CookieJar()
cookie_jar.extract_cookies(response, response.request)
# 去对象中将 cookie 解析到字典
for k, v in cookie_jar._cookies.items():
for i, j in v.items():
for m, n in j.items():
cookie_dict[m] = n.value
print(cookie_dict)
(2) 携带 cookie
import scrapy
from scrapy.http import Request
from scrapy.dupefilters import RFPDupeFilter
from demo_project_01.items import DemoProject01Item
from urllib.parse import urlencode
from scrapy.http.cookies import CookieJar
class DemoSpider02Spider(scrapy.Spider):
# 爬虫名
name = 'demo_spider_02'
# 允许的域名-定向爬虫
allowed_domains = ['dig.chouti.com']
# 起始的url
start_urls = ['https://dig.chouti.com/']
def parse(self, response):
"""
第一次访问抽屉返回的内容
:param response:
:return:
"""
# 去响应头中获取cookie
cookie_dict = {}
# 去响应头中获取cookie,cookie保存在 cookie_jar 对象
cookie_jar = CookieJar()
cookie_jar.extract_cookies(response, response.request)
# 去对象中将 cookie 解析到字典
for k, v in cookie_jar._cookies.items():
for i, j in v.items():
for m, n in j.items():
cookie_dict[m] = n.value
print(cookie_dict)
v = {
"password": "19971215qwe",
"loginType": "2",
"phone": "+8618582896532",
}
data = urlencode(v)
yield Request(
url="https://dig.chouti.com/login",
method="POST",
body=data,
headers={
"Content-Type": "application/json;charset=UTF-8"
},
cookies=cookie_dict,
callback=self.check_login
)
def check_login(self, response):
print(response.text)
if __name__ == '__main__':
from scrapy import cmdline
# cmdline.execute('scrapy crawl demo_spider_01'.split())
cmdline.execute('scrapy crawl demo_spider_02 --nolog'.split())
8、Scrapy 中间件
(1) 下载中间件
process_request
process_reqsponse
process_exception
1-1 内置代理 - 通过环境变量
import os
import scrapy
from scrapy.http import Request
class DemoSpider02Spider(scrapy.Spider):
name = 'demo_spider_02'
allowed_domains = ['www.netbian.com']
start_urls = ['http://www.netbian.com/meinv/']
def start_requests(self):
# 设置代理
# 有用户名有密码
os.environ["HTTP_PROXY"] = "http://username:password@192.168.11.11:8080"
os.environ["HTTPS_PROXY"] = "192.168.11.11:8000"
for url in self.start_urls:
yield Request(url=url, callback=self.parse)
def parse(self, response):
print(response.meta["depth"])
print(response.meta.get("depth"))
if __name__ == '__main__':
from scrapy import cmdline
# cmdline.execute('scrapy crawl demo_spider_02'.split())
cmdline.execute('scrapy crawl demo_spider_02 --nolog'.split())
1-2 内置代理 - 通过 meta 参数
import os
import scrapy
from scrapy.http import Request
class DemoSpider02Spider(scrapy.Spider):
name = 'demo_spider_02'
allowed_domains = ['www.netbian.com']
start_urls = ['http://www.netbian.com/meinv/']
def start_requests(self):
for url in self.start_urls:
yield Request(url=url, callback=self.parse, meta={'proxy': 'http://username:password@192.168.11.11:8080'})
def parse(self, response):
pass
if __name__ == '__main__':
from scrapy import cmdline
# cmdline.execute('scrapy crawl demo_spider_02'.split())
cmdline.execute('scrapy crawl demo_spider_02 --nolog'.split())
1-3 自定义代理
proxy.py
# 自定义代理
import base64
import random
from six.moves.urllib.parse import unquote
try:
from urllib2 import _parse_proxy
except ImportError:
from urllib.request import _parse_proxy
from six.moves.urllib.parse import urlunparse
from scrapy.utils.python import to_bytes
class TestProxyMiddleware(object):
def _basic_auth_header(self, username, password):
user_pass = to_bytes(
'%s:%s' % (unquote(username), unquote(password)),
encoding='latin-1')
return base64.b64encode(user_pass).strip()
def process_request(self, request, spider):
PROXIES = [
"http://root:woshiniba@192.168.11.11:9999/",
"http://root:woshiniba@192.168.11.12:9999/",
"http://root:woshiniba@192.168.11.13:9999/",
"http://root:woshiniba@192.168.11.14:9999/",
"http://root:woshiniba@192.168.11.15:9999/",
"http://root:woshiniba@192.168.11.16:9999/",
]
url = random.choice(PROXIES)
orig_type = ""
proxy_type, user, password, hostport = _parse_proxy(url)
proxy_url = urlunparse((proxy_type or orig_type, hostport, '', '', '', ''))
if user:
creds = self._basic_auth_header(user, password)
else:
creds = None
request.meta['proxy'] = proxy_url
if creds:
request.headers['Proxy-Authorization'] = b'Basic ' + creds
class DBProxyMiddleware(object):
def process_request(self, request, spider):
PROXIES = [
{'ip_port': '111.11.228.75:80', 'user_pass': ''},
{'ip_port': '120.198.243.22:80', 'user_pass': ''},
{'ip_port': '111.8.60.9:8123', 'user_pass': ''},
{'ip_port': '101.71.27.120:80', 'user_pass': ''},
{'ip_port': '122.96.59.104:80', 'user_pass': ''},
{'ip_port': '122.224.249.122:8088', 'user_pass': ''},
]
proxy = random.choice(PROXIES)
if proxy['user_pass'] is not None:
request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port'])
encoded_user_pass = base64.b64encode(to_bytes(proxy['user_pass']))
request.headers['Proxy-Authorization'] = to_bytes('Basic ' + encoded_user_pass)
else:
request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port'])
settings.py
DOWNLOADER_MIDDLEWARES = {
'demo_project_01.proxy.TestProxyMiddleware': 543,
'demo_project_01.proxy.DBProxyMiddleware': 533,
}
1-4 自定义下载中间
middlewares.py
# 下载中间件
from scrapy import signals
from scrapy.http import HtmlResponse
from scrapy.http import Request
from scrapy.exceptions import IgnoreRequest
# 下载中间1
class DMD1(object):
@classmethod
def from_crawler(cls, crawler):
s = cls()
return s
# 下载之前执行
def process_request(self, request, spider):
"""
# Must either:
# - return None: continue processing this request
# - or return a Response object
# - or return a Request object
# - or raise IgnoreRequest: process_exception() methods of
# installed downloader middleware will be called
"""
print('MD1.process_request', request)
# 1. 返回Response对象
"""
import requests
res = requests.get(url=request.url)
return HtmlResponse(url=request.url, status=200, headers=None, body=res.content)
"""
# 2. 返回Request对象
"""
# 递归了
return Request(url="http://www.netbian.com/youxi/")
"""
# 3. 抛出异常
"""
raise IgnoreRequest
"""
# 4. 对请求进行加工(***)
"""
request.headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36"
request.headers["Cookie"] = "_cfduid=df20ecd30e73ab4ba86d137daa7b1dc1e1613540129"
"""
return None
# 下载完成之后执行
def process_response(self, request, response, spider):
"""
# Must either;
# - return a Response object
# - return a Request object
# - or raise IgnoreRequest
"""
print('MD1.process_response', request, response)
return response
# 出现异常是执行
def process_exception(self, request, exception, spider):
"""
# Must either:
# - return None: continue processing this exception
# - return a Response object: stops process_exception() chain
# - return a Request object: stops process_exception() chain
"""
pass
# 下载中间2
class DMD2():
@classmethod
def from_crawler(cls, crawler):
s = cls()
return s
def process_request(self, request, spider):
print('MD2.process_request', request)
return None
def process_response(self, request, response, spider):
print('MD2.process_response', request, response)
return response
# 出现异常是执行
def process_exception(self, request, exception, spider):
pass
settings.py
DOWNLOADER_MIDDLEWARES = {
'demo_project_01.utils.middlewares.DMD1': 666,
'demo_project_01.utils.middlewares.DMD2': 667,
}
(2) 爬虫中间件
2-1 自定义爬虫中间件
middlewares.py
# 爬虫中间件1
class SMD1(object):
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
return s
# 拿到返回结果后执行
def process_spider_input(self, response, spider):
# Called for each response that goes through the spider
# middleware and into the spider.
# Should return None or raise an exception.
return None
# 爬虫返回 item 对象 或 Request 对象 后执行
def process_spider_output(self, response, result, spider):
# Called with the results returned from the Spider, after
# it has processed the response.
# Must return an iterable of Request, or item objects.
for i in result:
yield i
# 抛出异常是执行
def process_spider_exception(self, response, exception, spider):
# Called when a spider or process_spider_input() method
# (from other spider middleware) raises an exception.
# Should return either None or an iterable of Request or item objects.
pass
# 只在爬虫启动时执行一次
def process_start_requests(self, start_requests, spider):
"""
Called with the start requests of the spider, and works
similarly to the process_spider_output() method, except
that it doesn’t have a response associated.
与爬行器的启动请求一起调用,并工作
类似于process_spider_output()方法,除了
它没有相应的响应。
"""
# 必须只返回请求(而不是项目)。
for r in start_requests:
yield r
class SMD2(object):
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the spider middleware does not modify the
# passed objects.
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
return s
def process_spider_input(self, response, spider):
# Called for each response that goes through the spider
# middleware and into the spider.
# Should return None or raise an exception.
return None
def process_spider_output(self, response, result, spider):
# Called with the results returned from the Spider, after
# it has processed the response.
# Must return an iterable of Request, or item objects.
for i in result:
yield i
def process_spider_exception(self, response, exception, spider):
# Called when a spider or process_spider_input() method
# (from other spider middleware) raises an exception.
# Should return either None or an iterable of Request or item objects.
pass
def process_start_requests(self, start_requests, spider):
# Called with the start requests of the spider, and works
# similarly to the process_spider_output() method, except
# that it doesn’t have a response associated.
# Must return only requests (not items).
for r in start_requests:
yield r
settings.py
# 爬虫中间件
SPIDER_MIDDLEWARES = {
'demo_project_01.utils.middlewares.SMD1': 666,
'demo_project_01.utils.middlewares.SMD2': 667,
}
9、Scrapy scrapy-redis组件
分布式爬虫的组件
安装:
pip install scrapy-redis
(1) redis 集合
import redis
# 连接 redis
conn = redis.Redis(host='127.0.0.1', port=6379)
# 查看所有键名
v = conn.keys()
# 集合中添加数据
# v1 = conn.sadd('urls', "https://www.baidu.com")
# v2 = conn.sadd('urls', "https://www.cnblogs.com")
# print(v1)
# print(v2)
result = conn.sadd('urls', "https://www.bing.com")
if result == 1:
print("之前未访问的链接")
else:
print("之前访问的链接")
print(conn.smembers('urls'))
(2) URL去重
2-1 完全自定义
dupefilter.py
import redis
from scrapy.dupefilters import BaseDupeFilter
from scrapy.utils.request import referer_str, request_fingerprint
class DupeFilter(BaseDupeFilter):
def __init__(self):
# 连接 redis
self.conn = redis.Redis(host='127.0.0.1', port=6379)
def request_seen(self, request):
"""
检测当前请求是否已经被访问过
True: 表示已经访问
False: 表示没有被访问
"""
# 生成固定长度的位移标识
fid = request_fingerprint(request)
result = self.conn.sadd('visited_urls', fid)
if result == 1:
print("未访问过的链接: ", request.url)
return False
else:
print("已访问过的链接: ", request.url)
return True
settings.py
# 修改默认的去重规则
DUPEFILTER_CLASS = 'demo_02.utils.dupefilter.DupeFilter'
2-2 使用 scrapy_redis
| 配置项 | 说明 |
|---|---|
| REDIS_HOST = 'localhost' | 主机名 |
| REDIS_PORT = 6379 | 端口 |
| REDIS_URL = 'redis://user:pass@hostname:9001' | 连接URL(优先于以上配置) |
| REDIS_PARAMS = {} | Redis连接参数,默认:REDIS_PARAMS = {'socket_timeout': 30,'socket_connect_timeout': 30,'retry_on_timeout': True,'encoding': REDIS_ENCODING,}) |
| REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient' | 指定连接Redis的Python模块 默认:redis.StrictRedis |
| REDIS_ENCODING = "utf-8" | redis编码类型, 默认:'utf-8' |
settings.py
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_PARAMS = {}
REDIS_ENCODING = "utf-8"
# 这里最好写死, 因为每次运行的时候都会创建新的 key
# DUPEFILTER_KEY = 'dupefilter: %(timestamp)s'
DUPEFILTER_KEY = 'dupefilter: demo'
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
3-2 继承 scrapy_redis 重写
dupefilter.py
from scrapy_redis.dupefilter import RFPDupeFilter
from scrapy_redis.connection import get_redis_from_settings
from scrapy_redis import defaults
class RedisDupefilter(RFPDupeFilter):
@classmethod
def from_settings(cls, settings):
server = get_redis_from_settings(settings)
# 将 key 写死
key = defaults.DUPEFILTER_KEY % {'timestamp': "demo"}
debug = settings.getbool('DUPEFILTER_DEBUG')
return cls(server, key=key, debug=debug)
settings.py
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_PARAMS = {}
REDIS_ENCODING = "utf-8"
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RedisDupefilter'
(3) 调度器
3-1 配置选项
| 配置选项 | 实例值 | 说明 |
|---|---|---|
| SCHEDULER | 'scrapy_redis.scheduler.Scheduler' | 由 scrapy_redis 的调度器来负责调配 |
| SCHEDULER_QUEUE_CLASS | 'scrapy_redis.queue.FifoQueue' | 规定任务存放的顺序 |
| SCHEDULER_QUEUE_KEY | '%(spider)s:requests' | 调度器中请求存放在redis中的key |
| SCHEDULER_SERIALIZER | "scrapy_redis.picklecompat" | 对保存到redis中的数据进行序列化,默认使用pickle |
| SCHEDULER_PERSIST | True | 是否在关闭时候保留原来的调度器和去重记录,True=保留,False=清空 |
| SCHEDULER_FLUSH_ON_START | False | 是否在开始之前清空 调度器和去重记录,True=清空,False=不清空 |
| SCHEDULER_IDLE_BEFORE_CLOSE | 10 | 去调度器中获取数据时,如果为空,最多等待时间(最后没数据,未获取到) |
| SCHEDULER_DUPEFILTER_KEY | '%(spider)s:dupefilter' | 去重规则,在redis中保存时对应的key |
| SCHEDULER_DUPEFILTER_CLASS | 'scrapy_redis.dupefilter.RFPDupeFilter' | 去重规则对应处理的类 |
(4) 数据持久化
(5) 起始URL相关
(6) 案例
10、Scrapy 信号
(1) 内置信号
| 信号 | 说明 |
|---|---|
| engine_started = object() | 引擎开始时执行 |
| engine_stopped = object() | 引擎结束后执行 |
| spider_opened = object() | 爬虫开始时执行 |
| spider_idle = object() | 爬虫闲置时执行 |
| spider_closed = object() | 爬虫关闭时执行 |
| spider_error = object() | 爬虫出错时执行 |
| request_scheduled = object() | 将爬虫任务放在调度器时执行 |
| request_dropped = object() | 将爬虫任务丢掉时执行 |
| request_reached_downloader = object() | |
| request_left_downloader = object() | |
| response_received = object() | 接收到返回结果时执行 |
| response_downloaded = object() | 下载完成时执行 |
| bytes_received = object() | |
| item_scraped = object() | |
| item_dropped = object() | |
| item_error = object() |
(2) 自定义信号
signals.py
from scrapy import signals
class MyExtension(object):
def __init__(self, value):
self.value = value
@classmethod
def from_crawler(cls, crawler):
val = crawler.settings.getint('MMMM')
ext = cls(val)
crawler.signals.connect(ext.spider_opened, signal=signals.spider_opened)
crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)
return ext
def spider_opened(self, spider):
print('open')
def spider_closed(self, spider):
print('close')
settings.py
EXTENSIONS = {
'demo_project_01.utils.signal.MyExtension': 666,
}
11、Scrapy 扩展
(1) Scrapy 深度&优先级
1-1 源码
from scrapy.spidermiddlewares.depth import DepthMiddleware
import logging
from scrapy.http import Request
logger = logging.getLogger(__name__)
class DepthMiddleware:
def __init__(self, maxdepth, stats, verbose_stats=False, prio=1):
self.maxdepth = maxdepth
self.stats = stats
self.verbose_stats = verbose_stats
self.prio = prio
@classmethod
def from_crawler(cls, crawler):
settings = crawler.settings
maxdepth = settings.getint('DEPTH_LIMIT')
verbose = settings.getbool('DEPTH_STATS_VERBOSE')
prio = settings.getint('DEPTH_PRIORITY')
return cls(maxdepth, crawler.stats, verbose, prio)
def process_spider_output(self, response, result, spider):
def _filter(request):
if isinstance(request, Request):
depth = response.meta['depth'] + 1
request.meta['depth'] = depth
if self.prio:
# 请求优先级
request.priority -= depth * self.prio
if self.maxdepth and depth > self.maxdepth:
logger.debug(
"Ignoring link (depth > %(maxdepth)d): %(requrl)s ",
{'maxdepth': self.maxdepth, 'requrl': request.url},
extra={'spider': spider}
)
return False
else:
if self.verbose_stats:
self.stats.inc_value(f'request_depth_count/{depth}',
spider=spider)
self.stats.max_value('request_depth_max', depth,
spider=spider)
return True
# 如果 response.meta 中没有 'depth' 就设置 response.meta['depth']=0
# base case (depth=0)
if 'depth' not in response.meta:
response.meta['depth'] = 0
if self.verbose_stats:
self.stats.inc_value('request_depth_count/0', spider=spider)
# result = [Request(1), Request(2), Request(3), Request(10)]
# 循环 result 中的每一个 request 对象, 每个对象再去执行 _filter() 方法
# _filter() 返回True: 放到调度器中
# _filter() 返回False: 丢弃
return (r for r in result or () if _filter(r))
1-2 深度
最开始是0 每次yield时,会根据原来请求中的 depth + 1
1-3 优先级
优先级 -= 深度 * DEPTH_PRIORITY
1-4 配置
settings.py
# 限制深度
DEPTH_LIMIT = 3
# 配置优先级
DEPTH_PRIORITY = 1
(2) Scrapy start_urls
2-1 内部原理
scrapy 引擎爬虫中取起始URL
1. 调用 start_requests 并获取返回值
2. v = iter(返回值)
3. 执行 v.__next__(), v.__next__(),v.__next__(),v.__next__()
4. req 全部放到调度器中
2-2 基本使用
import scrapy
class DemoSpider02Spider(scrapy.Spider):
name = 'demo_spider_02'
allowed_domains = ['dig.chouti.com']
start_urls = ['https://dig.chouti.com/']
def start_requests(self):
# 方式一:
for url in self.start_urls:
yield Request(url=url)
# 方式二:
"""
req_obj_list = []
for url in self.start_urls:
req_obj_list.append(Request(url=url))
return req_obj_list
"""
2-3 定制:可以去redis中获取
(3) Scrapy 依赖 twisted
内部基于事件循环的机制实现爬虫的开发
3-1 原来的你
import requests
url_list = ['http://www.baidu.com', 'http://www.baidu.com', 'http://www.baidu.com']
for item in url_list:
response = requests.get(item)
print(response.text)
3-2 现在的你
非阻塞,不等待。
异步,回调。
事件循环
twisted 基于事件循环的异步非阻塞模块
一个线程同时可以向多个目标发送http请求
from twisted.web.client import getPage, defer
from twisted.internet import reactor
# 第一部分:代理开始接收任务
def callback(contents):
print(contents)
deferred_list = []
url_list = ['http://www.bing.com', 'https://stackoverflow.com/', 'https://segmentfault.com/']
for url in url_list:
deferred = getPage(bytes(url, encoding='utf8'))
deferred.addCallback(callback)
deferred_list.append(deferred)
# 第二部分: 代理执行完任务后,停止
dlist = defer.DeferredList(deferred_list)
# 停止任务
def all_done(arg):
reactor.stop()
dlist.addBoth(all_done)
# 第三部分:代码开始去处理吧
reactor.run()
(4) 定制命令
4-1 单个爬虫运行
from scrapy.cmdline import execute
if __name__ == '__main__':
# 方式一:
# cmdline.execute('scrapy crawl demo_spider_02'.split())
# execute('scrapy crawl demo_spider_02 --nolog'.split())
# 方式二
execute(['scrapy', 'crawl', 'demo_spider_02', '--nolog'])
4-2 运行多个爬虫
-
在spiders同级创建任意目录,如:commands
-
在其中创建 crawlall.py 文件 (此处文件名就是自定义的命令)
from scrapy.commands import ScrapyCommand from scrapy.utils.project import get_project_settings class Command(ScrapyCommand): requires_project = True def syntax(self): return '[options]' def short_desc(self): return 'Runs all of the spiders' def run(self, args, opts): # 找到所有的爬虫 spider_list = self.crawler_process.spiders.list() for name in spider_list: self.crawler_process.crawl(name, **opts.__dict__) self.crawler_process.start() -
在settings.py 中添加配置 COMMANDS_MODULE = '项目名称.目录名称'
-
在项目目录执行命令:
scrapy crawlall scrapy crawlall --nolog
(5) 源码流程扩展
"""
1. scrapy crawl demo --nolog
2. 找到 SCHEDULER = "scrapy_redis.scheduler.Scheduler" 配置并实例化调度器对象
- 执行 from_crawler()
- 执行 from_settings()
- 读取配置文件:
SCHEDULER_PERSIST: True
SCHEDULER_FLUSH_ON_START: False
SCHEDULER_IDLE_BEFORE_CLOSE: 10
- 读取配置文件:
SCHEDULER_QUEUE_KEY: '%(spider)s:requests'
SCHEDULER_QUEUE_CLASS: 'scrapy_redis.queue.FifoQueue'
SCHEDULER_DUPEFILTER_KEY: '%(spider)s:dupefilter'
DUPEFILTER_CLASS: 'scrapy_redis.dupefilter.RFPDupeFilter'
SCHEDULER_SERIALIZER: 'scrapy_redis.picklecompat'
- 读取配置文件:
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_PARAMS = {}
REDIS_ENCODING = "utf-8"
- 实例化 Scheduler 对象
3. 爬虫开始执行起始的URL
- 调用 Scheduler.enqueue_request()
def enqueue_request(self, request):
# 判断请求是否过滤
# 去重规则中是否已经有了?(是否已经访问过)
if not request.dont_filter and self.df.request_seen(request):
self.df.log(request, self.spider)
# 已经访问过就不要在访问了
return False
if self.stats:
self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)
# 没有访问过,将任务添加到调度器中
self.queue.push(request)
return True
4. 下载器去调度器中获取任务,去下载
- 调用 Scheduler.next_request()
def next_request(self):
block_pop_timeout = self.idle_before_close
request = self.queue.pop(block_pop_timeout)
if request and self.stats:
self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
return request
面试问题:
1. 什么是深度优先?, 什么是广度优先?
- 深度优先: https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3631837805 ,71405325&fm=15&gp=0.jpg
- 广度优先: https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=228494906 ,243424301&fm=26&gp=0.jpg
2. 在 scrapy 中如何实现深度和广度优先?
- 使用先进先出:广度优先 'scrapy_redis.queue.FifoQueue'
- 使用后进先出:深度优先 'scrapy_redis.queue.LifoQueue'
- 优先级队列
DEPTH_PRIORITY = -1 # 负数-深度优先
DEPTH_PRIORITY = 1 # 正数-广度优先
3. scrapy 中调度器 队列 dupefilter的关系
- 调度器: 调配添加或获取某个request
- 队列:存放request
- dupefilter: 存放访问记录
4. 配置
- 链接 redis 配置
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_PARAMS = {}
REDIS_ENCODING = "utf-8"
- 去重的配置
DUPEFILTER_KEY = 'dupefilter: %(timestamp)s'
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
- 调度器配置
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DEPTH_PRIORITY = -1
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
SCHEDULER_QUEUE_KEY = '%(spider)s:requests'
SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat"
SCHEDULER_PERSIST = False
SCHEDULER_FLUSH_ON_START = True
SCHEDULER_IDLE_BEFORE_CLOSE = 10
SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter'
SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
"""
浙公网安备 33010602011771号