还是租房

初来上海入职,最要紧的事就是找房子。十分不巧的是看房子的时候和一个女生同时相中一间房,由于房屋还处于退租中的状态,房管表示要等到第二天打扫干净后会在APP上挂出来,到时候就看手速和人品了…

房子是发布在自如的官网和APP上的,由于还要看其他房子不可能一直把精力盯在这间房的发布上,于是将考虑了一下写一个程序自动监控房源状态并推送通知,如果能自动下单那就更好了。

踩点

虽然没有前端和爬虫的知识和经验,但是还是打算借此机会摸索一下,毕竟还挺有趣的。我们先打开官网来研究一下。这是配置中的房源在官网的状态:

这是正常出租中的房源状态:

观察了一下界面,似乎两者没太大区别。再查看看其他地方是否有更明显的区别。 把两个房源都加入收藏,我们可以在这个界面看到更明显的区别:

一个状态是可入住,一个是配置中。OK我们要关注的就是这个页面。

打开charles,这是一个http调试的工具,通过设置http proxy我们可以劫持中间的http请求和响应。观察一下这个页面的源码,我们可以看到如下代码:

具体如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$.ajax({
  type:'POST',
  url:"index.php?uri=collect/myCollect",
  data:'pg='+pg,
  // ...
})
  // ...
var collect = json.data;
for(var k in collect)
	{
  	if(collect[k]['room_status']=='dzz') table +='可入住';
		else if(collect[k]['room_status']=='ycz') table +='已入住';
		else if(collect[k]['room_status']=='yxd') table +='已预定';
		else if(collect[k]['room_status']=='zxpzz') table +='配置中';
		else if(collect[k]['room_status']=='tzpzz') table +='配置中';
	}
  • 在这里我们可以看到第一个关键信息,通过room_status这个key的value保存了房源的出租状态,且分别有dzz, ycz, yxd, zxpzz, tzpzz这个取值,分别对应着可入住, 已入住, 已预订配置中这几种状态。
  • 其次,透露给了我们一个更重要的信息,collect即收藏的url。

我们来看一下index.php?uri=collect/myCollect的请求和响应。

果然不出所料,返回消息是一个json的字符串,里面包含了关于这个房源的详细信息,包括我们关心的房源状态。接下来我们就可以构造请求来处理这个字段。

构造请求

先用PostMan测试一下。我构造了一个GET请求,在headers域中带上了CookieRefer字段。其中Cookie是直接从Charles截取的response中直接复制的。

PostMan测试成功,于是已经可以来写第一部分程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def ready_status(uid, token):

    url = 'http://i.ziroom.com/index.php?uri=collect/myCollect'
    cookie = '_csrf=_ZEK2MgzAs0b3DQXJWXzkbJXG7L0tTsj; ' \
             'passport_token=gzAs0b3DgzAs0b3D; ' \
             '_ga=GA1.2.1609902308.1487406424; ' \
             'CURRENT_CITY_NAME=%E4%B8%8A%E6%B5%B7; ' \
             'CURRENT_CITY_CODE=310000; ' \
             'Hm_lpvt_038002b56790c097b74c8gzAs0b3D68e=1487892118; ' \
             'Hm_lvt_038002b56790gzAs0b3D4c818a80e3a68e=1487407834; ' \
             'gr_user_id=gzAs0b3D; ' \
             'PHPSESSID=rqltrream1frpeam1fnfau6064'
    refer = 'http://i.ziroom.com/index.php?uri=collect'
    ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9'

    request = urllib2.Request(url)
    request.add_header('User-Agent', ua)
    request.add_header('Cookie', cookie)
    request.add_header('Referer', refer)

    response = urllib2.urlopen(request)
    resp = response.read()
    resp = json.loads(resp)

    rooms_dict = resp['data']
    room_dict = rooms_dict.values()[0]
    room_status = room_dict['room_status']

    return room_status

这是最简单也是最清晰明了的。就这样通过每0.5s请求一次获得房源状态。然而这套代码跑了大约两个小时之后意外终止了。再用PostMan请求时提示需要登录。

看来是Cookie过期了。需要在代码中再加上cookie的自动更新。

登录和Cookie

再观察一下Charles寻找login和cookie的相关请求,于是我们可以看到下图

我们只需要构造一个POST请求到index.php?r=user/login这个uri,并在body中附上phone=xxxxxxx&password=xxxxxxxx&imgVValue=&seven=1这样的明文用户名和密码即可。 响应消息中带上了cookie中最重要的uidtoken信息。

继续使用PostMan构造请求来测试一下,我带上了6个header,分别是Refer, User-Agent, Origin, Cookie, X-Requested-With, Content-Type,并在body中带上用户名和密码信息:

测试成功。我们只需要每隔一个小时请求这个url获取到的cookie和uid填充到之前我们构造的请求头里即可。

