参考阅读
基于python的新浪微博模拟登陆
Python模拟登录新浪微薄(使用RSA加密方式和Cookies文件
新浪微博登录rsa加密方法
模拟登录新浪微博(直接填入Cookie)
模拟登录新浪微博(Python)
1. 事前准备
- 阅读上篇scrapy笔记(2)
- 下载Fiddler并掌握其基本用法
- 阅读urllib2文档
- 下载本文我的源码
2. 微博登录分析
2.1 截包分析
以下的内容需要掌握Fiddler截包、改包重发等基本知识, 如果不想了解微博的模拟登录的流程及原理, 那么可以跳过这部分直接到第3步. 不过建议还是去熟悉下Fiddler这个前端调试神器, 当然,用其它截包工具代替也是可以的.比如Firefox的插件httpfox
微博的登录入口有好几个, 我们选择http://weibo.com/login.php 这个. 其实只要登录的逻辑不变, 其它的入口也是可以的.
然后, 我们让Fiddler开始截包, 并在登录页面上输入账号密码登录一次.
截到关于登录的包如下:
实际上截到的包更多, 但我们只需要关注图中红色标记的那几个
我们先来看看第一个
这些数据中除了su、sp、rsakv、servertime、nonce是经过js处理动态生成的,其它都是个固定值,可以在代码中写死.
怎么获得这些值呢?
http://login.sina.com.cn/sso/prelogin.phpentry=weibo&callback=sinaSSOController.preloginCallBack&su=yourusername&rsakt=mod&checkpin=1&client=ssologin.js(v1.4.11)
注意上面url的su=yourusername部分, 这里的su是经过js处理后的用户名.
请求这个url可以得到servertime,nonce,pubkey,rsakv等等
2.2 查看json
我们还需要知道js是怎么处理我们填入的用户名及密码的, 即su与sp.
首先我们要在未登录状态到http://login.sina.com.cn/signup/signin.php?entry=sso 这个页面,并得到http://login.sina.com.cn/js/sso/ssologin.js 这个js文件.
查看ssologin.js的makeRequest函数, 原型如下:
var makeRequest = function (username, password, savestate) {
var request = {
entry: me.getEntry(),
gateway: 1,
from: me.from,
savestate: savestate,
useticket: me.useTicket ? 1 : 0
};
if (me.failRedirect) {
me.loginExtraQuery.frd = 1
}
request = objMerge(request, {pagerefer: document.referrer || ""});
request = objMerge(request, me.loginExtraFlag);
request = objMerge(request, me.loginExtraQuery);
request.su = sinaSSOEncoder.base64.encode(urlencode(username));
if (me.service) {
request.service = me.service
}
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;
try {
request.sr = window.screen.width + "*" + window.screen.height
} catch (e) {
}
return request
};
从代码中我们可以知道su就是经过html字符转义再转成base64编码
在python中我们可以这样转化:
def get_su(user_name):
username_ = urllib.quote(user_name) # html字符转义
username = base64.encodestring(username_)[:-1]
return username
再看sp, 关于密码的这部分有点复杂, 我自己对密码学这部分并不大了解, 不过可以从js中看到, weibo登录对密码有两种加密方式:rsa2与wsse,我们从图2的pwnencode=rsa2可知, js处理走的是这一部分逻辑(至于另一部分wsse是什么时候用到, 我不清楚)
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)
}
可以看到servertime, nonce, rsakv都被用上了.我们只要把这部分js在python中转义就行了.
我也是看别人的文章才知道,0x10001要转化成10进制的65537, 还有要经过servertime + +'\t' + nonce + '\n' + passwd拼接字符串再进行Rsa加密, 最后转成16进制即得到sp. 代码如下
def get_sp_rsa(passwd, servertime, nonce):
# 这个值可以在prelogin得到,因为是固定值,所以写死在这里
weibo_rsa_n = 'EB2A38568661887FA180BDDB5CABD5F21C7BFD59C090CB2D245A87AC253062882729293E5506350508E7F9AA3BB77F4333231490F915F6D63C55FE2F08A49B353F444AD3993CACC02DB784ABBB8E42A9B1BBFFFB38BE18D78E87A0E41B9B8F73A928EE0CCEE1F6739884B9777E4FE9E88A1BBE495927AC4A799B3181D6442443'
weibo_rsa_e = 65537 # 10001对应的10进制
message = str(servertime) + '\t' + str(nonce) + '\n' + passwd
key = rsa.PublicKey(int(weibo_rsa_n, 16), weibo_rsa_e)
encropy_pwd = rsa.encrypt(message, key)
return binascii.b2a_hex(encropy_pwd)
3 模拟登录
准备工作做完了,我们要模拟正常登录那样发几个http包, 基本上是以下几个步骤
-
从
http://login.sina.com.cn/sso/prelogin.phpentry=weibo&callback=sinaSSOController.preloginCallBack&su=yourusername&rsakt=mod&checkpin=1&client=ssologin.js(v1.4.11)
获取servertime,nonce,rsakv等值
- 把这些值与其它固定值一起提交到
http://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.11)
, 这个地址会跳转到passport.weibo.com/wbsso/login/(省略, 见图2)/
,并返回图2下划线标注的我们需要的地址. -
用正则表达式取出图2的地址并请求, 得到如下结果则登录成功
此时要保存该请求的cookie
以后每次抓取微博时的请求带上该cookie即可.
到这里, 模拟登录就算完成了. 当然这是人工的模拟登录与结合scrapy的模拟登录还是有所不同,不过区别也不会大到哪去, 只要把保存的cookie持久化到文件, scrapy每次请求时带上这个cookie就可以了,相信这部分不会有多大难度. 如果还是有困难, 请提出来, 有时间我再补完这部分内容.
4 抓取微博内容
4.24 补充说明,本来以为微博抓取内容跟其它一样简单,结果发现微博用js渲染所有内容,scrapy抓的网页源文件一个链接都没有, 考虑用其它方法解决, 正在坑中, 搞定后再来更新文章吧.
4.26 抓取微博内容, 搞了很久, 终于算是实现了.说说我的方法吧, 根据观察,微博的内容放在页面上某个script标签内.
我们可以通过正则, 取出这部分内容,然后替换response的body,再用scrapy的选择器提取其中的内容和链接,具体看代码及注释吧
# -- coding: utf-8 --
from scrapy import Request
from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.contrib.linkextractors import LinkExtractor
from weibo_spider.items import WeiboSpiderItem
from weibo_spider.spiders.login_api import get_login_cookie
class WeiboSpider(CrawlSpider):
name = 'weibo'
allowed_domains = ['weibo.com']
start_urls = ['http://www.weibo.com/u/1876296184'] # 不加www,则匹配不到cookie, get_login_cookie()方法正则代完善
rules = (
Rule(LinkExtractor(allow=r'^http:\/\/(www\.)?weibo.com/[a-z]/.*'), # 微博个人页面的规则,或/u/或/n/后面跟一串数字
process_request='process_request',
callback='parse_item', follow=True), )
cookies = None
def process_request(self, request):
request = request.replace(**{'cookies': self.cookies})
return request
def start_requests(self):
for url in self.start_urls:
if not self.cookies:
self.cookies = get_login_cookie(url) # 得到该url下的cookie
yield Request(url, dont_filter=True, cookies=self.cookies, meta={'cookiejar': 1}) # 这里填入保存的cookies
def extract_weibo_response(self, response): # 提取script里的weibo内容,替换response
script_set = response.xpath('//script')
script = ''
for s in script_set:
try:
s_text = s.xpath('text()').extract()[0].encode('utf8').replace(r'\"', r'"').replace(r'\/', r'/')
except:
return response
if s_text.find('WB_feed_detail') > 0:
script = s_text
break
kw = {'body': script}
response = response.replace(**kw)
return response
def _parse_response(self, response, callback, cb_kwargs, follow=True): # 继承crawlspider这个方法,这个方法在解析页面/提取链接前调用
response = self.extract_weibo_response(response)
return super(WeiboSpider, self)._parse_response(response, callback, cb_kwargs, follow)
def parse_item(self, response):
msg_nodes = response.xpath('//*[@class="WB_feed WB_feed_profile"][2]/div') # 提取weibo的内容div
items = []
if msg_nodes:
for msg in msg_nodes:
item = WeiboSpiderItem()
try:
c = msg.xpath('.//div[@class="WB_detail"]/div/text()').extract()[0] #提取每条微博的内容,不包括at人
content = c[38:].encode('utf8') #从38位开始, 是为了去掉\n和一大堆空格
except Exception, e:
pass
else:
item['content'] = content
item['url'] = response.url
items.append(item)
return items
继承CrawlSpider类是因为要用到它根据定制的Rule提取/跟进链接的功能, 当然你也可以选择最基础的Spider类,不过其中的parse方法就得自己写了.
不过这份代码仍然有些问题:
- 暂时只能提取某人weibo第1页内容
- weibo向下拉滚动条会新增内容,而这部分是通过ajax动态请求json实现的,暂时只能提取第1页第1段内容
解决思路是有的
这是weibo单页地址
http://www.weibo.com/u/1832810372page=1 改page即可跳页
这是weibo分段地址
http://weibo.com/p/aj/v6/mblog/mbloglist?domain=100505&page=2&pre_page=3&pagebar=1&id=1005053190764044
domain与id可以在页面上第2个script标签内找 ,替换page和pre_page即可加载不同段的json
只要在抓取某人首页时请求page=?与pre_page=1,2,3(每页最多只有3段),就可以实现加载任一页所有内容.
于是问题来了,在scrpay中哪里嵌入这些逻辑呢?我还没想好,但我认为如果写个小爬虫的话,用urllib2/request+beatifulsoup自己写肯定比用scrpay舒爽的多.
说实话,改出上面的代码已经折腾了我将近1天多的时间,这还是跟踪源码好半天,搞清楚这些调用的来龙去脉才弄出来的,官方的文档实在不够看.
也许该去抓移动端的包来解析一下,感觉这应该比抓PC端简单很多.
总结:
本章尽在折腾,跟踪weibo各种包并解析,同时为了达到效果跟踪调试scrapy的源码,最后也只搞出个半成品,就先挖个坑在这吧.有需要的话才花时间折腾一番.
下篇内容应该是介绍一下如何跟踪调试scrapy或者选某网站写个定时增量抓取的爬虫.