为了租个房 写了个程序去监控😂

还是租房

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

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

踩点

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

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

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

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

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

具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$.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
28
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:
  • token (required) - your application’s API token
  • 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)
  • message (required) - your message
    Some optional parameters may be included:
  • 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)
  • title - your message’s title, otherwise your app’s name is used
  • url - a supplementary URL to show with your message
  • url_title - a title for your supplementary URL, otherwise just the URL is shown
  • 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
  • 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
  • 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()
如果您觉得这篇文章对您有帮助,不妨支持我一下!