链家APP以前可以查看房屋的价格变动信息,但新版本后取消了该功能,这样便不利于了解该房源的情况,比如某些房源一路调价3个月仍没卖出,要么不诚心卖,要么有各种问题。
  那么对于想购房的同学,有了目标区域重点关注的房源,如何能够获知其价格变动信息呢?很容易想到,自己轮个爬虫,每天自动爬一次,把房屋信息爬下后存起来,然后展示出来。
  主要的设计思路为:
1、 页面的爬取,house info的获取,主要涉及beautifulsoup库的使用;仅爬取指定的若干小区。
2、 信息存取:使用MySql数据库,数据库名称lianjiaHouse,包含两张表,两张表通过houseid进行关联:
  a) houseinfo,保存房屋基本信息,字段包括 价格、朝向、小区、户型、年代、税费等;
  b) hisprice,保存每天价格信息,字段包括日期、价格等;
3、 信息的更新,包括:
  a) new house的加入:当前爬取的houseid,数据库中没有,则插入记录。 validflag 标记为1,validdate标记为当前日期。并插入hisprice表
  b) old house失效:每次爬取前,首先将数据库中所有validflag标记为0;若当前爬取的houseid,数据库中有,则validflag 标记为1,validdate标记为当前日期,并更新价格信息;若为同一日期,则覆盖更新价格。这样若非当前爬取的houseid,数据库中保留validflag =0,validdate为最后有效日期,价格信息也仅更新到最后有效日期。
4、 信息展示
  a) 前端页面,输入指定小区,可展示所有房源信息和价格变动情况。
5、 信息触发发送用户
  当有新上房源或者价格变动时,可触发邮件或短信给用户。   

  做为新手,边学边做,花了四天,基本实现了除展示外的功能,对异常处理暂未过多考虑,可先凑合直接在数据库里看。涉及到一些有用的技术点如下:
  1、两列list组成dict,用zip方法:

1
2
nvs = zip(info_list,list(values[0][1:]))
Qres = dict( (info_list,value) for info_list,value in nvs)

  2、mysql的使用:
  当更新数据库的数据量大时,报Lock wait timeout exceeded; try restarting transaction这个错误,原因在于mysql配置参数innodb_lock_wait_timeout设置锁等待的时间是50s,有的锁等待超过此时间,故抱错. 可以把这个时间加长,改为500后消除。
  另外,mysql的命令,多个参数可以组成turple来输入,对数据的修改,需要conn.commit()才能生效。

1
cursor.execute('insert into hisprice (houseID,date,totalPrice) values (%s,%s,%s)', t2)

  3、BS库的使用:
  主要用的find、get_text、get attribute等:

1
2
3
housetitle = name.find("div",{"class":"title"})
info_dict.update({u'Title':housetitle.get_text()})
info_dict.update({u'link':housetitle.a.get('href')})

  
  后续还可以考虑进一步扩展功能:
  i、用flask或django,部署于后端,自动每天更新;
  ii、增加抓取全北京小区列表信息,然后再按列表抓取房屋信息,从而获得全北京房屋信息;
  iii、前端页面展示,按小区或房屋查询,可视化价格变动信息。
  
  这样,使用这个小工具,配合链家APP,希望能够帮助大家更好找房。
  
  下面列出了主要功能函数,完整请参考我的github,欢迎测试与批评。

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def database_init(dbflag='local'):
if dbflag=='local':
conn = mysql.connector.connect(user='root', password='password', database='lianjiaSpider',host='localhost')
else:
conn = mysql.connector.connect(user='user', password='password', database='qdm1940_db',host='qdm1940.my3w.com')
dbc = conn.cursor()
# 创建houseinfo and hisprice表:
dbc.execute('create table if not exists houseinfo (id int(10) NOT NULL AUTO_INCREMENT primary key,houseID varchar(50) , Title varchar(200), link varchar(200), cellname varchar(100),\
years varchar(200),housetype varchar(50),square varchar(50), direction varchar(50),floor varchar(50),taxtype varchar(200), \
totalPrice varchar(200), unitPrice varchar(200),followInfo varchar(200),validdate varchar(50),validflag varchar(20))')
dbc.execute('create table if not exists hisprice (id int(10) NOT NULL AUTO_INCREMENT primary key,houseID varchar(50) , date varchar(50), totalPrice varchar(200))')
conn.commit()
dbc.close()
return conn
def houseinfo_insert_mysql(conn,info_dict):
info_list = [u'houseID',u'Title',u'link',u'cellname',u'years',u'housetype',u'square',u'direction',u'floor',\
u'taxtype',u'totalPrice',u'unitPrice',u'followInfo',u'validdate',u'validflag']
t=[]
for il in info_list:
if il in info_dict:
t.append(info_dict[il])
else:
t.append('')
t=tuple(t) # for houseinfo
today = get_today()
t2=(info_dict[u'houseID'],today,info_dict[u'totalPrice']) # for hisprice
cursor = conn.cursor()
cursor.execute('select * from houseinfo where houseID = (%s)',(info_dict[u'houseID'],))
values = cursor.fetchall() #turple type
if len(values)>0:
nvs = zip(info_list,list(values[0][1:]))
Qres = dict( (info_list,value) for info_list,value in nvs)
else:
pass
if len(values)==0: # new house
cursor.execute('insert into houseinfo (houseID,Title,link,cellname,years,housetype,square,direction,floor,\
taxtype,totalPrice,unitPrice,followInfo,validdate,validflag) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)', t)
cursor.execute('insert into hisprice (houseID,date,totalPrice) values (%s,%s,%s)', t2)
# trigger_notify_email(info_dict,'newhouse')
else:
if int(today)>int(Qres[u'validdate']):
cursor.execute('update houseinfo set validdate = %s,validflag= %s where houseid = %s',(today,'1',info_dict[u'houseID']))
cursor.execute('insert into hisprice (houseID,date,totalPrice) values (%s,%s,%s)', t2)
else:
cursor.execute('update houseinfo set validflag= %s where houseid = %s',('1',info_dict[u'houseID']))
cursor.execute('update hisprice set totalPrice= %s where houseid = %s',(info_dict[u'totalPrice'],info_dict[u'houseID']))
if int(Qres[u'totalPrice']) != int(info_dict[u'totalPrice']):
info_dict[u'oldprice'] = Qres[u'totalPrice']
# trigger_notify_email(info_dict,'updateprice')
conn.commit()
cursor.close()
def trigger_notify_email(info_dict,reason='newhouse'):
if reason == 'newhouse':
title = u'新上房源' + ' ' + info_dict[u'cellname'] + ' ' + info_dict[u'housetype'] + ' ' + info_dict[u'totalPrice']+'万'
cotent_txt = u'新上房源' + ' ' + info_dict[u'cellname'] + ' ' + info_dict[u'housetype'] + ' 朝向' + info_dict[u'direction'] + \
' 面积' + info_dict[u'square'] + ' 总价' + info_dict[u'totalPrice']+'万 单价' + info_dict[u'unitPrice']+'万每平米' \
+ u' 房源编号' + info_dict[u'houseID'] + ' ' + u'查看链接 ' + info_dict[u'link']
content_html = '<html><body><h1>新上房源</h1>' + '<p>' + info_dict[u'cellname'] + ' ' + info_dict[u'housetype'] + \
' 朝向' + info_dict[u'direction'] + ' 面积' + info_dict[u'square'] + ' 总价' + info_dict[u'totalPrice'] + \
'万 单价' + info_dict[u'unitPrice']+'万每平米' + u' 房源编号' + info_dict[u'houseID'] + ' ' + u'点击查看链接 ' + \
'<a href=\"'+ info_dict[u'link'] + '\">' + info_dict[u'link'] + '</a>...</p>' + '</body></html>'
else:
title = u'房屋价格变动' + ' ' + info_dict[u'cellname'] + ' ' + info_dict[u'housetype'] + info_dict[u'oldprice'] + '----> ' + info_dict[u'totalPrice']+'万'
cotent_txt = u'房屋价格变动' + ' ' + info_dict[u'cellname'] + ' ' + info_dict[u'housetype'] + ' 朝向' + info_dict[u'direction'] + \
' 面积' + info_dict[u'square'] + ' 总价 ' + info_dict[u'oldprice']+'万 ' + '----> ' + info_dict[u'totalPrice']+'万 单价' + \
info_dict[u'unitPrice']+'万每平米' + u' 房源编号' + info_dict[u'houseID'] + ' ' + u'查看链接 ' + info_dict[u'link']
content_html = '<html><body><h1>房屋价格变动</h1>' + '<p>' + info_dict[u'cellname'] + ' ' + info_dict[u'housetype'] + \
' 朝向' + info_dict[u'direction'] + ' 面积' + info_dict[u'square'] + ' 总价 ' + info_dict[u'oldprice']+'万 ' \
+ '----> '+ info_dict[u'totalPrice'] + '万 单价' + info_dict[u'unitPrice']+'万每平米' + u' 房源编号' + \
info_dict[u'houseID'] + ' ' + u'点击查看链接 ' + '<a href=\"'+ info_dict[u'link'] + '\">' + info_dict[u'link'] + \
'</a>...</p>' + '</body></html>'
from_addr = 'ziyubiti@qq.com'
password = 'password'
to_addr = 'ziyubiti@139.com'
smtp_server = 'smtp.qq.com'
msg = MIMEMultipart('alternative')
msg.attach(MIMEText(cotent_txt, 'plain', 'utf-8'))
msg.attach(MIMEText(content_html, 'html', 'utf-8'))
msg['From'] = _format_addr(u'紫雨 <%s>' % from_addr)
msg['To'] = _format_addr(u'紫雨 <%s>' % to_addr)
msg['Subject'] = Header(title, 'utf-8').encode()
smtp_port = 465 #25 or 587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls() # 使用安全链接SSL,port 587,qq still is 25
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
def all_set_unvalid(conn):
cursor = conn.cursor()
cursor.execute('update houseinfo set validflag= %s',('0',))
conn.commit()
cursor.close()
def house_percell_spider(conn,cellname = u'荣丰2008'):
url=u"http://bj.lianjia.com/ershoufang/rs" + quote(cellname) +"/"
#try:
req = Request(url,headers=hds[random.randint(0,len(hds)-1)])
source_code = urlopen(req,timeout=5).read()
soup = BeautifulSoup(source_code,'lxml')
#except (HTTPError, URLError), e:
# print e
# return
#except Exception,e:
# print e
# return
total_pages = 0
page_info= soup.find('div',{'class':'page-box house-lst-page-box'}).get('page-data').split(',')[0] #'{"totalPage":5,"curPage":1}'
total_pages= int(page_info[-1])
info_dict_all = {} # if each house info_dict insert into database ,this info_dict_all is not needed
for page in range(total_pages):
if page>0:
url_page=u"http://bj.lianjia.com/ershoufang/pg%drs%s/" % (page+1,quote(cellname))
req = Request(url_page,headers=hds[random.randint(0,len(hds)-1)])
source_code = urlopen(req,timeout=50).read()
soup = BeautifulSoup(source_code,'lxml')
nameList = soup.findAll("li", {"class":"clear"})
i = 0
for name in nameList: # per house loop
i = i + 1
info_dict = {}
info_dict_all.setdefault(i+page*30,{})
housetitle = name.find("div",{"class":"title"}) #html
info_dict.update({u'Title':housetitle.get_text()})
info_dict.update({u'link':housetitle.a.get('href')}) #atrribute get
houseaddr = name.find("div",{"class":"address"})
info = houseaddr.div.get_text().split('|')
info_dict.update({u'cellname':info[0]})
info_dict.update({u'housetype':info[1]})
info_dict.update({u'square':info[2]})
info_dict.update({u'direction':info[3]})
housefloor = name.find("div",{"class":"flood"})
floor_all = housefloor.div.get_text().split('-')[0].strip().split(' ')
info_dict.update({u'floor':floor_all[0]})
info_dict.update({u'years':floor_all[-1]})
followInfo = name.find("div",{"class":"followInfo"})
info_dict.update({u'followInfo':followInfo.get_text()})
tax = name.find("div",{"class":"tag"})
info_dict.update({u'taxtype':tax.get_text()}) # none span
#info_dict.update({u'taxtype':tax.span.get_text()})
totalPrice = name.find("div",{"class":"totalPrice"})
info_dict.update({u'totalPrice':totalPrice.span.get_text()})
unitPrice = name.find("div",{"class":"unitPrice"})
info_dict.update({u'unitPrice':unitPrice.get('data-price')})
info_dict.update({u'houseID':unitPrice.get('data-hid')})
today = get_today()
info_dict.update({u'validdate':today})
info_dict.update({u'validflag':str('1')})
# adding open houseid url,and save the images for each house,TBC
# houseinfo insert into mysql
houseinfo_insert_mysql(conn,info_dict)
info_dict_all[i+page*30] = info_dict
return info_dict_all
def house_celllist_spider(conn,celllist = [u'荣丰2008',u'保利茉莉公馆']):
all_set_unvalid(conn)
for cellname in celllist:
house = house_percell_spider(conn,cellname)
return house # unit test