Python 最近因开发项目的需要,有一个需求,就是很多SNS网站都有的通过 Email地址 导入好友列表,不过这次要导入的不是Email 列表,而是QQ的好友列表。 实现方式: 通过goog

最近因开发项目的需要,有一个需求,就是很多SNS网站都有的通过 Email地址 导入好友列表,不过这次要导入的不是Email 列表,而是QQ的好友列表。

实现方式:

通过google一搜,实现的方式大概有下面这篇文章提到的几种方法:

http://www.cnblogs.com/hblhs/archive/2008/07/30/1256597.html

 

最后我选择了通过模拟登录QQ邮箱的方式来实现,该实现方式在海内网上的好友查找功能也可以看到。

QQ邮箱的官方登陆地址是http://mail.qq.com/

与其他大部分邮箱不同的是,如果使用纯数字的QQ号登录的话,除了密码,还需要输入验证码。

看到海内上的QQ好友导入功能也是需要输入验证码的,而且验证码的样子和QQ邮箱的很像。由于这是需要在用户手动输入密码的情况下才能实现的功能,因此输入验证码的工作也可以让用户手动来完成。

 

验证码处理:

通过对  http://mail.qq.com/ 页面的分析,  QQ邮箱的验证码方式实现原理其实是很简单,当需要一张验证码图片或看不清而需要换一张时,它都是向地址 http://ptlogin2.qq.com/getimage?aid=23000101 发出请求,(页面上该地址是通过js生成的,为了防止浏览器缓存,地址末尾还会带有随机一个随机数),而该链接不但返回一张图片,还在http头部带有设置cookie的一段header。这样当用户提交表单的时候,浏览器就会把该cookie发送回服务器,服务器通过比较 该cookie值和经过某种运算后的表单中的验证码值 就可以判断验证码是否填写正确。

 

现在的问题是由于cookie的安全机制,验证码图片不能直接从腾讯的服务器上去取,那样用户在将QQ和密码发送到我们的服务器时,验证码的cookie不会一起发过来。

 

解决方式其实也很简单,将验证码的获取地址改为我们自己的服务器,我们的服务器作为简单的代理,从腾讯的服务器上去获取真正的验证码,再将图片内容和那段cookie发送回用户浏览器。那样用户提交表单的时候,那段cookie就又会发送回我们的服务器了。

 

绕过其他验证安全机制:

一般上有了账号,密码,验证码这3样东西就可以实现模拟登录很多网站了,但是QQ邮箱还有其他的安全机制,在QQ邮箱登陆的表单中还有一个像这样 <input type="hidden" name="ts" value="1234672721" />  的 hidden 域,该value每次刷新页面都会改变,同时在表单提交的时候,还会通过js将该值与其他hidden 域的值进行某些计算才正式提交表单。

 

通过多次模拟登录,估计该值是用来判断登录session超时的,同时也参与其他的一些干扰加密的计算。而且该值与验证码是完全无关的,因此在显示我们表单时,只要先去抓取一下  http://mail.qq.com/  页面,从里面提取出ts 值, 连同其他所有 hidden 域 和相关计算的js代码放入我们的表单中就可以了。

 

因此,实际上我们的表单只需要稍微修改一下   http://mail.qq.com/  页面的内容就可以作为显示给用户的表单。主要包括以下几个方面,这里我使用的django,所以使用django的模板语法:

1、<input type="hidden" name="ts" value="1234672721" />    改为  <input type="hidden" name="ts" value="{{ts}}" />

2、表单的action地址改为我们自己这里假设为 /friends/    因此

<form name="form1" method="post" action="http://m11.mail.qq.com/cgi-bin/login?sid=0,2,zh_CN" onSubmit="return checkInput();" >

改为

<form name="form1" method="post" action="/friends/" onSubmit="return checkInput();">

 

3、图片验证码地址,有两个地方要改:

document.write("<img id='vfcode' src='http://ptlogin2.qq.com/getimage?aid=23000101&",Math.random(),"' style='cursor:pointer;border:1px solid #e4eef9' οnclick='changeimg()'>");

改为

document.write("<img id='vfcode' src='/qq-captcha/?aid=23000101&", Math.random(), "' style='cursor:pointer;border:1px solid #e4eef9' οnclick='changeimg()'>");

 

另外一个changeimg 函数内, 也将相应的地址改为我们自己的服务器即可。

 

改了这些,页面看上去和原来几乎一样,只是所有交互都改到了我们的服务器上,出于版权和页面统一的需要,在使用到自己的网站上时,可以使用自己设计的页面,只要表单的初始化和提交与原来一样就可以了,甚至也可以通过阅读js部分的源代码,把ts部分的计算移到服务器端进行。

 

示例代码:

以下是整个views.py的代码,包括后面会讲到模拟登录部分,login和qq_captcha分别用来初始化登陆页和获取图片验证码:

