返回目录

Hotmail,MSN.com,Live.com,Live.cn账号都可以用来登录MSN(也就是Windows Live Message),甚至用户可以用自己其他邮箱去注册MSN的账户。

但是不同的产品对于联系人的页面都不一样,如此,通过抓取网页导入联系人就变得异常麻烦。

不过,换一个思路,我们可以直接模拟MSN的客户端,来获取用户的联系人列表。

Python是一个很好很强大的工具,自然少不了连接MSN的库。这里我们选用Alberto Bertogli的msnlib。

http://blitiri.com.ar/p/msnlib/ ,下载安装msnlib。导入联系人的代码如下:

#coding:utf-8
import socket, select, time, msnlib, msncb

def void(s): pass
msnlib.debug = msncb.debug = void

def get_friend_list(email, password):
    m = msnlib.msnd()
    m.cb = msncb.cb()

    m.email = email.strip()
    m.pwd = password.strip()
    m.encoding = 'utf-8'
    try:
        m.login()
        m.sync()
    except:
        return
    m.change_status('invisible')

    begin_time = time.time()

    users = set()
    while True:
        fds = m.pollable()
        infd = fds[0]
        outfd = fds[1]

        fds = select.select(infd, outfd, [], 0)

        for i in fds[0] + fds[1]:
            try:
                m.read(i)
            except ('SocketError', socket.error), err:
                if i != m:
                    m.close(i)
        merge_users = users|set(m.users.keys())
        if len(users) == len(merge_users):
            end_time = time.time()
            if len(users):
                if end_time-begin_time >= 2:
                    break
            elif end_time-begin_time >= 3:
                break
            time.sleep(0.05)
        else:
            users = merge_users
            begin_time = time.time()
    result = {}
    for i in users:
        nick = m.users[i].nick
        i_lower = i.lower()
        if nick.lower() == i_lower:
            nick = nick.split("@", 1)[0]
        result[i_lower] = nick

    return result

if __name__ == "__main__":
    result = get_friend_list(raw_input("email:"), raw_input("password:"))
    if result is None:
        print "email or password or network error , login failed"
    else:
        for k, v in result.iteritems():
            print k, v.decode("utf-8", "ignore")

运行该脚本,输入用户名密码,输出内容如下

youngvleo@gmail.com youngvleo
tk@msn.cn tk
...

导入联系人往往是长达几十秒漫长的过程,为了不让它阻塞正常请求。我们将其纳入方便支持异步长连接的tornado框架下,作为一个远程调用的服务。

和往常一样,还是用ajax向来调用rpc。先实现一个从cookie获取用户id的基类

import tornado.web
from mysite.model.auth import UserSession
class BaseHandler(tornado.web.RequestHandler):
    def get_user_id(self):
        session = self.get_cookie("S")
        user_id = UserSession.verify_user_id(session)
        return user_id

然后开始写用户导入的模块

#coding:utf-8
import thread
import tornado.web
from mysite.model.auth import User, UserEmail
from mysite.model.consts import USER_STATE_IMPORT
from mysite.model.friend import is_friend
from get_friend_list import get_friend_list
from myrpc.base_handler import BaseHandler
import tornado.ioloop
def _async_get_friend_list(user_id, email, password, callback):
    result = get_friend_list(email, password)
    if result is None:
        r = "N"
    else:
        not_friend = []
        not_join = []
        User.begin()
        for email, name in result.iteritems():
            email = email.strip()
            user = User.by_email(email)
            if user is None:
                user_email = UserEmail.get(email=email)
                if user_email:
                    print "!!!", email, user_email.id
                else:
                    user_email = UserEmail(email=email)
                    user_email.save()
                user = User(id=user_email.id, name=name, state=USER_STATE_IMPORT)
                user.save()
                not_join.append(user.id)
            else:
                if user.state == USER_STATE_IMPORT:
                    not_join.append(user.id)
                elif not is_friend(user_id, user.id):
                    not_friend.append(user.id)
        User.commit()
        r = "%s-%s"%(
            "_".join(map(str, not_friend)),
            "_".join(map(str, not_join)),
        )
    #这样线程安全
    tornado.ioloop.IOLoop.instance().add_callback(lambda: callback(r))

class ImportMsnHandler(BaseHandler):
    @tornado.web.asynchronous
    def get(self, email, password):
        user_id = self.get_user_id()
        if user_id:
            thread.start_new_thread(
                _async_get_friend_list,
                (user_id, email, password, self.async_callback(self._on_get))
            )
        else:
            self.finish('N')

    def _on_get(self, r):
        self.finish(r)

返回值 - 前的是已经注册,但是尚未成为好友的用户id,- 后的是尚未注册的用户id。

好了,到此为止MSN联系人导入算是写完了。全部代码见下面这个网址。

http://mayoufour.googlecode.com/svn/trunk/stdyun/myrpc/import_contact/import_msn/

最后配置一下Tornado的server,文件见 http://mayoufour.googlecode.com/svn/trunk/stdyun/tornado_server.py

#!/usr/bin/env python
#coding:utf-8
from myconf import config
config.DISABLE_LOCAL_CACHED = True
config.THREAD_SAFE = True

import logging
import os.path
import tornado.auth
import tornado.escape
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
from tornado.options import define, options

define("host", default="0.0.0.0", help="run on the given port", type=int)
define("port", default=15050, help="run on the given port", type=int)


from myrpc.import_contact.import_msn import ImportMsnHandler

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            (r"/rpc/import/msn/([^/]+)/(.+)", ImportMsnHandler),
        ]
        settings = {
            "login_url":"/auth/login",
            "xsrf_cookies": False,
            "debug": False
        }
        tornado.web.Application.__init__(self, handlers, **settings)

def main(port=None):
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    port = port or options.port
    http_server.listen(port)
    print "port", port
    tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1 and sys.argv[1].isdigit():
        port = int(sys.argv[1])
    else:
        port = 15050
    main(port=port)

最后在nginx配置中加上url转发

upstream tornado {
    server localhost:15050 weight=1;
}
server {
    listen          80;
    server_name     stdyun.com;
    charset utf-8;
    location / {
        proxy_pass http://stdyun;
        proxy_set_header X-Real-IP  $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
    }
    location ~ ^/rpc {
        proxy_pass http://tornado;
        proxy_set_header X-Real-IP  $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
    }
    ... ... ...
}

OK,导入MSN联系人的后台就算是写好了。

访问这里试试吧,不过要先登录哦:)

http://stdyun.com/rpc/import/msn/MSN账号名/密码

比如我的MSN账号返回值如下

10000268-10003385_10001307_10003386_10003387
上一节 返回目录
京ICP备10008809号