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'))
# 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、总结

  1. socket默认是否是阻塞的?阻塞体现在哪里?

  2. 如何让socket编程非阻塞?

  3. IO多路复用作用?

    检测多个socket是否发生变化。
    操作系统检测socket是否发生变化,有三种模式:
    select:最多1024个socket;循环去检测。
    poll:不限制监听socket个数;循环去检测(水平触发)。
    epoll:不限制监听socket个数;回调方式(边缘触发)。
    Python模块:
    select.select
    select.epoll

  4. 提高并发方案:

    多进程
    多线程
    异步非阻塞模块(Twisted) scrapy框架(单线程完成并发)

  5. 什么是异步非阻塞?

    非阻塞,不等待。
    比如创建socket对某个地址进行connect、获取接收数据recv时默认都会等待(连接成功或接收到数据),才执行后续操作。
    如果设置setblocking(False),以上两个过程就不再等待,但是会报BlockingIOError的错误,只要捕获即可。
    异步,通知,执行完成之后自动执行回调函数或自动执行某些操作(通知)。
    比如做爬虫中向某个地址baidu.com发送请求,当请求执行完成之后自执行回调函数。

  6. 什么是同步阻塞?

    阻塞:等

    同步:按照顺序逐步执行

    key_list = ['alex','db','sb']
    for item in key_list:
    	ret = requests.get('https://www.baidu.com/s?wd=%s' %item)
    	print(ret.text)
    
  7. 概念

    # 之前:
    # 你写的代码: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

参考文档:

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工作流程:(这张图是在百度下载的)

img

(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 运行流程

  1. 引擎: Hi! Spider,你要处理哪一个网站?

  2. Spider: 老大要我处理 xxx.xxx.com.

  3. 引擎: 你把第一个需要处理的URL给我吧。

  4. Spider: 给你,第一个URL是 xxx.xxx.com.

  5. 引擎: Hi! 调度器,我这里有request请求你帮我排序入队一下。

  6. 调度器: 好的,正在处理你等一下。

  7. 引擎: Hi! 调度器,你把处理好的request请求给我。

  8. 调度器: 给你,这是我处理好的request请求。

  9. 引擎: Hi! 下载器,你你按照老大的 下载中间件 的设置当我下载一下这个request请求。

  10. 下载器: 好的!给你这是下载好的东西。(如果下载失败:sorry,这个request下载失败了,然后 引擎 告诉 调度器 ,这个request 下载失败了,你记录一下,我们待会再下载)

  11. 引擎:Hi! Spider,给你,这是下载好的东西,并且已经按照老大的 下载中间件 处理过了,你自己处理一下(注意!这儿responses默认式交给 def parse() 这个函数处理的)

  12. Spider: (处理完毕数据之后对于需要跟进的URL),Hi!引擎,我这里有两个结果,这个是我需要跟进的URL,还有这个是我获取到的item数据。

  13. 引擎: Hi! 管道 我这里有个item你帮我处理一下!调度器!这是需要跟进的URL你帮我吃力一下。然后从第四步开始循环,知道老大去玩需要的全部数据。

  14. 管道 调度器: 好的,现在就做!

注意!只有当调度器中不存在任何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
img

(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) 项目结构

项目目录

img

文件 说明
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。

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)
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'
"""
posted @ 2023-05-30 16:35  菜鸟程序员_python  阅读(266)  评论(0)    收藏  举报