模擬新浪微博登陸:從原理分析到實現

上一篇文章小試牛刀:使用Python模擬登陸知乎介紹瞭如何模擬知乎登陸,雖然用到了驗證碼信息,但請求的參數都是原封不動的傳遞,剛開始接觸的時候,以爲難度適中,回頭再看的時候,反而感受挺容易的。在這篇文章,將繼續介紹模擬登陸。與以前不同的是,此次選擇的對象是新浪微博,難度稍微提高了點,好在以往的許多碼友們都留有許多經驗貼,通過幾番斟酌,微博的模擬登陸算是實現了。這兩天還在研究如何高性能地爬取微博數據,業餘之際乘着還有點記憶,索性將先前的小實驗加工成文,算是一份小結吧。下面來看看整個實驗過程。php

開發工具

一如既往,筆者使用的仍是以前的工具,以下:html

  • Windows 7 + Python 2.75
  • Chrome + Fiddler

微博登陸請求過程分析

新浪微博的登陸有多個URL連接,筆者在實驗的時候試了兩個,這兩個都是新浪通行證登陸頁面,都是不須要驗證碼的。一個是 【http://login.sina.com.cn】,另外一個是 【https://login.sina.com.cn/signup/signin.php?entry=sso】。兩個URL雖然很大部分相同,登陸過程當中僅僅是傳遞參數不同。第一個URL傳遞的過程對「password」進行了加密,而第二個沒有加密,因此若是使用第二個URL進行模擬登陸,就簡單多了。在這裏,筆者決定選擇使用第一種方式進行分析,下面來看詳細過程。python

請求登陸過程可概括爲三部分git

  1. 請求登陸login.php頁面的參數預獲取
  2. 請求登陸login.php頁面的參數分析
  3. 提交POST請求時的參數構造

Step 1:GET方式請求prelogin.php頁面

在模擬登陸以前,先觀察瀏覽器登陸過程當中Fiddler抓到的包,在/sso/login.php打開以前會先使用「GET」方式請求「/sso/prelogin.php」,請求的URL爲:【https://login.sina.com.cn/sso/prelogin.php?entry=account&callback=sinaSSOController.preloginCallBack&su=bGl1ZGl3ZWkxOCU0MHNpbmEuY29t&rsakt=mod&client=ssologin.js(v1.4.15)】,能夠看看下面這張圖:github

在Fiddler中,能夠點擊「Preview」查看具體詳情,也能夠直接將Request URL複製到瀏覽器上查看,效果圖以下:算法

能夠看出,這是一個json數據,而且攜帶了幾個參數,咱們關心的有如下四個:json

  • servertime
  • nonce
  • pubkey
  • rsakv

說明一下,之因此認爲這幾個參數比較重要,那是由於後面對「password」的加密須要用到,對其餘參數沒有說起的緣由是在提交POST時其它的參數並無用到。好了,爲了進行進一步探索,咱們從Fiddler的結果能夠看出,接下來到了「/sso/login.php」。瀏覽器

Step 2:POST方式請求login.php頁面

從這裏開始,就進行「login.php」頁面的請求分析了(詳細的Request URL:【https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.15)】,後面的時間戳可省略)。點擊查看詳情,結果圖以下:cookie

能夠發現/sso/login.php頁面有以下參數(From Data):session

cdult: 3
domain: sina.com.cn
encoding: UTF-8
entry: account
from:
gateway: 1
nonce: AFE3O9
pagerefer: http://login.sina.com.cn/sso/logout.php
prelt: 41
pwencode: rsa2
returntype: TEXT
rsakv: 1330428213
savestate: 30
servertime: 1478568922
service: sso
sp: password
sr: 1366*768
su: username
useticket: 0
vsnf: 1

到了這裏,咱們大概能夠知道咱們須要哪些參數了。在From Data 參數列表中,須要咱們指定的參數有下面幾個:

  • servertime
  • nonce
  • rsakv
  • sp:加密後的密碼
  • su:加密後的用戶名

對於參數「nonce」、「servertime」、「rsakv」,均可以從第一步中的「prelogin.php」 中直接獲取,而「sp」和「su」則是通過加密後的字符串值,至於具體的加密規則,咱們下面經過查看源碼分析得出。

Step 3:探索加密規則

首先看看請求「/sso/prelogin.php」的具體狀況,看到「client」爲「ssologin.js」,見下圖:

而後咱們到登陸頁面https://login.sina.com.cn中查看源碼【view-source:https://login.sina.com.cn/】並搜索「ssllogin.js」,接着點擊進入ssologin.js文件,這時咱們可在文件中搜索「username」字符串,找到與「username」相應的加密部分(需仔細查看+揣測),接着搜索「password」,找到「password」的加密部分,最後分析出「username」和「password」的加密規則。加密部分的代碼以下圖:

加密用戶名的代碼:

1
request.su = sinaSSOEncoder.base64.encode(urlencode(username));

加密密碼的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if ((me.loginType & rsa) && me.servertime && sinaSSOEncoder && sinaSSOEncoder.RSAKey) {
request.servertime = me.servertime;
request.nonce = me.nonce;
request.pwencode = "rsa2";
request.rsakv = me.rsakv;
var RSAKey = new sinaSSOEncoder.RSAKey();
RSAKey.setPublic(me.rsaPubkey, "10001");
password = RSAKey.encrypt([me.servertime, me.nonce].join("\t") + "\n" + password)
} else {
if ((me.loginType & wsse) && me.servertime && sinaSSOEncoder && sinaSSOEncoder.hex_sha1) {
request.servertime = me.servertime;
request.nonce = me.nonce;
request.pwencode = "wsse";
password = sinaSSOEncoder.hex_sha1("" + sinaSSOEncoder.hex_sha1(sinaSSOEncoder.hex_sha1(password)) + me.servertime + me.nonce)
}
}

微博對於「username」的加密規則比較單一,使用的是「Base64」加密算法,而對「password」的加密規則比較複雜,雖然使用的是「RSA2」(python中須要使用pip install rsa 安裝rsa模塊),但加密的邏輯比較多。根據上面的代碼,能夠看出「password」加密是這樣的一個過程:首先建立一個「rsa」公鑰,公鑰的兩個參數都是固定值,第一個參數是登陸過程當中「prelogin.php」中的「pubkey」,第二個參數是加密的「js」文件中指定的「10001」(這兩個值須要先從16進制轉換成10進制,把「10001」轉成十進制爲「65537」)。最後再加入「servertime」和「nonce」進行進一步加密。

通過上面的分析以後,發起「POST」請求時的「post_data」基本上已經所有能夠獲得了,接下來就跟模擬登陸其它網站相似了,可使用「request」,也可使用「urllib2」。下面來看詳細代碼部分。

源碼實現

Github源碼連接:https://github.com/csuldw/WSpider/tree/master/SinaLogin,源碼包括下列文件:

  • dataEncode.py:用於對提交POST請求的數據進行編碼處理
  • Logger.py:用於打印log
  • SinaSpider.py:用於爬取sina微博數據的文件(主文件)

爲了方便擴展,筆者將代碼進行了封裝,因此看起來代碼量比較多,不過我的以爲可讀性仍是比較良好,算是湊合吧。

1.dataEncode.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# -*- coding: utf-8 -*-
"""
Created on Tue Nov 08 10:14:38 2016

@author: liudiwei
"""
import base64
import rsa
import binascii
import requests
import json
import re

#使用base64對用戶名進行編碼
def encode_username(username):
return base64.encodestring(username)[:-1]

#使用rsa2對password進行編碼
def encode_password(password, servertime, nonce, pubkey):
rsaPubkey = int(pubkey, 16)
RSAKey = rsa.PublicKey(rsaPubkey, 65537) #建立公鑰
codeStr = str(servertime) + '\t' + str(nonce) + '\n' + str(password) #根據js拼接方式構造明文
pwd = rsa.encrypt(codeStr, RSAKey) #使用rsa進行加密
return binascii.b2a_hex(pwd) #將加密信息轉換爲16進制。

#讀取preinfo.php,獲取servertime, nonce, pubkey, rsakv四個參數值
def get_prelogin_info():
url = r'http://login.sina.com.cn/sso/prelogin.php?entry=weibo&callback=sinaSSOController.preloginCallBack&su=&rsakt=mod&client=ssologin.js(v1.4.18)'
html = requests.get(url).text
jsonStr = re.findall(r'\((\{.*?\})\)', html)[0]
data = json.loads(jsonStr)
servertime = data["servertime"]
nonce = data["nonce"]
pubkey = data["pubkey"]
rsakv = data["rsakv"]
return servertime, nonce, pubkey, rsakv