这部分代码补充如下;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def get_cookie_token():

    url = 'http://passport.ziroom.com/api/index.php?r=user/login'
    refer = 'http://passport.ziroom.com/login.html?redrect_url=http%3A%2F%2Fi.ziroom.com%2Findex.php%3Furi%3Dcollect'
    ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8'
    origin = 'http://passport.ziroom.com'
    x_request_with = 'XMLHttpRequest'
    content_type = 'application/x-www-form-urlencoded; charset=UTF-8'
    body = 'phone=xxxxxx&password=xxxxxxx&imgVValue=&seven=1'

    headers = {
        'Referer': refer,
        'User-Agent': ua,
        'Origin': origin,
        'X-Requested-With' : x_request_with,
        'Content-Type': content_type
    }
    # body = urllib.urlencode(body)

    request = urllib2.Request(url, body, headers)
    response = urllib2.urlopen(request)
    msg = response.read()

    msgdict = json.loads(msg)
    uid = msgdict['resp']['uid']
    token = msgdict['resp']['token']
    return uid, token

消息推送

当监控到状态改变之后,最重要的就是推送消息给我们,之所以没有继续研究自动下单的请求,是因为第一个不好测试,第二个自如只能在APP上下单,而不能在网页端。虽然可以在iOS上继续劫持http请求,但由于时间不早了暂时没有继续深入。

这一部分就是当房源状态变为可入住时,第一时间通知到我们。想到的几个方式是: - 利用IM的API,之前一直使用的是百度hi,然后由于离职之后无法调用百度HI的API,故放弃; - 微信公众号信息发送接口,这个之前用过,但记得有一个24小时后不能主动发起消息的限制,以及最重要的是我忘记公众号的用户名和密码了…遂弃之; - 邮件; - IFTTT; - 利用iOS自带的notification通知消息。

邮件

最初测试了一下邮件,然后邮件有两个很重要的缺陷导致我放弃了这一方法。第一便是利用smtplib发出的邮件经常被邮件提供商判断为垃圾邮件,导致无法顺利发出,即使是相同的收件人和发件人也经常收不到(笔者使用的是免费版的阿里云邮),其次就是iOS上的新邮件推送可能存在分钟级别的延迟。

所以邮件的方案也被放弃。

IFTTT

于是想到前不久接触到IFTTT,于是考虑使用IFTTT这个方案来测试一下。IFTTT的全称是IF THIS THEN THAT,一个简单的执行逻辑,如果某个条件发生,那么就执行一个动作。但是由于支持丰富的第三方接口可以完成的事情相当多,感兴趣的读者可以阅读IFTTT 有哪些有趣的玩法

利用IFTTT对RSS Feed的支持,我们可以构造一个rss xml, 当IFTTT 检测到xml中新的item或者keyword后,那么使用IFTTT向iOS发送推送消息。

我构造的xml内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?xml version="1.0" encoding="ISO-8859-1" ?>
<rss version="2.0">

<channel>
  <title>Ziroom Inspection</title>
  <link>http://tyr.gift</link>
  <description>Monitor favourite room status</description>
  <item>
    <title>room_status</title>
    <link>http://tyr.gift</link>
    <description>tzpzz</description>
  </item>
</channel>

</rss>

将房源状态作为description或者title传给xml,当IFTTT检测到dzz这个关键词时就发送推送消息。然而这个方案在测试时,始终没有成功,且IFTTT的时效性是存在最多一小时的延迟,于是这个方案也被放弃。

那么现在就剩最后一种,利用iOS的推送消息。

Pushover

我们自然想到的是,是否存在这样一个服务,我们安装一个App之后,通过调用这个APP的一个借口来触发这个APP发出一条我们需要的推送消息。那么有没有这样的服务呢?

Google一番之后,无疑是存在的。利用Pushover这个网站提供的服务,可以完美达到我们的要求,且推送的实时性非常强,几乎是调用接口的同时APP的推送消息就来了。

首先我们需要iOS上安装Pushover这个APP并完成用户的注册,注册和登录完成后,打开pushover的官网,进入后台管理后我们就可以在网页上向自己的设备发送推送消息了。

现在我们来看一下如何调用Pushover的API。首先我们需要新建一个application,以获取我们的application api。按照下图创建一个application:

创建完成之后记录下我们的API TOKEN。另外在这个页面中还可以看到发送推送的统计信息。

API如下,写的很详细就不详细解释了~:

  1. Register your application, set its name and upload an icon, and get an API token in return (often referred to as APP_TOKEN in our documentation and code examples).
  2. POST an HTTPS request to https://api.pushover.net/1/messages.json with the following parameters:
  3. token (required) - your application’s API token
  4. user (required) - the user/group key (not e-mail address) of your user (or you), viewable when logged into our dashboard (often referred to as USER_KEY in our documentation and code examples)
  5. message (required) - your message Some optional parameters may be included:
  6. device - your user’s device name to send the message directly to that device, rather than all of the user’s devices (multiple devices may be separated by a comma)
  7. title - your message’s title, otherwise your app’s name is used
  8. url - a supplementary URL to show with your message
  9. url_title - a title for your supplementary URL, otherwise just the URL is shown
  10. priority - send as -2 to generate no notification/alert, -1 to always send as a quiet notification, 1 to display as high-priority and bypass the user’s quiet hours, or 2 to also require confirmation from the user
  11. timestamp - a Unix timestamp of your message’s date and time to display to the user, rather than the time your message is received by our API
  12. sound - the name of one of the sounds supported by device clients to override the user’s default sound choice

