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


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

開發工具

一如既往,筆者使用的還是之前的工具,如下:

  • 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進行模擬登錄,就簡單多了。在這里,筆者決定選擇使用第一種方式進行分析,下面來看詳細過程。

請求登錄過程可歸納為三部分

  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)】,可以看看下面這張圖:

在Fiddler中,可以點擊“Preview”查看具體詳情,也可以直接將Request URL復制到瀏覽器上查看,效果圖如下:

可以看出,這是一個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)】,后面的時間戳可省略)。點擊查看詳情,結果圖如下:

可以發現/sso/login.php頁面有如下參數(From Data):

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM