爬蟲的基礎知識
爬蟲的定義
只要是瀏覽器可以做的事情池充,原則上躯保,爬蟲都可以幫助我們做,即:瀏覽器不能夠做到的贩猎,爬蟲也不能做
網(wǎng)絡爬蟲:又叫網(wǎng)絡蜘蛛(spider)熊户,網(wǎng)絡機器人琼牧,就是模擬客戶端發(fā)送網(wǎng)絡請求躯护,接受請求響應,一種按照一定的規(guī)則义图,自動地抓取互聯(lián)網(wǎng)信息的程序
爬蟲的分類
-
通用爬蟲
通常指搜索引擎的爬蟲(面對整個互聯(lián)網(wǎng))
-
聚焦爬蟲:
針對特定網(wǎng)站的爬蟲
-
流程:
ROBOTS協(xié)議
網(wǎng)站通過robots協(xié)議告訴搜索引擎那些頁面可以抓取艇棕,那些頁面不能抓取
例如:https://www.taobao.com/robots.txt(通常是網(wǎng)站后面加/robots.txt即可以看到蝌戒,就是一個文本文件)
部分內(nèi)容如下:
User-agent: Baiduspider #用戶代理,可以理解為瀏覽器的身份標識沼琉,通過這個字段可以告訴服務器是什么樣的程序在請求網(wǎng)站北苟,Baiduspider即百度的搜索引擎
Allow: /article #表示允許爬的內(nèi)容
Allow: /oshtml
Allow: /ershou
Allow: /$
Disallow: /product/ #表示不允許該用戶代理爬的內(nèi)容
Disallow: /
但是robots只是道德層面的約束
http和https
為了拿到和瀏覽器一模一樣的數(shù)據(jù),就必須要知道http和https
-
http:
超文本傳輸協(xié)議打瘪,明文方式傳輸友鼻,默認端口80
-
https:
http+ssl(安全套接字層),會對數(shù)據(jù)進行加密闺骚,默認端口443
https更安全彩扔,但是性能更低(耗時更長)
瀏覽器發(fā)送http請求的過程
ps:爬蟲在爬取數(shù)據(jù)的時候,不會主動的請求css僻爽、圖片虫碉、js等資源,就算自己爬取了js的內(nèi)容进泼,也只是字符串蔗衡,而不會執(zhí)行,故乳绕,瀏覽器渲染出來的內(nèi)容和爬蟲請求的頁面并不一樣
爬蟲要根據(jù)當前url地址對應的響應為準绞惦,當前url地址的elements的內(nèi)容和url的響應不一樣,特別要注意tbody洋措,經(jīng)常在elements中有而響應中無
-
頁面上的數(shù)據(jù)在哪里济蝉?
- 當前url地址對應的相應中
- 其他url地址對應的相應中,如ajax請求
- js生成:1、部分數(shù)據(jù)在響應中王滤,2贺嫂、全部由js生成
url的格式
host:服務器的ip地址或者是域名
port:服務器的端口
path:訪問資源的路徑
query-string:參數(shù),發(fā)送給http服務器的數(shù)據(jù)
anchor:錨點(跳轉到網(wǎng)頁的制定錨點位置雁乡,anchor也有主播的意思)
例:http://item.jd.com/11936238.html#product-detail第喳,就是一個帶錨點的url,會自動跳轉到商品詳情踱稍,但是要注意曲饱,一個頁面帶錨點和不帶錨點的響應是一樣的(寫爬蟲的時候,就可以直接刪掉錨點的部分)
http請求格式
如珠月,在訪問百度時扩淀,查看request headers的source時,就可以看到如下內(nèi)容
GET http://www.baidu.com/ HTTP/1.1
#請求方法:get啤挎;url:http:xxxx.com/驻谆;協(xié)議版本:http1.1,然后換行
Host: www.baidu.com
#請求頭部:host庆聘;值:www.baidu.com胜臊;換行,以下類似
Proxy-Connection: keep-alive #keep-alive表示支持長鏈接掏觉。為什么要用長連接:不用頻繁握手揮手区端,提高效率
Upgrade-Insecure-Requests: 1 #升級不安全的請求:把http請求轉換為https的請求
DNT: 1 #Do not track
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36 #瀏覽器的標識值漫。名字/版本號澳腹。如果有模擬手機版的請求,改user agent即可杨何,不同的user agent訪問相同的url酱塔,可能會得到不同的內(nèi)容
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 #瀏覽器告訴服務器自己可以接受什么樣的數(shù)據(jù)
Referer: http://baidu.com/
Accept-Encoding: gzip, deflate #告訴服務器自己可以接受什么樣的壓縮方式
Accept-Language: en-US,en;q=0.9 #告訴服務器自己可以接受什么樣語言的數(shù)據(jù),q:權重危虱,更愿意接受哪種語言
Cookie: BAIDUID=B8BE58B25611B7BBA38ECFE9CE75841F:FG=1; BIDUPSID=B8BE58B25611B7BBA38ECFE9CE75841F; PSTM=1565080210; BD_UPN=12314753; delPer=0; BD_HOME=0; H_PS_PSSID=26522_1453_21118_29523_29521_29098_29568_28830_29221_26350_22159; BD_CK_SAM=1; PSINO=7; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; COOKIE_SESSION=218_0_3_0_0_7_1_1_1_3_76_2_0_0_0_0_0_0_1565599571%7C3%230_0_1565599571%7C1; rsv_jmp_slow=1565599826516; H_PS_645EC=2c80At1Is237xdMOfC3ju2q0qlWJ%2FFlbD5N50IQeTrCHyIEsZN6yQYBgLHI; B64_BOT=1 #cookie:保存用戶的個人信息羊娃。ps:cookie和session的區(qū)別:cookie保存在瀏覽器本地,不安全埃跷,存儲量有上限蕊玷,session保存在服務器,更安全弥雹,往往沒有上限垃帅。cookie又分為request cookie和reponse cookie,在瀏覽器中可以查看
除了以上字段剪勿,可能還有referer字段贸诚,表示當前url是從哪個url過來的;x-request-with字段,表示是ajax異步請求
以上字段中酱固,主要是user agent(模擬瀏覽器)械念,cookie(反反爬蟲)
常見的請求方式
get:除了post,基本都用get运悲,更常用
post:常用于提交表單的時候(安全)龄减,傳輸大文件的時候(美觀)
響應狀態(tài)碼(status code)
- 200:成功
- 302/307:臨時轉移至新的url
- 404:not found
- 500:服務器內(nèi)部錯誤
字符串知識復習
-
str類型和bytes類型
- bytes:二進制類型,互聯(lián)網(wǎng)上數(shù)據(jù)都是以二進制的方式傳輸?shù)?/li>
str:unicode的呈現(xiàn)形式
ps:ascii碼是一個字節(jié)班眯,unicode編碼通常是2個字節(jié)欺殿,utf-8是unicode的實現(xiàn)方式之一,是一變長的編碼方式鳖敷,可以是1脖苏、2、3個字節(jié)
編碼和解碼的方式必須一致定踱,否則會亂碼
爬蟲部分重要的是理解棍潘,而不是記憶
Request模塊使用入門
Q:為什么要學習requests,而不是urllib崖媚?
- requests的底層實現(xiàn)是就urllib亦歉,urllib能做的事情,requests都可以做畅哑;
- requests在python2和python3中通用肴楷,方法完全一樣;
- requests簡單易用荠呐;
- request能夠自動幫我們解壓(gzip等)網(wǎng)頁內(nèi)容
中文文檔api:http://docs.python-requests.org/zh_CN/latest/index.html
基礎使用
"""基礎入門"""
import requests
r = requests.get('http://www.baidu.com') #r即為獲得的響應赛蔫。所請求的所有東西都在r中
# 必須要包含協(xié)議(http或https);還有dlelete泥张,post等方法
print(r)
print(r.text) #text是一個屬性呵恢,其實可以通過他的意思判斷,text是一個名字媚创,所以是屬性渗钉,如果是方法,常為動詞
#會自動根據(jù)推測的編碼方式解碼為str
print(r.encoding) #根據(jù)頭部推測編碼方式钞钙,如果猜錯了鳄橘,我們解碼的時候就必須自己指定解碼的方式
print(r.content) #也是屬性,是一個bytes類型的數(shù)據(jù)芒炼。
print(r.content.decode()) #將bytes類型轉換為str類型瘫怜。默認的解碼方式為utf-8
print(r.status_code) #獲取狀態(tài)碼
assert r.status_code == 200 #斷言狀態(tài)碼為200,如果斷言失敗焕议,會報錯:assertionerror
#可以用此方法判斷請求是否成功
print(r.headers) #響應頭宝磨,我們主要關注其中的set-cookie(在本地設置cookie)字段
print(r.request) #關于對應相應的請求部分弧关,是一個對象
print(r.request.url) #請求的url地址
print(r.url) #響應地址,請求的url地址和響應的url地址可能會不一樣(因為可能和重定向)
print(r.request.headers) #請求頭唤锉,如果不設置世囊,默認的user-agent是python-requests/x.xx.x
with open('baidu_r.txt','w') as f: #測試:查看默認的user-agent訪問時返回的內(nèi)容
f.write(r.content.decode())
"""
requests中解編碼的方法:
1. r.content.decode() #content的類型為bytes,必須再次解碼
2. r.content.decode('gbk')
3. r.text #text是按照推測的編碼方式進行解碼后的數(shù)據(jù)窿祥,他的類型為str
"""
發(fā)送帶header的請求
具體的header到瀏覽器中進行查看
"""
為什么請求需要帶上header株憾?
模擬瀏覽器,欺騙服務器晒衩,獲取和瀏覽器一致的內(nèi)容
header的形式:字典嗤瞎,形式:{request headers冒號前面的值:request headers冒號后面的值},大部分情況听系,我們帶上user-agent即可贝奇,少數(shù)情況需要cookie
用法:requests.get(url,headers=headers)
"""
import requests
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0'}
response = requests.get('https://www.baidu.com',headers=headers)
print(response.content.decode()) #會發(fā)現(xiàn)響應中的數(shù)據(jù)比不帶header多許多
發(fā)送帶參數(shù)的請求
#在url中帶參數(shù)的形式
#例如:在我們百度搜索某東西時,就會帶上一大堆參數(shù)靠胜,但是大部分可能是沒有用的掉瞳,我們可以嘗試刪除,然后我們在爬蟲中帶的參數(shù)只需要為其中不能刪除的部分即可
"""
參數(shù)的形式:字典
kw={'wd':'長城'}
用法:requests.get(url,params=kw)
"""
import requests
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0'}
params = {'wd':'這里是中文'}
#如果參數(shù)含中文浪漠,其就會被自動編碼陕习,編碼的后的形式含百分號,我們可以使用url解碼來查看原來的內(nèi)容
r = requests.get('https://www.baidu.com',params=params,headers=headers)
print(r.status_code)
print(r.request.url)
print(r.url)
print(r.content.decode())
#當然址愿,我們也可以直接把參數(shù)拼接到url中去该镣,而不單獨傳參(也不需要手動編碼),eg:r = requests.get('https://www.baidu.com/s?wd={}'.formate('傳智播客'))
小練習:爬貼吧前1000頁
import requests
kw = input('請輸入您要爬取的貼吧名:')
url = 'https://tieba.baidu.com/f?kw=%{kw}8&pn='.format(kw=kw)
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0'}
for i in range(1000):
url = urlr.formate(str(i*50))
r = requests.get(url=url, headers=headers)
with open('./tieba_pages/{}-第{}頁.html'.format(kw,i), 'w', encoding='utf-8') as f:
# 為什么是utf-8响谓,因為r.content.decode()為utf-8的格式
f.write(r.content.decode())
扁平勝于嵌套
:比如损合,多用列表推倒式替代某些循環(huán)
Request深入
發(fā)送post請求
用法:
response = requests.post('https://www.baidu.com',data=data,headers=headers)
post時不僅需要地址,還需要我們post的數(shù)據(jù)歌粥,該數(shù)據(jù)就放在data中
data的形式:字典
使用代理
正向代理與反向代理
反向代理:瀏覽器不知道服務器的地址塌忽,比如以上的圖例中拍埠,瀏覽器知道的只是nginx服務器失驶,因此,及時有攻擊枣购,也只能攻擊nginx嬉探,不能攻擊到我們的服務器
正向代理:瀏覽器知道服務器的地址
爬蟲為什么要使用代理
- 讓服務器以為不是同一個客戶端在請求
- 防止我們的真實地址被泄漏,防止被追究
使用代理
用法:requests.get('http://www.baidu.com',proxies=proxies)
proxies的形式是字典proxies={ 'htttp':'http://12.34.56.78:8888', #如果請求的是http 'https':'https://12.34.56.78:8888' #如果請求的是https的地址 }
免費代理的網(wǎng)址:https://proxy.mimvp.com/free.php
代理一般可以分為3種:
- 透明代理
- 普匿代理棉圈,透明以及普匿涩堤,對方是可以追查到我們的真實ip的
- 高匿代理
要注意,不是所有的ip都支持發(fā)送https的請求分瘾,有些也不支持發(fā)送post請求
代碼示例:
"""
0. 準備大量ip地址胎围,組成ip池,隨機選擇一個ip地址來使用
- 如何隨機選擇ip
- {'ip':ip,'times':0}
- [{},{},...{}],對這個ip的列表按照使用次數(shù)進行排序
選擇使用次數(shù)較少的幾個ip,從中隨機選擇一個
1. 檢查代理的可用性
- 使用request添加超時參數(shù)白魂,判斷ip的質(zhì)量
- 在線代理ip質(zhì)量檢測的網(wǎng)站
"""
import requests
proxies = {"http":'http://123.56.74.13:8080'}
headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'
}
r = requests.get('http://www.baidu.com', proxies=proxies,headers=headers)
print(r.status_code)
print(r.request.url)
session和cookie的使用與處理
cookie和session的區(qū)別
- cookie存儲在客戶的瀏覽器上汽纤,session存儲在服務器上
- cookie的安全性不如session,別人可以分析存放在本地的cokie并進行cookie欺騙
- session會在一定時間內(nèi)保存在服務器上福荸,當訪問增多蕴坪,會比較比較占用服務器的性能
- cookie保存的數(shù)據(jù)容量有限(通常是4k),很多瀏覽器限制一個站點最多保存20個cookie
爬蟲處理cookie和session
-
帶上 cookie和session的好處:
能夠請求到登錄之后的頁面
-
帶上cookie和session的弊端:
一套cookie和session往往和一個用戶對應敬锐,請求太快背传、次數(shù)太多,容易被服務器識別為爬蟲
不需要cookie的時候盡量不去使用cookie
但是為了獲取登錄之后的頁面台夺,我們必須發(fā)送帶有cookies的請求
攜帶cookie請求:
- 攜帶一堆cookie進行請求径玖,把cookie組成cookie池
如何使用
requests提供了一個叫做session的類,來實現(xiàn)客戶端和服務器端的會話保持
- 實例化一個session對象:session = requests.session()
- 讓session發(fā)送get或post請求:r = sessioon.get(url=url,data=post_data, headers=headers)
請求登錄之后的網(wǎng)站的思路:
- 實例化session
- 先使用session發(fā)送請求颤介,登陸對應網(wǎng)站挺狰,把cookie保存在session中,
這里請求時买窟,url應是表單的action的值丰泊,如果沒有action項,就嘗試抓包始绍,看看當我們提交的時候瞳购,究竟給哪個網(wǎng)址發(fā)送了post請求;post_data是表單中的要提交的數(shù)據(jù)亏推,其鍵為name
- 再使用session請求登錄之后才能訪問的網(wǎng)站学赛,session能夠自動的攜帶登錄成功時保存在其中的cookie,進行請求
案例:訪問淘寶的登錄后的頁面
import requests
sesssion = requests.session()
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0'}
#注意:在copy User-Agent時吞杭,一定要復制全盏浇,不能直接在查看的時候copy,容易帶上省略號
post_url = 'https://login.m.taobao.com/login.htm'
#post的url一般是在源碼中表單的action中找
post_data = {
'TPL_username':'xxxx',
'TPL_password2':'xxxx'
}#表單中要填寫的項
sesssion.post(url=post_url, data=post_data, headers=headers)
r = sesssion.get('https://h5.m.taobao.com/mlapp/mytaobao.html',headers=headers)
with open('taobao.html', 'w', encoding='utf-8') as f:
f.write(r.content.decode()) #會發(fā)現(xiàn)taobao.html中的代碼與我們登錄淘寶后的https://h5.m.taobao.com/mlapp/mytaobao.html的代碼一樣芽狗,即成功訪問了登錄淘寶后的頁面
不發(fā)送post請求绢掰,使用cookie獲取登錄后的頁面
即:直接將cookie加在headers里面,而不必使用session進行post
如:
import requests
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36',
'Cookie':'xxxx'
}
url = 'https://i.taobao.com/my_taobao.htm'
r = requests.get(url=url, headers=headers)
print(r.content.decode())
也可以對cookies以參數(shù)形式傳遞童擎,cookies為字典
r = requests.get('http://xxxx',headers=headers, cookies=cookies)
- cookie過期時間很長的
- 在cookie過期之前能夠拿到所有的數(shù)據(jù)滴劲,比較麻煩
- 配合其他程序一起使用,其他程序專門獲取cookie顾复,當前程序專門請求頁面
尋找登錄的post地址
-
在form表單中查找actiond的url地址
- post的數(shù)據(jù)是input標簽中的name的值作為鍵班挖,真正的用戶名密碼作為值的字典,post的url地址就是action對應的url地址
-
抓包芯砸,看看當我們提交的時候萧芙,究竟給哪個網(wǎng)址發(fā)送了post請求
勾選perserve log按鈕给梅,防止頁面跳轉找不到url
-
尋找post數(shù)據(jù),確定參數(shù)
參數(shù)不會變:(如何確定參數(shù)會不會變双揪?多請求幾次)破喻,直接用,比如密碼不是動態(tài)加密的時候
-
參數(shù)會變
- 參數(shù)在當前的響應中
- 通過js生成:定位到對應的js查看
定位想要的js
-
法一:對于Chrome瀏覽器
- 選擇登錄按鈕(或任意綁定了js事件的對象)
- Eventlistener
- 勾選Framework listeners
- 查看對應的js
- 找到登錄按鈕對應的函數(shù)
- (如果遇到某個元素(如:$('.password').value)是干嘛的盟榴,可以copy到console中去進行查看曹质;也可以直接對js添加斷點)
-
法二:對于Chrome瀏覽器
- 直接通過Chrome中的search all file的搜索url中的關鍵字
-
法三
添加斷點的方式來查看js的操作,通過python進行同樣的操作擎场,就可以得到js生成的數(shù)據(jù)
Requests的小技巧
cookie對象與字典的相互轉化與url編解碼
"""1. 把cookie對象(cookiejar)轉化為字典"""
import requests
r = requests.get('http://www.baidu.com')
print(r.cookies)
ret = requests.utils.dict_from_cookiejar(r.cookies)
print(ret) #輸出:{'BDORZ': '27315'}
"""將字典轉化為cookiejar"""
print(requests.utils.cookiejar_from_dict(ret))
"""2. url地址的編解碼"""
url = 'http://www.baidu.com'
print(requests.utils.quote(url)) #輸出:http%3A//www.baidu.com
print(requests.utils.unquote(requests.utils.quote(url))) #輸出:http://www.baidu.com
"""輸出結果如下:
<RequestsCookieJar[<Cookie BDORZ=27315 for .baidu.com/>]>
{'BDORZ': '27315'}
<RequestsCookieJar[<Cookie BDORZ=27315 for />]>
http%3A//www.baidu.com
http://www.baidu.com
"""
請求SSL證書驗證與超時設置
如果某網(wǎng)站使用的是https羽德,但是又沒有購買ssl證書,等瀏覽器訪問時就會提示不安全迅办,而當我們使用爬蟲爬取的就會報錯宅静,此時,我們可以用verify=False來解決
import requests
r = requests.get('https://www.12306.cn/mormhweb/',verify=False, timeout=10) #如果超時站欺,會報錯姨夹,因此要結合try使用
"""注意:此時不會報錯,但是會warning"""
配合狀態(tài)碼判斷是否請求成功
assert response.status_code == 200 #如果斷言失敗矾策,會報錯磷账,因此應該結合try使用
重新請求
使用retrying模塊,通過裝飾器的方式使用
"""重新請求"""
import requests
from retrying import retry
headers= {
'User-Agent':'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'
}
@retry(stop_max_attempt_number=3) #即下面的方法最多可以報錯3次贾虽,如果3次都報錯逃糟,則程序報錯
def _parse_url(url):
r = requests.get(url,headers=headers,timeout=0.01)
assert r.status_code == 200
return r.content.decode()
def parse_url(url):
try:
html_str = _parse_url(url)
except:
html_str = None
return html_str
if __name__ == "__main__":
url = 'http://www.baidu.com'
print(parse_url(url))
ps:安裝第三方模塊的方法
- pip install
- 下載源碼文件,進入解壓后的目錄:
python setup.py install
-
xxx.whl
文件蓬豁,安裝方法:pip install xxx.whl
數(shù)據(jù)提取方法
基礎知識
什么是數(shù)據(jù)提取
從響應中獲取我們想要的數(shù)據(jù)
數(shù)據(jù)的分類
-
非結構話數(shù)據(jù):html等
- 處理方法:正則表達式绰咽、xpath
-
結構化數(shù)據(jù):json、xml等
- 處理方法:轉化為python數(shù)據(jù)類型
主要是看結構清不清晰
數(shù)據(jù)提取之JSON
由于把json數(shù)據(jù)轉化為python內(nèi)奸數(shù)據(jù)類型很簡單地粪,所以爬蟲綴取募,我們常使用能夠返回json數(shù)據(jù)的url
JSON(JavaScript Object Notation)是一種輕量級的數(shù)據(jù)交換格式供填,它使得人們很容易進行閱讀和編寫 宁仔。同時也方便了機器進行解析和生成,適用于進行數(shù)據(jù)交換的場景熬甚,比如網(wǎng)站前后臺間的數(shù)據(jù)交換
Q:哪里能夠找到返回json的url呢付魔?
- 使用chrome切換到手機頁面
- 抓包手機app的軟件
json.loads與json.dumps
"""
1. json.loads 能夠把json字符串轉換成python類型
2. json.dumps 能夠把python類型轉換為json字符串聊品,當我們把數(shù)據(jù)保存問文本的時候常常需要這么做,如果要使其顯示中文几苍,可以使用參數(shù):ensure_ascii=False;還使用使用參數(shù):indent=2陈哑,使下一級相對上一級有兩個空格的縮進
"""
使用json的注意點:
-
json中的引號都是雙引號妻坝;
-
如果不是雙引號
- eval:能實現(xiàn)簡單的字符串和python類型的轉化
- replace:把單引號替換為雙引號
-
-
往一個文件中寫如多個json串伸眶,不再是一個json串
- 可以一行寫一個json串,按照行來讀取
json.load與json.dump
類文件對象:具有read和write方法的對象就是類文件對象刽宪,比如:f = open('a.txt','r')厘贼,f就是類文件對象(fp)
"""
1. json.load(類文件對象) #類型為dict
2. json.dump(python類型, 類文件對象) #把python類型放入類文件對象中,也可以使用ensure_ascii和indent參數(shù)
"""
json在數(shù)據(jù)交換中起到了一個載體的作用圣拄,承載著相互傳遞的數(shù)據(jù)
案例:爬取豆瓣
import requests
from pprint import pprint #pprint:pretty print嘴秸,實現(xiàn)美化輸出
import json
from retrying import retry
url = 'https://m.douban.com/rexxar/api/v2/skynet/playlists?from_rexxar=true&for_mobile=1'
headers = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Mobile Safari/537.36',
# 'Sec-Fetch-Mode': 'cors'
'Referer': 'https://m.douban.com/movie/beta'
#在本次爬取過程中,必須加上Referer才行
}
@retry(stop_max_attempt_number=3)
def parse_url(url):
r = requests.get(url,headers=headers, timeout=10)
assert r.status_code == 200
return r.content.decode()
resp_html = parse_url(url)
p_resp = json.loads(resp_html)
pprint(p_resp)
with open('douban.json','w', encoding='utf-8') as f:
f.write(json.dumps(p_resp, indent=2, ensure_ascii=False))
douban.json中的部分內(nèi)容如下:
案例:爬取36kr
"""爬取36kr"""
import requests,json
from pprint import pprint
import re
url = 'https://36kr.com/'
headers = {
'User-Agent':'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Mobile Safari/537.36'
}
r = requests.get(url=url, headers=headers,timeout=3)
html_str = r.content.decode()
reg = '<span class="item-title weight-bold ellipsis-2">(.*?)</span>' #新聞的標題是直接在html中的
ret = re.findall(reg, html_str)
pprint(ret)
部分輸出結果如下:
爬蟲思路總結
- 通常庇谆,我們訪問某個網(wǎng)站時岳掐,得到的是其主頁的url
- 得到了主頁的url后,觀察我們所需要的數(shù)據(jù)是否在主頁對應的響應中饭耳,如果在串述,直接利用主頁的url爬取
- 如果不在主頁的url中,查找我們需要的數(shù)據(jù)寞肖,得到其對應url纲酗,用該url進行數(shù)據(jù)的爬取
- 如果相應數(shù)據(jù)不是在html中,而是json中新蟆,用json.loads對數(shù)據(jù)進行處理
正則表達式復習
所謂正則表達式觅赊,即:事先定義好一些特定字符、及這些特定字符的組合琼稻,組成一個“規(guī)則字符串”茉兰,這個“規(guī)則字符串”用來表達對字符串的一種過濾
常用的正則表達式的方法有
- re.compile:編譯
- pattern.math:從頭找一個
- pattern.search:找一個
- pattern.findall:找所以
- pattern.sub:替換
說明:
.
的Dotall即模式即是在匹配時加上re.Dotall參數(shù),或者re.S欣簇,使.
能夠匹配任意字符記憶:d:digit规脸;s:space
sub的使用,re.sub(reg, new_str, old_str)熊咽,將匹配到的內(nèi)容替換為new_str
re.findall('a(.*)b', 'str')莫鸭,能夠返回括號中的內(nèi)容,括號前后的內(nèi)容起到定位和過濾的效果
r'a\nb' 可以匹配'a\nb'横殴;r'a\nb'而不能匹配'a\nb'被因,r可以忽略轉義符號帶來的影響,待匹配的字符串里面有 幾個\衫仑,正則表達式里面也寫幾個\即可
-
compile的作用
- 將對應正則表達式能夠匹配到的內(nèi)容放到內(nèi)存中去梨与,加快匹配的速度
- 使用方法:re.compile(reg)
-
compile和sub的結合使用
b = hello1world2 p = re.compile('\d') p.findall(b) p.sub('_',b) #將b中的所有數(shù)字替換為下劃線
ps:如果是對
.
進行編譯,若想使其能夠匹配換行符等文狱,則re.S需要加在編譯的使用粥鞋,而不是匹配的時候
貪婪模式與非貪婪模式
- 非貪婪模式:
.*?
或者.+?
- 貪婪模式:
.*
或者.+
XPATH和lXML
基礎知識
lxml是一款高性能的python HTML/XML解析器,利用xpath瞄崇,我們可以快速的定位特定元素以及獲取節(jié)點信息
什么是xpath
xpath(XML Path Language)是一門在HTML/XML文檔中查找信息的語言(既然是一種語言呻粹,就有自己的語法)壕曼,克用來在html/xml中對元素和屬性進行遍歷
W3School官方文檔:http://www.w3school.com.cn/xpath/index.asp
xml與html對比
節(jié)點的概念:每個xml標簽都稱之為節(jié)點,比如下圖中的<book>
等浊、<title>
等
book節(jié)點是title等節(jié)點的父節(jié)點腮郊,title和author等是兄弟節(jié)點,此外筹燕,還有祖先節(jié)點等概念
常用節(jié)點選擇工具
- Chrome插件XPATH Helper
- 開源的XPATH表達式編輯工具:XMLQuire(XML格式文件可用)
- FireFox插件 XPath Checker
XPATH語法
XPATH使用路徑表達式來選取xml文檔中的節(jié)點或者節(jié)點集轧飞。這些路徑表達式和我們在常規(guī)的電腦文件系統(tǒng)中看到的表達式非常類似
注意:我們寫xpath的時候,看的是請求頁的響應撒踪,而不是elements
使用Chrome插件的時候过咬,當我們選中標簽,該標簽會添加屬性class='xh-highlight'
/html 即表示從根節(jié)點開始選中html標簽
/html/head 選中html標簽下的head標簽
/html/head/link 選中html標簽下的head標簽中的所有l(wèi)ink標簽
xpath學習重點
使用xpath helper或者是Chrome瀏覽器中的copy xpath都是從element中提取的數(shù)據(jù)糠涛,但是爬蟲獲取的是url對應的響應援奢,往往和elements不一樣
-
獲取屬性
-
/html/head/link/@href
選擇html標簽下的head標簽下的(所有)link標簽中的href屬性的值
-
-
獲取文本
-
/html/head/link/text()
即選取標簽中的內(nèi)容,innerHtml -
/html//text()
獲取html下所有的標簽文本 -
//a[text()='下一頁']
選擇文本為下一頁三個字的a標簽
-
-
從當前節(jié)點往下開始選擇與使用@進行元素的定位
-
//li
選中文檔中所有的li標簽 -
//li//a
文檔中的所有l(wèi)i中的所有a標簽 -
//ul[@id='detail-list']/li
選中文檔中的id為'detail-list'的ul標簽下的li標簽忍捡;如果沒有id集漾,也可以@class等
-
選擇特定節(jié)點
上圖中的部分說明:
-
/bookstore/book[price>35.00]
book用的是子節(jié)點中的price標簽進行的修飾,此處price的形式為:<price>35.00</price>
選擇未知節(jié)點
選擇若干路徑(或的運用)
lXML庫
使用入門
"""
1. 導入lxml的etree庫:from lxml import etree砸脊,注意具篇,如果是在pycharm中,可能會報紅凌埂,但是不影響使用
2. 利用etree.HTML驱显,將字符串轉化為Element對象
3. Element對象具有xpath的方法:html = etree.HTML(text) html.xpath('字符串格式的xpath語法')
"""
應用舉例:
from lxml import etree
from pprint import pprint
text = """
<tr>
<td class="opr-toplist1-right">586萬<i class="opr-toplist1-st c-icon c-icon-up"></i></td>
</tr>
<tr>
<td class="opr-toplist1-right">539萬<i class="opr-toplist1-st c-icon c-icon-up"></i></td>
</tr>
<tr>
<td class="opr-toplist1-right">444萬<i class="opr-toplist1-st c-icon c-icon-up"></i></td>
</tr>
<tr>
<td class="opr-toplist1-right">395萬<i class="opr-toplist1-st c-icon "></i></td>
"""
html = etree.HTML(text)
#html為一個Element對象
pprint(html)
#查看element對象中包含的字符串(bytes類型)
pprint(etree.tostring(html).decode()) #會發(fā)現(xiàn)把缺少的標簽進行了補全,包括html和body標簽
print(html.xpath('//td/text()')) #這里的html是上面etree.HTML(text)獲得的對象瞳抓,結果為列表
#只要是element對象埃疫,就可以使用xpath進行數(shù)據(jù)的提取
lxml注意點
-
lxml可以自動修正html代碼(但是不一定能正確修正,也可能改錯了)
- 使用etree.tostring查看修改后的樣子孩哑,根據(jù)修改之后的html字符串寫xpath
-
提取頁面數(shù)據(jù)的思路
- 先分組栓霜,渠道:一個包含分組標簽的列表
- 遍歷:取其中每一組進行數(shù)據(jù)的提取,不會造成數(shù)據(jù)對應錯亂
xpath的包含
-
//div[contains(@class='li')]
獲取包含有l(wèi)i樣式類的標簽的標簽
爬蟲的思路總結
-
準備url
-
準備start_url
url地址規(guī)律不明顯横蜒,總數(shù)不確定
-
通過代碼提取下一頁url
- xpath:url在當前的響應里面
- 尋找url地址胳蛮,部分參數(shù)在當前的響應中,比如當前頁面數(shù)和總的頁碼數(shù)(eg:通過js生成)
-
準備url_list
- 頁碼總數(shù)明確
- url地址規(guī)律明顯
-
-
發(fā)送請求丛晌,獲取響應
添加隨機的User-Agent:反反爬蟲
添加隨機的代理ip:反反爬蟲
-
在對方判斷出我們是爬蟲之后仅炊,添加更多的header字段,包括cookie
cookie的處理可以通過session來解決
-
準備一堆能用的cookie澎蛛,組成cookie池
-
如果不登錄抚垄,準備當開始能夠請求對方網(wǎng)站的cookie,即接受對方網(wǎng)站設置在response的cookie
- 下一次請求的時候,使用之前的列表中的cookie來請求
- 即:專門用一個小程序來獲取cookie督勺,爬取數(shù)據(jù)再用另一個程序
-
如果登錄
- 準備一堆賬號
- 使用程序獲取每個賬號的cookie
- 之后請求登錄之后才能訪問的網(wǎng)站隨機的選擇cookie
-
-
提取數(shù)據(jù)
-
確定數(shù)據(jù)的位置
-
如果數(shù)據(jù)在當前的url地址中
-
提取的是列表頁的數(shù)據(jù)
- 直接請求列表頁的url地址渠羞,不用進入詳情頁
-
提取的是詳情頁的數(shù)據(jù)
- 確定url地址
- 發(fā)送請求
- 提取數(shù)據(jù)
- 返回
-
-
如果數(shù)據(jù)不在當前的url地址中
- 在其他的響應中斤贰,尋找數(shù)據(jù)的位置
- 使用chrome的過濾條件智哀,選擇除了js,css荧恍,img之外的按鈕(但是可能出錯)
- 使用chrome的search all file瓷叫,搜索數(shù)字和英文(有時候不支持搜索中文)
-
-
數(shù)據(jù)的提取
- xpath,從html中提取整塊數(shù)據(jù)送巡,先分組摹菠,之后沒一組再提取
- json
- re,提取max_time骗爆,price次氨,html中的json字符串
-
-
保存
- 保存在本地,text摘投、json煮寡、csv
- 保存在數(shù)據(jù)庫
CSV
逗號分隔值,一種文件后綴犀呼,以純文本的形式存儲表格數(shù)據(jù)
其文件中的一行對應表格的一行幸撕,以逗號分隔列
多線程爬蟲
動態(tài)HTML技術
Selenium和PhantomJS
-
Selenium
Selenium是一個Web的自動化測試工具,可以控制一些瀏覽器(比如phantomJS)外臂,可以接受指令坐儿,讓瀏覽器自動加載頁面,獲取需要的數(shù)據(jù)宋光,甚至頁面截屏
-
PhantomJS
phantomJS是一個基于Webkit的“無界面”瀏覽器貌矿,它會把網(wǎng)站加載到內(nèi)存并執(zhí)行頁面上的javascript
下載地址:http://phantomjs.org/download.html
入門
"""1. 加載網(wǎng)頁"""
from selenium import webdriver
driver = webdriver.PhantomJS("xxxx/phantom.exe")
"""除了PhantomJS,還有Chrome罪佳,F(xiàn)ireFox等"""
driver.get("http://www.baiud.com/")
driver.save_screenshot("長城.pnh")
"""2. 定位和操作"""
driver.find_element_byid("kw").send_keys("長城")
drvier.finde_element_by_id("su").click()
"""3. 查看和請求信息"""
driver.page_source()
driver.get_cookies()
driver.current_url()
"""4. 退出"""
driver.close() #退出當前頁面
driver.quit() #退出瀏覽器
"""5. 其他"""
#ps:無論是使用PhantomJS還是Chrome或是FireFox逛漫,driver的操作是一樣的
基礎使用示例
ps:chromedriver的下載地址(注意:版本一定要和你安裝的Chrome瀏覽器的版本號一致):http://npm.taobao.org/mirrors/chromedriver/
from selenium import webdriver
"""
selenium請求的速度很慢,因為是使用瀏覽器菇民,會請求js尽楔、css等
"""
phantom_path = r"D:\Green\phantomjs-2.1.1-windows\bin\phantomjs.exe"
"""在使用phatnomjs時,報了unexpected exit, status code 0的錯誤第练,尚未找到原因"""
chrome_path = r"C:\Users\25371\Desktop\chromedriver_win32\chromedriver.exe"
"""注意:這里一定要是chromedriver.exe阔馋,而不是chrome.exe"""
driver = webdriver.Chrome(executable_path=chrome_path) #實例化對象
# driver.maximize_window() #最大化窗口
driver.set_window_size(width=1920,height=1080) #設置窗口大小
driver.get("http://www.baidu.com")
driver.find_element_by_id('kw').send_keys("python")
#kw是百度的輸入框的表單的id;send_keys就是往一個input標簽里面輸入內(nèi)容
#以上的一行代碼就可以時Chrome自己百度搜索python
driver.find_element_by_id('su').click()
#su是百度一下的按鈕的id
#click實現(xiàn)對按鈕的點擊
"""獲取當前的url"""
print(driver.current_url) #注意:因為已經(jīng)click了娇掏,所以是click后的地址
"""截屏"""
driver.save_screenshot("./baidu_python.png")
"""在本次截屏中呕寝,由于截屏太快而網(wǎng)頁加載太慢,截屏的圖中未能截到百度出來的結果"""
"""driver獲取cookie"""
cookies = driver.get_cookies()
print(cookies)
cookies = {i['name']:i['value'] for i in cookies} #使用字典推導式重新生成requests模塊能用的cookies
print(cookies)
"""獲取html字符串"""
"""即elements"""
print(driver.page_source) #page_source是一個屬性婴梧,獲得html字符串后下梢,就可以直接交給xpath
"""退出當前頁面"""
driver.close() #如果只有一個窗口客蹋,close就是退出瀏覽器
"""退出瀏覽器"""
driver.quit()
示例二
from selenium import webdriver
from time import sleep
chrome_path = r"C:\Users\25371\Desktop\chromedriver_win32\chromedriver.exe"
driver = webdriver.Chrome(executable_path=chrome_path)
driver.get('https://www.qiushibaike.com/text/page/1/')
ret = driver.find_elements_by_xpath(".//div[@id='content-left']/div")
for r in ret:
print(r.find_element_by_xpath("./a[1]/div[@class='content']/span").text)
"""通過text屬性獲取文本"""
print(r.find_element_by_xpath("./a[1]").get_attribute("href"))
"""通過get_attribute獲取屬性"""
driver.quit()
元素的定位方法
- find_element_by_id 返回一個
- find_elements_by_id 返回一個列表
- find_elements_by_link_text
- find_elements_by_partial_link_text
- find_elements_by_tag_name
- find_element_by_class_name
- find_elements_by_class_name
- find_elements_by_css_selector
注意:
- 獲取文本或屬性時,需要先定位到對應元素孽江,再使用text屬性或者get_attribute方法
- element返回一個讶坯,elements返回列表
- link_text和partial_link_text的區(qū)別:全部文本和包含的某個文本,即partial可以只寫部分文本岗屏,而link_text需要寫完整
- by_css_selector的用法:#food span.dairy.aged
- by_xpath中獲取屬性和文本需要使用get_attribute()和.text
- selenium使用class定位標簽的時候辆琅,只需要其中的一個class樣式名即可,而xpath必須要寫所有的class樣式類名
示例:
from selenium import webdriver
from time import sleep
chrome_path = r"C:\Users\25371\Desktop\chromedriver_win32\chromedriver.exe"
driver = webdriver.Chrome(executable_path=chrome_path)
driver.get('https://www.qiushibaike.com/text/page/1/')
ret = driver.find_elements_by_xpath(".//div[@id='content-left']/div")
for r in ret:
print(r.find_element_by_xpath("./a[1]/div[@class='content']/span").text)
"""通過text屬性獲取文本"""
print(r.find_element_by_xpath("./a[1]").get_attribute("href"))
"""通過get_attribut獲取屬性"""
print('-'*50)
"""find_element_by_link_text"""
"""根據(jù)標簽里面的文字獲取元素"""
print(driver.find_element_by_link_text("下一頁").get_attribute('href'))
"""partial_link_text"""
print(driver.find_element_by_partial_link_text("下一").get_attribute('href'))
"""以上兩行代碼獲得的東西相同"""
driver.quit()
深入
iframe
iframe或frame里面的html代碼和外面的html代碼實質(zhì)上是兩個不同的頁面这刷,因此婉烟,有時候我們在定位元素時,明明elements里面有暇屋,但是會定位失敗
解決辦法:使用driver.switch_to.frame或driver.switch_to_frame(已經(jīng)被棄用)方法切換到對應frame中去
driver.switch_to.frame的使用說明:
def frame(self, frame_reference):
"""
Switches focus to the specified frame, by index, name, or webelement.
:Args:
- frame_reference: The name of the window to switch to, an integer representing the index,
or a webelement that is an (i)frame to switch to.
:Usage:
driver.switch_to.frame('frame_name')
driver.switch_to.frame(1)
driver.switch_to.frame(driver.find_elements_by_tag_name("iframe")[0])
"""
源碼
代碼示例:豆瓣登錄
from selenium import webdriver
import time
chrome_path = r"C:\Users\25371\Desktop\chromedriver_win32\chromedriver.exe"
driver = webdriver.Chrome(executable_path=chrome_path)
driver.get("https://douban.com/")
login_frame = driver.find_element_by_xpath("http://iframe[1]")
driver.switch_to.frame(login_frame)
driver.find_element_by_class_name("account-tab-account").click()
driver.find_element_by_id("username").send_keys("784542623@qq.com")
driver.find_element_by_id("password").send_keys("zhoudawei123")
driver.find_element_by_class_name("btn-account").click()
time.sleep(10) #暫停以手動進行驗證
"""獲取cookies"""
cookies = {i['name']:i['value'] for i in driver.get_cookies()}
print(cookies)
time.sleep(3)
driver.quit()
注意
- selenium獲取的頁面數(shù)據(jù) 是瀏覽器中elements的內(nèi)容
- selenium請求第一頁的時候似袁,會等待頁面加載完了之后再獲取數(shù)據(jù),但是在點擊翻頁之后咐刨,會立馬獲取數(shù)據(jù)昙衅,此時可能由于頁面還沒有加載完而報錯
其他
-
cookies相關用法
- {cookie['name']:cookie['value'] for cookie in driverr.get_cookies()} 獲取字典形式的cookie
- driver.delete_cookie('cookiename')
- driver.delete_all_cookies()
-
頁面等待
頁面等待的原因:如果網(wǎng)站采用了動態(tài)html技術,那么頁面上的部分元素出現(xiàn)時間便不能確定所宰,這個時候就需要一個等待時間
- 強制等待:time.sleep(10)
- 顯示等待(了解)
- 隱式等待(了解)
Tesseract的使用
tesseract是一個將圖像翻譯成文字的OCR庫绒尊,ocr:optical character recognition
-
在python中安裝tesseract模塊:pip install pytesseract,使用方法如下:
import pytesseract from PIL import Image image = Image.open(jpg) #jpg為圖片的地址 pytesseract.image_to_string(image)
使用tesseract和PIL仔粥,我們就可以使程序能夠識別驗證碼(當然婴谱,也可以通過打碼平臺進行驗證碼的識別)
示例:對如下圖片進行識別
import pytesseract
from PIL import Image
img_url = "verify_code.jpg"
pytesseract.pytesseract.tesseract_cmd = r'D:\Tesseract-OCR\tesseract.exe'
image = Image.open(img_url)
print(pytesseract.image_to_string(image))
"""
使用過程中的問題:
1. 電腦上必須先安裝tesseract客戶端,然后才能結合pytesseract使用
2. 將tesseract加入環(huán)境變量
3. 在環(huán)境變量中新建項:名字:TESSDATA_PREFIX躯泰,值:你的tesseract的安裝目錄(tessdata的父級目錄)
4. 在代碼中加入:pytesseract.pytesseract.tesseract_cmd = r"tesseract的安裝路徑\tesseract.exe"
5. 默認只識別英語谭羔,如果要識別其他語言,需要下載相關語言的.traneddata文件到Tesseract的安裝目錄下的tessdata路徑下:https://github.com/tesseract-ocr/tesseract/wiki/Data-Files
"""
"""識別結果如下:
Happy
Birthday
"""
Mongodb
注意:以下用到的集合大多為stu(學生信息)麦向,少部分為products
基礎入門
Mongodb是一種NoSQL數(shù)據(jù)庫
mysql的擴展性差瘟裸,大數(shù)據(jù)下IO壓力大,表結構更改困難诵竭;而nosql易擴展话告,大數(shù)據(jù)量高性能,靈活的數(shù)據(jù)模型卵慰,高可用
下載地址:https://www.mongodb.com/download-center/community
mongodb的使用
-
在終端運行
MongoDB\bin\mongod.exe --dbpath D:\MongoDB\data
沙郭,其中,D:\MongoDB是安裝路徑裳朋,(注意:下圖中我在安裝時把MongoDB寫成了MonggoDB- 因為在安裝時我們默認安裝了MongoDB Compass Community病线,我們打開該軟件,直接連接即可,不用對其做任何更改送挑,成功后如圖所示:
- 如果我們要使用數(shù)據(jù)庫绑莺,還需要將安裝目錄下小bin目錄加入系統(tǒng)的環(huán)境變量,然后在終端輸入mongo即可
ps:在mongo的交互環(huán)境中惕耕,可以通過tab鍵進行命令的補全
database的基礎命令
- 查看當前數(shù)據(jù)庫:db
- 查看所有的數(shù)據(jù)庫:show dbs或show databases
- 切換數(shù)據(jù)庫:use db_name
- 刪除當前數(shù)據(jù)庫:db.dropDatabase()
在mongodb里面纺裁,是沒有表的概念的,數(shù)據(jù)是存儲在集合中
向不存在的集合中第一次插入數(shù)據(jù)時赡突,集合會被創(chuàng)建出來
手動創(chuàng)建集合:
- db.createCollection(name,options)
- options是一個字典对扶,例如:{size:10, capped:true} #表示存儲的上限為10條數(shù)據(jù)区赵,capped為true表示當數(shù)據(jù)達到上限時惭缰,會覆蓋之前的數(shù)據(jù)
- 查看集合:show collections
- 刪除集合:db.collection_name.drop()
mongodb中的數(shù)據(jù)類型
- ObjectID:文檔id,所謂文檔笼才,即我們即將存儲到數(shù)據(jù)庫中的一個個的字典
- String
- Boolean:必須是小寫漱受,true或false
- Integer
- Double
- Arrays
- Object:用于嵌入式的文檔,即一個值為一個文檔
- Null
- Timestamp:時間戳
- Date:創(chuàng)建日期的格式:new Date("2019-02-01")
注意點:
- 每個文檔的都有一個屬性骡送,為_id昂羡,保證文檔的唯一性
- 可以自己設置_id插入文檔,如果沒有提供摔踱,自動生成虐先,類型為Object_id
- objecID是一個12字節(jié)的16進制數(shù),4:時間戳派敷,3:機器id蛹批,2:mongodb的服務進程id,3:簡單的增量值
數(shù)據(jù)的操作
- 用insert插入:先use數(shù)據(jù)庫篮愉,
db.集合名.insert({"name":"zhang3", "age":23})
腐芍,實質(zhì)上插入的數(shù)據(jù)不是字典,而是json试躏,因此鍵可以不用引號猪勇。insert的時候如果文檔id已經(jīng)存在,會報錯 - 查看表中的數(shù)據(jù):
db.表名.find()
- 用save進行數(shù)據(jù)的插入:
db.集合名.save(要插入的數(shù)據(jù))
颠蕴,如果文檔id(對應我們要插入的數(shù)據(jù))已經(jīng)存在泣刹,就是修改,否則新增 - 查看集合中的數(shù)據(jù):
db.集合名稱.find()
- 更新:db.集合名稱.
update(<query>,<update>,{multi:<boolean>})
犀被,用法如下:db.stu.upate({name:'hr'}, {name:'mnc'})#更新一條的全部 db.stu.update({name:'hr'}, {$set:{name:'mnc'}}) #更新一條中的對應鍵值 """這種更改用得更多""" db.stu.update({},{$set:{gender:0}}, {multi:true}) #更新全部 """注意:multi這個參數(shù)必須和$符號一起使用才有效果"""
- 使用remove刪除數(shù)據(jù):db.集合名.remove({name:"zhang3"},{justOne:true})椅您,表示只刪除一條名字為zhang3的數(shù)據(jù),如果不指定justOne弱判,就是刪除全部符合的數(shù)據(jù)
高級查詢
find
"""find"""
db.stu.find() #查詢所有的數(shù)據(jù)
db.stu.find({age:23}) #查詢滿足條件的數(shù)據(jù)
db.stu.findOne({age:23}) #查詢滿足條件的一個數(shù)據(jù)
db.stu.find().pretty() #對數(shù)據(jù)進行美化
比較運算符
1. 等于:默認是等于判斷襟沮,沒有運算符
2. 小于:$lt(less than)
3. 大于:$gt(greater than)
4. 小于等于:$lte(less than equal)
5. 大于等于:$gte(greater than equal)
6. 不等于:$ne(not equal)
使用舉例:
db.stu.find({age:{$le(18)}}) #查詢年齡小于18的
范圍運算符
1. $in:在某個范圍
2. $nin:不在某個范圍
用法舉例:
db.stu.find({age:{$in[18,28,38]}}) #查詢年齡為18或28或38的
邏輯運算符
and:直接寫多個條件即可,例:db.stu.find({age:{$gte:18},gender:true})
or:使用$or,值為數(shù)組开伏,數(shù)組中的每個元素為json膀跌,例:查詢年齡大于18或性別為false的數(shù)據(jù):db.stu.find({$or:[{age:{$gt:18}},{gender:{false}}]})
正則表達式
1. db.products.find({sku:/^abc/}) #查詢以abc開頭的sku
2. db.products.find({sku:{$regex:"789$"}}) #查詢以789結尾的sku
limit和skip
1. db.stu.find().limit(2) #查詢兩個學生的信息
2. db.stu.find().skip(2) #跳過前兩個學生的信息
3. db.stu.find().skip(2).limit(4) #先跳過2個,再查找4個
自定義查詢
db.stu.find({$where:function(){
return this.age > 30;
}}) #查詢年齡大于30的學生
投影
即返回滿足條件的數(shù)據(jù)中的部分內(nèi)容固灵,而不是返回整條數(shù)據(jù)
db.stu.find({$where:function(){
return this.age > 30, {name:1,hometown:1};
}}) #查詢年齡大于30的學生捅伤,并返回其名字和hometown,其中巫玻,this是指從前到后的每一條數(shù)據(jù)丛忆;如果省略{name:xxx}就會返回該條數(shù)據(jù)的全部內(nèi)容
db.stu.find({},{_id:0,name:1,hometown:1}) #顯示所有數(shù)據(jù)的name和hometown,不顯示_id仍秤,但是要注意熄诡,只有_id可以使用0;一般對其他字段來說诗力,要顯示的寫1凰浮,不顯示的不寫即可,_id默認是會顯示的
排序
db.stu.find().sort({age:-1}) #按年齡的降序排列苇本,如果是{age:1}就是按按鈴升序排序
db.stu.find().sort({age:1,gender:-1}) #按年齡的升序排列袜茧,如果年齡相同,按gender的降序排列
count方法
db.stu.find({條件}).count() #查看滿足條件的數(shù)據(jù)有多少條
db.stu.count({條件})
消除重復
db.stu.distinct("去重字段",{條件})
db.stu.distinct("hometown",{age:{$gt:18}}) #查看年齡大于18的人都來自哪幾個地方
聚合aggregate
聚合(aggregate)是基于數(shù)據(jù)處理的聚合管道瓣窄,每個文檔通過一個由多個階段(stage)組成的管道笛厦,可以對每個階段的管道進行分組、過濾等功能俺夕,然后經(jīng)過一系列的處理裳凸,輸出相應的結果。
db.集合名稱.aggregate({管道:{表達式}})
所謂管道啥么,即把上一次的輸出結果作為下一次的輸入數(shù)據(jù)
常用管道如下:
- $group: 將集合中的?檔分組登舞, 可?于統(tǒng)計結果
- $match: 過濾數(shù)據(jù), 只輸出符合條件的?檔悬荣,match:匹配
- $project: 修改輸??檔的結構菠秒, 如重命名、 增加氯迂、 刪除字段践叠、 創(chuàng)建計算結果
- $sort: 將輸??檔排序后輸出
- $limit: 限制聚合管道返回的?檔數(shù)
- $skip: 跳過指定數(shù)量的?檔, 并返回余下的?檔
- $unwind: 將數(shù)組類型的字段進?拆分嚼蚀,即展開的意思
表達式
語法:表達式:'$列名'
常?表達式:
- sum:1 表示以?倍計數(shù)
- $avg: 計算平均值
- $min: 獲取最?值
- $max: 獲取最?值
- $push: 在結果?檔中插?值到?個數(shù)組中
- $first: 根據(jù)資源?檔的排序獲取第?個?檔數(shù)據(jù)
- $last: 根據(jù)資源?檔的排序獲取最后?個?檔數(shù)據(jù)
用法示例:
"""group的使用"""
db.Temp.aggregate(
{$group:{_id:"$gender"}}
) #按性別分組
"""輸出結果:
{ "_id" : 1 }
{ "_id" : 0 }
"""
db.Temp.aggregate(
{$group:{_id:"$gender",count:{$sum:1}}}
) #按性別分組并計數(shù),sum:1是指每條數(shù)據(jù)作為1
"""輸出結果如下:
{ "_id" : 1, "count" : 7 }
{ "_id" : 0, "count" : 1 }
"""
"""注意:_id和count的鍵不能變"""
db.Temp.aggregate(
{$group:{_id:"$gender",
count:{$sum:1},
avg_age:{$avg:"$age"}}}
) #按年齡分組并計數(shù)轿曙,再分別計算其年齡的平均值
"""結果如下:
{ "_id" : 1, "count" : 7, "avg_age" : 22.857142857142858 }
{ "_id" : 0, "count" : 1, "avg_age" : 32 }
"""
"""注意:如果分組時_id:null病涨,則會將整個文檔作為一個分組"""
"""管道的使用"""
db.Temp.aggregate(
{$group:{_id:"$gender",count:{$sum:1},avg_age:{$avg:"$age"}}},
{$project:{gender:"$_id",count:"$count",avg_age:"$avg_age"}}
) #將group的輸出再作為project的輸入,因為前面已經(jīng)有了_id觅够,count行瑞,avg_age等輸出鍵当悔,所以在后面的管道中可以直接使用(此例中用了_id和avg_age),也可以使用1使其顯示,0使其不顯示
"""輸出結果如下
{ "count" : 7, "gender" : 1, "avg_age" : 22.857142857142858 }
{ "count" : 1, "gender" : 0, "avg_age" : 32 }
"""
"""match管道的使用"""
#為什么使用match過濾而不是find的過濾?match可以將其數(shù)據(jù)交給下一個管道處理荞雏,而find不行
db.Temp.aggregate(
{$match:{age:{$gt:20}}},
{$group:{_id:"$gender",count:{$sum:1}}},
{$project:{_id:0,gender:"$_id",count:1}}
) #先選擇年齡大于20的數(shù)據(jù);然后將其交給group管道處理平酿,按照性別分組凤优,對每組數(shù)據(jù)進行計數(shù);然后再將其數(shù)據(jù)交給project處理蜈彼,讓_id字段顯示為性別筑辨,不顯示_id字段,顯示count字段
"""sort管道的使用"""
db.Temp.aggregate(
{$group:{_id:"$gender",count:{$sum:1}}},
{$sort:{count:-1}}
) #將第一個管道的數(shù)據(jù)按照其count字段的逆序排列柳刮,和find中的排序使用方式一樣
"""結果如下:
{ "_id" : 1, "count" : 7 }
{ "_id" : 0, "count" : 1 }
"""
"""skip和limit的用法示例:
{$limit:2}
{$skip:5}
"""
"""unwind使用使例:"""
eg:假設某條數(shù)據(jù)的size字段為:['S','M','L']挖垛,要將其拆分
db.Temp.aggregate(
{$match:{size:["S","M","L"]}}, #先找到該數(shù)據(jù)
{$unwind:"$size"}
)
"""結果如下:
{ "_id" : ObjectId("5d57cada783675188ccd7ee0"), "size" : "S" }
{ "_id" : ObjectId("5d57cada783675188ccd7ee0"), "size" : "M" }
{ "_id" : ObjectId("5d57cada783675188ccd7ee0"), "size" : "L" }
"""
小練習:
"""數(shù)據(jù)和需求:
{ "country" : "china", "province" : "sh", "userid" : "a" }
{ "country" : "china", "province" : "sh", "userid" : "b" }
{ "country" : "china", "province" : "sh", "userid" : "a" }
{ "country" : "china", "province" : "sh", "userid" : "c" }
{ "country" : "china", "province" : "bj", "userid" : "da" }
{ "country" : "china", "province" : "bj", "userid" : "fa" }
需求:統(tǒng)計出每個country/province下的userid的數(shù)量(同一個userid只統(tǒng)計一次)
"""
db.Exci.aggregate(
{$group:{_id:{userid:"$userid",province:"$province",country:"$country"}}}, #先按照三個字段分組(去重)
{$group:{_id:{country:"$_id.country",province:"$_id.province"},count:{$sum:1}}},
{$project:{_id:0,country:"$_id.country",province:"$_id.province",count:"$count"}}
)
"""注意:取字典里面的元素用(.)操作符;group的_id可以為字典"""
"""三個管道處理過后的數(shù)據(jù)分別如下:
#第一次group
{ "_id" : { "userid" : "a", "province" : "sh", "country" : "china" } }
{ "_id" : { "userid" : "b", "province" : "sh", "country" : "china" } }
{ "_id" : { "userid" : "c", "province" : "sh", "country" : "china" } }
{ "_id" : { "userid" : "da", "province" : "bj", "country" : "china" } }
{ "_id" : { "userid" : "fa", "province" : "bj", "country" : "china" } }
#第二次group
{ "_id" : { "country" : "china", "province" : "bj" }, "count" : 2 }
{ "_id" : { "country" : "china", "province" : "sh" }, "count" : 3 }
#最終結果
{ "country" : "china", "province" : "sh", "count" : 3 }
{ "country" : "china", "province" : "bj", "count" : 2 }
"""
"""也可以寫成如下形式:"""
db.Temp.aggregate(
{$match:{size:["S","M","L"]}},
{$unwind:{path:"$size",preserveNullAndEmptyArrays:true}}
) #path字段是要拆分的字段秉颗,參數(shù)表示保存Null和EmptyArrays,因為如果是原數(shù)據(jù)中有某字段為null或[]送矩,那么在拆分一個數(shù)據(jù)后蚕甥,表中原來含nul或[]的那幾條數(shù)據(jù)會消失
索引
作用:提升查詢速度
db.t1.find({查詢條件})
db.t1.find({查詢條件}).explain('executionStats') 可以通過其中的"executionTimeMillisEstimate"字段查看查詢所花費的時間
建立索引
語法:db.集合.ensureIndex({屬性1:1}, {unique:true}) 其中,1表示升序栋荸,-1表示降序菇怀,一般來說,升序或降序的影響不大晌块;unique字段可以省略爱沟,加上后,保證索引唯一匆背,即:如果我們用name作為索引呼伸,那么在集合中就不能有name值相同的數(shù)據(jù);
聯(lián)合索引钝尸,即ensureIndex的參數(shù)為{屬性1:1或-1, 屬性2:-1或1}括享,聯(lián)合索引是為了保證數(shù)據(jù)的唯一性
唯一索引的作用:比如,當我們爬取數(shù)據(jù)時珍促,如果使用了唯一索引铃辖,那么,當我們爬到重復數(shù)據(jù)時猪叙,就不會存儲到數(shù)據(jù)庫中
默認有一個index:_id
查看索引:db.集合.getIndex()
刪除索引:db.集合.dropIndex("索引名稱") #索引名稱即我們創(chuàng)建索引時傳入的字典{屬性:1或-1}娇斩,可以通過getIndex()查看時的key項
爬蟲數(shù)據(jù)去重仁卷,實現(xiàn)增量式爬蟲
使用數(shù)據(jù)庫建立唯一索引進行去重
-
url地址去重
-
使用場景
- 如果url對應的數(shù)據(jù)不會變,url地址能夠唯一的判別一條數(shù)據(jù)的情況
-
思路
- url地址存在redis中
- 拿到url地址犬第,判斷url地址在url的集合中是否存在
- 存在:不再請求
- 不存在:請求五督,并將該url地址存儲到redis數(shù)據(jù)庫中
-
布隆過濾器
- 使用加密算法加密url地址,得到多個值
- 往對應值的位置把結果設置為1
- 新來一個url地址瓶殃,一樣通過加密算法生成多個值
- 如果對應位置的值全為1充包,說明這個url已經(jīng)被抓過
- 否則沒有被抓過,就把對應位置的值設置為1
-
-
根據(jù)數(shù)據(jù)本身去重
- 選擇特定字段遥椿,使用加密算法(md5基矮,shal)將字段進行加密,生成字符串冠场,存入redis集合中
- 如果新來一條數(shù)據(jù)家浇,同樣的方法進行加密,如果得到的數(shù)據(jù)在redis中存在碴裙,說明數(shù)據(jù)存在钢悲,要么插入,要么更新舔株,否則不存在莺琳,直接插入
數(shù)據(jù)的備份與恢復
備份的語法:
mongodump -h dbhost -d dbname -o dbdirectory
-h: 服務器地址, 也可以指定端?號载慈,如果是本機上惭等,就可以省略
-d: 需要備份的數(shù)據(jù)庫名稱
-o: 備份的數(shù)據(jù)存放位置, 此?錄中存放著備份出來的數(shù)據(jù)
備份的數(shù)據(jù)中办铡,一個json和一個bson表示一個集合
恢復的語法:
mongorestore -h dbhost -d dbname --dir dbdirectory
-h: 服務器地址
-d: 需要恢復的數(shù)據(jù)庫實例辞做,即數(shù)據(jù)庫的名字
--dir: 備份數(shù)據(jù)所在位置
pymongo的使用
pip install pymongo
from pymongo import MongoClient
用法示例
from pymongo import MongoClient
client = MongoClient(host='127.0.0.1',port=27017)
#實例化client,即和數(shù)據(jù)庫建立連接
collection = client['admin']['Temp'] #使用[]選擇數(shù)據(jù)庫和集合
collection.insert_one({"name":"laowang","age":33}) #插入一條數(shù)據(jù)
it_data = [{"name":"laoli","age":23},{"name":"laozhao","age":43}]
collection.insert_many(it_data) #插入多條數(shù)據(jù)
print(collection.find_one({"name":"laoli"}))
print(collection.find()) #是一個Cursor(游標)對象寡具,一個Cursor對象只能進行一次遍歷
for ret in collection.find():
print(ret) #遍歷查看cursor對象
print(list(collection.find())) #強制轉化為list
collection.delete_one({"name":"laoli"}) #刪除一個
collection.delete_many({"age":33}) #刪除所有age為33的
# mongodb不需要我們手動斷開連接
scrapy
scrapy簡介
為什么要使用scrapy:使我們的爬蟲更快更強
scrapy是一個為了爬取網(wǎng)站數(shù)據(jù)秤茅,提取結構性數(shù)據(jù)而編寫的應用框架
scrapy使用了Twisted異步網(wǎng)絡框架,可以加快我們的下載
scrapy的工作流程
scheduler里面實際上存放的并不是url地址童叠,而是request對象
在spiders處框喳,url要先組裝成request對象再交到scheduler調(diào)度器
scrapy引擎的作用:scheduler將request交給scrapy engine,engine再交給下載器拯钻,response也是先由下載器交給scrapy engine帖努,然后再由engine交給spiders,url類似粪般,先交給scrapy engine拼余,再交給scheduler
engine實現(xiàn)了程序的解耦,response和request在經(jīng)過scrapy后亩歹,還要經(jīng)過各自的middleware匙监,再交到目的地凡橱,因此我們就可以定義自己的中間件,對reponse和request進行一些額外的處理
爬蟲中間件不會對爬蟲提取的數(shù)據(jù)進行數(shù)據(jù)(實際上可以亭姥,但是因為有專門的部分進行這項工作稼钩,所以我們通常不這么做)
scrapy入門
-
創(chuàng)建一 個scrapy項目:scrapy startproject 項目名(eg:myspider)
生成一個爬蟲:scrapy genspider 爬蟲名字 "允許爬取的域名"
-
提取數(shù)據(jù)
- 完善spider,使用xpath等方法
-
保存數(shù)據(jù)
- pipline中保存數(shù)據(jù)
scrapy具體流程及spider和pipline講解
此處的項目名為mySpider达罗,創(chuàng)建的爬蟲名字為itcast
新建一個python項目
在Terminal中:scrapy startproject mySpider
在Terminal中坝撑,根據(jù)提示:cd mySpider
-
在Terminal中:scrapy genspider itcast "itcast.cn",此時粮揉,如果創(chuàng)建成功巡李,就會在spiders目錄中有了itcast.py;在里面扶认,我們寫上如下代碼段內(nèi)容:
-
在項目文件夾中使用scrapy新建的爬蟲都在spider文件夾中侨拦,每個spider文件即對應上面流程圖中的spiders,其中有幾個默認字段:
- name:爬蟲的名字辐宾,默認有
- allowed_domains:默認有(在使用scrapy新建spider的時候通常會指定)
- start_urls:默認有狱从,但是通常需要我們自己修改,其值為我們最開始請求的url地址
- parse方法:處理start_url對應的響應地址叠纹,通過yield將我們提取到的數(shù)據(jù)傳遞到pipline
import scrapy class ItcastSpider(scrapy.Spider): name = 'itcast' #爬蟲名 allowed_domains = ['itcast.cn'] #允許爬取的范圍 start_urls = ['http://www.itcast.cn/channel/teacher.shtml'] #最開始請求的url地址 def parse(self, response): """處理start_url對應的響應""" # ret1 = respnse.xpath("http://div[@class='tea_con']//h3/text()").extract() # #提取數(shù)據(jù) # #extract()方法可以提取其中的文字 # print(ret1) li_list = response.xpath("http://div[@class='tea_con']//li") for li in li_list: item = {} item['name'] = li.xpath(".//h3/text()").extract_first() #提取第一個字符串 #使用extract_first季研,如果是沒有拿到元素,會返回none吊洼,而不是報錯(extract()[0]在拿不到元素的情況下會報錯) item['title'] = li.xpath(".//h4/text()").extract_first() yield item #將item傳給piplines
-
在Terminal中训貌,進入項目文件夾下:scrapy crawl itcast,就會自動開始爬让扒稀;然后在terminal中輸出一些結果和一些日志豺鼻,我們可以在settings.py中對日志的級別進行設置综液,比如添加:LOG_LEVEL = "WARNING",比warning等級小的日志都不會輸出
-
pipline的使用
pipline對應流程圖中的item pipline
-
要使用pipline儒飒,需要在項目的settings.py文件中取消對pipline的注釋谬莹,使其可用,即
# Configure item pipelines # See https://docs.scrapy.org/en/latest/topics/item-pipeline.html ITEM_PIPELINES = { 'mySpider.pipelines.MyspiderPipeline': 300, #意為:mySpider項目下的piplines模塊中的MyspiderPipline類桩了,后面的數(shù)字(300)表示距離引擎的遠近附帽,越近,數(shù)據(jù)就會越先經(jīng)過該pipline #所謂管道井誉,就是把前面管道處理后的數(shù)據(jù)再交給后面的管道處理蕉扮,這里ITEM_PIPLINES的格式為字典:管道類的位置:距離引擎的遠近 #把spiders提取的數(shù)據(jù)由近及遠交給各個管道處理 #亦即:我們可以在piplines中定義自己的多個管道,然后在這里進行注冊颗圣,使其可用喳钟,如下 'mySpider.pipelines.MyspiderPipeline1': 301, }
執(zhí)行爬蟲爬取數(shù)據(jù):scrapy crawl 爬蟲的名字(類下面的name的值)
-
在piplines.py中屁使,我們定義了如下的兩個管道
- 為了讓數(shù)據(jù)能夠在各個管道間進行傳遞,每個管道必須return item奔则,這里的item即為spider中傳遞過來的數(shù)據(jù)
- process_item方法是必須的蛮寂,專門用于對數(shù)據(jù)的處理,只有它可以接受傳遞過來的item
- spider就是爬蟲在傳item的時候易茬,把自己也傳遞過來了酬蹋,即:這里的參數(shù)spider就是我們在spiders目錄下的爬蟲名的py文件中定義的spider類
class MyspiderPipeline(object): def process_item(self, item, spider): print(item) return item class MyspiderPipeline1(object): def process_item(self, item, spider): print(item.items()) return item
-
為什么需要有多個pipline
- 一個項目通常有多個爬蟲,而對于爬取的數(shù)據(jù)抽莱,我們通常要進行的處理不相同范抓,因此就需要使用不同的pipline
- 此時需要對傳過來的item進行判別,比如可以使用在item中添加某字段以判別(或者使用spider進行判別岸蜗,比如:if spider.name == xxx)尉咕,如果是我們要處理的數(shù)據(jù)才進行處理,否則傳遞給其他pipline
- 一個spider的內(nèi)容可能要做不同的操作璃岳,比如存入不同的數(shù)據(jù)庫中年缎,我們就可以使用多個pipline分多步進行
- 一個項目通常有多個爬蟲,而對于爬取的數(shù)據(jù)抽莱,我們通常要進行的處理不相同范抓,因此就需要使用不同的pipline
logging模塊的使用
在settings.py文件中,可以添加字段:LOG_LEVEL="log等級"铃慷,以控制當前爬蟲的輸出的log等級(大于)
在spider中輸出log的兩種常用方法:
import logging
单芜,然后使用logging.warning(要輸出的信息)
,無法顯示log來自哪個文件-
impot logging
犁柜,然后logger = logging.getLogger(__name__)
洲鸠,使用logger.warning(要輸出的數(shù)據(jù))
,此種方法可以輸出日志來自哪個文件- ps:我們實例化了一個logger之后馋缅,在其他的文件中如果要使用log扒腕,不必單獨再去實例化一個對象,直接導入現(xiàn)有的logger即可
如果我們要想使log輸出到文件中萤悴,而非terminal瘾腰,則需要在settings.py中添加字段:LOG_FILE = "保存log的文件路徑"
如果要自定義log的格式,在使用logging前:logging.basicConfig(xxx)覆履,其中的xxx即我們要自定義的log格式
logging.basicConfig()示例:
import logging
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S',
filemode='w')
"""basicConfig參數(shù)"""
logging.basicConfig函數(shù)各參數(shù):
filename: 指定日志文件名
filemode: 和file函數(shù)意義相同蹋盆,指定日志文件的打開模式,'w'或'a'
format: 指定輸出的格式和內(nèi)容硝全,format可以輸出很多有用信息栖雾,如上例所示:
%(levelno)s: 打印日志級別的數(shù)值
%(levelname)s: 打印日志級別名稱
%(pathname)s: 打印當前執(zhí)行程序的路徑,其實就是sys.argv[0]
%(filename)s: 打印當前執(zhí)行程序名
%(funcName)s: 打印日志的當前函數(shù)
%(lineno)d: 打印日志的當前行號
%(asctime)s: 打印日志的時間
%(thread)d: 打印線程ID
%(threadName)s: 打印線程名稱
%(process)d: 打印進程ID
%(message)s: 打印日志信息
datefmt: 指定時間格式伟众,同time.strftime()
level: 設置日志級別析藕,默認為logging.WARNING
stream: 指定將日志的輸出流,可以指定輸出到sys.stderr,sys.stdout或者文件赂鲤,默認輸出到sys.stderr噪径,當stream和filename同時指定時柱恤,stream被忽略
翻頁請求
- 在爬蟲中,首先找爱,獲得下一頁的url:
next_url = response.xpath("http://a[text()='下一頁']/@href").extract()
以獲得下一頁的url - 然后使用scrapy.Request構造一個request梗顺,同時指定回調(diào)函數(shù)
- 在回調(diào)函數(shù)中,對于要pipline處理的數(shù)據(jù)车摄,同樣要yield
while next_url: yield scrapy.Request(next_url, callback=self.parse) #如果下一頁數(shù)據(jù)的處理和當前頁相同寺谤,那么回調(diào)函數(shù)就直接指定當前函數(shù)即可,如果處理方式吮播,則另外定義一個函數(shù)進行回調(diào)即可变屁;這里實際上也是實例化了一個request對象交給引擎
scrapy.request的其他知識點
- 如圖所示,在crapy中意狠,cookies就不能再放到headers中去
- 所謂解析函數(shù)粟关,可以簡單理解為回調(diào)函數(shù),meta的格式為字典环戈,在解析函數(shù)中獲取該數(shù)據(jù)時:response.meta["鍵"]
設置user-agent
- user-agent的使用:在項目的設置文件中找到對應項進行設置即可
案例(結合下面的item)
爬取陽光熱線問政平臺:
# -*- coding: utf-8 -*-
import scrapy
from yangguang.items import YangguangItem
class YgSpider(scrapy.Spider):
name = 'yg'
allowed_domains = ['sun0769.com']
start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page=0']
def parse(self, response):
tr_list = response.xpath("http://div[@class='greyframe']/table[2]/tr/td/table/tr")
for tr in tr_list:
item = YangguangItem()
item['title'] = tr.xpath("./td[2]/a[@class='news14']/@title").extract_first()
item['href'] = tr.xpath("./td[2]/a[@class='news14']/@href").extract_first()
item['publish_data'] = tr.xpath("./td[last()]/text()").extract_first()
yield scrapy.Request(
item['href'],
callback= self.parse_detail,
meta={"item":item}
)
next_url = response.xpath("http://a[text()='>']/@href").extract_first()
if next_url:
yield scrapy.Request(
next_url,
callback=self.parse
)
def parse_detail(self,response):
"""處理詳情頁"""
item = response.meta['item']
item['content'] = response.xpath("http://td[@class='txt16_3']//text()").extract_first()
item['content_img'] = response.xpath("http://td[@class='txt16_3']//img/@src").extract()
yield item
scrapy深入
Items
使用流程:
-
先在items.py中寫好我們要使用的字段闷板,如圖:
- 說明:就相當于我們定義了一個字典類,規(guī)定好了里面可以有的鍵院塞,以后如果使用這個字典的實例時遮晚,發(fā)現(xiàn)想往里面存入未定義的鍵,程序就會報錯
使用時拦止,在spider中導入該類(這里是Myspideritem)县遣,然后用其實例化一個對象(當作字典使用即可)
-
然后通常是用這個字典存儲我們提取的數(shù)據(jù),然后把其在各個piplines間傳遞
注意:
item對象不能直接插入mongodb(只是像字典汹族,畢竟不是字典)萧求,可以強制將其轉化為字典,然后存入即可ps:對不同爬蟲爬取的數(shù)據(jù)顶瞒,我們可以定義多個item類饭聚,然后在pipline中處理時,可以用isinstance判斷是否為某個item類的實例搁拙,如果是,我們才處理
debug信息
scrapy shell
Scrapy shell是一個交互終端法绵,我們可以在未啟動spider的情況下嘗試及調(diào)試代碼箕速,也可以用來測試XPath表達式
使用方法:
scrapy shell http://www.itcast.cn/channel/teacher.shtml
就會自動進入shell,在shell中進行一些操作朋譬,會自動提示
然后就能得到response
response:
- response.url:當前響應的url地址
- response.request.url:當前響應對應的請求的url地址
- response.headers:響應頭
- response.body:響應體盐茎,也就是html代碼,默認是byte類型
- response.requests.headers:當前響應的請求頭
- response.xpath()
spider:
- spider.name
- spider.log(log信息)
settings
settings中的字段
默認已有字段:
- bot_name:項目名
- spider_modules:爬蟲位置
- newspider_module:新建爬蟲的位置
- user-agent:用戶代理
- robotstxt_obey:是否遵守robot協(xié)議
- CONCURRENT_REQUESTS:并發(fā)請求的最大數(shù)量
- DOWNLOAD_DELAY:下載延遲
- CONCURRENT_REQUESTS_PER_DOMAIN:每個域名的最大并發(fā)請求數(shù)
- CONCURRENT_REQUESTS_PER_IP:每個代理ip的最大并發(fā)請求數(shù)
- COOKIES_ENABLED:是否開啟cookies
- TELNETCONSOLE_ENABLED:是否啟用teleconsole插件
- DEFAULT_REQUEST_HEADERS:默認請求頭
- spider_midddleware:爬蟲中間件
- downlowd_middleware:下載中間件
- EXTENSIONS:插件
- ITEM_PIPELINES:管道徙赢,其格式為:
管道的位置:權重
- AUTOTHROTTLE_ENABLED:自動限速
- 緩存的配置項
可自己添加字段:
- LOG_LEVEL
在其他位置中要使用配置中的數(shù)據(jù):
- 法一:直接導入settings模塊使用
- 如果是在spider中:可以直接用self.settings.get()或是self.settings[]以字典的形式存取相關數(shù)據(jù)
- 如果是在pipline中字柠,由于傳過來了spider探越,就以spider.settings.get()或spider.settings[]存取
piplines
import json
class myPipline(object):
def open_spider(self,spider):
"""在爬蟲開啟的時候執(zhí)行一次"""
#比如實例化MongoClient
pass
def close_spider(self,spider):
"""在爬蟲關閉的時候執(zhí)行一次"""
pass
def process_item(self,item,spider):
"""對spider yield過來的數(shù)據(jù)進行處理"""
pass
return item
"""如果不return item,其他的pipline就無法獲得該數(shù)據(jù)"""
CrawlSpider
之前爬蟲的思路:
1窑业、從response中提取所有的a標簽對應的url地址
2钦幔、自動的構造自己requests請求,發(fā)送給引擎
改進:
滿足某個條件的url地址常柄,我們才發(fā)送給引擎鲤氢,同時能夠指定callback函數(shù)
如何生成crawlspider:
eg:
scrapy genspider –t crawl csdn “csdn.com"
crawlspider示例:
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
import re
class CfSpider(CrawlSpider): #繼承的父類不再是scrapy.spider
name = 'cf'
allowed_domains = ['circ.gov.cn']
start_urls = ['http://circ.gov.cn/web/site0/tab5240/module14430/page1.htm']
"""定義提取url規(guī)則的地方"""
"""每個Rule是一個元組"""
"""注意:每個url提取出來后被構造成一個請求,他們沒有先后順序"""
rules = (
#Rule是一個類西潘,LinkExtractor: 鏈接提取器卷玉,其參數(shù)是一個正則表達式,提取到了link喷市,就交給parse函數(shù)進行請求
#所以我們在crawlspider中不能自己定義parse函數(shù)
#url請求的響應數(shù)據(jù)會交給callback處理相种,如果不用提取該url中的數(shù)據(jù),就可以不指定callback
#follow品姓,當前url地址的相應是否重新進入rules來提取url地址(會挨個按規(guī)則提取寝并,如果被前面的正則表達式匹配到了,就不會再被后面的進行匹配提取缭黔,所以寫正則表達式的時候應該盡可能明確)
#注意:crawlspider能夠幫助我們自動把鏈接補充完整食茎,所以我們下面的allow中并沒有手動補全鏈接
Rule(LinkExtractor(allow=r'/web/site0/tab5240/info\d+.htm'), callback='parse_item', follow=False),
Rule(LinkExtractor(allow=r'/web/site0/tab5240/module14430/page\d+.htm'), follow=True),
)
"""parse函數(shù)不見了,因為其有特殊功能馏谨,不能定義"""
def parse_item(self, response):
"""解析函數(shù)"""
item = {}
#item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
#item['name'] = response.xpath('//div[@id="name"]').get()
#item['description'] = response.xpath('//div[@id="description"]').get()
item['title'] = re.findall(r"<!--TitleStart-->(.*?)<!--TitleEnd-->",response.body.decode())[0]
item['date'] = re.findall(r"發(fā)布時間:(\d{4}-\d{2}-\d{2})",response.body.decode())
print(item)
# 也可以自己再yiel scrapy.Request
# yield scrapy.Request(
# url,
# callback=self.parse_detail,
# meta={"item":item}
# )
#
# def parse_detail(self,response):
# item = response.meta['item']
# pass
# yield item
LinkExtractor和Rule的更多知識點
中間件
下載中間件
下載中間件是我們要經(jīng)常操作的一個中間件别渔,因為我們常常需要在下載中間件中對請求和響應做一些自定義的處理
如果我們要使用中間件,需要在settings中開啟惧互,其格式也是:位置:權重(或者說是距離引擎的位置哎媚,越小越先經(jīng)過)
Downloader Middlewares默認的方法:
- process_request(self, request, spider):
當每個request通過下載中間件時,該方法被調(diào)用喊儡。 - process_response(self, request, response, spider):
當下載器完成http請求拨与,傳遞響應給引擎的時候調(diào)用
案例:使用隨機user-agent:
在settings.py中定義USER_AGENTS_LIST:
USER_AGENTS_LIST = ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0',
]
在middlewares.py中定義下載中間件:
import random
class RandomUserAgentMiddleware:
"""自定義一個下載中間件"""
def process_request(self,request,spider):
ua = random.choice(spider.settings.get("USER_AGENTS_LIST"))
request.headers["User-Agent"] = ua
#request.meta['proxy'] = "你的proxy" #也可以通過此種方法來使用代理
# return request,process不能返回request
class CheckUserAgentMiddleware:
def process_response(selfs,request,response,spider):
print(request.headers["User-Agent"]) #查看是否實現(xiàn)了隨機用戶代理
return response
# process_response必須返回reponse
并在settings.py中對自己的中間件進行注冊:
DOWNLOADER_MIDDLEWARES = {
# 'circ.middlewares.CircDownloaderMiddleware': 543,
'circ.middlewares.RandomUserAgentMiddleware': 544,
'circ.middlewares.CheckUserAgentMiddleware': 545, #這里的權重并不重要,因為他們一個是處理請求艾猜,一個是處理響應的
}
spiders內(nèi)容省略
scrapy模擬登錄
對于scrapy來說买喧,有兩個方法模擬登陸:
1、直接攜帶cookie
2匆赃、找到發(fā)送post請求的url地址淤毛,帶上信息,發(fā)送請求
直接攜帶cookie
案例:爬取人人網(wǎng)登錄后的信息
import scrapy
import re
class RenrenSpider(scrapy.Spider):
name = 'renren'
allowed_domains = ['renren.com']
start_urls = ['http://www.renren.com/971962231/profile'] #人人網(wǎng)的個人主頁
def start_requests(self):
"""覆蓋父類的start_request方法算柳,從而使其攜帶我們自己的cookies"""
cookies = "我的cookies"
cookies = {i.split("=")[0]:i.split("=")[1] for i in cookies.split(";")}
yield scrapy.Request(
self.start_urls[0],
callback=self.parse, #表示當前請求的響應會發(fā)送到parse
cookies=cookies
) #因為cookies默認是enable的低淡,所以下次請求會自動拿到上次的cookies
def parse(self, response):
print(re.findall(r"假裝",response.body.decode())) #驗證是否請求成功
yield scrapy.Request(
"http://www.renren.com/971962231/profile?v=info_timeline",
callback=self.parse_data,
)
def parse_data(self,response):
""""訪問個人資料頁,驗證cookie的傳遞"""
print(re.findall(r"學校信息",response.body.decode()))
此外,為了查看cookies的傳遞過程蔗蹋,可以在settings中加上字段:COOKIES_DEBUG = True
scrapy發(fā)送post請求
案例:爬取github
spider:
import scrapy
import re
class GitSpider(scrapy.Spider):
name = 'git'
allowed_domains = ['github.com']
start_urls = ['https://github.com/login']
def parse(self, response):
authenticity_token = response.xpath("http://input[@name='authenticity_token']/@value").extract_first()
utf8 = response.xpath("http://input[@name='utf8']/@value").extract_first()
webauthn_support = response.xpath("http://input[@name='webauthn-support']/@value").extract_first()
post_data = {
"login":"你的郵箱",
"password":"密碼",
"webauthn-support":webauthn_support,
"authenticity_token":authenticity_token,
"utf8":utf8
}
print(post_data)
yield scrapy.FormRequest(
"https://github.com/session", #數(shù)據(jù)提交到的地址
formdata=post_data,
callback=self.after_login
#無論這個post請求成功沒有何荚,響應都會交給after_login
)
def after_login(self,response):
print(response)
print(re.findall(r"Trial_Repo", response.body.decode())) #匹配我的某個倉庫名,以驗證是否成功
settings中的修改字段:
USER_AGENT = 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Mobile Safari/537.36'
ROBOTSTXT_OBEY = False
scrapy還可以幫我們自動從表單中提取action的地址
爬取github登錄后的頁面:
spider:
# -*- coding: utf-8 -*-
import scrapy
import re
class Github2Spider(scrapy.Spider):
name = 'github2'
allowed_domains = ['github.com']
start_urls = ['https://github.com/login']
def parse(self, response):
post_data = {
"login":"2537119279@qq.com",
"password":"A1d9b961017#"
}
yield scrapy.FormRequest.from_response(
#自動從response中尋找form表單猪杭,然后將formdata提交到對應的action的url地址
#如果有多個form表單餐塘,可以通過其他參數(shù)對表單進行定位,其他參數(shù)見源碼
response,
formdata=post_data,
callback=self.after_login,
)
def after_login(self,response):
print(re.findall(r"Trial_Repo", response.body.decode()))
另:無需在settings中設置任何東西
scrapy_redis
- scrapy_redis的作用:reqeust去重胁孙,爬蟲持久化唠倦,和輕松實現(xiàn)分布式
scrapy_redis爬蟲的流程
上圖中的指紋是指request指紋,指紋能夠唯一標識一個request涮较,從而避免重復的reqeust請求
所以redis中常存儲待爬的request對象和已爬取的request的指紋
redis基礎
scrapy_redis的地址:https://github.com/rmax/scrapy-redis