爬蟲簡介
網路爬蟲
爬蟲指在使用程式模擬瀏覽器向服務端發出網路請求,以便獲取服務端返回的內容。
但這些內容可能涉及到一些機密資訊,所以爬蟲領域目前來講是屬於灰色領域,切勿違法犯罪。
爬蟲本身作為一門技術沒有任何問題,關鍵是看人們怎麼去使用它
《中華人民共和國刑法》第二百八十五條規定:非法獲取計算機資訊系統資料、非法控制計算機資訊系統罪,是指違反國家規定,侵入國家事務、國防建設、尖端科學技術領域以外的計算機資訊系統或者採用其他技術手段,獲取該計算機資訊系統中儲存、處理或者傳輸的資料,情節嚴重的行為。刑法第285條第2款明確規定,犯本罪的,處三年以下有期徒刑或者拘役,並處或者單處罰金;情節特別嚴重的,處三年以上七年以下有期徒刑,並處罰金。
《反不正當競爭法》第九條規定:以不正當手段獲取他人商業祕密的行為即已經構成侵犯商業祕密。而後續如果進一步利用,或者公開該等資訊,則構成對他人商業祕密的披露和使用,同樣構成對權利人的商業祕密的侵犯。
《刑法》第二百八十六條規定:違反國家規定,對計算機資訊系統功能進行刪除、修改、增加、干擾,造成計算機資訊系統不能正常執行,後果嚴重的,構成犯罪,處五年以下有期徒刑或者拘役;後果特別嚴重的,處五年以上有期徒刑。而違反國家規定,對計算機資訊系統中儲存、處理或者傳輸的資料和應用程式進行刪除、修改、增加的操作,後果嚴重的,也構成犯罪,依照前款的規定處罰。
《網路安全法》第四十四條規定:任何個人和組織不得竊取或者以其他非法方式獲取個人資訊。因此,如果爬蟲在未經使用者同意的情況下大量抓取使用者的個人資訊,則有可能構成非法收集個人資訊的違法行為。
《民法總則》第111條規定:任何組織和個人需要獲取他人個人資訊的,應當依法取得並確保資訊保安。不得非法收集、使用、加工、傳輸他人個人資訊
爬蟲分類
根據爬蟲的應用範疇,可有一些三種區分:
通用爬蟲
搜尋引擎本質就是一個巨大的爬蟲,首先該爬蟲會爬取整張頁面,並且對該頁面做備份,之後對其進行資料內容處理如抓取關鍵字等,然後向使用者提供檢索介面。
聚焦式爬蟲
只關注於頁面上某一部分內容,如只關注圖片、連結等。
增量式爬蟲
用於檢索內容是否更新,如開發了一個增量式爬蟲每天檢視一下雲崖部落格有沒有更新,有更新就爬下來等等...
robots協議
robots
協議是爬蟲領域非常出名的一種協議,由入口網站提供。
它規定了該站點哪些內容允許爬取,哪些內容不允許爬取。
如果爬取不允許的內容,可對其追究法律責任。
requests模組
requests
模組是Python
中傳送網路請求的一款非常簡潔、高效的模組。
pip install requests
傳送請求
支援所有的請求方式:
import requests
requests.get("https://www.python.org/")
requests.post("https://www.python.org/")
requests.put("https://www.python.org/")
requests.patch("https://www.python.org/")
requests.delete("https://www.python.org/")
requests.head("https://www.python.org/")
requests.options("https://www.python.org/")
# 指定請求方式
requests.request("get","https://www.python.org/")
當請求傳送成功後,會返回一個response
物件。
get請求
基本的get
請求引數如下:
引數 | 描述 |
---|---|
params | 字典,get請求的引數,value支援字串、字典、位元組(ASCII編碼內) |
headers | 字典,本次請求攜帶的請求頭 |
cookies | 字典,本次請求攜帶的cookies |
演示如下:
import requests
res = requests.get(
url="http://127.0.0.1:5000/index",
params={"key": "value"},
cookies={"key": "value"},
)
print(res.content)
post請求
基本的post
請求引數如下:
引數 | 描述 |
---|---|
data | 字典,post請求的引數,value支援檔案物件、字串、字典、位元組(ASCII編碼內) |
headers | 字典,本次請求攜帶的請求頭 |
cookies | 字典,本次請求攜帶的cookies |
演示如下:
import requests
res = requests.post(
url="http://127.0.0.1:5000/index",
# 依舊可以攜帶 params
data={"key": "value"},
cookies={"key": "value"},
)
print(res.content)
高階引數
更多引數:
引數 | 描述 |
---|---|
json | 字典,傳入json資料,將自動進行序列化,支援get/post,請求體傳遞 |
files | 字典,傳入檔案物件,支援post |
auth | 認證,傳入HTTPDigestAuth物件,一般場景是路由器彈出的兩個輸入框,爬蟲獲取不到,將使用者名稱和密碼輸入後會base64加密然後放入請求頭中進行交給服務端,base64("名字:密碼"),請求頭名字:authorization |
timeout | 超時時間,傳入float/int/tuple型別。如果傳入的是tuple,則是 (連結超時、返回超時) |
allow_redirects | 是否允許重定向,傳入bool值 |
proxies | 開啟代理,傳入一個字典 |
stream | 是否返回檔案流,傳入bool值 |
cert | 證照地址,這玩意兒來自於HTTPS請求,需要傳入該網站的認證證照地址,通常來講如果是大公司的網站不會要求這玩意兒 |
演示:
def param_method_url():
# requests.request(method='get', url='http://127.0.0.1:8000/test/')
# requests.request(method='post', url='http://127.0.0.1:8000/test/')
pass
def param_param():
# - 可以是字典
# - 可以是字串
# - 可以是位元組(ascii編碼以內)
# requests.request(method='get',
# url='http://127.0.0.1:8000/test/',
# params={'k1': 'v1', 'k2': '水電費'})
# requests.request(method='get',
# url='http://127.0.0.1:8000/test/',
# params="k1=v1&k2=水電費&k3=v3&k3=vv3")
# requests.request(method='get',
# url='http://127.0.0.1:8000/test/',
# params=bytes("k1=v1&k2=k2&k3=v3&k3=vv3", encoding='utf8'))
# 錯誤
# requests.request(method='get',
# url='http://127.0.0.1:8000/test/',
# params=bytes("k1=v1&k2=水電費&k3=v3&k3=vv3", encoding='utf8'))
pass
def param_data():
# 可以是字典
# 可以是字串
# 可以是位元組
# 可以是檔案物件
# requests.request(method='POST',
# url='http://127.0.0.1:8000/test/',
# data={'k1': 'v1', 'k2': '水電費'})
# requests.request(method='POST',
# url='http://127.0.0.1:8000/test/',
# data="k1=v1; k2=v2; k3=v3; k3=v4"
# )
# requests.request(method='POST',
# url='http://127.0.0.1:8000/test/',
# data="k1=v1;k2=v2;k3=v3;k3=v4",
# headers={'Content-Type': 'application/x-www-form-urlencoded'}
# )
# requests.request(method='POST',
# url='http://127.0.0.1:8000/test/',
# data=open('data_file.py', mode='r', encoding='utf-8'), # 檔案內容是:k1=v1;k2=v2;k3=v3;k3=v4
# headers={'Content-Type': 'application/x-www-form-urlencoded'}
# )
pass
def param_json():
# 將json中對應的資料進行序列化成一個字串,json.dumps(...)
# 然後傳送到伺服器端的body中,並且Content-Type是 {'Content-Type': 'application/json'}
requests.request(method='POST',
url='http://127.0.0.1:8000/test/',
json={'k1': 'v1', 'k2': '水電費'})
def param_headers():
# 傳送請求頭到伺服器端
requests.request(method='POST',
url='http://127.0.0.1:8000/test/',
json={'k1': 'v1', 'k2': '水電費'},
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
def param_cookies():
# 傳送Cookie到伺服器端
requests.request(method='POST',
url='http://127.0.0.1:8000/test/',
data={'k1': 'v1', 'k2': 'v2'},
cookies={'cook1': 'value1'},
)
# 也可以使用CookieJar(字典形式就是在此基礎上封裝)
from http.cookiejar import CookieJar
from http.cookiejar import Cookie
obj = CookieJar()
obj.set_cookie(Cookie(version=0, name='c1', value='v1', port=None, domain='', path='/', secure=False, expires=None,
discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False,
port_specified=False, domain_specified=False, domain_initial_dot=False, path_specified=False)
)
requests.request(method='POST',
url='http://127.0.0.1:8000/test/',
data={'k1': 'v1', 'k2': 'v2'},
cookies=obj)
def param_files():
# 傳送檔案
# file_dict = {
# 'f1': open('readme', 'rb')
# }
# requests.request(method='POST',
# url='http://127.0.0.1:8000/test/',
# files=file_dict)
# 傳送檔案,定製檔名
# file_dict = {
# 'f1': ('test.txt', open('readme', 'rb'))
# }
# requests.request(method='POST',
# url='http://127.0.0.1:8000/test/',
# files=file_dict)
# 傳送檔案,定製檔名
# file_dict = {
# 'f1': ('test.txt', "hahsfaksfa9kasdjflaksdjf")
# }
# requests.request(method='POST',
# url='http://127.0.0.1:8000/test/',
# files=file_dict)
# 傳送檔案,定製檔名
# file_dict = {
# 'f1': ('test.txt', "hahsfaksfa9kasdjflaksdjf", 'application/text', {'k1': '0'})
# }
# requests.request(method='POST',
# url='http://127.0.0.1:8000/test/',
# files=file_dict)
pass
def param_auth():
# 認證,瀏覽器BOM物件彈出對話方塊
# 在HTML文件中是找不到該標籤的,所以需要用這個對其進行傳入,一般來說常見於路由器登入頁面
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
ret = requests.get('https://api.github.com/user', auth=HTTPBasicAuth('wupeiqi', 'sdfasdfasdf'))
print(ret.text)
# ret = requests.get('http://192.168.1.1',
# auth=HTTPBasicAuth('admin', 'admin'))
# ret.encoding = 'gbk'
# print(ret.text)
# ret = requests.get('http://httpbin.org/digest-auth/auth/user/pass', auth=HTTPDigestAuth('user', 'pass'))
# print(ret)
#
def param_timeout():
# 超時時間,如果連結時間大於1秒就返回
# ret = requests.get('http://google.com/', timeout=1)
# print(ret)
# 如果連結時間大於5秒就返回,或者響應時間大於1秒就返回
# ret = requests.get('http://google.com/', timeout=(5, 1))
# print(ret)
pass
def param_allow_redirects():
ret = requests.get('http://127.0.0.1:8000/test/', allow_redirects=False)
print(ret.text)
def param_proxies():
# 配置代理
# proxies = {
# "http": "61.172.249.96:80",
# "https": "http://61.185.219.126:3128",
# }
# proxies = {'http://10.20.1.128': 'http://10.10.1.10:5323'}
# ret = requests.get("http://www.proxy360.cn/Proxy", proxies=proxies)
# print(ret.headers)
# from requests.auth import HTTPProxyAuth
#
# proxyDict = {
# 'http': '77.75.105.165',
# 'https': '77.75.105.165'
# }
# auth = HTTPProxyAuth('username', 'mypassword')
#
# r = requests.get("http://www.google.com", proxies=proxyDict, auth=auth)
# print(r.text)
pass
def param_stream():
# 檔案流,直接寫入檔案即可
ret = requests.get('http://127.0.0.1:8000/test/', stream=True)
print(ret.content)
ret.close()
# from contextlib import closing
# with closing(requests.get('http://httpbin.org/get', stream=True)) as r:
# # 在此處理響應。
# for i in r.iter_content():
# print(i)
session物件
如果爬取一個網站,該網站可能會返回給你一些cookies
,對這個網站後續的請求每次都要帶上這些cookies
比較麻煩。
所以可以直接使用session
物件(自動儲存cookies
)傳送請求,它會攜帶當前物件中所有的cookies
。
def requests_session():
import requests
# 使用session時,會攜帶該網站中所返回的所有cookies傳送下一次請求。
# 生成session物件
session = requests.Session()
### 1、首先登陸任何頁面,獲取cookie
i1 = session.get(url="http://dig.chouti.com/help/service")
### 2、使用者登陸,攜帶上一次的cookie,後臺對cookie中的 gpsd 進行授權
i2 = session.post(
url="http://dig.chouti.com/login",
data={
'phone': "8615131255089",
'password': "xxxxxx",
'oneMonth': ""
}
)
i3 = session.post(
url="http://dig.chouti.com/link/vote?linksId=8589623",
)
print(i3.text)
response物件
以下是response
物件的所有引數:
引數 | 描述 |
---|---|
response.text | 返回文字響應內容 |
response.content | 返回二進位制響應內容 |
response.json | 如果返回內容是json格式,則進行序列化 |
response.encoding | 返回響應內容的編碼格式 |
response.status_code | 狀態碼 |
response.headers | 返回頭 |
response.cookies | 返回的cookies物件 |
response.cookies.get_dict() | 以字典形式展示返回的cookies物件 |
response.cookies.items() | 以元組形式展示返回的cookies物件 |
response.url | 返回的url地址 |
response.history | 這是一個列表,如果請求被重定向,則將上一次被重定向的response物件新增到該列表中 |
編碼問題
並非所有網頁都是utf8
編碼,有的網頁是gbk
編碼。
此時如果使用txt
檢視響應內容就要指定編碼格式:
import requests
response=requests.get('http://www.autohome.com/news')
response.encoding='gbk'
print(response.text)
下載檔案
使用response.context
時,會將所有內容存放至記憶體中。
如果訪問的資源是一個大檔案,而需要對其進行下載時,可使用如下方式生成迭代器下載:
import requests
response=requests.get('http://bangimg1.dahe.cn/forum/201612/10/200447p36yk96im76vatyk.jpg')
with open("res.png","wb") as f:
for line in response.iter_content():
f.write(line)
json返回內容
如果確定返回內容是json
資料,則可以通過response.json
進行檢視:
import requests
response = requests.get("http://127.0.0.1:5000/index")
print(response.json())
歷史記錄
如果訪問一個地址卻被重定向了,被重定向的地址會被存放到response.history
這個列表中:
import requests
r = requests.get('http://127.0.0.1:5000/index') # 被重定向了
print(r.status_code) # 200
print(r.url) # http://127.0.0.1:5000/new # 重定向的地址
print(r.history)
# [<Response [302]>]
如果在請求時,指定allow_redirects
引數為False
,則禁止重定向:
import requests
r = requests.get('http://127.0.0.1:5000/index',allow_redirects=False) # 禁止重定向
print(r.status_code) # 302
print(r.url) # http://127.0.0.1:5000/index
print(r.history)
# []
bs4模組
request
模組可以傳送請求,獲取HTML
文件內容。
而bs4
模組可以解析出HTML
與XML
文件的內容,如快速查詢標籤等等。
pip3 install bs4
bs4模組只能在Python中使用
bs4
依賴解析器,雖然有自帶的解析器,但是目前使用最多的還是lxml
:
pip3 install lxml
基本使用
將request
模組請求回來的HTML
文件內容轉換為bs4
物件,使用其下的方法進行查詢:
如下示例,解析出蝦米音樂中的歌曲,歌手,歌曲時長:
import requests
from bs4 import BeautifulSoup
from prettytable import PrettyTable
# 例項化表格
table = PrettyTable(['編號', '歌曲名稱', '歌手', '歌曲時長'])
url = r"https://www.xiami.com/list?page=1&query=%7B%22genreType%22%3A1%2C%22genreId%22%3A%2220%22%7D&scene=genre&type=song"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
}
response = requests.get(url=url, headers=headers)
# step01: 將文字內容例項化出bs物件
soup_obj = BeautifulSoup(response.text, "lxml")
# step02: 查詢標籤
main = soup_obj.find("div", attrs={"class": "table idle song-table list-song"})
# step03: 查詢存放歌曲資訊的tbody標籤
tbody = main.select(".table-container>table>tbody")[0]
# step04: tbody標籤中的每個tr都是一首歌曲
tr = tbody.find_all("tr")
# step04: 每個tr裡都存放有歌曲資訊,所以直接迴圈即可
for music in tr:
name = music.select(".song-name>a")[0].text
singer = music.select(".COMPACT>a")[0].text
time_len = music.select(".duration")[0].text
table.add_row([tr.index(music) + 1, name, singer, time_len])
# step05: 列印資訊
print(table)
結果如下:
+------+--------------------------------------------------+--------------------+----------+
| 編號 | 歌曲名稱 | 歌手 | 歌曲時長 |
+------+--------------------------------------------------+--------------------+----------+
| 1 | Love Story (Live from BBC 1's Radio Live Lounge) | Taylor Swift | 04:25 |
| 2 | Five Hundred Miles | Jove | 03:27 |
| 3 | I'm Gonna Getcha Good! (Red Album Version) | Shania Twain | 04:30 |
| 4 | Your Man | Josh Turner | 03:45 |
| 5 | Am I That Easy To Forget | Jim Reeves | 02:22 |
| 6 | Set for Life | Trent Dabbs | 04:23 |
| 7 | Blue Jeans | Justin Rutledge | 04:25 |
| 8 | Blind Tom | Grant-Lee Phillips | 02:59 |
| 9 | Dreams | Slaid Cleaves | 04:14 |
| 10 | Remember When | Alan Jackson | 04:31 |
| 11 | Crying in the Rain | Don Williams | 03:04 |
| 12 | Only Worse | Randy Travis | 02:53 |
| 13 | Vincent | The Sunny Cowgirls | 04:22 |
| 14 | When Your Lips Are so Close | Gord Bamford | 03:02 |
| 15 | Let It Be You | Ricky Skaggs | 02:42 |
| 16 | Steal a Heart | Tenille Arts | 03:09 |
| 17 | Rylynn | Andy McKee | 05:13 |
| 18 | Rockin' Around The Christmas Tree | Brenda Lee | 02:06 |
| 19 | Love You Like a Love Song | Megan & Liz | 03:17 |
| 20 | Tonight I Wanna Cry | Keith Urban | 04:18 |
| 21 | If a Song Could Be President | Over the Rhine | 03:09 |
| 22 | Shut'er Down | Doug Supernaw | 04:12 |
| 23 | Falling | Jamestown Story | 03:08 |
| 24 | Jim Cain | Bill Callahan | 04:40 |
| 25 | Parallel Line | Keith Urban | 04:14 |
| 26 | Jingle Bell Rock | Bobby Helms | 04:06 |
| 27 | Unsettled | Justin Rutledge | 04:01 |
| 28 | Bummin' Cigarettes | Maren Morris | 03:07 |
| 29 | Cheatin' on Her Heart | Jeff Carson | 03:18 |
| 30 | If My Heart Had a Heart | Cassadee Pope | 03:21 |
+------+--------------------------------------------------+--------------------+----------+
Process finished with exit code 0
HTML文件
準備一個HTML
文件,對他進行解析:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<form action="#" method="post" enctype="multipart/form-data">
<fieldset>
<legend><h1>歡迎註冊</h1></legend>
<p>頭像: <input type="file" name="avatar"/></p>
<p>使用者名稱: <input type="text" name="username" placeholder="請輸入使用者名稱"/></p>
<p>密碼: <input type="text" name="pwd" placeholder="請輸入密碼"/></p>
<p>性別: 男<input type="radio" name="gender" value="male"/>女<input type="radio" name="gender" value="female"/></p>
<p>愛好: 籃球<input type="checkbox" name="hobby" value="basketball" checked/>足球<input type="checkbox" name="hobby"
value="football"/></p>
居住地
<select name="addr">
<optgroup label="中國">
<option value="bejing" selected>北京</option>
<option value="shanghai">上海</option>
<option value="guangzhou">廣州</option>
<option value="shenzhen">深圳</option>
<option value="other">其他</option>
</optgroup>
<optgroup label="海外">
<option value="America">美國</option>
<option value="Japanese">日本</option>
<option value="England">英國</option>
<option value="Germany">德國</option>
<option value="Canada">加拿大</option>
</optgroup>
</select>
</fieldset>
<fieldset>
<legend>請填寫註冊理由</legend>
<p><textarea name="register_reason" cols="30" rows="10" placeholder="請填寫充分理由"></textarea></p>
</fieldset>
<p><input type="reset" value="重新填寫資訊"/> <input type="submit" value="提交註冊資訊"> <input type="butoon"
value="聯絡客服" disabled>
</p>
</form>
</body>
</html>
基本選擇器
基本選擇器如下 :
選擇器方法 | 描述 |
---|---|
TagName | 唯一選擇器,根據標籤名來選擇 |
find() | 唯一選擇器,可根據標籤名、屬性來做選擇 |
select_one() | 唯一選擇器,可根據CSS選擇器語法做選擇 |
find_all() | 集合選擇器,可根據標籤名、屬性來做選擇 |
select() | 集合選擇器,可根據CSS選擇器語法做選擇 |
.TagName
選擇器只會拿出第一個匹配的內容,必須根據標籤名選擇:
input = soup.input
print(input)
# <input name="avatar" type="file"/>
.find()
選擇器只會拿出第一個匹配的內容,可根據標籤名、屬性來做選擇
input= soup.find("input",attrs={"name":"username","type":"text"}) # attrs指定屬性
print(input)
# <input name="username" placeholder="請輸入使用者名稱" type="text"/>
.select_one()
根據css
選擇器來查詢標籤,只獲取第一個:
input = soup.select_one("input[type=text]")
print(input)
# <input name="username" placeholder="請輸入使用者名稱" type="text"/>
.find_all()
可獲取所有匹配的標籤,返回一個list
,可根據標籤名、屬性來做選擇
input_list = soup.find_all("input",attrs={"type":"text"})
print(input_list)
# [<input name="username" placeholder="請輸入使用者名稱" type="text"/>, <input name="pwd" placeholder="請輸入密碼" type="text"/>]
.select()
根據css
選擇器獲取所有匹配的標籤,返回一個list
input_list = soup.select("input[type=text]")
print(input_list)
# [<input name="username" placeholder="請輸入使用者名稱" type="text"/>, <input name="pwd" placeholder="請輸入密碼" type="text"/>]
關係與操作
使用較少,選讀:
屬性/方法 | 描述 |
---|---|
children | 獲取所有的後代標籤,返回迭代器 |
descendants | 獲取所有的後代標籤,返回生成器 |
index() | 檢查某個標籤在當前標籤中的索引值 |
clear() | 刪除後代標籤,保留本標籤,相當於清空 |
decompose() | 刪除標籤本身(包括所有後代標籤) |
extract() | 同.decomponse()效果相同,但會返回被刪除的標籤 |
decode() | 將當前標籤與後代標籤轉換字串 |
decode_contents() | 將當前標籤的後代標籤轉換為字串 |
encode() | 將當前標籤與後代標籤轉換位元組串 |
encode_contents() | 將當前標籤的後代標籤轉換為位元組串 |
append() | 在當前標籤內部追加一個標籤(無示例) |
insert() | 在當前標籤內部指定位置插入一個標籤(無示例) |
insert_before() | 在當前標籤前面插入一個標籤(無示例) |
insert_after() | 在當前標籤後面插入一個標籤(無示例) |
replace_with() | 將當前標籤替換為指定標籤(無示例) |
.children
獲取所有的後代標籤,返回迭代器
form = soup.find("form")
print(form.children)
# <list_iterator object at 0x0000025665D5BDD8>
.descendants
獲取所有的後代標籤,返回生成器
form = soup.find("form")
print(form.descendants)
# <generator object descendants at 0x00000271C8F0ACA8>
.index()
檢查某個標籤在當前標籤中的索引值
body = soup.find("body")
form = soup.find("form")
print(body.index(form))
# 3
.clear()
刪除後代標籤,保留本標籤,相當於清空
form = soup.find("form")
form.clear()
print(form) # None
print(soup)
# 清空了form
.decompose()
刪除標籤本身(包括所有後代標籤)
form = soup.find("form")
form..decompose()
print(form) # None
print(soup)
# 刪除了form
.extract()
同.decomponse()
效果相同,但會返回被刪除的標籤
form = soup.find("form")
form..extract()
print(form) # 被刪除的內容
print(soup)
# 被刪除了form
.decode()
將當前標籤與後代標籤轉換字串,.decode_contents()
將當前標籤的後代標籤轉換為字串
form = soup.find("form")
print(form.decode()) # 包含form
print(form.decode_contents()) # 不包含form
.encode()
將當前標籤與後代標籤轉換位元組串,.encode_contents()
將當前標籤的後代標籤轉換為位元組串
form = soup.find("form")
print(form.encode()) # 包含form
print(form.encode_contents()) # 不包含form
標籤內容
以下方法都比較常用:
屬性/方法 | 描述 |
---|---|
name | 獲取標籤名稱 |
attrs | 獲取標籤屬性 |
text | 獲取該標籤下的所有文字內容(包括後代) |
string | 獲取該標籤下的直系文字內容 |
is_empty_element | 判斷是否是空標籤或者自閉合標籤 |
get_text() | 獲取該標籤下的所有文字內容(包括後代) |
has_attr() | 檢查標籤是否具有該屬性 |
.name
獲取標籤名稱
form = soup.find("form")
print(form.name)
# form
.attrs
獲取標籤屬性
form = soup.find("form")
print(form.attrs)
# {'action': '#', 'method': 'post', 'enctype': 'multipart/form-data'}
.is_empty_element
判斷是否是空標籤或者自閉合標籤
input = soup.find("input")
print(input.is_empty_element)
# True
.get_text()
與text
獲取該標籤下的所有文字內容(包括後代)
form = soup.find("form")
print(form.get_text())
print(form.text)
string
獲取該標籤下的直系文字內容
form = soup.find("form")
print(form.get_text())
print(form.string)
.has_attr()
檢查標籤是否具有該屬性
form = soup.find("form")
print(form.has_attr("action"))
# True
xPath模組
xPath
模組的作用與bs4
相同,都是查詢標籤。
但是xPath
模組的通用性更強,它的語法規則並不限於僅在Python
中使用。
作為一門小型的專業化查詢語言,xPath
在Python
中被整合在了lxml
模組中,所以直接下載安裝就可以開始使用了。
pip3 install lxml
載入文件:
from lxml import etree
# 解析網路爬取的html原始碼
root = etree.HTML(response.text,,etree.HTMLParser()) # 載入整個HTML文件,並且返回根節點<html>
# 解析本地的html檔案
root = etree.parse(fileName,etree.HTMLParser())
基本選取符
基本選取符:
符號 | 描述 |
---|---|
/ | 從根節點開始選取 |
// | 不考慮層級關係的選取節點 |
. | 選取當前節點 |
.. | 選取當前節點的父節點 |
@ | 屬性檢測 |
[num] | 選取第n個標籤元素,從1開始 |
/@attrName | 選取當前元素的某一屬性 |
* | 萬用字元 |
/text() | 選取當前節點下的直系文字內容 |
//text() | 選取當前文字下的所有文字內容 |
| | 返回符號兩側所匹配的全部標籤 |
以下是示例:
注意:xPath選擇完成後,返回的始終是一個list,與jQuery類似,可以通過Index取出Element物件
from lxml import etree
root = etree.parse("./testDataDocument.html",etree.HTMLParser())
# 從根節點開始找 /
form_list = root.xpath("/html/body/form")
print(form_list) # [<Element form at 0x203bd29c188>]
# 不考慮層級關係的選擇節點 //
input_list = root.xpath("//input")
print(input_list)
# 從當前的節點開始選擇 即第一個form表單 ./
select_list = form_list[0].xpath("./fieldset/select")
print(select_list)
# 選擇當前節點的父節點 ..
form_parent_list = form_list[0].xpath("..")
print(form_parent_list) # [<Element body at 0x1c946e4c548>]
# 屬性檢測 @ 選取具有name屬性的input框
input_username_list = root.xpath("//input[@name='username']")
print(input_username_list)
# 屬性選取 @ 獲取元素的屬性
attrs_list = root.xpath("//p/@title")
print(attrs_list)
# 選取第n個元素,從1開始
p_text_list = root.xpath("//p[2]/text()")
print(p_text_list)
# 萬用字元 * 選取所有帶有屬性的標籤
have_attrs_ele_list = root.xpath("//*[@*]")
print(have_attrs_ele_list)
# 獲取文字內容-直系
print(root.xpath("//form/text()"))
# 結果:一堆\r\n
# 獲取文字內容-非直系
print(root.xpath("//form//text()"))
# 結果:本身和後代的text
# 返回所有input與p標籤
ele_list = root.xpath("//input|//p")
print(ele_list)
表示式形式
你可以指定邏輯運算子,大於小於等。
from lxml import etree
root = etree.parse("./testDataDocument.html",etree.HTMLParser())
# 返回屬性值price大於或等於20的標籤
price_ele_list = root.xpath("//*[@price>=20]")
print(price_ele_list)
xPath軸關係
xPath
中擁有軸這一概念,不過相對來說使用較少,它就是做關係用的。瞭解即可:
軸 | 示例 | 說明 |
---|---|---|
ancestor | xpath(‘./ancestor::*’) | 選取當前節點的所有先輩節點(父、祖父) |
ancestor-or-self | xpath(‘./ancestor-or-self::*’) | 選取當前節點的所有先輩節點以及節點本身 |
attribute | xpath(‘./attribute::*’) | 選取當前節點的所有屬性 |
child | xpath(‘./child::*’) | 返回當前節點的所有子節點 |
descendant | xpath(‘./descendant::*’) | 返回當前節點的所有後代節點(子節點、孫節點) |
following | xpath(‘./following::*’) | 選取文件中當前節點結束標籤後的所有節點 |
following-sibing | xpath(‘./following-sibing::*’) | 選取當前節點之後的兄弟節點 |
parent | xpath(‘./parent::*’) | 選取當前節點的父節點 |
preceding | xpath(‘./preceding::*’) | 選取文件中當前節點開始標籤前的所有節點 |
preceding-sibling | xpath(‘./preceding-sibling::*’) | 選取當前節點之前的兄弟節點 |
self | xpath(‘./self::*’) | 選取當前節點 |
功能函式
功能函式更多的是做模糊搜尋,這裡舉幾個常見的例子,一般使用也不多:
函式 | 示例 | 描述 |
---|---|---|
starts-with | xpath(‘//div[starts-with(@id,”ma”)]‘) | 選取id值以ma開頭的div節點 |
contains | xpath(‘//div[contains(@id,”ma”)]‘) | 選取id值包含ma的div節點 |
and | xpath(‘//div[contains(@id,”ma”) and contains(@id,”in”)]‘) | 選取id值包含ma和in的div節點 |
text() | xpath(‘//div[contains(text(),”ma”)]‘) | 選取節點文字包含ma的div節點 |
element物件
上面說過,使用xPath
進行篩選後得到的結果都是一個list
,其中的成員就是element
標籤物件。
以下方法都是操縱element
標籤物件的,比較常用。
首先是針對自身標籤的操作:
屬性 | 描述 |
---|---|
tag | 返回元素的標籤型別 |
text | 返回元素的直系文字 |
tail | 返回元素的尾行 |
attrib | 返回元素的屬性(字典形式) |
演示如下:
from lxml import etree
root = etree.parse("./testDataDocument.html",etree.HTMLParser())
list(map(lambda ele:print(ele.tag),root.xpath("//option")))
list(map(lambda ele:print(ele.text),root.xpath("//option"))) # 常用
list(map(lambda ele:print(ele.tail),root.xpath("//option")))
list(map(lambda ele:print(ele.attrib),root.xpath("//option"))) # 常用
針對當前element
物件屬性的操作,用的不多:
方法 | 描述 |
---|---|
clear() | 清空元素的後代、屬性、text和tail也設定為None |
get() | 獲取key對應的屬性值,如該屬性不存在則返回default值 |
items() | 根據屬性字典返回一個列表,列表元素為(key, value) |
keys() | 返回包含所有元素屬性鍵的列表 |
set() | 設定新的屬性鍵與值 |
針對當前element
物件後代的操作,用的更少:
方法 | 描述 |
---|---|
append() | 新增直系子元素 |
extend() | 增加一串元素物件作為子元素 |
find() | 尋找第一個匹配子元素,匹配物件可以為tag或path |
findall() | 尋找所有匹配子元素,匹配物件可以為tag或path |
findtext() | 尋找第一個匹配子元素,返回其text值。匹配物件可以為tag或path |
insert() | 在指定位置插入子元素 |
iter() | 生成遍歷當前元素所有後代或者給定tag的後代的迭代器 |
iterfind() | 根據tag或path查詢所有的後代 |
itertext() | 遍歷所有後代並返回text值 |
remove() | 刪除子元素 |
高效能爬蟲
後端準備
Flask
作為後端伺服器:
from flask import Flask
import time
app = Flask(__name__,template_folder="./")
@app.route('/index',methods=["GET","POST"])
def index():
time.sleep(2)
return "index...ok!!!"
@app.route('/news')
def news():
time.sleep(2)
return "news...ok!!!"
@app.route('/hot')
def hot():
time.sleep(2)
return "hot...ok!!!"
if __name__ == '__main__':
app.run()
同步爬蟲
如果使用同步爬蟲對上述伺服器的三個url
進行爬取,花費的結果是六秒:
import time
from requests import Session
headers = {
"user-agent": "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}
urls = [
"http://127.0.0.1:5000/index",
"http://127.0.0.1:5000/news",
"http://127.0.0.1:5000/hot",
]
start = time.time()
def func(url):
session = Session()
response = session.get(url)
return response.text
# 回撥函式,處理後續任務
def callback(result): # 獲取結果
print(result)
for url in urls:
res = func(url)
callback(res)
end = time.time()
print("總用時:%s秒" % (end - start))
ThreadPoolExecutor
使用多執行緒則基本兩秒左右即可完成:
import time
from concurrent.futures import ThreadPoolExecutor
from requests import Session
headers = {
"user-agent": "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}
urls = [
"http://127.0.0.1:5000/index",
"http://127.0.0.1:5000/news",
"http://127.0.0.1:5000/hot",
]
start = time.time()
def func(url):
session = Session()
response = session.get(url)
return response.text
# 回撥函式
def callback(obj): # 期程物件
print(obj.result())
pool = ThreadPoolExecutor(max_workers=4)
for url in urls:
res = pool.submit(func, url)
# 為期程物件繫結回撥
res.add_done_callback(callback)
pool.shutdown(wait=True)
end = time.time()
print("總用時:%s秒" % (end - start))
asyncio&aiohttp
執行緒的切換開銷較大,可使用切換代價更小的協程進行實現。
由於協程中不允許同步方法的出現,requests
模組下的請求方法都是同步請求方法,所以需要使用aiohttp
模組下的非同步請求方法完成網路請求。
現今的所謂非同步,其實都是用I/O
多路複用技術來完成,即在一個執行緒下進行where
迴圈,監聽描述符,即eventLoop
。
import asyncio
import time
import aiohttp
headers = {
"user-agent": "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}
urls = [
"http://127.0.0.1:5000/index",
"http://127.0.0.1:5000/news",
"http://127.0.0.1:5000/hot",
]
start = time.time()
async def func(url):
# 在async協程中,所有的阻塞方法都需要通過await手動掛起
# 並且,如果存在同步方法,則還是同步執行,必須是非同步方法,所以這裡使用aiohttp模組傳送請求
async with aiohttp.ClientSession() as session:
async with await session.get(url) as response:
# text():返回字串形式的響應資料
# read(): 返回二進位制格式響應資料
# json(): json格式反序列化
result = await response.text() # aiohttp中是一個方法
return result
# 回撥函式
def callback(obj): # 期程物件
print(obj.result())
# 建立協程任務列表
tasks = []
for url in urls:
g = func(url) # 建立協程任務g
task = asyncio.ensure_future(g) # 註冊協程任務
task.add_done_callback(callback) # 繫結回撥,傳入期程物件
tasks.append(task) # 新增協程任務到任務列表
# 建立事件迴圈
loop = asyncio.get_event_loop()
# 執行任務,並且主執行緒會等待協程任務列表中的所有任務處理完畢後再執行
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print("總用時:%s秒" % (end - start))
selenium模組
selenium
是Python
實現瀏覽器自動化操作的一款模組。
通過它我們可以讓程式碼控制瀏覽器,從而進行資料爬取,尤其在以下兩個地方該模組的作用更加強大:
- 獲取整張頁面的資料,對有的頁面來說前後端分離的API介面太難找了,使用requests模組根本找不到傳送載入資料的介面
- 進行自動登入
下載安裝:
pip3 install selenium
由於要操縱瀏覽器,所以要下載對應的驅動檔案,需要注意的是驅動版本需要與瀏覽器版本一一對應:
如果是MAC
平臺,解壓到如下路徑,win
平臺解壓到任意位置皆可:
/usr/local/bin
由於我們使用的是chorme
瀏覽器,所以只需要例項化出其操縱物件即可:
from selenium import webdriver
driver = webdriver.Chrome()
以後的操縱都是操縱該例項物件,如果你使用其他版本瀏覽器,請自行下載驅動,支援的瀏覽器如下:
driver = webdriver.Firefox()
driver = webdriver.Edge()
driver = webdriver.PhantomJS()
driver = webdriver.Safari()
基本使用
以下是基本操縱例項,例項將展示如何搜尋部落格園:
from selenium import webdriver
import time
# 載入驅動
driver = webdriver.Chrome(r"./chromedriver.exe")
# 開啟百度頁面
driver.get("https://www.baidu.com")
# 找到搜尋框,輸入部落格園
driver.find_element_by_id("kw").send_keys("部落格園")
time.sleep(2)
driver.find_element_by_id('su').click()
time.sleep(2)
# 關閉瀏覽器
driver.quit()
元素定位
webdriver
提供了很多元素定位方法,常用的如下:
driver.find_element_by_id()
driver.find_element_by_name()
driver.find_element_by_class_name()
driver.find_element_by_tag_name()
driver.find_element_by_link_text()
driver.find_element_by_partial_link_text()
driver.find_element_by_xpath()
driver.find_element_by_css_selector()
ifarme定位
對於webdriver
來說,它擁有一層作用域。
預設是在頂級作用域中,如果出現了ifarme
標籤,則必須切換到ifarme
標籤的作用域才能查詢其裡面的元素。
如下,想查詢其中的button
:
<div id="modal">
<iframe id="buttonframe"name="myframe"src="https://seleniumhq.github.io">
<button>Click here</button>
</iframe>
</div>
如果直接獲取button
則不會生效,因為目前作用域是外部的html
標籤中,不能獲取內部iframe
的作用域:
# 這不會工作
driver.find_element(By.TAG_NAME, 'button').click()
正確的方法是找到ifarme
標籤,對其進行切換作用域的操作:
# 儲存網頁元素
iframe = driver.find_element(By.CSS_SELECTOR, "#modal > iframe")
# 切換到選擇的 iframe
driver.switch_to.frame(iframe)
# 單擊按鈕
driver.find_element(By.TAG_NAME, 'button').click()
如果您的frame
或iframe
具有id
或name
屬性,則可以使用該屬性。如果名稱或 id 在頁面上不是唯一的, 那麼將切換到找到的第一個。
# 通過 id 切換框架
driver.switch_to.frame('buttonframe')
# 單擊按鈕
driver.find_element(By.TAG_NAME, 'button').click()
還可以通過索引值進行切換:
# 切換到第 2 個框架
driver.switch_to.frame(1)
退出當前iframe
的作用域,使用以下程式碼:
# 切回到預設內容
driver.switch_to.default_content()
互動相關
我們可以與瀏覽器BOM
或者element
進行互動。
如找到搜尋框,使用send_keys()
即可輸入內容,clear()
即可清空內容。
再比如找到button
使用click()
即可觸發單擊事件。
更多方法請參照官方文件,截圖也在其中:
動作鏈
如果碰到滑動驗證的操作,則需要使用動作鏈進行。
上述的互動中,如send_keys()
與click()
都是一次性完成的,如果是非一次性的操作如拖拽,滑動的就可以通過動作鏈完成。
動作鏈的官方文件,包括獲取當前元素的大小,配合截圖使用有奇效,舉個例子,截圖到當前的驗證碼頁面,然後使用第三方打碼工具進行解析驗證碼:
from selenium import webdriver
from time import sleep
#匯入動作鏈對應的類
from selenium.webdriver import ActionChains
bro = webdriver.Chrome(executable_path='./chromedriver')
bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')
#如果定位的標籤是存在於iframe標籤之中的則必須通過如下操作在進行標籤定位
bro.switch_to.frame('iframeResult')#切換瀏覽器標籤定位的作用域
div = bro.find_element_by_id('draggable')
#動作鏈
action = ActionChains(bro)
#點選長按指定的標籤
action.click_and_hold(div)
for i in range(5):
#perform()立即執行動作鏈操作
#move_by_offset(x,y):x水平方向 y豎直方向
action.move_by_offset(17,0).perform()
sleep(0.5)
#釋放動作鏈
action.release()
bro.quit()
執行指令碼
如果webdriver
例項中沒有實現某些方法,則可以通過執行Js
程式碼來完成,比如下拉滑動條:
from selenium import webdriver
driver = webdriver.Chrome(r"./chromedriver.exe")
driver.get('https://www.jd.com/')
# 執行指令碼:滑動整個頁面
driver.execute_script('window.scrollTo(0, document.body.scrollHeight)')
原始碼資料
上面提到過,如果使用requets
模組訪問某一url
卻沒有拿到想要的資料,那麼很可能是前後端分離通過RESTful APIs
進行資料互動。
這個時候我們可以使用selenium
模組來對同一url
發起請求,由於是瀏覽器開啟,所有的RESTFUL API
都會進行請求,然後直接通過屬性page_source
解析返回的原始碼資料:
from selenium import webdriver
from lxml import etree
driver=webdriver.Chrome(r"./chromedriver.exe",)
driver.get('https://www.baidu.com/')
source_code = driver.page_source # 獲取網頁原始碼
# 直接獲取百度的圖片地址
root = etree.HTML(source_code,parser=etree.HTMLParser())
driver.close()
img_src = "http:" + root.xpath(r"//*[@id='s_lg_img_new']")[0].attrib.get("src")
print(img_src)
節點操作
上面我們通過使用lxml
模組來解析原始碼中的百度圖片地址,其實可以不用這麼麻煩。
Selenium
也提供了節點操作,選取節點、獲取屬性等:
from selenium import webdriver
from selenium.webdriver.common.by import By # 按照什麼方式查詢,By.ID,By.CSS_SELECTOR
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait # 等待頁面載入某些元素
driver = webdriver.Chrome(r"./chromedriver.exe",)
driver.get('https://www.amazon.cn/')
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.ID, 'cc-lm-tcgShowImgContainer')))
tag = driver.find_element(By.CSS_SELECTOR, '#cc-lm-tcgShowImgContainer img')
# 獲取標籤屬性,
print(tag.get_attribute('src'))
# 獲取標籤ID,位置,名稱,大小(瞭解)
print(tag.id)
print(tag.location)
print(tag.tag_name)
print(tag.size)
driver.close()
延時等待
在Selenium
中,get()
方法會在網頁框架載入結束後結束執行,此時如果獲取page_source
,可能並不是瀏覽器完全載入完成的頁面,如果某些頁面有額外的Ajax
請求,我們在網頁原始碼中也不一定能成功獲取到。所以,這裡需要延時等待一定時間,確保節點已經載入出來。這裡等待的方式有兩種:一種是隱式等待,一種是顯式等待。
隱式等待:
當使用隱式等待執行測試的時候,如果Selenium
沒有在DOM
中找到節點,將繼續等待,超出設定時間後,則丟擲找不到節點的異常。換句話說,當查詢節點而節點並沒有立即出現的時候,隱式等待將等待一段時間再查詢DOM
,預設的時間是0。示例如下:
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By #按照什麼方式查詢,By.ID,By.CSS_SELECTOR
from selenium.webdriver.common.keys import Keys #鍵盤按鍵操作
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait #等待頁面載入某些元素
driver=webdriver.Chrome(r"./chromedriver.exe",)
#隱式等待:在查詢所有元素時,如果尚未被載入,則等10秒
driver.implicitly_wait(10)
driver.get('https://www.baidu.com')
input_tag=driver.find_element_by_id('kw')
input_tag.send_keys('美女')
input_tag.send_keys(Keys.ENTER)
contents=driver.find_element_by_id('content_left') #沒有等待環節而直接查詢,找不到則會報錯
print(contents)
driver.close()
顯示等待:
隱式等待的效果其實並沒有那麼好,因為我們只規定了一個固定時間,而頁面的載入時間會受到網路條件的影響。這裡還有一種更合適的顯式等待方法,它指定要查詢的節點,然後指定一個最長等待時間。如果在規定時間內載入出來了這個節點,就返回查詢的節點;如果到了規定時間依然沒有載入出該節點,則丟擲超時異常。
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By #按照什麼方式查詢,By.ID,By.CSS_SELECTOR
from selenium.webdriver.common.keys import Keys #鍵盤按鍵操作
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait #等待頁面載入某些元素
driver=webdriver.Chrome(r"./chromedriver.exe",)
driver.get('https://www.baidu.com')
input_tag=driver.find_element_by_id('kw')
input_tag.send_keys('美女')
input_tag.send_keys(Keys.ENTER)
#顯式等待:顯式地等待某個元素被載入
wait=WebDriverWait(driver,10)
wait.until(EC.presence_of_element_located((By.ID,'content_left')))
contents=driver.find_element(By.CSS_SELECTOR,'#content_left')
print(contents)
driver.close()
關於等待條件,其實還有很多,比如判斷標題內容,判斷某個節點內是否出現了某文字等。more
cookie操作
使用Selenium
,還可以方便地對Cookies
進行操作,例如獲取、新增、刪除Cookies
等。示例如下:
from selenium import webdriver
driver = webdriver.Chrome(r"./chromedriver.exe",)
driver.get('https://www.zhihu.com/explore')
print(driver.get_cookies())
driver.add_cookie({'name': 'name', 'domain': 'www.zhihu.com', 'value': 'germey'})
print(driver.get_cookies())
driver.delete_all_cookies()
print(driver.get_cookies())
異常處理
遮蔽掉所有可能出現的異常:
from selenium import webdriver
from selenium.common.exceptions import TimeoutException,NoSuchElementException,NoSuchFrameException
try:
driver=webdriver.Chrome()
driver.get('http://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')
driver.switch_to.frame('iframssseResult')
except TimeoutException as e:
print(e)
except NoSuchFrameException as e:
print(e)
finally:
driver.close()
無頭操作
每次使用selenium
時都會開啟一個瀏覽器,能不能有什麼辦法讓他隱藏介面呢?
指定引數即可,這種沒有介面的瀏覽也可以稱其為無頭瀏覽器:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# 設定配置項
chrome_options = Options()
chrome_options.add_argument('--headless')
# 指定配置
driver = webdriver.Chrome(r"./chromedriver.exe",chrome_options=chrome_options)
driver.get("http://www.baidu.com")
driver.close()
規避檢測
可能有的入口網站已經對selenium
做出了檢測,如果檢測到是該指令碼執行可能不允許你訪問API
,此時就可以通過偽造資訊達到潛行的效果。
將selenium
偽裝成人為操作:
#實現規避檢測
from selenium.webdriver import ChromeOptions
#實現規避檢測
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])
# 指定配置
driver = webdriver.Chrome(executable_path='./chromedriver',options=option)
driver.get("http://www.baidu.com")
driver.close()
Scrapy框架基礎
基本介紹
Scrapy
框架是Python
中最出名的一款爬蟲框架,本身基於twisted
非同步框架封裝完成。
它有著基本的五大元件,整個框架架構如下圖所示:
Scrapy 元件介紹
-
引擎(EGINE)
引擎負責控制系統所有元件之間的資料流,並在某些動作發生時觸發事件。有關詳細資訊,請參見下面的資料流部分。
-
排程器(SCHEDULER) 用來接受引擎發過來的請求, 壓入佇列中, 並在引擎再次請求的時候返回. 可以想像成一個URL的優先順序佇列, 由它來決定下一個要抓取的網址是什麼, 同時去除重複的網址
-
下載器(DOWLOADER) 用於下載網頁內容, 並將網頁內容返回給EGINE,下載器是建立在twisted這個高效的非同步模型上的
-
爬蟲(SPIDERS) SPIDERS是開發人員自定義的類,用來解析responses,並且提取items,或者傳送新的請求
-
專案管道(ITEM PIPLINES) 在items被提取後負責處理它們,主要包括清理、驗證、持久化(比如存到資料庫)等操作
-
下載器中介軟體(Downloader Middlewares)
位於Scrapy引擎和下載器之間,主要用來處理從EGINE傳到DOWLOADER的請求request,已經從DOWNLOADER傳到EGINE的響應response,你可用該中介軟體做以下幾件事
- 在將請求傳送到下載器之前處理請求(即,在Scrapy將請求傳送到網站之前);
- 在傳遞給SPIDERS之前更改收到的響應;
- 傳送新的請求,而不是將收到的響應傳遞給SPIDERS;
- 將響應傳遞給SPIDERS,而無需獲取網頁;
- 默默地丟棄一些請求。
-
爬蟲中介軟體(Spider Middlewares) 位於EGINE和SPIDERS之間,主要工作是處理SPIDERS的輸入(即responses)和輸出(即requests)
整個爬取的資料流:
- 引擎開啟一個網站(open a domain),找到處理該網站的Spider並向該Spider請求第一個要爬取的URL(s)。
- 引擎從Spider中獲取到第一個要爬取的URL並在排程器(Scheduler)以Request排程。
- 引擎向排程器請求下一個要爬取的URL。
- 排程器返回下一個要爬取的URL給引擎,引擎將URL通過下載中介軟體(請求(request)方向)轉發給下載器(Downloader)。
- 一旦頁面下載完畢,下載器生成一個該頁面的Response,並將其通過下載中介軟體(返回(response)方向)傳送給引擎。
- 引擎從下載器中接收到Response並通過Spider中介軟體(輸入方向)傳送給Spider處理。
- Spider處理Response並返回爬取到的Item及(跟進的)新的Request給引擎。
- 引擎將(Spider返回的)爬取到的Item給Item Pipeline,將(Spider返回的)Request給排程器。
- (從第二步)重複直到排程器中沒有更多地request,引擎關閉該網站。
下載安裝
在MAC/LINUX
下安裝該框架十分簡單:
pip3 install scrapy
如果是Windows
平臺,則稍微有些麻煩,因為你需要安裝很多依賴庫:
pip3 install wheel # 安裝後,便支援通過wheel檔案安裝軟體,wheel檔案官網:https://www.lfd.uci.edu/~gohlke/pythonlibs
pip3 install lxml
pip3 install pyopenssl
下載並安裝pywin32
:
pip3 install pywin32
下載並安裝twisted
的wheel
檔案,CP
對應Python
版本:
# 下載whell檔案:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
# pip3 install 下載目錄\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
安裝Scrapy
:
pip3 install scrapy
安裝完成後,在終端輸入scrapy
,如果有反應則代表安裝成功。
如果沒有反應,重新安裝scrapy
:
pip uninstall scrapy
pip3 install scrapy
它會給你一個提示:
Installing collected packages: scrapy
WARNING: The script scrapy.exe is installed in 'C:\Users\yunya\AppData\Roaming\Python\Python36\Scripts' which is not on PATH.
Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed scrapy-2.4.1
你需要將提示中的路徑加入環境變數即可。
命令列工具
以下是常用的命令列,首先是全域性命令,即意味著你在終端中任何目錄下都能夠執行:
命令 | 描述 |
---|---|
scrapy -h | 檢視幫助 |
scrapy [command] -h | 檢視某條命令的幫助 |
scrapy startproject [ProjectName] | 建立專案 |
scrapy genspider [SpiderName] <ur> | 建立爬蟲程式 |
scrapy settings [options] [command] | 檢視爬蟲的程式配置資訊,如果是在專案下,則獲取到專案的部署配置資訊 |
scrapy runspider [options] <spider_file> | 單獨的執行某一個py檔案 |
scrapy fetch [options] <url> | 獨立爬取一個頁面,可以拿到請求頭,如 scrapy fetch --headers http://www.baidu.com |
scrapy shell [options] <url> | 開啟shell除錯,直接向某一地址傳送請求 |
scrapy view [options] <url> | 開啟瀏覽器,傳送本次請求 |
scrapy version [-v] | 檢視scrapy的版本,新增-v檢視scrapy依賴庫的版本 |
其次是區域性命令,指只有在Scrapy
專案下執行才能生效的命令:
命令 | 描述 |
---|---|
scrapy crawl [options] <spider> | 執行爬蟲程式,必須建立專案才行,確保配置檔案中ROBOTSTXT_OBEY = False |
scrapy check [options] <spider> | 檢測爬蟲程式中語法是否有錯誤 |
scrapy list | 獲取該專案下所有爬蟲程式的名稱 |
scrapy parse [options] <url> | scrapy parse url地址 --callback 回撥函式以此可以驗證我們的回撥函式是否正確 |
scrapy bench | 壓力測試 |
一些常用的全域性options
:
options | 描述 |
---|---|
--help, -h | 獲取幫助資訊 |
--logfile=FILE | 日誌檔案,如果省略,將丟擲stderr |
--loglevel=LEVEL, -L LEVEL | 日誌級別,預設為info |
--nolog | 禁止顯示日誌資訊 |
--profile=FILE | 將python cProfile統計資訊寫入FILE |
--pidfile=FILE | 將程式ID寫入FILE |
--set=NAME=VALUE, -s NAME=VALUE | 設定/替代設定(可以重複) |
--pdb | 在失敗時啟用pdb |
預設的命令只能在CMD
中執行,如果向在IDE
中執行,則需要新建一個py
檔案,使用execute
函式進行命令的執行。
# 在專案目錄下新建:entrypoint.py
from scrapy.cmdline import execute
execute(['scrapy', 'crawl', 'xiaohua'])
目錄介紹
以下是一個Scrapy
專案的目錄:
-- ScrapyProject/ # 專案資料夾
-- scrapy.cfg # 專案的主配置資訊,用來部署scrapy時使用,爬蟲相關的配置資訊在settings.py檔案中。
-- project_name/ # 專案全域性資料夾
__init__.py
items.py # 設定資料儲存模板,用於結構化資料,如:Django的Model
pipelines.py # 資料處理行為,如:一般結構化的資料持久化
settings.py # 配置檔案,如:遞迴的層數、併發數,延遲下載等。配置變數名必須大寫
-- spiders/ # 爬蟲資料夾,如:建立檔案,編寫爬蟲規則
__init__.py
爬蟲1.py
爬蟲2.py
爬蟲3.py
Scrapy-爬蟲
基本介紹
Spiders
的主要工作、進行資料爬取和資料解析。
以下是一個爬蟲程式的初始程式碼:
import scrapy
class CnblogsSpider(scrapy.Spider): # 基礎的爬蟲類
name = 'cnblogs' # 爬蟲程式名稱,非空且唯一
allowed_domains = ['www.cnblogs.com'] # 允許網路請求的域名,一般來說直接註釋即可
start_urls = ['http://www.cnblogs.com/'] # 初始的網路請求
def parse(self, response): # 資料解析函式
pass
預設情況下,當執行該爬蟲程式,會從start_urls
中自動發生網路請求,並將返回的資訊傳入parse()
方法,response
是一個物件,可從中進行xpath
解析等工作。
parse()
方法的返回值非常有趣,一般來說當我們解析工作完成後就進行持久化儲存,但是也可以再次的傳送網路請求,所以parse()
方法的返回值是多種多樣的:
- 包含解析資料的字典
- Item物件,專案管道,用於持久化儲存,臨時儲存資料的地方
- yield新的Request物件(新的Requests也需要指定一個回撥函式)
- 或者是可迭代物件(物件中只包含Items或Request)
一般來說,我們都是這麼做的,但是某些情況下你可能會發現我們需要爬取多個url
並且會指定不同的回撥函式(預設start_urls
列表中的url
回撥函式都是parse()
方法),那麼該怎麼做呢?你可以書寫一個名為start_requests()
的方法,並且自己使用Request
物件來傳送請求與繫結回撥函式,當有start_request()
方法後,start_urls
列表中的url不會被自動傳送請求:
import scrapy
from scrapy.http import Request
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs'
# allowed_domains = ['www.cnblogs.com']
start_urls = ['http://www.cnblogs.com/'] # 具有start_request()方法,start_urls列表中的urls不會自動發起請求
def start_requests(self):
yield Request(url="http://www.baidu.com",callback=self.baidu)
yield Request(url="http://www.biying.com",callback=self.biying)
def baidu(self,response):
print("baidu爬取完成...")
def biying(self,response):
print("biying爬取完成...")
def parse(self, response): # 失效
print(response)
如果你在爬蟲程式中遇到編碼問題無法正常解析response
的內容,則更改編碼格式:
import sys,os
sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')
Spiders自定製
在Spiders
類中,你可以進行各種各樣的自定義:
屬性/方法 | 描述 |
---|---|
name = "spiderName" | 定義爬蟲名,scrapy會根據該值定位爬蟲程式,非空且唯一 |
allowed_domains = ['www.cnblogs.com'] | 定義允許爬取的域名,如果OffsiteMiddleware啟動(預設就啟動), 那麼不屬於該列表的域名及其子域名都不允許爬取 |
start_urls = ['http://www.cnblogs.cn/'] | 如果沒有指定url,就從該列表中讀取url來生成第一個請求 |
custom_settings | 值為一個字典,定義一些配置資訊,在執行爬蟲程式時,這些配置會覆蓋專案級別的配置 所以custom_settings必須被定義成一個類屬性,由於settings會在類例項化前被載入 |
settings | 通過self.settings['配置項的名字']可以訪問settings.py中的配置,如果自己定義了custom_settings還是以自己的為準 |
logger | 日誌名預設為spider的名字,可通過self.settings['BOT_NAME']進行指定 |
start_requests() | 該方法用來發起第一個Requests請求,且必須返回一個可迭代的物件。它在爬蟲程式開啟時就被Scrapy呼叫,Scrapy只呼叫它一次。 預設從start_urls裡取出每個url來生成Request(url, dont_filter=True) |
closed(reason) | 爬蟲程式結束時自動觸發的方法 |
Request請求
傳送請求時,如何指定cookies
或這請求頭呢?其實在Request
物件中擁有很多引數:
引數 | 描述 |
---|---|
url | str或者bytes型別,傳送請求的地址 |
callback | 回撥函式,必須是一個可呼叫物件 |
method | str型別,傳送請求的方式 |
header | dict型別,本次請求所攜帶的請求頭 |
body | str型別或者bytes型別,傳送的請求體 |
cookies | dict型別,本次請求所攜帶的cookies |
meta | dict型別,如當前的request物件指定meta是{"name":"test"},則後面的response物件可通過response.meta.get("name")獲得該值,主要用於不同元件之間的資料傳遞 |
encoding | str型別,編碼方式,預設為utf8 |
priority | int型別,請求優先順序,優先順序高的先執行 |
dont_filter | bool型別,取消過濾?預設是false,當多次請求的地址、引數均相同時,預設後面的請求將取消 |
errback | 請求出現異常時的回撥函式 |
meta
是一個值得注意的地方:
import scrapy
from scrapy.http import Request
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs'
# allowed_domains = ['www.cnblogs.com']
def start_requests(self):
yield Request(url="https://www.cnblogs.com",meta={"name":"部落格園"},callback=self.parse,dont_filter=True)
yield Request(url="https://www.baidu.com",meta={"name":"百度"},callback=self.parse,dont_filter=True)
def parse(self, response):
print(response.meta.get("name"))
# 部落格園
# 百度
# meta通常傳遞跨元件資料
Response返回
來看一下response
物件中的一些基本方法/屬性:
屬性 | 描述 |
---|---|
url | 獲取本次request請求的url地址 |
status | 獲取本次request請求的狀態碼 |
body | 獲取HTML響應正文,返回的是bytes格式內容,因此如果請求的是圖片,可直接拿到它進行寫入 |
text | 獲取HTML響應正文,返回的是str格式內容 |
encoding | 獲取本次請求的編碼格式,你也可以對本次請求的編碼格式進行設定 |
request | 獲取傳送本次請求的request物件,如:response.request.method進行獲取本次的請求方式 |
meta | 獲取本次request請求中傳遞的一些引數 |
資料解析
在response
物件中,會包含xpath()
方法與css()
方法。他們本身都是屬於response.selector
中的方法,完整寫法與簡寫形式如下:
response.selector.css()
response.css()
response.selector.xpath()
response.xpath()
注意這裡的xpath()
方法返回的不是一個單純的List
,而是selector
的List
:
def parse(self, response):
print(response.xpath("//title"))
# [<Selector xpath='//title' data='<title>部落格園 - 開發者的網上家園</title>'>]
下面是一些xpath
返回列表的常用方法:
方法 | 描述 |
---|---|
extract() | 從返回的selector列表中拿到全部的元素的xpath選取內容 |
extract_first() | 從返回的selector列表中拿到第一個元素的xpath選取內容 |
如果是css
語法進行選擇,則更多的是在選擇器中拿到想要的東西:
選取符 | 描述 |
---|---|
::text | 拿到文字 |
::attr(attrName) | 獲取屬性 |
extract() | 從返回的selector列表中拿到全部的元素的xpath選取內容 |
extract_first() | 從返回的selector列表中拿到第一個元素的xpath選取內容 |
示例如下:
print(response.css("a::text"))
print(response.css("a::attr(href)"))
去重規則
去重規則的意思就是說如果一個爬蟲程式已經爬取過該URL
,則其他的爬蟲程式就不要繼續爬取了。
預設為指定去重:
import scrapy
from scrapy.http import Request
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs'
def start_requests(self):
# dont_filter=False為開啟去重
yield Request("http://www.cnblogs.com/",callback=self.parse,dont_filter=False)
yield Request("http://www.cnblogs.com/",callback=self.parse,dont_filter=False)
def parse(self, response):
print("爬取...")
# 只執行一次
如果想要修改去重規則,如第一次訪問被拒絕後嘗試更換代理繼續訪問,就可以進行自定製:
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter' # 預設的去重規則幫我們去重,去重規則在記憶體中維護了一個set,當請求成功Response後就會將URL進行記錄,如果再次爬取該URL就直接跳過
DUPEFILTER_DEBUG = False
JOBDIR = "儲存範文記錄的日誌路徑,如:/root/" # 最終路徑為 /root/requests.seen,去重規則放檔案中
自己寫一個類:
class MyDupeFilter:
@classmethod
def from_settings(cls, settings):
return cls()
def request_seen(self, request):
# 書寫去重規則,如果返回False則代表沒有重複,如果返回True則代表有重複,取消本次請求
return False
def open(self): # can return deferred
pass
def close(self, reason): # can return a deferred
pass
def log(self, request, spider): # log that a request has been filtered
pass
最後記得在settings.py
中修改配置項為自己的類。
headers&cookies
在scrapy
中,cookies
都是預設攜帶的,就像requests
模組的session
一樣。
在settings.py
中可以將其幹掉。
# Disable cookies (enabled by default)
# COOKIES_ENABLED = False
處了在傳送Request
物件時指定headers
,也可以在settings.py
中進行,配置完成後所有的Request
都會攜帶該請求頭字典:
# Override the default request headers:
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
}
Scrapy-持久化
Item物件
在資料解析paser()
方法完成後,可以返回一個Item
物件。
Item
物件你可以將它理解為在記憶體中臨時儲存一組資料的地方,因為每次爬取的欄位都是有限的,如歌曲名與歌手。
所以我們可以將每一次的資料解析出的歌曲名和歌手返回給Item
物件,由Item
物件交給PIPE
物件進行持久化儲存。
可以這麼認為,一共分為三部分:
一般來講,前兩步都很簡單,以下以爬取網易雲音樂TOP100
為例:
第一步,書寫Spider
內容:
import scrapy
from ..items import WangyiMusic
class WangyimusicSpider(scrapy.Spider):
name = 'wangyiMusic'
# allowed_domains = ['music.163.com']
start_urls = ['https://music.163.com/discover/toplist']
def parse(self, response):
message = response.xpath("//textarea[@id='song-list-pre-data']/text()").extract()[0]
import json
result = json.loads(message)
for row in result:
item = WangyiMusic() # 例項化Item物件
item["name"] = row.get("album").get("name") # 解析出的歌曲名字
item["singer"] = row.get("artists")[0].get("name") # 解析出歌手的名字
yield item
第二步,書寫Item.py
,新建一個類:
import scrapy
class WangyiMusic(scrapy.Item):
name = scrapy.Field()
singer = scrapy.Field()
現在,當我們執行爬蟲程式,它就會將每一次迴圈到的歌曲和歌手資訊放入Item
物件中做臨時儲存了。
PIPE物件
光有臨時儲存還不夠,我們需要指定永久儲存,而PIPE
則是從Item
中取出臨時資料進行永久儲存的。
當我們開啟pipelines.py
後,會發現它給定了一個類:
class ProjectNamePipeline:
def process_item(self, item, spider):
# spider是爬蟲物件,可通過settings拿到配置檔案,將是一個字典
# 如 spider.settings.get("xxx")等等...
return item
其實,該類可以指定很多鉤子函式:
class ProjectNamePipeline(object):
def __init__(self,v):
# 正常例項化執行,一般不會走,如果走只執行一次,在美喲㐉form_crawler方法是才會走它
self.value = v
@classmethod
def from_crawler(cls, crawler):
"""
# 通過配置檔案進行例項化的過程,一般都是走這個方法,只執行一次
"""
val = crawler.settings.getint('MMMM')
return cls(val)
def open_spider(self,spider):
"""
# 爬蟲剛啟動時執行一次
"""
print('start')
def close_spider(self,spider):
"""
# 爬蟲關閉時執行一次
"""
print('close')
def process_item(self, item, spider):
# 操作並進行持久化邏輯函式
# return item表示會被後續的pipeline繼續處理。可進行多方儲存,MySQL、Redis等地方
return item
# 如果丟擲異常,則表示將item丟棄,
# from scrapy.exceptions import DropItem
# raise DropItem()
這裡的process_item()
方法和open_spider()
以及close_spider()
方法比較常用。
注意,持久化儲存可以存入多個地方,如MySQL/Redis/Files
中,前提是上一個類的process_item()
方法必須將item
物件返回。
光看了這些還不夠,你需要在配置檔案中配置預設的持久化儲存方案類:
ITEM_PIPELINES = {
'scrapyProject01.pipelines.FilesPipeline': 300,
'scrapyProject01.pipelines.RedisPipeline': 200, # 優先順序小的先進行儲存
}
嘗試一下,將爬取到的歌手資訊和歌曲名稱存放到Redis/Fiels
中:
注意:持久化儲存對應的文字檔案的型別只可以為:'json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle'
from itemadapter import ItemAdapter
# 從Item中提取資料儲存到檔案 優先順序300 後
class FilesPipeline:
def open_spider(self,spider):
self.f = open(file="./MusicTop100.cvs",mode="a+",encoding="utf-8")
def process_item(self, item, spider):
name = item["name"]
singer = item["singer"]
self.f.write("歌曲名:%s 歌手:%s\n"%(name,singer))
return item
def close_spider(self,spider):
self.f.close()
# 從Item中提取資料儲存到Redis 優先順序200 先
class RedisPipeline:
def open_spider(self,spider):
import redis
self.conn = redis.Redis(host="localhost", port=6379)
def process_item(self, item, spider):
name = item["name"]
singer = item["singer"]
self.conn.lpush(singer,name)
return item
def close_spider(self,spider):
self.conn.close()
圖片儲存
如果是爬取的圖片,則資料直接處理出img
的src
屬性,交給Item
,再由Item
交由一個繼承於ImagesPipline
的類直接儲存即可。
依賴於pillow
模組:
pip3 install pillow
如下所示,爬取B
站的封面圖,首先第一步是要確定爬取下來的圖片存放路徑:
# settings.py
# 圖片儲存的路徑
IMAGES_STORE = './BiliBiliimages'
接下來就要書寫spider
爬蟲程式:
import scrapy
from scrapy.http import Request
class BilibiliSpider(scrapy.Spider):
name = 'bilibili'
def start_requests(self):
# 取消去重規則,每次爬取到的圖片都不一樣
yield Request(url="https://manga.bilibili.com/twirp/comic.v1.Comic/GetRecommendComics", method="POST",
callback=self.parse, dont_filter=True)
def parse(self, response):
import json
result = json.loads(response.text).get("data").get("comics")
for img_message in result:
img_title = img_message.get("title")
img_src = img_message.get("vertical_cover")
# 將圖片名字和src傳入item物件
from ..items import BiliBiliImageItem
item = BiliBiliImageItem()
item["title"] = img_title
item["src"] = img_src
yield item
Item
十分簡單:
import scrapy
class BiliBiliImageItem(scrapy.Item):
title = scrapy.Field()
src = scrapy.Field()
最後是pipelines
的書寫,取出src
並進行下載:
from scrapy.pipelines.images import ImagesPipeline
import scrapy
class DownloadImagesPipeline(ImagesPipeline):
def get_media_requests(self, item, info):
# 下載圖片
yield scrapy.Request(url=item["src"],method="GET",meta={"filename":item["title"]})
def file_path(self, request, response=None, info=None):
# 設定儲存圖片的名稱
filename = request.meta.get('filename')
return filename + ".jpg"
def item_completed(self, results, item, info):
# 請求傳送後執行的函式,用於執行後續操作,如返回Item物件等
"""
:returns :
[
(True,
{
'url': 'http://i0.hdslb.com/bfs/manga-static/9351bbb71a9726af47e3abce3ce8f3cecbed5b08.jpg',
'path': '新世紀福音戰士.jpg', 'checksum': '95ce0e970b7198f23c4d67687bd56ba6',
'status': 'downloaded'
}
)
]
"""
if results[0][0] == True:
print("下載圖片並儲存成功...")
return item
else:
print("下載圖片並儲存失敗...")
from scrapy.exceptions import DropItem
raise DropItem("download img fail,url\n%s"%results[0][1].get("url"))
別忘記在settings.py
中指定PIPE
:
ITEM_PIPELINES = {
'scrapyProject01.pipelines.DownloadImagesPipeline': 200,
}
Scrapy-中介軟體
配置中介軟體
settings.py
中進行配置即可,優先順序越小執行越靠前:
# 爬蟲中介軟體
SPIDER_MIDDLEWARES = {
'spider1.middlewares.Spider1SpiderMiddleware': 543,
}
# 下載中介軟體
DOWNLOADER_MIDDLEWARES = {
'spider1.middlewares.Spider1DownloaderMiddleware': 543,
}
如果要進行自定製,就將自定製的類按照字串的形式進行新增。
多箇中介軟體的攔截方式同Falsk
相同,並非同級返回。
如,下載中介軟體A/B/C
,在執行A
的process_start_request()
時候丟擲了錯誤,此時就執行C/B/A
的process_spider_exception()
方法。
爬蟲中介軟體
以下是爬蟲中介軟體的鉤子函式,是Spiders
和引擎的中介軟體,一般來講不會涉及到網路:
from scrapy import signals
class Spider1SpiderMiddleware:
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
# 建立spider(爬蟲物件)的時候,註冊一個訊號
# 訊號: 當爬蟲的開啟的時候 執行 spider_opened 這個方法
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_spider_input(self, response, spider):
# 下載完成後,執行,然後交給parse處理
return None
def process_spider_output(self, response, result, spider):
"""
經歷過parse函式之後執行
:param response: 上一次請求返回的結果
:param result: yield的物件 包含 [item/Request] 物件的可迭代物件
:param spider: 當前爬蟲物件
:return: 返回Request物件 或 Item物件
"""
for i in result:
yield i
def process_spider_exception(self, response, exception, spider):
"""如果執行parse丟擲異常的話 會執行這個函式 預設不對異常處理交給下一個中介軟體處理"""
pass
def process_start_requests(self, start_requests, spider):
"""
爬蟲啟動時呼叫
:param start_requests: 包含 Request 物件的可迭代物件
:param spider:
:return: Request 物件
"""
for r in start_requests:
yield r
def spider_opened(self, spider):
# 生成爬蟲日誌
spider.logger.info('Spider opened: %s' % spider.name)
下載中介軟體
下面是下載中間的鉤子函式,下載中介軟體是Download
與引擎中的中介軟體,涉及網路,因此代理等相關配置應該在下載中介軟體中進行:
class Spider1DownloaderMiddleware:
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
# 這個方法同上,和爬蟲中介軟體一樣的功能
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_request(self, request, spider):
"""
# 可進行UA偽裝,user-agent
請求需要被下載時,經過所有下載中介軟體的process_request呼叫
spider處理完成,返回時呼叫
:param request:
:param spider:
:return:
None,繼續往下執行,去下載
Response物件,停止process_request的執行,開始執行process_response
Request物件,停止中介軟體的執行,將Request重新放到排程器中
raise IgnoreRequest異常,停止process_request的執行,開始執行process_exception
"""
return None
def process_response(self, request, response, spider):
"""
下載得到響應後,執行
:param request: 請求物件
:param response: 響應物件
:param spider: 爬蟲物件
:return:
返回request物件,停止中介軟體,將Request物件重新放到排程器中
返回response物件,轉交給其他中介軟體process_response
raise IgnoreRequest 異常: 呼叫Request.errback
"""
return response
def process_exception(self, request, exception, spider):
"""當下載處理器(download handler)或process_request() (下載中介軟體)丟擲異常
:return
None: 繼續交給後續中介軟體處理異常
Response物件: 停止後續process_exception方法
Request物件: 停止中介軟體,request將會被重新呼叫下載
"""
pass
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
操縱cookie
可能有的頁面需要你手動攜帶一個cookie
,比如token
驗證等,此時就可以在下載中介軟體的process_request()
方法中手動攜帶,
如下所示:
def process_request(self, request, spider):
# 先獲取token token = ....
request.cookies.update({"token":"xxx"})
print(request.cookies)
return None
代理設定
為下載中介軟體中新增代理:
def get_proxy():
"""獲取代理的函式"""
response = requests.get('http://134.175.188.27:5010/get/')
data = response.json()
return data["proxy"]
class ProxyDownloaderMiddleware(object):
"""下載中介軟體中的代理中介軟體"""
def process_request(self, request, spider):
request.meta['proxy'] = get_proxy()
return None
如果代理不可用,配置檔案中設定重試:
RETRY_ENABLED = True # 是否開啟超時重試
RETRY_TIMES = 2 # initial response + 2 retries = 3 requests 重試次數
RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429] # 重試的狀態碼
DOWNLOAD_TIMEOUT = 1 # 1秒沒有請求到資料,主動放棄
Scrapy-settings.py
基本配置
配置檔案中的配置項:
# -*- coding: utf-8 -*-
# Scrapy settings for step8_king project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
# http://doc.scrapy.org/en/latest/topics/settings.html
# http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
# http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
# 1. 爬蟲名稱
BOT_NAME = 'step8_king'
# 1.2 日誌級別,強烈建議
LOG_LEVEL = "ERROR"
# 2. 爬蟲應用路徑
SPIDER_MODULES = ['step8_king.spiders']
NEWSPIDER_MODULE = 'step8_king.spiders'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
# 3. 客戶端 user-agent請求頭
# USER_AGENT = 'step8_king (+http://www.yourdomain.com)'
# Obey robots.txt rules
# 4. 禁止爬蟲配置
# ROBOTSTXT_OBEY = False
# Configure maximum concurrent requests performed by Scrapy (default: 16)
# 5. 併發請求數
# CONCURRENT_REQUESTS = 4
# Configure a delay for requests for the same website (default: 0)
# See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
# 6. 延遲下載秒數
# DOWNLOAD_DELAY = 2
# The download delay setting will honor only one of:
# 7. 單域名訪問併發數,並且延遲下次秒數也應用在每個域名
# CONCURRENT_REQUESTS_PER_DOMAIN = 2
# 單IP訪問併發數,如果有值則忽略:CONCURRENT_REQUESTS_PER_DOMAIN,並且延遲下次秒數也應用在每個IP
# CONCURRENT_REQUESTS_PER_IP = 3
# Disable cookies (enabled by default)
# 8. 是否支援cookie,cookiejar進行操作cookie
# COOKIES_ENABLED = True
# COOKIES_DEBUG = True
# Disable Telnet Console (enabled by default)
# 9. Telnet用於檢視當前爬蟲的資訊,操作爬蟲等...
# 使用telnet ip port ,然後通過命令操作
# TELNETCONSOLE_ENABLED = True
# TELNETCONSOLE_HOST = '127.0.0.1'
# TELNETCONSOLE_PORT = [6023,]
# 命令est()
# 10. 預設請求頭(優先順序低於request物件中的請求頭)
# Override the default request headers:
# DEFAULT_REQUEST_HEADERS = {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
# }
# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
# 11. 定義pipeline處理請求 值越小優先順序越高 0-1000
# ITEM_PIPELINES = {
# 'step8_king.pipelines.JsonPipeline': 700,
# 'step8_king.pipelines.FilePipeline': 500,
# }
# 12. 自定義擴充套件,基於訊號進行呼叫
# Enable or disable extensions
# See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
# EXTENSIONS = {
# # 'step8_king.extensions.MyExtension': 500,
# }
# 13. 爬蟲允許的最大深度,可以通過meta檢視當前深度;0表示無深度
# DEPTH_LIMIT = 3
# 14. 爬取時,0表示深度優先Lifo(預設);1表示廣度優先FiFo
# 後進先出,深度優先
# DEPTH_PRIORITY = 0
# 基於硬碟的 DISK
# SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleLifoDiskQueue'
# 基於記憶體的 MEMORY
# SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.LifoMemoryQueue'
# 先進先出,廣度優先
# DEPTH_PRIORITY = 1
# SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleFifoDiskQueue'
# SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.FifoMemoryQueue'
# 15. 排程器佇列
# SCHEDULER = 'scrapy.core.scheduler.Scheduler' 這是一個類
# from scrapy.core.scheduler import Scheduler
# 16. 訪問URL去重
# DUPEFILTER_CLASS = 'step8_king.duplication.RepeatUrl'
# Enable and configure the AutoThrottle extension (disabled by default)
# See http://doc.scrapy.org/en/latest/topics/autothrottle.html
"""
18. 啟用快取 一般不太用
目的用於將已經傳送的請求或相應快取下來,以便以後使用,
from scrapy.downloadermiddlewares.httpcache import HttpCacheMiddleware
from scrapy.extensions.httpcache import DummyPolicy
from scrapy.extensions.httpcache import FilesystemCacheStorage
"""
# 是否啟用快取策略
# 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'
爬蟲速率
如果請求過於頻繁,可能會遭遇封禁,因此可以設定爬蟲的頻次:
"""
17. 自動限速演算法
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
"""
# 開始自動限速
# AUTOTHROTTLE_ENABLED = True
# The initial download delay
# 初始下載延遲
# AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
# 最大下載延遲
# AUTOTHROTTLE_MAX_DELAY = 10
# The average number of requests Scrapy should be sending in parallel to each remote server
# 平均每秒併發數
# AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
# 是否顯示
# AUTOTHROTTLE_DEBUG = True
# Enable and configure HTTP caching (disabled by default)
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
"""
代理相關
預設代理,一般放在環境變數中,即os.environ
裡,用的時候取就好了:
# 一般不用,取代理費事
from scrapy.contrib.downloadermiddleware.httpproxy import HttpProxyMiddleware
方式一:使用預設
os.environ =
{
http_proxy:http://root:woshiniba@192.168.11.11:9999/
https_proxy:http://192.168.11.11:9999/
}
自定義代理配置:
def to_bytes(text, encoding=None, errors='strict'):
if isinstance(text, bytes):
return text
if not isinstance(text, six.string_types):
raise TypeError('to_bytes must receive a unicode, str or bytes '
'object, got %s' % type(text).__name__)
if encoding is None:
encoding = 'utf-8'
return text.encode(encoding, errors)
class ProxyMiddleware(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.encodestring(to_bytes(proxy['user_pass']))
request.headers['Proxy-Authorization'] = to_bytes('Basic ' + encoded_user_pass)
print "**************ProxyMiddleware have pass************" + proxy['ip_port']
else:
print "**************ProxyMiddleware no pass************" + proxy['ip_port']
request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port'])
# 在配置檔案中註冊中介軟體
DOWNLOADER_MIDDLEWARES = {
'step8_king.middlewares.ProxyMiddleware': 500,
}
Scrapy高階
全站爬取
上面的Scrapy
都是基於spiders
這個類,而全站爬取則是基於CrawlSpider
這個類。
全站爬取的意思就是說將該網站所有的資料爬取下來,如下例項,爬取蝦米音樂的目前所有動漫遊戲相關曲目,共十條:
from prettytable import PrettyTable
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
table = PrettyTable(['歌曲名稱', "專輯"])
# 全站爬取
class XiamiSpider(CrawlSpider):
name = 'xiami'
start_urls = [
'https://www.xiami.com/list?page=1&query=%7B%22genreType%22%3A2%2C%22genreId%22%3A%223344%22%7D&scene=genre&type=song']
# 連結提取器:根據指定規則(allow="正則")進行指定連結的提取
link = LinkExtractor(allow=r'page=\d+')
rules = (
# 規則解析器:將連結提取器提取到的連結進行指定規則(callback)的解析操作
# 自動傳送請求
# 如果 follow 為True,則可以將連結提取器 繼續作用到 連線提取器提取到的連結 所對應的頁面中
Rule(link, callback="parse", follow=True), # 自動匹配 a標籤,page自動翻頁,自動執行回撥
)
def parse(self, response, *args, **kwargs):
music_name_list = response.xpath(
"//*[@id='app']//div[@class='table-container'][1]//tr[@class]//div[@class='song-name em']//text()").extract()
music_album_list = response.xpath(
"//*[@id='app']//div[@class='table-container'][1]//tr[@class]//div[@class='album']//text()").extract()
for index in range(len(music_name_list)):
table.add_row([music_name_list[index].strip(), music_album_list[index].strip()])
def close(spider, reason):
print(table)
分散式爬蟲
分散式爬蟲就是在一臺遠端的機器上儲存爬取的地址,以及爬取的結果。
由多臺計算機在遠端計算機上拿到爬取地址進行爬取,並且將爬取結果儲存到遠端計算機上。
單純的Scrapy
框架不能實現分散式,所以要用到scrapy-redis
這個第三方模組實現:
- 如何實現分散式?
- 安裝一個scrapy-redis的元件
- 原生的scarapy是不可以實現分散式爬蟲,必須要讓scrapy結合著scrapy-redis元件一起實現分散式爬蟲。
- 為什麼原生的scrapy不可以實現分散式?
- 排程器不可以被分散式機群共享
- 管道不可以被分散式機群共享
- scrapy-redis元件作用:
- 可以給原生的scrapy框架提供可以被共享的管道和排程器
- 實現流程
- 建立一個工程
- 建立一個基於CrawlSpider的爬蟲檔案
- 修改當前的爬蟲檔案:
- 導包:from scrapy_redis.spiders import RedisCrawlSpider
- 將start_urls和allowed_domains進行註釋
- 新增一個新屬性:redis_key = 'sun' 可以被共享的排程器佇列的名稱
- 編寫資料解析相關的操作
- 將當前爬蟲類的父類修改成RedisCrawlSpider
- 修改配置檔案settings
- 指定使用可以被共享的管道:
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 400
}
- 指定排程器:
# 增加了一個去重容器類的配置, 作用使用Redis的set集合來儲存請求的指紋資料(去重規則), 從而實現請求去重的持久化
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis元件自己的排程器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 配置排程器是否要持久化, 也就是當爬蟲結束了, 要不要清空Redis中請求佇列和去重指紋的set。如果是True, 就表示要持久化儲存, 就不清空資料, 否則清空資料
SCHEDULER_PERSIST = True
- 指定redis伺服器:
- redis相關操作配置:
- 配置redis的配置檔案:
- linux或者mac:redis.conf
- windows:redis.windows.conf
- 代開配置檔案修改:
- 將bind 127.0.0.1進行刪除
- 關閉保護模式:protected-mode yes改為no
- 結合著配置檔案開啟redis服務
- redis-server 配置檔案
- 啟動客戶端:
- redis-cli
- 執行工程:
- scrapy runspider xxx.py
- 向排程器的佇列中放入一個起始的url:
- 排程器的佇列在redis的客戶端中
- lpush xxx www.xxx.com
- 爬取到的資料儲存在了redis的proName:items這個資料結構中
首先第一步:
pip install scrapy-redis
程式碼如下:
# 爬蟲檔案
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from fbsPro.items import FbsproItem
from scrapy_redis.spiders import RedisCrawlSpider
class FbsSpider(RedisCrawlSpider):
name = 'fbs'
redis_key = 'sun' # 從sun這個佇列中取出url
rules = (
Rule(LinkExtractor(allow=r'type=4&page=\d+'), callback='parse_item', follow=True),
)
def parse_item(self, response):
tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr')
for tr in tr_list:
new_num = tr.xpath('./td[1]/text()').extract_first()
new_title = tr.xpath('./td[2]/a[2]/@title').extract_first()
item = FbsproItem()
item['title'] = new_title
item['new_num'] = new_num
yield item
然後是items.py
:
import scrapy
class FbsproItem(scrapy.Item):
title = scrapy.Field()
new_num = scrapy.Field()
需要在settings.py
中做配置:
#指定管道
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 400
}
#指定排程器
# 增加了一個去重容器類的配置, 作用使用Redis的set集合來儲存請求的指紋資料, 從而實現請求去重的持久化
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis元件自己的排程器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 配置排程器是否要持久化, 也就是當爬蟲結束了, 要不要清空Redis中請求佇列和去重指紋的set。如果是True, 就表示要持久化儲存, 就不清空資料, 否則清空資料
SCHEDULER_PERSIST = True
#指定redis
REDIS_HOST = '127.0.0.1' #redis遠端伺服器的ip(修改)
REDIS_PORT = 6379
增量式爬蟲
增量式爬蟲也非常簡單,維護一個set
(可以是redis
),將每次爬取的url
進行檢測。
如果該url
未被爬取,則爬取完成後將url
放入set
中,下次啟動爬蟲程式時就會檢測,如果url
在set
中,就跳過本次爬取。
增量式就是在原本的資料基礎上做增加。
反反扒策略
代理
如果一個網站對IP
進行了頻率限制,可以在傳送請求時指定一個代理,由代理幫助你傳送本次請求,且將返回結果交給你。
而使用代理又有以下三個名詞:
透明:被請求伺服器明確知道本次請求是由代理髮起,並且也知道真實請求的IP地址
匿名:被請求伺服器明確知道本次請求是由代理髮起,但是不知道真實請求的IP地址
高匿:被請求伺服器不知道本次請求是由代理髮起,並且也不知道真實請求的IP地址
常用的代理相關網站:
- 快代理
- 西祠代理
- www.goubanjia.com
代理的型別一般有HTTP
代理和HTTPS
代理,我們在使用requests
模組傳送請求時可指定代理:
如下所示:
from requests import Session
headers = {
"user-agent":"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}
proxies = {
"http": "36.230.165.45:8088",
}
session = Session()
response = session.get("https://www.baidu.com/s?wd=ip",headers=headers,proxies=proxies)
print(response.status_code)
with open(file="./testDataDocument.html",mode="w",encoding="utf-8") as f:
f.write(response.text)
驗證碼
自動登陸時碰到驗證碼認證,則可以藉助第三方工具超級鷹,新使用者會獲取1000題分。
headers
一般來說,發起請求時我們要觀察NETWORK
的變化,除了User-Agent
之外,如果有以下的請求頭也可以對其新增上:
Host
Referer
token
尤其注意token
,他的命名可能不太一樣如xsrf-token
,或者jwt
等等字樣的都應該帶上。
這是為使用者登入之後儲存狀態得到的隨機字串。
一般都會在登入成功後通過cookie
進行返回,可以先從cookie
中get
獲取,再新增到請求頭中。