爬蟲日記

2018/07/04 d91 爬蟲入門css

1、爬蟲
1.基本操做(自動投票、點贊)
- 登錄任意網站(僞造瀏覽器的任何行爲)
2.性能相關(多線程、進程)
- 併發方案:
- 異步IO:gevent/Twisted/asyncio/aiohttp #如今都採起這種
- 自定義異步IO模塊
- IO多路複用:select(回顧)
3.Scrapy框架
介紹: 異步IO:Twisted
- 基於scrapy源碼自定義爬蟲框架
- 使用scrapy
2、Tornado外部框架(異步非阻塞)
1.基本使用
- 小示例
- 自定義組件(form驗證,session)
2.源碼剖析

3.自定義異步非阻塞框架(window基於select實現)



1.爬蟲基本操做
a.爬蟲概念:搜索引擎出現前,黃頁來記住一個個域名。搜索引擎爬取信息提供
- 定向:爬取指定網站
- 非定向:
b.
需求一:
下載頁面:
http://www.autohome.com.cn/news/

篩選:
正則表達式

========== 開源模塊 ==========

1. requests
安裝:pip3 install requests

response = requests.get('http://www.autohome.com.cn/news/')
response.text #文本內容,字符串類型,須要先編碼


總結:

response = requests.get('URL')
response.text
response.content #下載好是字節
response.encoding #以什麼方式進行編碼
response.aparent_encoding #下載爬取時字符串解碼成字節的方式
response.status_code #狀態碼,301:永久重定向,訪問這個網站就是訪問另外一個
response.cookies.get_dict() #拿到cookie字典


requests.get('http://www.autohome.com.cn/news/',cookie={'xx':'xxx'})

2. beautisoup模塊
安裝:pip3 install beautifulsoup4

from bs4 import BeautiSoup
soup = BeautiSoup(response.text,features='html.parser') #轉換成對象,對象嵌套對象(標籤)
#指定引擎或者方式,內置的,
另外有lxml,性能更好,安裝便可引用
target = soup.find(id='auto-channel-lazyload-article')
print(target)

總結:
soup = beautifulsoup('<html>...</html>',features='html.parser') #也能夠傳html標籤
v1 = soup.find('div') #找到第一個標籤,對象類型
v1 = soup.find(id='i1')
v1 = soup.find('div',id='i1') #組合

v2 = soup.find_all('div') #找到全部的,列表[],不是對象了,不能find
v2 = soup.find_all(id='i1')
v2 = soup.find_all('div',id='i1')

obj = v1
obj = v2[0] #列表轉爲對象

obj.text #該標籤的文本內容
obj.attrs #該標籤的全部屬性,字典類型,a.attrs.get('href')

保存圖片
import uuid
img_response = requests.get(url='http:'+img_url)
file_name_new = str(uuid.uuid4())+'.jpg' #txt包括/,不能做爲文件名
with open(file_name_new,'wb') as f:
f.write(img_response.content) #write是二進制寫入,傳content字節類型

需求二:
經過程序自動登陸抽屜

post_dict = {
"phone": '111111111',
'password': 'xxx',
'oneMonth': 1
}
response = requests.post(
url="http://dig.chouti.com/login",
data = post_dict
)

print(response.text)
cookie_dict = response.cookies.get_dict() #登錄成功後服務器返回session對應的cookie
response.cookies是cookie對象,get_dict轉爲字典

通常狀況下,帶着登錄成功返回的cookie再登錄便可免data

但抽屜網在第一次登錄時返回一個cookie,登錄成功又返回一個cookie,並在服務器對第一個cookie進行受權,
下次登錄帶着第一個cookie去才行,這是特殊案例

第一次請求網站,返回cookie
r1 = requests.get(
url='http://dig.chouti.com/'
)
第二次帶着信息登錄
post_dict = {
"phone": '111111111',
'password': 'xxx',
'oneMonth': 1
}
r2 = requests.post(
url="http://dig.chouti.com/login",
data=post_dict,
cookies = r1.cookies.get_dict() #得帶着過去,讓服務器進行受權,這是抽屜的特殊案例
)
第三次爬取網頁:
response = requests.get(
url="http://dig.chouti.com/profile",
cookies = {'gpsd':r1.cookies.get('gpsd')}
)
print(response.text) #此次帶着第一次的cookie便可登錄成功

注:有些網站post還須要帶着csrf_token,先訪問網站get拿到csrf,之後帶着發post請求便可
總結:web知識:cookie、session
http知識:csrf

c. 模塊詳細使用
requests

- 方法關係
requests.get(.....)
requests.post(.....)
requests.put(.....)
requests.delete(.....)
...

requests.request('POST'...) #本質調用這個
- 參數
request.request
- method: 提交方式
- url: 提交地址
- params: 在URL中傳遞的參數,GET方式
requests.request(
method='GET',
url= 'http://www.oldboyedu.com',
params = {'k1':'v1','k2':'v2'}
)
# http://www.oldboyedu.com?k1=v1&k2=v2
- data: 在請求體裏傳遞的數據,POST,字典、字節字符串或者文件對象

requests.request(
method='POST',
url= 'http://www.oldboyedu.com',
params = {'k1':'v1','k2':'v2'},
data = {'use':'alex','pwd': '123','x':[11,2,3]} 本質上轉爲data="use=alex&pwd=123123"
)
發送時,如下格式:
請求頭:
content-type: application/url-form-encod.....請求頭是這個的話,把請求體的數據放到request.POST

請求體:
use=alex&pwd=123

- json 在請求體裏傳遞的數據,會json.dumps一下,變成總體字符串
requests.request(
method='POST',
url= 'http://www.oldboyedu.com',
params = {'k1':'v1','k2':'v2'},
json = {'use':'alex','pwd': '123'} #本質上序列化成一個總體字符串發送
)
注;假設往django發送請求,根據不一樣格式不一樣處理,但都到request.body裏,而request.POST可能沒有值
依據請求頭來判斷
請求頭:
content-type: application/json

請求體:
"{'use':'alex','pwd': '123'}"

PS: 字典中嵌套字典時,只能這種方式,而data只發送字典的key

- headers 請求頭

requests.request(
method='POST',
url= 'http://www.oldboyedu.com',
params = {'k1':'v1','k2':'v2'},
json = {'use':'alex','pwd': '123'},
headers={
'Referer': 'http://dig.chouti.com/', #上一次登錄的網站,若是不一樣,能夠拒絕登錄
'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"
} #表示用的什麼瀏覽器訪問
)
- cookies Cookies,放到請求頭髮送,headers



- files 上傳文件
requests.post(
url='xxx',
file={
'f1':open('1.py','rb'), #讀取文件賦值給f1
'f2':('s1.py',open('1.py','rb')), #還能命名服務器的文件保存名字,文件對象
'f1': ('test.txt', "hahsfaksfa9kasdjflaksdjf") #還能本身寫文件內容
}
)

- auth 基本認知(headers中加入加密的用戶名和密碼,發送過去)
ret = requests.get('https://api.github.com/user', auth=HTTPBasicAuth('wupeiqi', 'sdfasdfasdf'))

- timeout 請求和響應的超時時間
(connect timeout,read timeout)

- allow_redirects 是否容許重定向

- proxies 代理
response = requests.post(
url = '',
data = dic,
proxys={
'http':"http://1.14.452.5:5000" #發送請求時先往代理髮,代理往url發
}
)

- verify 是否忽略證書

- cert 證書文件
requests.get(
url='https:', #SSL加密,而http本質是socket,發請求和響應沒加密,但https服務器會先發一個證書,瀏覽器用證書對服務器發來的name加密
#而後發回服務器,解密,接着就能夠發送數據了
#cert = 'fuck.pem', #能夠本身製做,或者系統自帶的購買後可使用,本地用證書加密後,服務器購買證書後給廠商驗證請求便可
verify = False, #忽略證書
cert = ('fuck.crt','xxx.key'), #第二種格式
)

- stream 流的方式
stream=True時一點點下載,False時當即下載

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: 用於保存客戶端歷史訪問信息

session = requests.Session()

### 一、首先登錄任何頁面,獲取cookie
i1 = session.get(url="http://dig.chouti.com/help/service") #cookies等保存在session中html

### 二、用戶登錄,攜帶上一次的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)

2018/07/04 day92

上節回顧:
- request
- requests.post()
- url
- data
- json
- params
- cookie
- headers #以上最重要
- files
- auth
- allow_redirects
- timeout
- verify
- cert
- stream
- proxies
- response
- session
- web知識 #實現爬取網頁
-請求頭、體:Referer,User-Agent
-正則表達式
-beautifulsoup
soup = BeautifulSoup(html_doc, features="lxml")
tag1 = soup.find(name='a')
tag2 = soup.find_all(name='a')
tag3 = soup.select('#link2') # 找到id=link2的標籤,select使用css的選擇器