#根據Fiddler抓取的數據,構造post_data
def encode_post_data(username, password, servertime, nonce, pubkey, rsakv):
su = encode_username(username)
sp = encode_password(password, servertime, nonce, pubkey)
#用於登陸到 http://login.sina.com.cn
post_data = {
"cdult" : "3",
"domain" : "sina.com.cn",
"encoding" : "UTF-8",
"entry" : "account",
"from" : "",
"gateway" : "1",
"nonce" : nonce,
"pagerefer" : "http://login.sina.com.cn/sso/logout.php",
"prelt" : "41",
"pwencode" : "rsa2",
"returntype" : "TEXT",
"rsakv" : rsakv,
"savestate" : "30",
"servertime" : servertime,
"service" : "sso",
"sp" : sp,
"sr" : "1366*768",
"su" : su,
"useticket" : "0",
"vsnf" : "1"
}
#用於登陸到 http://login.sina.com.cn/signup/signin.php?entry=ss,將POST替換成下面的便可
"""
post_data = {
"cdult" : "3",
"domain" : "sina.com.cn",
"encoding" : "UTF-8",
"entry" : "sso",
"from" : "null",
"gateway" : "1",
"pagerefer" : "",
"prelt" : "0",
"returntype" : "TEXT",
"savestate" : "30",
"service" : "sso",
"sp" : password,
"sr" : "1366*768",
"su" : su,
"useticket" : "0",
"vsnf" : "1"
}
"""
return post_data

2.Logger.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# -*- coding: utf-8 -*-
"""
Created on Thu Nov 02 14:01:17 2016

@author: liudiwei
"""
import os
import logging

class LogClient(object):
def __init__(self):
self.logger = None

"""#EXAMPLE
logger = createLogger('mylogger', 'temp/logger.log')
logger.debug('logger debug message')
logger.info('logger info message')
logger.warning('logger warning message')
logger.error('logger error message')
logger.critical('logger critical message')
"""
def createLogger(self, logger_name, log_file):
prefix = os.path.dirname(log_file)
if not os.path.exists(prefix):
os.makedirs(prefix)
# 建立一個logger
logger = logging.getLogger(logger_name)
logger.setLevel(logging.INFO)
# 建立一個handler,用於寫入日誌文件
fh = logging.FileHandler(log_file)
# 再建立一個handler,用於輸出到控制檯
ch = logging.StreamHandler()
# 定義handler的輸出格式formatter
formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 給logger添加handler
logger.addHandler(fh)
logger.addHandler(ch)
self.logger = logger
return self.logger

2.SinaSpider.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# -*- coding: utf-8 -*-
"""
Created on Tue Nov 08 10:14:38 2016

@author: liudiwei
"""
import os
import getpass
import json
import requests
import cookielib
import urllib
import urllib2
import gzip
import StringIO
import time

import dataEncode
from Logger import LogClient

class SinaClient(object):
def __init__(self, username=None, password=None):
#用戶輸入的用戶名與密碼
self.username = username
self.password = password
#從prelogin.php中獲取的數據
self.servertime = None
self.nonce = None
self.pubkey = None
self.rsakv = None
#請求時提交的數據列表
self.post_data = None
self.headers = {}
#用於存儲登陸後的session
self.session = None
self.cookiejar = None
#用於輸出log信息
self.logger = None
#存儲登陸狀態,初始狀態爲False
self.state = False
#初始時調用initParams方法,初始化相關參數
self.initParams()

#初始化參數
def initParams(self):
self.logger = LogClient().createLogger('SinaClient', 'out/log_' + time.strftime("%Y%m%d", time.localtime()) + '.log')
self.headers = dataEncode.headers
return self

#設置username 和 password
def setAccount(self, username, password):
self.username = username
self.password = password
return self

#設置post_data
def setPostData(self):
self.servertime, self.nonce, self.pubkey, self.rsakv = dataEncode.get_prelogin_info()
self.post_data = dataEncode.encode_post_data(self.username, self.password, self.servertime, self.nonce, self.pubkey, self.rsakv)
return self