Python代码   收藏代码
  1. # Create your views here.  
  2. from django.shortcuts import render_to_response  
  3. from urllib2 import Request, urlopen, build_opener, HTTPCookieProcessor  
  4. from urllib import urlencode  
  5. from cookielib import CookieJar  
  6. from django.http import HttpResponse  
  7. import re  
  8. from xml.sax.saxutils import unescape  
  9. from BeautifulSoup import BeautifulSoup  
  10. server_no = 'm11'  
  11. login_error_re = re.compile('"errtype=(\d)"')  
  12. login_succ_re = re.compile('"frame_html\?sid=(.+?)"')  
  13. hacked_friendlist_page_re = re.compile(r'\<ul\s+class="grouplist"\>.+?\</ul\>', re.DOTALL)  
  14. body_re = re.compile(r'\<body\sclass="tbody"\sid="list"\>.+?\</body\>', re.DOTALL)  
  15.   
  16. def login(request):  
  17.     url = 'http://mail.qq.com/'  
  18.     re_obj = re.compile(r'name="ts"\svalue="(\d+)"')  
  19.     match_obj = re_obj.search(urlopen(url).read())  
  20.     ts = match_obj.group(1)  
  21.     return render_to_response('login.html', locals())  
  22.   
  23. def qq_captcha(request):  
  24.     url = 'http://ptlogin2.qq.com/getimage?aid=%s' % request.GET['aid']  
  25.     f = urlopen(url)  
  26.     r = HttpResponse(f.read(), mimetype = f.info()['Content-Type'], )  
  27.     r['Pragma'] = 'no-cache'  
  28.     r.set_cookie('verifysession', f.info()['Set-Cookie'].split(';')[0].split('=')[1].strip())  
  29.     return r  
  30.   
  31. def qq_friends(request):  
  32.     for k in request.POST:  
  33.         print '%s : %s' % (k, request.POST[k])  
  34.     verifysession = request.COOKIES['verifysession']  
  35.     print verifysession  
  36.     headers = {'Cookie':'''''verifysession=%s''' % verifysession,  
  37.                'Content-Type':'application/x-www-form-urlencoded',  
  38.                'Referer':'http://mail.qq.com/',  
  39.                'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1',  
  40.                }  
  41.     data = urlencode(request.POST)  
  42.     login_request = Request('http://%s.mail.qq.com/cgi-bin/login?sid=0,2,zh_CN' % server_no, data, headers)  
  43.     result = urlopen(login_request)  
  44.     content = result.read()  
  45.     login_error = login_error_re.search(content)  
  46.     if login_error:  
  47.         error_no = login_error.group(1#1:password wrong 2: captcha wrong  
  48.         if error_no == '1':  
  49.             error_msg = 'password or qq wrong'  
  50.         elif error_no == '2':  
  51.             error_msg = 'captcha wrong'  
  52.         return render_to_response('friends.html', locals())  
  53.     sid = login_succ_re.search(content).group(1)  
  54.           
  55.     friends_list_headers = {'Referer':'http://mail.qq.com/',  
  56.                            'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1',  
  57.                            }  
  58.     friends_list_request = Request('http://%s.mail.qq.com/cgi-bin/addr_listall?sid=%s&sorttype=null&category=common' % (server_no, sid), headers = friends_list_headers)  
  59.     cj = CookieJar()  
  60.     cj.extract_cookies(result, friends_list_request)  
  61.     opener = build_opener(HTTPCookieProcessor(cj))  
  62.     result = opener.open(friends_list_request)  
  63.     grouplist = hacked_friendlist_page_re.search(result.read().decode('gb2312''ignore')).group(0)  
  64.     soup = BeautifulSoup(grouplist, fromEncoding = 'utf-8')  
  65.     grouplist = soup.findAll('li')  
  66.     friend_list = {}  
  67.     for group in grouplist:  
  68.         friend_list[group.a.string] = []  
  69.         list_request = Request('http://%s.mail.qq.com%s' % (server_no, group.a['href']), headers = friends_list_headers)  
  70.         result = opener.open(list_request)  
  71.         body = BeautifulSoup(body_re.search(result.read().decode('gb2312''ignore')).group(0), fromEncoding = 'utf-8')  
  72.         friends = body.findAll('div', attrs={'class':'M'})  
  73.         for friend in friends:  
  74.             friend_name = unescape(friend.p.span.contents[1].replace('&nbsp;'''1))  
  75.             friend_email = friend.p.img['addr']  
  76.             friend_list[group.a.string].append((friend_name, friend_email))  
  77.       
  78.     return render_to_response('friends.html', locals())  

 

模板 friends.html 的代码:

Html代码   收藏代码
  1. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">  
  2. <html>  
  3. <head>  
  4. <title>好友列表</title>  
  5. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
  6. </head>  
  7. <body>  
  8. {%if error_msg%}  
  9. {{error_msg}}  
  10. {%endif%}  
  11.   
  12. {%for group, friends in friend_list.items %}  
  13. <ul>  
  14. {{group}}:  
  15.     {%for name, email in friends%}   
  16.     <li>{{name|safe}} {{email}}</li>  
  17.     {%endfor%}  
  18. </ul>  
  19. {%endfor%}  
  20. </body>  
  21. </html>  

 

模拟登录部分都在view qq_friends 里面进行,基本上就像前面说得,获取图片验证码的cookie,将表单提交的值原样发送post到腾讯的服务器,如果都正确的话就可以登录了。

发送的结果大概分为3种,验证码错误,账号或密码错误,登陆成功。不管在哪种情况下,腾讯的服务器都是返回一段js代码,通过那段代码重定向到错误页或邮箱登陆后的首页。

其中验证码错误像是下面这样:

<script> var urlHead="http://m11.mail.qq.com/cgi-bin/"; var targetUrl=""; var mailto=""; targetUrl = urlHead + "loginpage?" +"errtype=2" +"&verify=true" +"&clientuin=12345678" +"&t=" +"&alias=" +"&regalias=" +"&delegate_url=" +"&title=" +"&url=%2Fcgi-bin%2Flogin%3Fsid%3D0%2C2%2Czh_CN" +"&org_fun=" +"&aliastype=@qq.com" +"&ss=" +"&from=" +"&autologin=n" if (targetUrl == "") { targetUrl = ""; } document.write("<META http-equiv=Refresh content=\'0; url=\"" + targetUrl + "\"\'/>"); </script> <script type="text/javascript"> location.href=targetUrl </script>

 

账号或密码错误是这样:

<script> var urlHead="http://m11.mail.qq.com/cgi-bin/"; var targetUrl=""; var mailto=""; targetUrl = urlHead + "loginpage?" +"errtype=1" +"&verify=false" +"&clientuin=172564012" +"&t=" +"&alias=" +"&regalias=" +"&delegate_url=" +"&title=" +"&url=%2Fcgi-bin%2Flogin%3Fsid%3D0%2C2%2Czh_CN" +"&org_fun=" +"&aliastype=@qq.com" +"&ss=" +"&from=" +"&autologin=n" if (targetUrl == "") { targetUrl = ""; } document.write("<META http-equiv=Refresh content=\'0; url=\"" + targetUrl + "\"\'/>"); </script> <script type="text/javascript"> location.href=targetUrl </script>

 

登录成功是这样:

<script> var urlHead="http://m11.mail.qq.com/cgi-bin/"; var targetUrl=""; var mailto=""; targetUrl = urlHead + "frame_html?sid=UJh1e2XMWhOEbWcu"; if (targetUrl == "") { targetUrl = ""; } document.write("<META http-equiv=Refresh content=\'0; url=\"" + targetUrl + "\"\'/>"); </script> <script type="text/javascript"> location.href=targetUrl </script>

 

其中登录成功后的反馈不但是上面的js代码,还包括一大堆cookie,与邮箱服务器的后续交互都需要用到这些cookie。

另外上面js代码中frame_html?sid=UJh1e2XMWhOEbWcu的sid部分需要多次用到在后面抓取好友列表页面的url中,所以也需要提取出来。

 

登录邮箱后,就可以抓取好友列表了,可以通过先访问页面

http://m11.mail.qq.com/cgi-bin/addr_listall?sid=UJh1e2XMWhOEbWcu&sorttype=null&category=common

该页面的<ul class="grouplist"></ul>部分可以获取好友分组名称和相应的url地址。

然后在相应的访问各个好友分组的页面的就可以获取好友列表了。

 

这里由于我自己的QQ号限制,不知道如果没有好友没有分组页面会是什么情况,以及如果某个分组的好友很多的话,该好友列表页面是否会有分页(目前是没有见到分页)。有什么错误的话可以留言给我。

 

 

其他说明:

1、由于有些QQ用户给自己取火星文昵称,导致抓取的页面上很容易出现字符编码错误及HTML转义问题。

2、不知道是故意为了防止抓取还是设计不规范,好多页面用BeautifulSoup直接实例化,会导致大量信息的丢失,所以上面的代码都是尽可能先用正则表达式提取必须部分的信息。

3、由于QQ邮箱服务器的代码也在升级(事实上,我前几天刚写的代码就刚失效了,马上做了一些修改,昨天海内上的那个功能也出现了问题,看来他们也是通过QQ邮箱方式来实现的),所以并不能保证你在看到本文章时,上面的代码依然使用有效。但是总体上的思路依然可以提供参考。

 

 

Demo地址:

http://www.playdjango.cn/qq-friends/

由于要输入自己的QQ密码,如果你觉得不可信的话还是别试了,你也可以把上面的代码整理一下弄到自己的机器上进行测试也一样。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值