1.soup對象的屬性和方法

-name,標籤名稱
tag.name
tag.name = 'span' #設置
-attr,標籤屬性
tag.attrs #字典
tag.attrs={'':''} #賦值
del tag.attrs[''] #刪除
-get,獲取屬性值
tag.get('id') #獲取該標籤的id屬性值

-children, 全部子標籤,找到標籤Tag和純文本(包括空以及換行)NavigableString
tag.children
-descendants,子子孫孫,先一條找到底
tag.descendants
-clear,將標籤的全部子標籤所有清空(保留標籤名)
tag.clear()
-decompose,遞歸的刪除全部的標籤(不保留標籤)
tag.decompose
-extract,遞歸的刪除全部的標籤,並獲取刪除的標籤
body.extract()
- decode,轉換爲字符串(含當前標籤);decode_contents(不含當前標籤)
v = body.decode()
v = body.decode_contents()
- encode,轉換爲字節(含當前標籤);encode_contents(不含當前標籤)
v = body.encode()
v = body.encode_contents()

-find,獲取匹配的第一個標籤
tag = soup.find(name='a', attrs={'class': 'sister'}, recursive=True, text='Lacie')
tag = soup.find(name='a', class_='sister', recursive=True, text='Lacie')

默認爲True,recursive=False時只在子代尋找,class_下劃線是由於class是python內置的類字段
注:soup子代沒有a,soup.find('body').find('p',recursive=False)

- find_all,獲取匹配的全部標籤
soup.find_all(name=['a','div']) 或者的關係

rep = re.compile('p') #找到p標籤
rep = re.compile('^p') #以p開頭的標籤
soup.find_all(name=rep)

rep = re.compile('sister.*') #找到sister開頭的,.*表示除換行之外全部的字符,0或多個
soup.find_all(class_=rep)

rep = re.compile('http://www.oldboy.com/static/.*') #找到這個域名開頭的
soup.find_all(href=rep)

def func(tag):
return tag.has_attr('class') and tag.has_attr('id')
v = soup.find_all(name=func)
print(v) #若是找到,返回true

-has_attr,檢查標籤是否具備該屬性
tag.has_attr('id')

-get_text,獲取標籤內部文本內容
tag.get_text()

-index,檢查標籤在某標籤中的索引位置
tag = soup.find('body')
tag.index(tag.find('div'))

-is_empty_element,是不是空標籤(是否能夠是空)或者自閉合標籤
判斷是不是以下標籤:'br' , 'hr', 'input', 'img', 'meta','spacer', 'link', 'frame', 'base'
tag.is_empty_element

-當前的關聯標籤
soup.next
soup.next_element
soup.next_elements
soup.next_sibling
soup.next_siblingspython


tag.previous
tag.previous_element
tag.previous_elements
tag.previous_sibling
tag.previous_siblingsreact


tag.parent
tag.parents
-查找某標籤的關聯標籤
tag.find_next(...)
tag.find_all_next(...)
tag.find_next_sibling(...)
tag.find_next_siblings(...)git

tag.find_previous(...)
tag.find_all_previous(...)
tag.find_previous_sibling(...)
tag.find_previous_siblings(...)github

tag.find_parent(...)
tag.find_parents(...)web

參數同find_all
-select,select_one, CSS選擇器
soup.select("title") 括號內填CSS選擇器

-標籤的內容
tag.string
tag.string = 'new content' # 設置ajax

對比tag.get_text(),只能獲取內容,不能設置正則表達式

tag.stripped_strings # 遞歸內部獲取全部標籤的文本,包括嵌套的,相似innerText

-append,在當前標籤內部追加一個標籤
tag = soup.find('body')
tag.append(soup.find('a')) #添加到body最後,原來標籤沒了

obj = Tag(name='i',attrs={'id': 'it'})
obj.string = '我是一個新來的'
tag.append(obj) #新增一個添加到後面

-insert,在當前標籤內部指定位置插入一個標籤
tag.insert(2, obj)

-insert_after,insert_before 在當前標籤後面或前面插入
tag.insert_after(obj) #不在內部,和append不一樣

-replace_with 在當前標籤替換爲指定標籤
tag.replace_with(obj)

- 建立標籤之間的關係
a = soup.find('a')
tag.setup(previous_sibling=a) #查找時變爲前一個兄弟關係,但soup輸出時仍是父子關係

-wrap,將指定標籤把當前標籤包裹起來
tag = soup.find('a')
tag.wrap(obj1) #最後div標籤包裹a標籤

- unwrap,去掉當前標籤,將保留其包裹的標籤或者內容
tag = soup.find('a')
tag.unwrap() #對比tag.clear(),整個標籤清空(保留標籤名)

2.抽屜示例
用第一次的cookie
3.GitHub示例
csrf
# 1. 訪問登錄頁面,獲取 authenticity_token
i1 = requests.get('https://github.com/login')
soup1 = BeautifulSoup(i1.text, features='lxml')
tag = soup1.find(name='input', attrs={'name': 'authenticity_token'})
authenticity_token = tag.get('value')
c1 = i1.cookies.get_dict() #第一次的cookie
i1.close()

# 2. 攜帶authenticity_token和用戶名密碼等信息,發送用戶驗證
form_data = {
"authenticity_token": authenticity_token,
"utf8": "",
"commit": "Sign in",
"login": "wupeiqi@live.com",
'password': 'xxoo'
}
i2 = requests.post('https://github.com/session', data=form_data, cookies=c1) #帶着第一次的cookie,以防須要
c2 = i2.cookies.get_dict() #第二次的cookie
c1.update(c2) #將第二次的cookie更新到第一次的cookie,c1就是一個集合

# 3. 帶着c1便可訪問任意用戶網頁
i3 = requests.get('https://github.com/settings/repositories', cookies=c1)

soup3 = BeautifulSoup(i3.text, features='lxml')
list_group = soup3.find(name='div', class_='listgroup')

from bs4.element import Tag

for child in list_group.children:
if isinstance(child, Tag):
project_tag = child.find(name='a', class_='mr-1')
size_tag = child.find(name='small')
temp = "項目:%s(%s); 項目路徑:%s" % (project_tag.get('href'), size_tag.string, project_tag.string, )
print(temp)
4.知乎
ajax,必須帶User-Agent,CSRF

import time
import requests
from bs4 import BeautifulSoup

session = requests.Session()
#1.第一次登錄,帶headers,獲取csrf
i1 = session.get(
url='https://www.zhihu.com/#signin',
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36',
}
)
soup1 = BeautifulSoup(i1.text, 'lxml')
xsrf_tag = soup1.find(name='input', attrs={'name': '_xsrf'})
xsrf = xsrf_tag.get('value')

#2.第二次登錄,拿驗證碼圖片
current_time = time.time() #圖片url帶着時間窗,可能用於生成驗證碼,或者其餘功能
i2 = session.get(
url='https://www.zhihu.com/captcha.gif', #url開頭部分
params={'r': current_time, 'type': 'login'}, #get方式的參數
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36',
})

with open('zhihu.gif', 'wb') as f:
f.write(i2.content)

captcha = input('請打開zhihu.gif文件,查看並輸入驗證碼:') #input停頓提示輸入驗證碼
#3.第三次登錄,帶着驗證碼
form_data = {
"_xsrf": xsrf,
'password': 'xxooxxoo',
"captcha": 'captcha',
'email': '424662508@qq.com'
}
i3 = session.post(
url='https://www.zhihu.com/login/email',
data=form_data,
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36',
}
)
#4.接着就能夠爬取數據
i4 = session.get(
url='https://www.zhihu.com/settings/profile',
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36',
}
)
soup4 = BeautifulSoup(i4.text, 'lxml')
tag = soup4.find(id='rename-section')
nick_name = tag.find('span',class_='name').string
print(nick_name)

注:知乎驗證碼改成倒立的字,要返回點擊區域"captcha":{"img_size":[200,44],"input_points":[[,],[,]],}

12306是8張小圖組成大圖,每張小圖有本身的m定值,要返回對的圖片的m定值。
創建數據庫,一類知道m定值的圖片放在數據庫一欄裏,之後要驗證牛的時候,匹配8張圖和數據庫牛的全部m定值,
而後返回便可

5.博客園
帳號密碼在js裏RSA加密
RSA模塊

import re
import json
import base64
import rsa #加密模塊
import requests

def js_encrypt(text):
b64der = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCp0wHYbg/NOPO3nzMD3dndwS0MccuMeXCHgVlGOoYyFwLdS24Im2e7YyhB0wrUsyYf0/nhzCzBK8ZC9eCWqd0aHbdgOQT6CuFQBMjbyGYvlVYU2ZP7kG9Ft6YV6oc9ambuO7nPZh+bvXH0zDKfi02prknrScAKC0XhadTHT3Al0QIDAQAB'
der = base64.standard_b64decode(b64der)