#使用requests庫登陸到 https://login.sina.com.cn
def login(self, username=None, password=None):
#根據用戶名和密碼給默認參數賦值,並初始化post_data
self.setAccount(username, password)
self.setPostData()
#登陸時請求的url
login_url = r'https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.15)'
session = requests.Session()
response = session.post(login_url, data=self.post_data)
json_text = response.content.decode('gbk')
res_info = json.loads(json_text)
try:
if res_info["retcode"] == "0":
self.logger.info("Login success!")
self.state = True
#把cookies添加到headers中
cookies = session.cookies.get_dict()
cookies = [key + "=" + value for key, value in cookies.items()]
cookies = "; ".join(cookies)
session.headers["Cookie"] = cookies
else:
self.logger.error("Login Failed! | " + res_info["reason"])
except Exception, e:
self.logger.error("Loading error --> " + e)
self.session = session
return session

#生成Cookie,接下來的全部get和post請求都帶上已經獲取的cookie
def enableCookie(self, enableProxy=False):
self.cookiejar = cookielib.LWPCookieJar() # 創建COOKIE
cookie_support = urllib2.HTTPCookieProcessor(self.cookiejar)
if enableProxy:
proxy_support = urllib2.ProxyHandler({'http': 'http://122.96.59.107:843'}) # 使用代理
opener = urllib2.build_opener(proxy_support, cookie_support, urllib2.HTTPHandler)
self.logger.info("Proxy enable.")
else:
opener = urllib2.build_opener(cookie_support, urllib2.HTTPHandler)
urllib2.install_opener(opener)

#使用urllib2模擬登陸過程
def login2(self, username=None, password=None):
self.logger.info("Start to login...")
#根據用戶名和密碼給默認參數賦值,並初始化post_data
self.setAccount(username, password)
self.setPostData()
self.enableCookie()
#登陸時請求的url
login_url = r'https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.15)'
headers = self.headers
request = urllib2.Request(login_url, urllib.urlencode(self.post_data), headers)
resText = urllib2.urlopen(request).read()
try:
jsonText = json.loads(resText)
if jsonText["retcode"] == "0":
self.logger.info("Login success!")
self.state = True
#將cookie加入到headers中
cookies = ';'.join([cookie.name + "=" + cookie.value for cookie in self.cookiejar])
headers["Cookie"] = cookies
else:
self.logger.error("Login Failed --> " + jsonText["reason"])
except Exception, e:
print e
self.headers = headers
return self

#打開url時攜帶headers,此header需攜帶cookies
def openURL(self, url, data=None):
req = urllib2.Request(url, data=data, headers=self.headers)
text = urllib2.urlopen(req).read()
return self.unzip(text)

#功能:將文本內容輸出至本地
def output(self, content, out_path, save_mode="w"):
self.logger.info("Download html page to local machine. | path: " + out_path)
prefix = os.path.dirname(out_path)
if not os.path.exists(prefix):
os.makedirs(prefix)
fw = open(out_path, save_mode)
fw.write(content)
fw.close()
return self

"""
防止讀取出來的HTML亂碼,測試樣例以下
req = urllib2.Request(url, headers=headers)
text = urllib2.urlopen(req).read()
unzip(text)
"""
def unzip(self, data):
data = StringIO.StringIO(data)
gz = gzip.GzipFile(fileobj=data)
data = gz.read()
gz.close()
return data

#調用login1進行登陸
def testLogin():
client = SinaClient()
username = raw_input("Please input username: ")
password = getpass.getpass("Please input your password: ")
session = client.login(username, password)

follow = session.post("http://weibo.cn/1669282904/follow").text.encode("utf-8")
client.output(follow, "out/follow.html")


#調用login2進行登陸
def testLogin2():
client = SinaClient()
username = raw_input("Please input username: ")
password = getpass.getpass("Please input your password: ")
session = client.login2(username, password)

info = session.openURL("http://weibo.com/1669282904/info")
client.output(info, "out/info2.html")

if __name__ == '__main__':
testLogin2()

關於源碼的分析,能夠參考代碼中的註解,若有不理解的地方,可在評論中提出。

運行

直接在Windows控制檯運行python SinaSpider.py,而後根據提示輸入用戶名和密碼便可。

運行結果展現

OK,匆忙之際趕出了本文,若有言之不合理之處,可在評論中指出。如今能夠成功地登陸到微博了,接下來想爬取什麼數據就盡情的爬吧。後續筆者將進一步介紹如何爬取微博數據,好了,後會有期吧!

References

原網址:http://www.csuldw.com/2016/11/10/2016-11-10-simulate-sina-login/?utm_source=tuicool&utm_medium=referral

相關文章
相關標籤/搜索