目录
网站模拟登录总结
一、环境
二、简介
三、模拟登录网站
1、拉勾网模拟登录
2、CSDN模拟登录
3、微博模拟登录
window7系统
python3语言
pycharm工具
由于需要爬取的网站大多需要先登录才能正常访问或者需要登录后的cookie值才能继续爬取,所以网站的模拟登录是必须要熟悉的,这里总结了拉勾、知乎、CSDN 、微博等的模拟登录过程以及遇到的问题。
以下的网站模拟登录重点是如何利用requests模拟浏览器请求、抓包分析等,涉及到验证码识别的没有做太多深入,往后会专门学习验证码识别这块,训练字库来提高OCR识别率等技术,而这里就简单的使用人工识别或者接入解码平台来识别验证码。
拉勾网爬取需要携带cookie值 , 不然很快就会被网站识别为爬取而被重定向到登录界面。上篇文章介绍的拉勾全站爬取,其获取cookie的方法是利用selenium模拟浏览器操作登录拉勾,其缺点是速度慢,影响爬虫性能。现在利用requests模拟浏览器请求登录 , 不用像selenium一样完全模拟浏览器操作需要解析执行CSS、Javascript,性能得到提高,算是对拉勾网爬取的一个改进吧
分析浏览器登录拉勾
发现浏览器登录拉勾的关键请求url依次为:
登录界面url:
表单提交url(一般为post方法):
登录成功后的重定向url,其实是向服务器请求授权证明的url(寻找的一个依据是其response中含有set-cookie):
携带ticket请求授权的url:
上面是用FireFox浏览器查看的url请求情况,发现跟Chrome浏览器不同的是最后那个请求授权的url,FireFox里该请求的response中并没有我们需要的Set-Cookie,但实际上是有的;我们来看看Chrome中的该请求的response:
以上的url分析就是拉勾登录请求的基本情况,最后在进入拉勾主页,保存整个登录过程的cookie值到本地;
表单提交和代码实现
整个登录过程的cookie传递我们很少关注是因为利用了requests的session方法创建了一个持久会话,只要是在该会话中的请求,其cookie会自动保存、携带到下一个请求,可通过该方法获取cookie:
拉勾的登录操作重点就在表单提交的url请求上,分析该request,发现其请求头信息中有两个可疑参数,
这两参数在哪冒出来的?只能找啊,一般有三个考虑途径:
我们在登录页面处发现了这两个参数:
故可以简单地编写程序将他们解析出来
def getTokenCode(self):
login_page = 'https://passport.lagou.com/login/login.html'
data = self.session.get(login_page, headers=self.HEADERS , allow_redirects=False) #allow_redirects
soup = BeautifulSoup(data.content, "lxml", from_encoding='utf-8')
'''
要从登录页面提取token,code, 然后在头信息里面添加
'''
anti_token = {'X-Anit-Forge-Token': 'None',
'X-Anit-Forge-Code': '0'}
anti = soup.findAll('script')[1].getText().splitlines()
anti = [str(x) for x in anti]
anti_token['X-Anit-Forge-Token'] = re.findall(r'= \'(.+?)\'', anti[1])[0]
anti_token['X-Anit-Forge-Code'] = re.findall(r'= \'(.+?)\'', anti[2])[0]
return anti_token
表单数据也是必须的
可见密码password是经过加密了的,那到底是怎么加密的呢?还是找, 在Chrome中Ctrl+ f 寻找,最后发现在这里:
https://img.lagou.com/www/static/pkg/widgets_120b982.js
密码先经过MD5加密,在将加密后的password两边连上‘veenike’再进行一个MD5加密。
参数request_form_verifyCode是验证码,此次无需验证码,所以值为空。
请求头部信息的参数和表单数据都准备好了,接下来就是post表单数据了
def login(self, user, passwd, captchaData=None, token_code=None):
postData = {'isValidate': 'true',
'password': passwd,
# 如需验证码,则添加上验证码
'request_form_verifyCode': (captchaData if captchaData != None else ''),
'submit': '',
'username': user
}
login_url = 'https://passport.lagou.com/login/login.json'
# 头信息添加token
login_headers = self.HEADERS.copy()
token_code = self.getTokenCode() if token_code is None else token_code
login_headers.update(token_code)
# data = {"content":{"rows":[]},"message":"该帐号不存在或密码错误,请重新输入","state":400}
response = self.session.post(login_url, data=postData, headers=login_headers , allow_redirects=False)
data = json.loads(response.content.decode('utf-8'))
if data['state'] == 1:
return response.content
elif data['state'] == 10010:
print(data['message'])
# captchaData = self.getCaptcha()
token_code = {'X-Anit-Forge-Code': data['submitCode'], 'X-Anit-Forge-Token': data['submitToken']}
return self.login(user, passwd, captchaData, token_code)
else:
print(data['message'])
return False
调试观察提交表单后的响应:
{"content":{"rows":[]},"message":"操作成功","state":1,"submitCode":98986628,"submitToken":"ec76e3b9-6921-4bf1-bdd9-3e7015dd242f"}
表示登录成功!但革命尚未成功啊,浏览器登录时还有两个重定向(请求授权)url需要请求,那就继续模拟请求
直接上代码
def get_ticket(self):
header = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9' ,
'Connection': 'keep-alive',
'Host': 'passport.lagou.com',
'Referer': 'https://passport.lagou.com/login/login.html',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
}
response = self.session.get(url= 'https://passport.lagou.com/grantServiceTicket/grant.html' , headers= header , allow_redirects=False)
print(self.get_cookie())
redir_url = response.next.url
url = re.sub('http:', 'https:', redir_url)
header = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Host': 'www.lagou.com',
'Upgrade-Insecure-Requests': '1' ,
'Referer' : 'https://passport.lagou.com/login/login.html' ,
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
}
res = self.session.get(url= url , headers= header , allow_redirects=False)
cookies = self.get_cookie()
print(cookies)
with open('cookie_requests.txt' , 'w') as wf:
wf.write(str(cookies))
经过两个请求后得到了拉勾网站登录后的完整cookie值,这里需要注意的是两个重定向url我设置了禁止重定向,你们也可以自己试着让他们自动重定向。
最后就是请求首页或者带着获取来的cookie爬取其他你想要的拉勾职位数据了,全部代码会连同以下的几个网站一起放在GitHub里。
CSDN模拟登录较拉勾网简便,因为它没有对账号、密码做任何加密,明文传输;同样,按下F12,观察分析其登录过程:
登录页面url:
表单提交url:
网站首页url:
首页不太能看出是否登录成功,故再请求个人中心url:https://my.csdn.net/
关键的表单提交数据:
当然上面的表单数据有时会多一个参数validateCode,为验证码参数,即当需要验证码验证时提交给参数数据,其中的fkid暂时不清楚其含义,不过实测没有它也能正常登录。
那问题来了,表单里的数据去哪找呢?
都在这里了,只要在登录页面解析出来就可以了。
就这样,按照上面分析的编写程序模拟登录就可以了。
def start_requests():
header = {
'Connection': 'keep-alive',
'Host': 'passport.csdn.net',
'Referer': 'https://www.csdn.net/',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:61.0) Gecko/20100101 Firefox/61.0',
}
response = session.get(url= start_urls , headers= header)
soup = BeautifulSoup(response.content , 'lxml')
gps = soup.find_all(id= 'gps')[0].get('value')
rememberMe = soup.find_all(id= 'rememberMe')[0].get('value')
lt = soup.find_all(name = 'input' , attrs={'name':'lt'})[0].get('value')
_eventId = soup.find_all(name = 'input' , attrs={'name':'_eventId'})[0].get('value')
execution = soup.find_all(name = 'input' , attrs={'name':'execution'})[0].get('value')
if 'validateCode' in response.text:
validateCode_url = soup.find_all(id= 'yanzheng')[0].get('src')
res = session.get(validateCode_url)
with open('verify.png' , 'wb') as wf:
wf.write(res.content)
validateCode = Image_to_string('verify.png')
post_data.update({
'validateCode': validateCode, #若需要验证码的话,OCR识别转化为字符串,更新到表单
})
post_data.update({
'gps' : gps ,
'rememberMe' : rememberMe ,
'lt' : lt ,
'_eventId' : _eventId ,
'execution' : execution ,
})
post_header = {
'Connection': 'keep-alive' ,
'Host': 'passport.csdn.net',
'Origin': 'https://passport.csdn.net',
'Referer': 'https://passport.csdn.net/account/login',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' ,
}
post_response = session.post(url= post_url , data= post_data , headers = post_header)
first_header = {
'Connection': 'keep-alive',
'Host': 'www.csdn.net',
'Referer': 'https://passport.csdn.net/account/verify',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' ,
}
first_response = session.get(url= 'https://www.csdn.net/' , headers = first_header)
is_login_header = {
'Connection': 'keep-alive',
'Host': 'my.csdn.net',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
}
is_login_response = session.get(url= 'https://my.csdn.net/' , headers = is_login_header)
if '一届草民' in is_login_response.text:
print('登录成功')
else:
print('登录失败')
微博模拟登录相对上两个网站来说还是有点复杂的,为何这么说呢?看表单数据:
经分析,su参数为账号username的base64加密
查找https://i.sso.sina.com.cn/js/ssologin.js
sp参数为密码password的非对称加密(ssologin.js)
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)
}
}
request.sp=password;
注意到password加密过程除了password参数外还需要servertime、nonce和rsaPubkey参数,所以还得找到这三个参数的出处,
模拟登录麻烦的就是要到处寻找各种参数、加密方法,尽可能的模拟浏览器请求,这三个参数可在这个url:https://login.sina.com.cn/sso/prelogin.php的响应中找到,其实我们也可在查找password加密方法的js文件中找到点线索,
在https://i.sso.sina.com.cn/js/ssologin.js中搜索其参数nonce或servertime,查看其涉及到的function
抱歉,这里的代码有点乱,我们可以看到上图出现了较多我们的关键词,其中的prelogin词很可疑呀,不妨在浏览器的请求列表中查找下这个关键词,会发现我们要找的就是它
其请求参数
‘_’参数应该是时间戳 , 'su'参数则是我们MD5加密后的username。
所以大概的登录流程是这样的:首先请求登录页面url,然后是请求登录的表单数据提交,其中表单参数nonce、servertime和rsakv可通过带相应参数请求https://login.sina.com.cn/sso/prelogin.php获取得到,表单参数su就是username的base64加密,参数sp就是password的RSA非对称加密,此加密所需的参数除password外都可在上一提到的请求响应中获取到;还有一验证码参数,因为我在登录模拟时很少碰到需要验证码,若需要验证码,只要相应的请求验证码获取url得到验证码,一起加入到表单数据中就可以了,识别方法可以是OCR识别或人工识别,我们这是人工识别;最后可请求https://passport.weibo.com/wbsso/login验证是否登录成功。
代码实现
username的base64加密:
def get_username(self):
"""
get legal username
"""
username_quote = urllib.parse.quote_plus(self.user_name)
username_base64 = base64.b64encode(username_quote.encode("utf-8"))
return username_base64.decode("utf-8")
password的RSA加密:
def get_password(self, servertime, nonce, pubkey):
"""
get legal password
"""
string = (str(servertime) + "\t" + str(nonce) + "\n" + str(self.pass_word)).encode("utf-8")
public_key = rsa.PublicKey(int(pubkey, 16), int("10001", 16))
password = rsa.encrypt(string, public_key)
password = binascii.b2a_hex(password)
return password.decode()
获取password加密需要的参数:
def get_json_data(self, su_value):
"""
get the value of "servertime", "nonce", "pubkey", "rsakv" and "showpin", etc
"""
params = {
"entry": "weibo",
"callback": "sinaSSOController.preloginCallBack",
"rsakt": "mod",
"checkpin": "1", #验证码相关
"client": "ssologin.js(v1.4.18)",
"su": su_value,
"_": int(time.time()*1000),
}
try:
response = self.session.get("http://login.sina.com.cn/sso/prelogin.php", params=params)
json_data = json.loads(re.search(r"\((?P.*)\)", response.text).group("data"))
except Exception as excep:
json_data = {}
logging.error("WeiBoLogin get_json_data error: %s", excep)
logging.debug("WeiBoLogin get_json_data: %s", json_data)
return json_data
表单提交模拟登录:
def login(self, user_name, pass_word):
"""
login weibo.com, return True or False
"""
self.user_name = user_name
self.pass_word = pass_word
self.user_uniqueid = None
self.user_nick = None
# get json data
s_user_name = self.get_username()
json_data = self.get_json_data(su_value=s_user_name)
if not json_data:
return False
s_pass_word = self.get_password(json_data["servertime"], json_data["nonce"], json_data["pubkey"])
# make post_data
post_data = {
"entry": "weibo",
"gateway": "1",
"from": "",
"savestate": "7",
"userticket": "1",
"vsnf": "1",
"service": "miniblog",
"encoding": "UTF-8",
"pwencode": "rsa2",
"sr": "1280*800",
"prelt": "529",
"url": "http://weibo.com/ajaxlogin.php?framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack",
"rsakv": json_data["rsakv"],
"servertime": json_data["servertime"],
"nonce": json_data["nonce"],
"su": s_user_name,
"sp": s_pass_word,
"returntype": "TEXT",
}
# get captcha code
if json_data["showpin"] == 1:
url = "http://login.sina.com.cn/cgi/pin.php?r=%d&s=0&p=%s" % (int(time.time()), json_data["pcid"])
with open("captcha.jpeg", "wb") as file_out:
file_out.write(self.session.get(url).content)
code = input("请输入验证码:")
post_data["pcid"] = json_data["pcid"]
post_data["door"] = code
# login weibo.com
login_url_1 = "http://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.18)&_=%d" % int(time.time())
json_data_1 = self.session.post(login_url_1, data=post_data).json()
if json_data_1["retcode"] == "0":
params = {
"callback": "sinaSSOController.callbackLoginStatus",
"client": "ssologin.js(v1.4.18)",
"ticket": json_data_1["ticket"],
"ssosavestate": int(time.time()),
"_": int(time.time()*1000),
}
response = self.session.get("https://passport.weibo.com/wbsso/login", params=params)
json_data_2 = json.loads(re.search(r"\((?P.*)\)", response.text).group("result"))
if json_data_2["result"] is True:
self.user_uniqueid = json_data_2["userinfo"]["uniqueid"]
self.user_nick = json_data_2["userinfo"]["displayname"]
logging.warning("WeiBoLogin succeed: %s", json_data_2)
else:
logging.warning("WeiBoLogin failed: %s", json_data_2)
else:
logging.warning("WeiBoLogin failed: %s", json_data_1)
return True if self.user_uniqueid and self.user_nick else False
完毕!