pk = rsa.PublicKey.load_pkcs1_openssl_der(der)
v1 = rsa.encrypt(bytes(text, 'utf8'), pk)
value = base64.encodebytes(v1).replace(b'\n', b'')
value = value.decode('utf8')

return value


session = requests.Session()
#第一次登錄獲取csrf
i1 = session.get('https://passport.cnblogs.com/user/signin')
rep = re.compile("'VerificationToken': '(.*)'")
v = re.search(rep, i1.text)
verification_token = v.group(1)

form_data = {
'input1': js_encrypt('wptawy'),
'input2': js_encrypt('asdfasdf'),
'remember': False
}
#第二次登錄拿cookie
i2 = session.post(url='https://passport.cnblogs.com/user/signin',
data=json.dumps(form_data),
headers={
'Content-Type': 'application/json; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'VerificationToken': verification_token}
)
#第三次爬取
i3 = session.get(url='https://i.cnblogs.com/EditDiary.aspx')
print(i3.text)

6.web微信
掃圖片:
本地瀏覽器長輪詢,1分鐘和服務器保持鏈接,pending,只要手機掃過了,服務器狀態改變,瀏覽器就接收到數據


2018/07/05 day93

應用:開發Web微信

前戲:
輪詢:好比一個聊天室,每一個人1秒鐘打開看看有沒有新消息。1秒鐘發一次請求,服務器壓力大,但代碼簡單,setIntervel
長輪詢:請求不斷開,直到服務器返回數據或者時間到,這樣服務器併發多,但壓力小,瀏覽器實時接受返回數據
時間到後繼續發請求
websocket:http請求是單向的,服務器不能主動發,而socket先建立鏈接,而後服務器和瀏覽器相互發請求,
不用每次請求都要建立鏈接,省掉鏈接時間

分析:
基於網絡請求
1.訪問web頁面,返回二維碼
2.瀏覽器長輪詢發請求給服務器,等待掃碼Status=pending
3.手機掃碼,手機向服務器發請求 window.code=201,這時還沒確認
4.服務器返回頭像給瀏覽器 avatar = re.findall("window.userAvatar = '(.*)';", r1.text)[0]
5.瀏覽器再發長輪詢請求給服務器等待確認登錄
6.手機點確認,發給服務器 window.code=200
7.服務器返回重定向url給瀏覽器 redirect_url = re.findall('window.redirect_uri="(.*)";',r1.text)[0]
8.瀏覽器請求新的重定向url,返回憑證 redirect_url = redirect_url + "&fun=new&version=v2&lang=zh_CN"
soup.find('error').children
9.憑證放在url和json中去post發請求我的信息
user_info_url = "xxxr=-1780597526&lang=zh_CN&pass_ticket="+ticket_dict['pass_ticket']
10.接收我的信息 user_init_dict = json.loads(r3.text)
11.有憑證或者cookie便可獲取任何信息,全部聯繫人、發收消息

用戶掃碼:
from django.shortcuts import render,HttpResponse
import requests
import time #時間窗,用於生成請求url
import re #匹配response.text
import json #序列化字符串爲字典
ctime=None #全局變量
qcode = None
tip = 1 #1表示爲掃碼,0表示已掃碼,請求url就變了這個tip
ticket_dict = {} #儲存憑證的字典

def login(request):
global ctime
ctime = time.time() #時間窗,用於生成請求url
response = requests.get(
url='https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&fun=new&lang=zh_CN&_=%s' % ctime
#r後面通常是時間窗,redirect_url=xxx完成操做後跳轉url
)
code = re.findall('uuid = "(.*)";',response.text) #請求微信服務器返回一個二維碼對應的code
global qcode
qcode = code[0] #保存該url碼,用於html生成二維碼
return render(request,'login.html',{'qcode':qcode})

def check_login(request):
global tip #聲明全局變量,保證函數中使用的是全局變量
ret = {'code':408,'data':None}
r1 = requests.get(
url='https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=%s&tip=%s&sr=-1767722401&_=%s' %(qcode,tip,ctime,) #傳請求二維碼的參數
)
# 這時向微信請求,pending多久看微信何時返回
if 'window.code=408' in r1.text:
print('無人掃碼')
return HttpResponse(json.dumps(ret))
elif 'window.code=201' in r1.text:
ret['code']=201
avatar = re.findall("window.userAvatar = '(.*)';", r1.text)[0] #掃碼後服務器返回頭像url
ret['data']=avatar #傳到前端顯示
tip = 0 #已掃碼,tip值改變
return HttpResponse(json.dumps(ret))
elif 'window.code=200' in r1.text:
redirect_url = re.findall('window.redirect_uri="(.*)";',r1.text)[0] #確認後返回重定向url
redirect_url = redirect_url + "&fun=new&version=v2&lang=zh_CN" #新的重定向url去請求
r2 = requests.get(url=redirect_url) #請求重定向url,返回憑證

from bs4 import BeautifulSoup
soup = BeautifulSoup(r2.text,'html.parser')
for tag in soup.find('error').children: #找到全部的登錄憑證
ticket_dict[tag.name]= tag.get_text() #字典類型,引用類型,修改值不用global
# print(ticket_dict)

user_info_url = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=-1780597526&lang=zh_CN&pass_ticket="+ticket_dict['pass_ticket']
user_info_data={
'BaseRequest':{
'DeviceID':"e459555225169136",
'Sid':ticket_dict['wxsid'],
'Skey':ticket_dict['skey'],
'Uin':ticket_dict['wxuin'],
}
}
r3 = requests.post(
url=user_info_url,
json=user_info_data, #不能data,不然只能拿到key,value傳不了
)
r3.encoding = 'utf-8' #編碼
user_init_dict = json.loads(r3.text) #loads將text字符串類型轉爲字典類型
print(user_init_dict) #拿到用戶信息
ret['code']=200 #前端便可做判斷,跳轉重定向,展現我的信息
return HttpResponse(json.dumps(ret))


2018/07/06 day94

微信:
爬取套路:先去請求裏分析url,get or post,帶cookie或者headers
用requests模塊請求,url
params
data:默認帶一個請求頭application/url-encode-form
json:將字典自動序列化,Content-Type:application/json
headers
cookies
性能相關
多個requests.get()是串型,一個個執行

1、微信開發示例:
def user(request):
"""
我的主頁
:param request:
:return:
"""
#獲取用戶信息
user_info_url = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=-1780597526&lang=zh_CN&pass_ticket=" + \
ticket_dict['pass_ticket'] #拿着憑證去獲取我的信息
user_info_data = {
'BaseRequest': {
'DeviceID': "e459555225169136",
'Sid': ticket_dict['wxsid'],
'Skey': ticket_dict['skey'],
'Uin': ticket_dict['wxuin'],
}
}
r3 = requests.post(
url=user_info_url,
json=user_info_data, #不能data,不然只能拿到key,value傳不了
)
r3.encoding = 'utf-8' #編碼
user_init_dict = json.loads(r3.text) #loads將text字符串類型轉爲字典類型
ALL_COOKIE_DICT.update(r3.cookies.get_dict()) #再次保存cookie,這樣就包含了以上全部流程的cookie

注:#USER_INIT_DICT已聲明爲空字典,內存地址已有,添加值不修改地址,但賦值會改變地址,好比=123,以前要聲明global便可。
#USER_INIT_DICT['123']=123,USER_INIT_DICT.update(user_init_dict)兩種作法都沒改變地址

USER_INIT_DICT.update(user_init_dict) #保存到全局變量,裏面有初始化的全部信息,
如我的的UserName數字,SyncKey接收信息的憑證
return render(request,'user.html',{'user_init_dict':user_init_dict})

def contact_list(request):
"""
獲取全部聯繫人
:param request:
:return:
"""
base_url = 'https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?lang=zh_CN&pass_ticket=%s&r=%s&seq=0&skey=%s'
ctime = str(time.time()) #加不加str都行,自己是float類型
url = base_url %(ticket_dict['pass_ticket'],ctime,ticket_dict['skey'])
response = requests.get(url=url,cookies=ALL_COOKIE_DICT) #看憑證行不行,不行就帶cookie,再不行就帶請求頭
response.encoding='utf-8'
contact_list_dict = json.loads(response.text)
return render(request,'contact_list.html',{'contact_list_dict':contact_list_dict})

def sendMsg(request):
"""
發送消息
:param request:
:return:
"""
to_user = request.GET.get('toUser')
msg = request.GET.get('msg')
url= 'https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?lang=zh_CN&pass_ticket=%s' %(ticket_dict['pass_ticket'])
ctime = str(time.time())
post_data = {
'BaseRequest': {
'DeviceID': "e459555225169136",
'Sid': ticket_dict['wxsid'],
'Skey': ticket_dict['skey'],
'Uin': ticket_dict['wxuin'],
},
'Msg':{
'ClientMsgId': ctime,
'Content':msg,
'FromUserName':USER_INIT_DICT['User']['UserName'],
'LocalID':ctime, #時間窗
'ToUserName': to_user.strip(), #兩端可能有空格,清除
'Type':1 #文本類型
},'Scene':0
}
#response = requests.post(url=url,json=post_data) #發消息要看官方要不要帶請求頭,要相同
#response = requests.post(url=url,data=json.dumps(post_data),headers={'Content-Type':'application/json;charset=utf-8'}) #兩種方式寫法相等
#注:有時候帶着請求頭可能會發送不了消息,與sendmsg請求裏相同
#發中文,顯示unicode編碼,須要聲明ensure_ascii=False,才能不轉化中文,但會觸發下面問題
# data字段傳的能夠是字典,字符串,字節,文件對象,傳的時候都會變成字節。json.dumps轉爲字符串後,因爲聲明不轉換中文,含中文的字符串
# dumps默認用'latin-1',轉不了字節,須要主動將中文經過utf-8轉爲字節

response = requests.post(url=url, data=bytes(json.dumps(post_data,ensure_ascii=False),encoding='utf-8'))
print(response.text) #包含這次發送的ID
return HttpResponse('ok')

def getMsg(request):
"""
獲取消息
:param request:
:return:
"""
# 1.登錄成功後,初始化操做時,獲取到一個SyncKey,
# 2.帶着SyncKey檢測是不是否有消息到來,
# 3.若是window.synccheck={retcode:"0",selector:"2"},有消息到來
# 3.1.獲取消息,以及新的SyncKey
# 3.2.帶着新的SyncKey去檢測是否有消息
synckey_list = USER_INIT_DICT['SyncKey']['List'] #獲取初始的值
sync_list = []
for item in synckey_list:
temp = "%s_%s" % (item['Key'], item['Val'],) #進行拼接
sync_list.append(temp)
synckey = "|".join(sync_list) #拼接
#base_url = 'https://webpush.wx2.qq.com/cgi-bin/mmwebwx-bin/synccheck?r=%s&skey=%s&sid=%s&uin=%s&deviceid=%s&synckey=%s'
r1 = requests.get(
url='https://webpush.wx2.qq.com/cgi-bin/mmwebwx-bin/synccheck',
params={
'r':time.time(),
'skey':ticket_dict['skey'],
'sid':ticket_dict['wxsid'],
'uin':ticket_dict['wxuin'],
'deviceid':'e843458050524287',
'synckey':synckey
},
cookies = ALL_COOKIE_DICT #不加cookie的話會hold不住,一直髮請求,加了後就等待pending
)
if 'retcode:"0",selector:"2"' in r1.text: #返回這個,說明有消息了,發請求獲取一下內容
post_data = {
'BaseRequest': {
'DeviceID': "e459555225169136",
'Sid': ticket_dict['wxsid'],
'Skey': ticket_dict['skey'],
'Uin': ticket_dict['wxuin'],
},
'SyncKey':USER_INIT_DICT['SyncKey'],
'rr':1 #值多少不要緊
}
r2 = requests.post(
url='https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsync',
params = {
'skey':ticket_dict['skey'],
'sid':ticket_dict['wxsid'],
'pass_ticket':ticket_dict['pass_ticket'],
'lang':'zh_CN'
},
json=post_data
)
r2.encoding='utf-8'
msg_dict=json.loads(r2.text) #返回的全部內容
for msg in msg_dict['AddMsgList']: #AddMsgList包含全部人這個時刻發來的消息
print(msg['Content']) #若是是羣聊,content還包含發送人的username,用於標記,根據它獲取暱稱
USER_INIT_DICT['SyncKey'] = msg_dict['SyncKey'] #更新值
return HttpResponse('ok')

2018/07/07 day95

爬蟲性能相關

- 傻等
response = requests.get(....)
- 機智
response = requests.get(....) #批量拋出請求,統一等待回來,哪一個回來就回調一下執行
response = requests.get(....) #怎麼回調,用的是別人的組件
response = requests.get(....)
response = requests.get(....)

角色:使用者
- 多線程:線程池(線程多了性能下降,上下線程切換耗時)
- 多進程:進程池
===============================================================================================
1.線程:cpu的最小工做單元,存在於進程裏,每個進程,裏面的線程共享全部的這個進程的工做資源,
真正的工做者是線程,好比一個教室和裏面的學生,多個學生共享教室資源
2.GIL鎖:同一時刻一個進程裏只能一個線程被cpu調用,計算要用到cpu,IO的話線程只是被cpu調用去操做,
調用時間幾乎能夠忽略,並且io與cpu沒交互,因此不受GIL鎖限制
3.每一個進程有本身獨立的GIL鎖
4.http請求屬於io操做

總結:IO密集用多線程,計算密集用多進程。二者導入庫不一樣,操做相似,都能實現併發。
===============================================================================================
- 協程(微線程) + 異步IO =》 1個線程發送N個Http請求,哪一個請求回來後執行處理
===============================================================================================
協程:一個線程在作不少東西,先執行一個函數,中途觸發後執行另外一個函數,再中途觸發回來執行第一個函數
主要做用是讓線程切換。
加上異步IO後自動回調。
協程+異步IO:遇到IO阻塞(請求都沒回來),就能併發N個請求(併發是由於發請求速度快,看似併發)
若是同時多個請求回來,只能一個個前後處理
===============================================================================================
- asyncio
- 示例1:asyncio.sleep(5) #不支持發送http請求,支持TCP請求(socket的)
- 示例2:本身封裝Http數據包 #構造http請求規則的字符串經過socket發TCP請求便可
- 示例3:asyncio+aiohttp
aiohttp模塊:封裝Http數據包
- 示例4:asyncio+requests
requests模塊:封裝Http數據包
- gevent,依賴greenlet協程模塊+異步IO,socket級別,須要封裝http請求
pip3 install greenlet
pip3 install gevent
- 示例1:gevent+requests
- 示例2:gevent(協程池,即最多發多少個請求)+requests
- 示例3:gevent+requests => grequests

- Twisted
scrapy的併發下載依賴於它,一個優秀網絡庫,不依賴其餘庫
- Tornado
既是外部框架,也是爬蟲模塊,不依賴其餘庫

順序:=====> gevent(gevent+協程池+requests) > Twisted > Tornado > asyncio

角色:NB開發者

1. socket客戶端、服務端
通常狀況下,瀏覽器請求一個網站時,鏈接會阻塞
setblocking(0): 這時鏈接非阻塞,但無數據(鏈接無響應;數據未返回)就報錯。
寫一個異常處理,繼續執行其餘操做,鏈接成功告知一下便可

2. IO多路複用
客戶端:
try:
socket對象1.connet()
socket對象2.connet()
socket對象3.connet()
except Ex..
pass #報錯就pass

while True:
r,w,e = select.select([socket對象1,socket對象2,socket對象3,],[socket對象1,socket對象2,socket對象3,],[],0.05)
r = [socket對象1,] #表示有數據返回了,哪一個對象有變化放到r中
xx = socket對象1.recv() #接收數據
w = [socket對象1,] #表示和服務器建立鏈接成功
socket對象1.send('"""GET /index HTTP/1.0\r\nHost: baidu.com\r\n\r\n"""') #發送數據
e:錯誤信息
0.05:超時時間

3. 自定義socket對象
class Foo:
def fileno(self):
obj = socket()
return obj.fileno() #拿到socket對象的文件描述符

r,w,e = select.select([socket對象?,對象?,對象?,Foo()],[],[])
#不必定是socket對象,但對象必須有:fileno方法,並返回一個文件描述符

========總結===========
a. select內部調用的是對象.fileno()
b. 自定義的Foo()內部必須利用socket建立文件描述符,對象能夠寫其餘對象,對socket進行封裝便可

4.自定義異步IO框架

異步IO含義:給出url,自動執行回調,這就是異步操做。編寫的時候不是異步。

====>幾個重要概念
IO多路複用: r,w,e = while 監聽多個socket對象,這個過程是同步,不是異步的。
利用其特性能夠開發異步IO模塊。

異步IO: 非阻塞的socket+IO多路複用,發IO請求,不等着,完成後自動異步執行回調
- 非阻塞socket
- select[本身對象],w,r
代碼:
class HttpRequest:
def __init__(self,sk,host,callback):
self.socket = sk
self.host = host
self.callback = callback
def fileno(self):
return self.socket.fileno() #返回文件描述符

class HttpResponse:
def __init__(self,recv_data):
self.recv_data = recv_data
self.header_dict = {}
self.body = None
self.initialize() #執行方法

def initialize(self):
headers, body = self.recv_data.split(b'\r\n\r\n', 1)#分離請求頭請求體,b表示字節,1由於請求體可能有,只找第一個
self.body = body
header_list = headers.split(b'\r\n') #分離請求頭
for h in header_list:
h_str = str(h,encoding='utf-8') #先變成字符串
v = h_str.split(':',1)
if len(v) == 2: #部分響應頭格式沒有冒號
self.header_dict[v[0]] = v[1] #響應頭字典形式,全部的外部框架都是這樣來作的

class AsyncRequest:
def __init__(self):
self.conn = [] # 用於檢測是否已經返回
self.connection = [] # 用於檢測是否已經鏈接成功

def add_request(self,host,callback):
try:
sk = socket.socket()
sk.setblocking(0)
sk.connect((host,80,))
except BlockingIOError as e:
pass
request = HttpRequest(sk,host,callback) #建立HttpRequest對象
self.conn.append(request)
self.connection.append(request)

def run(self):

while True: #事件循環
rlist,wlist,elist = select.select(self.conn,self.connection,self.conn,0.05)
for w in wlist:
print(w.host,'鏈接成功...')
# 只要能循環到,表示socket和服務器端已經鏈接成功
tpl = "GET / HTTP/1.0\r\nHost:%s\r\n\r\n" %(w.host,)#經過封裝socket對象爲httprequest對象,就能傳host
w.socket.send(bytes(tpl,encoding='utf-8'))
self.connection.remove(w) #已發送數據的從列表清除
for r in rlist:
# r是HttpRequest對象
recv_data = bytes() #空字節,
while True: #一直接收數據
try:
chunck = r.socket.recv(8096) #8096是大小,超過大小的分爲一塊塊chunks接受,沒數據則報錯
recv_data += chunck
except Exception as e: #沒數據執行這步
break
# print(r.host,'有數據返回',recv_data)
response = HttpResponse(recv_data)
r.callback(response)
r.socket.close()
self.conn.remove(r) #不需再監聽
if len(self.conn) == 0:
break

def f1(response):
print('保存到文件',response.header_dict) #打印響應頭字典

def f2(response):
print('保存到數據庫', response.header_dict)

url_list = [
{'host':'www.baidu.com','callback': f1}, #用用戶本身選擇哪一個回調函數處理返回結果
{'host':'cn.bing.com','callback': f2},
{'host':'www.cnblogs.com','callback': f2},
]

req = AsyncRequest()
for item in url_list:
req.add_request(item['host'],item['callback']) #發送請求,建立HttpRequest對象和檢測字典

req.run()

2018/07/10 day96 scrapy

Scrapy框架:
- 下載頁面
- 解析
- 併發
- 深度 循環或遞歸

安裝:http://www.cnblogs.com/wupeiqi/articles/6229292.html
Linux
pip3 install scrapy


Windows
a. pip3 install wheel
b. 下載twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
c. 進入下載目錄,執行 pip3 install Twisted‑17.1.0‑cp35‑cp35m‑win_amd64.whl
d. pip3 install scrapy
e. 下載並安裝pywin32:https://sourceforge.net/projects/pywin32/files/

介紹:
- Spiders:多個spiders,能爬取不一樣網站,解析,把深度url給隊列
- Scrapy engine:寫循環遞歸,深度抓取
- Scheduler:任務隊列
- Downloader:下載頁面,返回給spiders
- Item Pipeline:儲存數據

流程: 1.每一個spiders有初始的urls,引擎把url放到隊列中
2.隊列調度url給下載器進行下載
3.下載器返回結果
4.解析,給項目管道,給隊列

使用:
1. 指定初始URL
2. 解析響應內容
- 給調度器,深度url
- 給item格式化和pipeline持久化


scrapy startproject day96

cd day96
scrapy genspider chouti chouti.com

打開chouti.py進行編輯

scrapy crawl chout --nolog #沒有日記

window輸出:
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030') #標準輸出,在輸出時設置編碼

print(response.text)
# content = str(response.body,encoding='utf-8') #設置完標準輸出後,body須要encoding
# print(content)

選擇器:
// 全部子代
//a[2] 列表的第二個
.// 當前對象的子代中,obj.xpath()
/ 子代
/div 子代的div標籤
/div[@id="i1"] 子代的div標籤且id=i1
/div[@id="i1"][@href="link"] 雙條件
obj.extract() 列表中的每個對象轉換字符串,返回列表[]
obj.extract_first() 列表中的每個對象轉換字符串,返回列表第一個元素
//div/text() 獲取某個標籤的文本
//div/@href 獲取某個標籤的屬性
//a[contains(@href, "link")] href包含link的
//a[starts-with(@href, "link")] href以link開頭的
//a[re:test(@id, "i\d+")] 正則,id是i+數字的格式

示例:獲取新聞標題
def parse(self, response):
# print(response.url)
# print(response.text) #window要設置標準輸出格式
# content = str(response.body,encoding='utf-8') #設置完標準輸出後,body須要encoding
# print(content)
hxs = Selector(response=response).xpath('//div[@id="content-list"]/div[@class="item"]') #找到全部a標籤,列表,extract轉字符串,不加的話返回對象
for obj in hxs:
a = obj.xpath('.//a[@class="show-content color-chag"]/text()').extract_first()
print(a.strip()) #去掉空格

示例:獲取url
visited_urls=set() #集合,能去重
hxs = Selector(response=response).xpath('//div[@id="dig_lcpage"]//a/@href').extract()
for item in hxs:
if item in self.visited_urls:
print('已經存在',item)
else:
self.visited_urls.add(item) #集合用add
print(item)

問題:url過長時,索引速度慢
解決:md5等加密轉固定長度,再進行比較
def md5(self,url):
import hashlib
obj = hashlib.md5()
obj.update(bytes(url,encoding='utf-8'))
return obj.hexdigest()

深度爬取:
def start_requests(self):
for url in self.start_urls:
yield Request(url, callback=self.parse) #指定開始url的解析函數,能重寫指定新的函數

# hxs = Selector(response=response).xpath('//div[@id="dig_lcpage"]//a/@href').extract()
# hxs = Selector(response=response).xpath('//a[starts-with(@href, "/all/hot/recent/")]/@href').extract()
hxs = Selector(response=response).xpath('//a[re:test(@href, "/all/hot/recent/\d+")]/@href').extract()
for url in hxs:
md5_url = self.md5(url)
if md5_url in self.visited_urls:
pass
else:
print(url)
self.visited_urls.add(md5_url) #集合用add
url = "https://dig.chouti.com%s" %url #添加前綴
yield Request(url=url,callback=self.parse) #訪問新頁面,下載完成後調用解析,必須寫yield,才能發給調度器

1.重寫start_requests函數,指定最開始url的解析函數
2.yield的使用
3.DEPTH_LIMIT = 1 來指定「遞歸」的層數。0表示無限制

格式化和保存:
spiders.py中
from ..items import ChoutiItem
for obj in hxs1:
title = obj.xpath('.//a[@class="show-content color-chag"]/text()').extract_first().strip()
href = obj.xpath('.//a[@class="show-content color-chag"]/@href').extract_first().strip()
item_obj = ChoutiItem(title=title,href=href) #對象化,即格式化
#將item對象傳給pipeline
yield item_obj

items.py中
class ChoutiItem(scrapy.Item):
title = scrapy.Field()
href = scrapy.Field()

pipelin.py中
class Day96Pipeline(object):
def process_item(self, item, spider): #spider參數做判斷,用於指定爬蟲的定製保存
print(spider,item)
tpl = "%s\n%s\n\n" %(item['title'],item['href']) #要字典索引
f = open('news.json','a+')
f.write(tpl)
f.close()

settings.py中
ITEM_PIPELINES = {
'day96.pipelines.Day96Pipeline': 300, 300是優先級,越小越高
}
1.item將數據格式成對象,起到格式化的做用
2.而後將item對象傳給pipeline,字典索引,保存
3.pipeline要配置settings.py
4.全部的spider都會通過每個pipeline,指定時做if判斷spider參數


2018/07/10 day97

scrapy框架

上節回顧:
域名:allowed_domains = ['chouti.com',]
響應:response
response.meta = {'depth':'深度值',}:


1、去重url

scrapy默認已經做了篩選,yield Request()有個參數dont_filter=False,經過類RFPDupeFilter(BaseDupeFilter)來完成。
將url放在文件中,來判斷訪問url是否已經存在。

本身重寫:
1.setting配置 DUPEFILTER_CLASS = "day96.duplication.RepeatFilter"
2.class RepeatFilter(object)類下面的方法名不改

3.在方法from_settings前@classmethod,不用建立對象執行該方法。
- 內部經過obj = RepeatFilter.from_settings()先執行該方法。
- 該方法的return cls()中cls是當前類名,cls()返回對象,即調用類的構造方法,init()初始化

4.init初識化

5.request_seen方法做if判斷
self.visited_url.add(request.url)

6.便可實現去重從單個spider分離出來,統一配置好
7.RepeatFilter從程序開始就運行,結束再執行裏面的close()
8.順序:from_settings->__init__(self)-> open-> request_seen-> close
9.去重url能夠放在緩存、數據庫、內存、文件等

去重判斷:
def request_seen(self, request):
if request.url in self.visited_url:
return True
self.visited_url.add(request.url)
return False

2、pipeline補充

1.屢次爬取屢次打開文件耗時,分離這個步驟統一打開open_spider和close_spider

def open_spider(self, spider):
"""
爬蟲開始執行時,調用
:param spider:
:return:
"""
self.f = open('news.json', 'a+')

2.獲取settings配置,並初始化,便可建立數據庫鏈接
@classmethod
def from_crawler(cls, crawler):
"""
初始化時候,用於建立pipeline對象
:param crawler:
:return:
"""
val = crawler.settings.getint('DB') #settings即py文件,括號內是變量名,拿到變量,大寫
return cls(val)

def __init__(self,conn_str):
self.conn_str = conn_str

self.conn = open(self.conn_str, 'a+') #打開數據庫鏈接

self.conn.write(tpl)

self.conn.close()

3.多個pipeline時,第一個不return item的話,下一個不執行

專業寫法:raise DropItem() #這種寫法,自定義擴展能監聽何時丟棄item


3、cookie問題

cookie_obj = CookieJar()
cookie_obj.extract_cookies(response,response.request)
cookie_obj._cookies #即獲取到全部的cookie
self.cookie_dict = cookie_obj._cookies #保存到全局變量

示例:自動登錄抽屜並點贊
class ChoutiSpider(scrapy.Spider):
name = 'chouti'
allowed_domains = ['chouti.com']
start_urls = ['http://dig.chouti.com/']
cookie_dict = None #用於保存cookie信息

def parse(self, response):
cookie_obj = CookieJar()
cookie_obj.extract_cookies(response,response.request)
# print(cookie_obj._cookies) #全部的cookie
self.cookie_dict = cookie_obj._cookies
#帶上cookie、帳號密碼登錄
yield Request(
url='https://dig.chouti.com/login',
method='POST',
body="phone=8613729805358&password=long0486&oneMonth=1",
headers={'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'},
cookies=cookie_obj._cookies,callback=self.check_login
)

def check_login(self,response):
print(response.text) #登錄成功後打印信息
yield Request(
url='https://dig.chouti.com/',callback=self.good #訪問首頁,用於解析頁面
)

def good(self,response):
id_list = Selector(response=response).xpath('//div[@share-linkid]/@share-linkid').extract()
for id in id_list:
url = 'https://dig.chouti.com/link/vote?linksId=%s' % id
yield Request(url=url,method="POST",cookies=self.cookie_dict,callback=self.show)#點贊帶cookie

page_list = Selector(response=response).xpath('//div[@id="dig_lcpage"]//a/@href').extract()
for page in page_list:
url = 'https://dig.chouti.com%s' % page
yield Request(
url=url,callback=self.good #回調自身
)

def show(self,response):
print(response.text) #打印點同意功信息

4、擴展

在預留的幾個鉤子信息上註冊自定製函數

from scrapy import signals
class MyExtend:

def __init__(self, crawler):
pass

@classmethod
def from_crawler(cls, crawler):
obj = cls(crawler)
crawler.signals.connect(obj.start, signals.engine_started)
crawler.signals.connect(obj.close, signals.spider_closed) #在指定信息上註冊函數
return obj

def start(self):
print('signals.engine_started.')

def close(self):
print('spider_closed')

settings.py中配置
EXTENSIONS = {
# 'scrapy.extensions.telnet.TelnetConsole': None,
'day96.extensions.MyExtend': 300,
}

5、配置文件

BOT_NAME = 'day96' #爬蟲名字

SPIDER_MODULES = ['day96.spiders'] #爬蟲路徑
NEWSPIDER_MODULE = 'day96.spiders'

USER_AGENT = 'day96 (+http://www.yourdomain.com)' #帶着這個去請求,就認爲是爬蟲,真實爬取時僞造這個

網站創建時的配置文件,裏面篩選誰的爬蟲能夠來

ROBOTSTXT_OBEY = True #true時遵循爬蟲規則,網站配置文件容許就爬

CONCURRENT_REQUESTS = 32 #併發請求數,最大值,默認16,依據反爬蟲策略來決定

DOWNLOAD_DELAY = 3 #秒,下載延遲

CONCURRENT_REQUESTS_PER_DOMAIN = 16 #每一個域名併發16個
CONCURRENT_REQUESTS_PER_IP = 16 #每一個IP併發16個,一個域名可能有多臺服務器ip

COOKIES_ENABLED = False #是否拿cookie

TELNETCONSOLE_ENABLED = False #是否容許中途暫停,telnet 127.0.0.1 6023進入監聽,能夠輸入命令查看
#當前爬蟲的狀態,指標

DEFAULT_REQUEST_HEADERS = { #每一個請求攜帶,單獨在Request設置
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
}

AUTOTHROTTLE_ENABLED = True #智能限速,延遲時間智能化
AUTOTHROTTLE_START_DELAY = 5 #初識延遲,最小延遲是DOWNLOAD_DELAY
AUTOTHROTTLE_MAX_DELAY = 60
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
AUTOTHROTTLE_DEBUG = False

DEPTH_PRIORITY = 0 #0或者1,廣度仍是深度優先,默認0深度優先



2018/07/11 day98

1、配置

1. 自動限速算法
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 #slot.delay表示上一次的延遲時間
new_delay = max(target_delay, new_delay)
new_delay = min(max(self.mindelay, new_delay), self.maxdelay)
slot.delay = new_delay

2、緩存

目的:解決頻繁數據庫鏈接,省時。放於文件或者內存中。

from scrapy.downloadermiddlewares.httpcache import HttpCacheMiddleware
from scrapy.extensions.httpcache import DummyPolicy
from scrapy.extensions.httpcache import FilesystemCacheStorage

# 是否啓用緩存策略
# HTTPCACHE_ENABLED = True

# 緩存策略1:全部請求均緩存,下次再請求直接訪問原來的緩存便可
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
# 緩存策略2:根據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'

3、代理

scrapy代理,依賴python的環境,要想增長,須要在python環境變量中設置
本質:代理即設置請求頭meta和headers['Proxy-Authorization']。

from scrapy.contrib.downloadermiddleware.httpproxy import HttpProxyMiddleware

方式一:使用默認
os.environ #鍵值對,key需以_proxy結尾
{
http_proxy:http://root:woshiniba@192.168.11.11:9999/ #用戶名:密碼@ip:端口的格式
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']) #python3要轉字節
encoded_user_pass = base64.encodestring(to_bytes(proxy['user_pass']))#規定要加密
request.headers['Proxy-Authorization'] = to_bytes('Basic ' + encoded_user_pass)#以Basic開頭
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'])

#settings配置一下
DOWNLOADER_MIDDLEWARES = {
'step8_king.middlewares.ProxyMiddleware': 500,
}

4、Https證書

Https訪問時有兩種狀況:
1. 要爬取網站使用的可信任證書(默認支持)
DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
DOWNLOADER_CLIENTCONTEXTFACTORY = "scrapy.core.downloader.contextfactory.ScrapyClientContextFactory"

2. 要爬取網站使用的自定義證書
DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
DOWNLOADER_CLIENTCONTEXTFACTORY = "step8_king.https.MySSLFactory" #修改路徑

# https.py
from scrapy.core.downloader.contextfactory import ScrapyClientContextFactory
from twisted.internet.ssl import (optionsForClientTLS, CertificateOptions, PrivateCertificate)

# 寫一個類繼承ScrapyClientContextFactory,重寫getCertificateOptions方法
# 增長兩個參數,傳自定義的證書給參數
class MySSLFactory(ScrapyClientContextFactory):
def getCertificateOptions(self):
from OpenSSL import crypto
v1 = crypto.load_privatekey(crypto.FILETYPE_PEM, open('/Users/wupeiqi/client.key.unsecure', mode='r').read())
v2 = crypto.load_certificate(crypto.FILETYPE_PEM, open('/Users/wupeiqi/client.pem', mode='r').read())
return CertificateOptions(
privateKey=v1, # pKey對象,證書文件轉對象,找loadfile方法
certificate=v2, # X509對象
verify=False,
method=getattr(self, 'method', getattr(self, '_ssl_method', None))
)

5、下載中間件

順序:
先順序執行中間件的process_request,直到scrapy的下載器。
若是某個中間件下載好,就把response給最後一箇中間件的process_response
再倒序執行中間件的process_response,直到spider進行處理。
若是某個中間件處理完,就再執行下一個任務。
若是下載process_request過程出現異常,交給process_exception消化,不能消化最後報錯。

class DownMiddleware1(object):
def process_request(self, request, spider):
'''
請求須要被下載時,通過全部下載器中間件的process_request調用
:param request:
:param spider:
:return:
None,繼續後續中間件去下載;
Response對象,中止process_request的執行,開始執行process_response
Request對象,中止中間件的執行,將Request從新調度器
raise IgnoreRequest異常,中止process_request的執行,開始執行process_exception
'''
pass



def process_response(self, request, response, spider):
'''
spider處理完成,返回時調用
:param response:
:param result:
:param spider:
:return:
Response 對象:轉交給其餘中間件process_response
Request 對象:中止中間件,request會被從新調度下載
raise IgnoreRequest 異常:調用Request.errback #丟棄Request異常
'''
print('response1')
return response #必須有返回值

def process_exception(self, request, exception, spider):
'''
當下載處理器(download handler)或 process_request() (下載中間件)拋出異常
:param response:
:param exception:
:param spider:
:return:
None:繼續交給後續中間件處理異常;
Response對象:中止後續process_exception方法
Request對象:中止中間件,request將會被從新調用下載
'''
return None

6、爬蟲中間件

下載完成後,response到達spider前,要通過爬蟲中間件

class SpiderMiddleware(object):

def process_spider_input(self,response, spider):
'''
下載完成,執行,而後交給parse處理
:param response:
:param spider:
:return:
'''
pass

def process_spider_output(self,response, result, spider):
'''
spider處理完成,返回時調用
:param response:
:param result:返回的兩種yield生成器
:param spider:
:return: 必須返回包含 Request 或 Item 對象的可迭代對象(iterable)
'''
return result

def process_spider_exception(self,response, exception, spider):
'''
異常調用
:param response:
:param exception:
:param spider:
:return: None,繼續交給後續中間件處理異常;
含 Response 或 Item 的可迭代對象(iterable),交給調度器或pipeline
'''
return None


def process_start_requests(self,start_requests, spider):
'''
爬蟲啓動時調用
:param start_requests:
:param spider:
:return: 包含 Request 對象的可迭代對象
'''
return start_requests #不返回不知道從哪開始

7、擴展總結

1.下載中間件
DOWNLOADER_MIDDLEWARES = {}
2.爬蟲中間件
SPIDER_MIDDLEWARES = {}
3.信號
EXTENSIONS = {
'day96.extensions.MyExtend': 300,
}
4.去重url
DUPEFILTER_CLASS = "day96.duplication.RepeatFilter"
5.pipeline
ITEM_PIPELINES
6.https證書
DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
DOWNLOADER_CLIENTCONTEXTFACTORY = "step8_king.https.MySSLFactory" #修改路徑
7.代理(使用下載中間件)
DOWNLOADER_MIDDLEWARES = {
'step8_king.middlewares.ProxyMiddleware': 500,
}

8、自定義命令

1.在spiders同級建立任意目錄,如:commands
2.在其中建立 crawlall.py 文件 (此處文件名就是自定義的命令)
from scrapy.commands import ScrapyCommand
from scrapy.utils.project import get_project_settings

class Command(ScrapyCommand):
requires_project = True

def syntax(self):
return '[options]'

def short_desc(self):
return 'Runs all of the spiders' #命令提示語

def run(self, args, opts):
spider_list = self.crawler_process.spiders.list() #找到全部的爬蟲名稱
for name in spider_list:
self.crawler_process.crawl(name, **opts.__dict__)
self.crawler_process.start()

3.在settings.py 中添加配置 COMMANDS_MODULE = '項目名稱.目錄名稱' "day96.commands"
4.在項目目錄執行命令:scrapy crawlall

9、源碼流程簡述

入口:自定義命令

# 1. 執行CrawlerProcess構造方法
# 2. CrawlerProcess對象(含有配置文件)的spiders
# 2.1,爲每一個爬蟲建立一個Crawler
# 2.2,執行 d = Crawler.crawl(...) # ************************ #
# d.addBoth(_done)
# 2.3, CrawlerProcess對象._active = {d,}

# 3. dd = defer.DeferredList(self._active)
# dd.addBoth(self._stop_reactor) # self._stop_reactor ==> reactor.stop()

# reactor.run

即twisted的原理

for url in url_list:
d = task(url)
_active.add(d)

dd = defer.DeferredList(_active)
dd.addBoth(stop)
reactor.run()


2018/07/12 day99 scrapy源碼剖析

今日概要
1. 前戲 Twisted 使用


2. Scrapy經驗 + Twisted功能
- Low
- High

3. Scrapy源碼剖析


1、Twisted框架

1.不包含特殊socket對象的框架,自動結束

from twisted.web.client import getPage #模塊功能:socket對象,自動完成移除
from twisted.internet import reactor #模塊功能:事件循環(全部的socket對象都移除)
from twisted.internet import defer #模塊功能:defer.Deferred,特殊的socket對象,不發請求,需手動移除


# 1.利用getPage建立socket
# 2.將socket添加到事件循環中
# 3.開始事件循環 (內部發送請求,並接受響應;當全部的socekt請求完成後,終止事件循環)

def response(content):
print(content)

@defer.inlineCallbacks #裝飾器和yield一下便可添加到事件循環
def task():
url = "http://www.baidu.com"
d = getPage(url.encode('utf-8')) #根據url找到ip建立socket對象,url是字節
d.addCallback(response)
yield d

def done(*args,**kwargs):
reactor.stop()

li=[]
for i in range(10):
d = task() #需先執行一下,添加到循環中
li.append(d) #建立了10個socket放到循環,即以前的異步IO

# d = task() #先執行一下,添加到循環中
dd = defer.DeferredList(li) #列表
dd.addBoth(done) #監聽完成沒,完成自動執行括號內函數 Both包括Callback和Errback
reactor.run() #開始循環

2.包含特殊socket對象的框架,手動計數器結束

from twisted.web.client import getPage #模塊功能:socket對象,自動完成移除
from twisted.internet import reactor #模塊功能:事件循環(全部的socket對象都移除)
from twisted.internet import defer #模塊功能:defer.Deferred,特殊的socket對象,不發請求,需手動移除

# 1.利用getPage建立socket
# 2.將socket添加到事件循環中
# 3.開始事件循環 (內部發送請求,並接受響應;當全部的socekt請求完成後,終止事件循環)
_close = None
count = 0
def response(content):
print(content)

global count
count += 1
_close.callback(None) #表示特殊對象手動終止

@defer.inlineCallbacks #裝飾器和yield一下便可添加到事件循環
def task():
url = "http://www.baidu.com"
d1 = getPage(url.encode('utf-8')) #根據url找到ip建立socket對象,url是字節
d1.addCallback(response)

url = "http://www.baidu.com"
d2 = getPage(url.encode('utf-8')) # 根據url找到ip建立socket對象,url是字節
d2.addCallback(response)

url = "http://www.baidu.com"
d3 = getPage(url.encode('utf-8')) # 根據url找到ip建立socket對象,url是字節
d3.addCallback(response)

global _close
_close = defer.Deferred()
yield _close #返回特殊的socket對象,不終止

def done(*args,**kwargs):
reactor.stop()

#每個都看爲一個爬蟲,每一個爬蟲內都是併發,之間也是併發
#BUG:每一個spider有本身的defer.Deferred(),列表或者封裝spider成對象,維護本身的_close關閉
spider1 = task() #先執行一下,添加到循環中
spider2 = task()
dd = defer.DeferredList([spider1,spider2]) #列表
dd.addBoth(done) #監聽完成沒,完成自動執行括號內函數 Both包括Callback和Errback
reactor.run() #開始循環


2、自定義scrapy框架

from twisted.web.client import getPage #模塊功能:socket對象,自動完成移除
from twisted.internet import reactor #模塊功能:事件循環(全部的socket對象都移除)
from twisted.internet import defer #模塊功能:defer.Deferred,特殊的socket對象,不發請求,需手動移除

class Request(object):
#封裝請求爲一個類,類.xx取url和parse
def __init__(self,url,callback):
self.url = url
self.callback = callback

class HttpResponse(object):
#將下載結果和request封裝成一個類,之後方法解析好,類.xxx就能取到全部內容
def __init__(self,content,request):
self.content = content
self.request = request
self.url = request.url

class ChoutiSpider(object):
name = 'chouti'
#爬蟲類,生成初始Request,定義parse方法
def start_requests(self):
start_url = ['https://www.baidu.com','https://www.bing.com']
for url in start_url:
yield Request(url,self.parse)

def parse(self,response):
print(response)
#1.crawling移除
#2.獲取parser yield值
#3.再次取隊列中獲取


import queue
Q = queue.Queue() #隊列,能夠放數據

class Engine(object):
#引擎類,添加初始Request到隊列,從隊列調度任務,執行回調,再次添加隊列
def __init__(self):
self._close = None
self.max = 5 #最大併發數
self.crawling = [] #正在爬取

def get_response_callback(self,content,request):
self.crawling.remove(request) #移除
req = HttpResponse(content,request)
result = request.callback(req) #即調用了parse
import types
if isinstance(result,types.GeneratorType): #解析返回生成器的時候,再放到隊列中
for req in result:
Q.put(req)

def _next_request(self):
#取Request對象,併發送請求
if Q.qsize() == 0 and len(self.crawling) == 0:
self._close.callback(None)
return
if len(self.crawling)>=self.max:
return
while len(self.crawling)<self.max:
try:
req = Q.get(block=False) #隊列沒有不等着
self.crawling.append(req)
d = getPage(req.url.encode('utf-8'))
d.addCallback(self.get_response_callback,req) #下載完成調用這個函數,默認把下載response傳給這個函數,手動也能傳參
# d.addCallback(self._next_request) #解析完成,再添加隊列
d.addCallback(lambda _:reactor.callLater(0,self._next_request)) #防止遞歸問題,多久後由事件循環reactor調用
except Exception as e:
return

@defer.inlineCallbacks
def crawl(self,spider):
#將初始Request對象添加到調度器
start_requests = iter(spider.start_requests()) #生成器轉爲迭代器,next取值
while True:
try:
request = next(start_requests) #每個初始url
Q.put(request) #將請求放到隊列
except StopIteration as e: #迭代器沒有了會報錯
break
#去隊列中取任務,下載
reactor.callLater(0, self._next_request)
self._close = defer.Deferred()
yield self._close

_active = set()
engine = Engine()
spider = ChoutiSpider()
d = engine.crawl(spider)
_active.add(d)

dd = defer.DeferredList(_active)
dd.addBoth(lambda _:reactor.stop()) #lambda也是一個函數,即自定義一個函數,_是一個形式參數,a也行

reactor.run()


3、TinyScrapy

from twisted.web.client import getPage #模塊功能:socket對象,自動完成移除
from twisted.internet import reactor #模塊功能:事件循環(全部的socket對象都移除)
from twisted.internet import defer #模塊功能:defer.Deferred,特殊的socket對象,不發請求,需手動移除
from queue import Queue

class Request(object):
"""
用於封裝用戶請求相關信息
"""
def __init__(self,url,callback):
self.url = url
self.callback = callback

class HttpResponse(object):
#將下載結果和request封裝成一個類,之後方法解析好,類.xxx就能取到全部內容
def __init__(self,content,request):
self.content = content
self.request = request

class Scheduler(object):
"""
任務調度器,封裝取出、添加隊列的方法
"""
def __init__(self):
self.q = Queue()

def open(self):
pass

def next_request(self):
try:
req = self.q.get(block=False)
except Exception as e:
req = None
return req

def enqueue_request(self,req):
self.q.put(req)

def size(self):
return self.q.qsize()

class ExecutionEngine(object):
"""
引擎:全部的調度,回調函數、取出隊列、添加初始隊列、添加特殊socket開始工做
"""
def __init__(self):
self._close = None
self.scheduler = None
self.max = 5
self.crawlling = []

def get_response_callback(self,content,request): #下載完成,執行回調函數
self.crawlling.remove(request)
response = HttpResponse(content, request)
result = request.callback(response) #即調用了parse
import types
if isinstance(result, types.GeneratorType): #解析返回生成器的時候,再放到隊列中
for req in result:
self.scheduler.enqueue_request(req)

def _next_request(self): #取任務、回調、再取任務
if self.scheduler.size() == 0 and len(self.crawlling) == 0:
self._close.callback(None)
return
while len(self.crawlling)<self.max:
req = self.scheduler.next_request()
if not req:
return
self.crawlling.append(req)
d = getPage(req.url.encode('utf-8'))
d.addCallback(self.get_response_callback,req)
d.addCallback(lambda _: reactor.callLater(0, self._next_request))

@defer.inlineCallbacks
def open_spider(self,start_requests): #添加初始隊列
self.scheduler = Scheduler()
while True:
try:
req = next(start_requests)
self.scheduler.enqueue_request(req)
except StopIteration as e:
break
yield self.scheduler.open() #必須有yield,不然twisted報錯,None表示沒有影響,可聽任意位置
reactor.callLater(0,self._next_request) #去隊列中取任務,下載


@defer.inlineCallbacks
def start(self):
self._close = defer.Deferred()
yield self._close #添加特殊對象,用於手動結束,這時引擎正式開始工做

class Crawler(object):
"""
用戶封裝調度器以及引擎
"""
def _create_engine(self):
return ExecutionEngine() #建立引擎對象,以引用引擎的方法

def _creat_spider(self,spider_cls_path):
"""
根據spider路徑建立spider對象,以導入
:param spider_cls_path:
:return:
"""
module_path,cls_name = spider_cls_path.rsplit('.',maxsplit=1) #模塊路徑,類名
import importlib #反射
m = importlib.import_module(module_path)
cls = getattr(m,cls_name)
return cls() #爬蟲類

@defer.inlineCallbacks
def crawl(self,spider_cls_path):
engine = self._create_engine() #引擎類
spider = self._creat_spider(spider_cls_path) #爬蟲類
start_requests = iter(spider.start_requests()) #初始請求的迭代器,迭代每個Request對象
yield engine.open_spider(start_requests)
#兩個yield,上面的yield已經返回值,下面的yield至關於寫到open_spider方法中
yield engine.start() #引擎的start方法,建立特殊socket對象

class CrawlerProcess(object):
"""
開啓事件循環,爬蟲進程
"""
def __init__(self):
self._active = set()

def crawl(self,spider_cls_path):
#建立爬蟲對象
crawler = Crawler()
d = crawler.crawl(spider_cls_path) #添加初始請求,取隊列,回調函數等,可看做建立爬蟲,正式工做
self._active.add(d) #添加到列表

def start(self):
#添加回調函數,事件循環開啓,只需執行一下該方法便可啓動
dd = defer.DeferredList(self._active)
dd.addBoth(lambda _:reactor.stop())
reactor.run()

class Command(object):

def run(self): #該方法建立多個爬蟲,開啓事件循環
crawl_process = CrawlerProcess()
spider_cls_path_list = ['spider.chouti.ChoutiSpider',]
for spider_cls_path in spider_cls_path_list:
crawl_process.crawl(spider_cls_path)
crawl_process.start()

if __name__ == '__main__': cmd = Command() cmd.run() #執行一下run方法 總結:- Command - run:建立CrawlerProcess對象,for執行crawl方法建立多個爬蟲,執行start方法 - CrawlerProcess - __init__:self._active - crawl:建立Crawler對象,執行crawl方法,添加返回值到self._active - start:建立defer.DeferredList,監聽全部爬蟲,reactor.run() - Crawler - _create_engine:建立ExecutionEngine對象 - _creat_spider:根據spider路徑return一個ChoutiSpider對象 - crawl:執行_create_engine()和_creat_spider(),迭代ChoutiSpider的start_requests方法返回值 yield引擎對象的open_spider方法,再yield引擎對象的start方法 - ExecutionEngine - __init__:self._close特殊socket對象,scheduler隊列變量,max最大併發數,crawlling正爬取請求列表 - get_response_callback:移除crawlling,建立HttpResponse對象,執行request.callback,判斷返回值類型並 執行scheduler.enqueue_request方法,放入任務 - _next_request:判斷是否self._close.callback(None),執行scheduler.next_request取出任務,添加到crawlling 執行getPage方法發送請求,callback(get_response_callback)和callback(_next_request) - open_spider:建立Scheduler對象,迭代取next(start_requests)以執行scheduler.enqueue_request方法放入任務, yield scheduler.open(),執行_next_request方法 - start:建立defer.Deferred(),yield self._close - Scheduler - __init__:self.q = Queue() - open:pass,yield open()時至關於yield None - next_request:取出任務q.get(block=False) - enqueue_request:放入任務q.put(req) - size:返回q.qsize(),隊列的大小 - HttpResponse - __init__:self.content和self.request,封裝 - Request - __init__:self.url和self.callback,封裝

相關文章
相關標籤/搜索