模拟新浪微博登录-原理分析到实现
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了模拟新浪微博登录-原理分析到实现相关的知识,希望对你有一定的参考价值。
原文地址:
上一篇文章 小试牛刀:使用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进行模拟登录,就简单多了。在这里,笔者决定选择使用第一种方式进行分析,下面来看详细过程。
请求登录过程主要分三部分
- 请求登录login.php页面前的参数获取
- 请求登录login.php页面时的参数分析
- 提交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”的加密规则。加密部分如下:
加密用户名的代码:
request.su = sinaSSOEncoder.base64.encode(urlencode(username));
加密密码的代码:
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微博数据的文件(主文件)
为了方便扩展,笔者将代码进行了封装,所以看起来代码量比较多,不过个人觉得可读性还是比较良好,算是凑合吧。
# -*- 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
# -*- 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
# -*- 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,可以成功的登录到微博了,接下来想爬取什么数据就尽情的爬吧。
更正一个我在运行时发现的问题:
password无法输入,将testLogin1和testLogin2中输入密码的
password = getpass.getpass("Please input your password: ")
改为 password = raw_input("Please input your password: ")
以上是关于模拟新浪微博登录-原理分析到实现的主要内容,如果未能解决你的问题,请参考以下文章