这部分的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def push_notification():
    url = 'https://api.pushover.net/1/messages.json'
    token = 'anncnejfjenvowojfeie5c1p2ejfiy'
    user = 'ugdzz4wwiejfjwenvowefyrefjieqew'
    message = 'Got Room!'
    title = '自如房源监控'
    sound = 'pushover'

    body = {
        'token': token,
        'user': user,
        'message': message,
        'title': title,
        'sound': sound
    }

    body = urllib.urlencode(body)
    request = urllib2.Request(url, body)
    request.add_header('Content-type', 'application/x-www-form-urlencoded')
    response = urllib2.urlopen(request)

    print response.read()

实际运行效果

幸运的是后来那个女孩儿后来不租这间了,最后这个程序也圆满完成了他短暂的使命,然而最终发现还是太年轻😂,房源状态改变后自如弹出了一个一小时倒计时,必须要等到一小时之后才能下单,悲伤ing😭…

完整代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#coding:utf-8

import urllib2
import urllib
import json
import time
import mail

def ready_status(uid, token):

    url = 'http://i.ziroom.com/index.php?uri=collect/myCollect'
    cookie = '_csrf=_xfwefeffwefxxkbJXG7ff; ' \
             'passport_token={}; ' \
             '_ga=GA1.2.16192938.1482936424; ' \
             'CURRENT_CITY_NAME=%E4%B8%8A%E6%B5%B7; ' \
             'CURRENT_CITY_CODE=310000; ' \
             'Hm_lpvt_03800jfei081033bf3c818a80e3a68e=11393203118; ' \
             'Hm_lvt_038002b56790c097b742i293923ff268e=14823992034; ' \
             'gr_user_id={}; ' \
             'PHPSESSID=rqltwe234e7enfeiow0129314'.format(token, uid)
    refer = 'http://i.ziroom.com/index.php?uri=collect'
    ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9'

    request = urllib2.Request(url)
    request.add_header('User-Agent', ua)
    request.add_header('Cookie', cookie)
    request.add_header('Referer', refer)

    response = urllib2.urlopen(request)
    resp = response.read()
    resp = json.loads(resp)

    rooms_dict = resp['data']
    room_dict = rooms_dict.values()[0]
    room_status = room_dict['room_status']

    return room_status

def get_cookie_token():

    url = 'http://passport.ziroom.com/api/index.php?r=user/login'
    refer = 'http://passport.ziroom.com/login.html?redrect_url=http%3A%2F%2Fi.ziroom.com%2Findex.php%3Furi%3Dcollect'
    ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8'
    origin = 'http://passport.ziroom.com'
    x_request_with = 'XMLHttpRequest'
    content_type = 'application/x-www-form-urlencoded; charset=UTF-8'
    body = 'phone=112233445566&password=999888333727273&imgVValue=&seven=1'

    headers = {
        'Referer': refer,
        'User-Agent': ua,
        'Origin': origin,
        'X-Requested-With' : x_request_with,
        'Content-Type': content_type
    }

    request = urllib2.Request(url, body, headers)
    response = urllib2.urlopen(request)
    msg = response.read()

    msgdict = json.loads(msg)
    uid = msgdict['resp']['uid']
    token = msgdict['resp']['token']
    return uid, token

def push_notification():
    url = 'https://api.pushover.net/1/messages.json'
    token = 'anncefijefijw2921enfwliewvnwy'
    user = 'ugdeifj92302n20fns02nfnyraetqe'
    message = 'Got Room!'
    title = '自如房源监控'
    sound = 'pushover'

    body = {
        'token': token,
        'user': user,
        'message': message,
        'title': title,
        'sound': sound
    }

    body = urllib.urlencode(body)
    request = urllib2.Request(url, body)
    request.add_header('Content-type', 'application/x-www-form-urlencoded')
    response = urllib2.urlopen(request)

    print response.read()


if __name__  ==  '__main__':

    try:
        uid, token = get_cookie_token()
    except:
        uid = ''
        token = ''

    status = ''
    i = 0
    while status != 'dzz':
        i += 1
        try:
            status = ready_status(uid, token)
        except:
            status = ''
        ti = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        print '%s %s' % (ti ,status)
        time.sleep(0.5)
        if i >= 7200: # 每隔一小时重新登录
            i = 0
            uid, token = get_cookie_token()

    push_notification()