房产中介网站爬虫实战(Python BS4+多线程)(一)

浏览: 3040

image.png

本文的两大贡献:

  1. 给出了爬取经纬度数据的方法。
  2. 给出了搜房网(房天下)爬取的可实现解决方案。爬该网站的困难有二:其网页是压缩过的以及网站只给出100页的内容。

本系列文章介绍了爬取链家和搜房网(房天下)数据的方法。
房产中介网站爬虫实战(Python BS4+多线程)(一) 
房产中介网站爬虫实战(Python BS4+多线程)(二)

0.废话

房地产市场向来是大数据分析的“重灾区”,它的数据易获得,且对每个人都有切肤之痛,所以无论是数据分析的菜鸟还是老鸟都纷纷投入其中,渴望着用大数据来改变自己的下半辈子生活。
鉴于本人没有买房需求,就只爬租房的数据了。

1.链家

1.1 爬取思路

链家网有两种查看房屋列表的方式:一是列表,二是地图,如下图所示。前者只显示100页,每页20条,后者以我的技术爬不下来。然而前者的问题被我解决,因此通过前者的途径进行爬取。

image.png


image.png

链家网在搜房的时候是二层结构,第一层是列表,可以爬取到每一个房子的唯一链接;第二层是访问这个链接,以爬取各房子的详细信息。
所以我采取的策略是先把所有的链接都爬下来,再依次去爬获取房屋信息。

1.2 解决只显示100页问题

如图,明明有12000+条的房源,却只显示2000条(100页×20条)。

image.png

image.png

实际页数已经到700页,然而页尾的页数仍然显示100

由此得到判断一共页码数的计算公式:总房源数除以20后向上取整,例如上图显示14557套房源,则总页数是728。

1.3 第一步:获取链接

#!/usr/bin/env python3

import urllib
from bs4 import BeautifulSoup
import inspect
from multiprocessing.dummy import Pool as ThreadPool
import math
import datetime

starturl="http://sh.lianjia.com/zufang/d1l2" #链家租房的首页,因本人需求,已过滤“两房”

req = urllib.request.Request(starturl)
content = urllib.request.urlopen(req).read()
soup = BeautifulSoup(content, "lxml")
page = soup.find_all('a')
pagenum1 = page[-2].get_text()
totalpage = int(math.ceil(float(soup.h2.span.get_text())/20)) #注1
first_urlset = []
for i in range(1, totalpage + 1):
url = "http://sh.lianjia.com/zufang/d" + str(i) + "l2"
first_urlset.append(url) #注2

def read_url(url): #注3
req = urllib2.Request(url)
fails = 0
while fails < 5:
try:
content = urllib.request.urlopen(req, timeout=20).read()
break
except:
fails += 1
print inspect.stack()[1][3] + ' occused error'
soup = BeautifulSoup(content, "lxml")
return soup

def get_houselinks(url):
soup = read_url(url)
firstlinkset = soup.find_all('h2') #注4
firstlinkset = firstlinkset[1:] #首个链接不是有效的房源信息
houselink = ['http://sh.lianjia.com' + i.a['href'] for i in firstlinkset] #注5
return houselink

pool = ThreadPool(4) #注6
finalset = pool.map(get_houselinks, first_urlset)
pool.close()
pool.join()

today = datetime.date.today().strftime("%Y%m%d") #获取今天的日期,YMD的格式
f = open("%s" %'lj_links' + today + '.txt',"w") #注7
f.write(str(finalset))
f.close()

注1:在首页获得房源数量,可以算出总共的有效页数,计算方法如上文,math.ceil()即为向上取整。
注2:如上文,获得所有页数。
注3:封装的一个BeautifulSoup的解析小函数,为了应对由于网络错误带来的读取网页失败。inspect.stack()[1][3]用来获取当前运行的类名/函数名,从而可以知道是这里发生了错误。
注4:因为所有的链接都在h2标签的子标签.a里,['href']是为了获得链接,只要是链接都是以href表示的。
注5:拼接成完整的链接。
注6:关于多线程的说明,参见我另一篇文章中的描述《用Python爬取妹子图——基于BS4+多线程的处理》。这里需要说明的是:pool.map输出的是列表类型,是两个列表的嵌套,最外层的列表的长度是len(first_urlset),代表有多少页,此列表中的每个元素是一个列表,代表每一页中的房源链接,该列表长度为len(houselink)
注7:记录下每一次链接更新的结果。

1.4 第二步:获取信息

这一步的结果是生成基本信息表。

#!/usr/bin/env python3

import os
import inspect
import urllib
from bs4 import BeautifulSoup
import re
from sqlalchemy import create_engine
import sqlite3
import pandas as pd
from multiprocessing.dummy import Pool as ThreadPool
from itertools import chain
import glob

txtlist = glob.glob(os.path.join("", 'lj_links*.txt')) #获取文件的列表
temp1 = {}
for i in txtlist:
temp1[i] = os.path.getmtime(i)
filename = sorted(temp1.items(),key=lambda item:item[1],reverse = True)[0][0] #选取日期最新的文件
f = open(filename,"r")
finalset = eval(f.read()) #读取文件的数据成原样
fullset = list(chain(*finalset)) #将嵌套列表展开

alreadylist = []
conn=sqlite3.connect('%s' %'SHRENT.db')
cur=conn.cursor()
query = 'select URL from basic_information'
alreadylist = list(pd.read_sql(query, conn)['URL'])

fullset = list(set(fullset).union(set(alreadylist)).difference(set(alreadylist))) #注1

errorlist = [] #创建一个存放错误的列表

engine = create_engine('sqlite:///%s' %'SHRENT.db', echo = False)

def read_url(url):
req = urllib.request.Request(url)
fails = 0
while fails < 5:
try:
content = urllib.request.urlopen(req, timeout=20).read()
break
except:
fails += 1
print(inspect.stack()[1][3] + ' occused error')
raise
soup = BeautifulSoup(content, "lxml")
return soup

def save(urlset):
title = []
price = []
room = []
area = []
floor1 = []
floor2 = []
direct = []
district1 = []
district2 = []
onsaledate = []
xiaoqu = []
address = []
number = []
longitude = []
latitude = []
URL = []

try:
soup = read_url(urlset)
title.append(soup.find('h1', class_ = 'main').get_text()) #标题
price1 = soup.find('div', class_ = 'price').get_text()
price.append(int(re.findall(r'\d+', price1)[0])) #价格
room.append(soup.find('div', class_ = 'room').get_text().strip()) #几室几厅
area1 = soup.find('div', class_ = 'area').get_text()
area.append(int(re.findall(r'\d+', area1)[0])) #面积
floor_ori = soup.find_all('td')[1].get_text()
floor1.append(floor_ori.split("/")[0]) #高中低层
floor2.append(int(re.findall(r'\d+', floor_ori.split("/")[1])[0])) #层数
direct.append(soup.find_all('td')[3].get_text().strip()) #朝向
district_ori = soup.find_all('td')[5].get_text()
district1.append(district_ori.split(" ")[0]) #行政区
district2.append(district_ori.split(" ")[1]) #二级区划
onsaledate.append(soup.find_all('td')[7].get_text()) #上架日期
xiaoqu.append(soup.p.get_text().strip()) #小区名
address.append(soup.find_all('p')[1].get_text().strip()) #地址
number.append(soup.find('span', class_ = 'houseNum').get_text()[5:]) #编号
temp1 = str(soup.find_all('div', class_='around js_content')[0])
temp2 = re.findall(r'\d+\.\d+',temp1)
longitude.append(temp2[1]) #经度
latitude.append(temp2[0]) #纬度
URL.append(urlset) #房源的链接
except:
errorlist.append(urlset) #把获取信息错误的链接放入errorlist

df_dic = {'title':title, 'price':price, 'room':room, 'area':area, 'floor1':floor1, 'floor2':floor2, \
'direct':direct, 'district1':district1, 'district2':district2, 'onsaledate':pd.to_datetime(onsaledate), \
'xiaoqu': xiaoqu, 'address': address, 'number':number, 'longitude':longitude, 'latitude':latitude, 'URL':URL, \
'source':"链家"} #建立一个字典
try:
dataset = pd.DataFrame(df_dic, index = number) #将字典转换成pandas的DataFrame
dataset = dataset.drop(['number'], axis = 1)
except:
dataset = pd.DataFrame()
dataset.to_sql('basic_information', engine, if_exists = 'append') #存入sqlite

pool = ThreadPool(4)
pool.map(save, fullset) #将所有的链接送入save函数来获取信息并存入sqlite
pool.close()
pool.join()

f = open('Notsaved.txt', 'w')
print(errorlist, file = f)
f.close()

注1:这是防止数据获取中断时,再次获取数据不会重复的机制。fullset是全部的待更新信息的链接,alreadylist是已经更新信息成功的链接。

1.5 第三步:更新价格

#!/usr/bin/env python3

from sqlalchemy import create_engine
import sqlite3
import pandas as pd
from multiprocessing.dummy import Pool as ThreadPool
import datetime
import re
import urllib
from bs4 import BeautifulSoup
import inspect

today = datetime.date.today().strftime("%Y%m%d")

conn=sqlite3.connect('SHRENT.db')
engine = create_engine('sqlite:///%s' %'SHRENT.db', echo = False)

query1 = "select URL from basic_information"
urlist_basic = list(pd.read_sql(query1, conn)['URL']) #在basic表中的列表
query2 = 'select * from price_temp'
try:
alreadylist = list(pd.read_sql(query2, conn)['URL']) #在price_temp表中的表
except:
alreadylist = []
#basic和price表的差
urlist2 = list(set(urlist_basic).union(set(alreadylist)).difference(set(alreadylist)))

def read_url(url):
req = urllib.request.Request(url)
fails = 0
while fails < 5:
try:
content = urllib.request.urlopen(req, timeout=20).read()
break
except:
fails += 1
print(inspect.stack()[1][3] + ' occused error')
soup = BeautifulSoup(content, "lxml")
return soup

errorlist = []

def save_price(urls):
soup = read_url(urls)
price = []
number = []
URLs = []

try:
price1 = soup.find('div', class_ = 'price').get_text()
price.append(int(re.findall(r'\d+', price1)[0])) #价格
number.append(soup.find('span', class_ = 'houseNum').get_text()[5:]) #房源编号
URLs.append(urls) #链接
except:
errorlist.append(urls)

df_dic = {'URL': URLs, 'price' + today : price, 'number':number}
try:
dataset = pd.DataFrame(df_dic, index = number)
dataset = dataset.drop(['number'], axis = 1)
except:
dataset = pd.DataFrame()
dataset.to_sql('price_temp', engine, if_exists = 'append') #先临时存放在price_temp的表里

pool = ThreadPool(4)
pool.map(save_price, urlist2)
pool.close()
pool.join()

f = open('Notupdated.txt', 'w') #把失败的链接存下来
print(errorlist, file = f)
f.close()

df1 = pd.read_sql("select * from price", conn)
df2 = pd.read_sql("select * from price_temp", conn)
df = pd.merge(df1, df2, how='outer', on=['index', 'URL']) #注1
df = df.set_index('index')
df.to_sql('price', engine, if_exists = 'replace') #存入数据

cu=conn.cursor()
cu.execute('DROP TABLE price_temp') #把price_temp删除
conn.close()

注1:price是已有的专门存放价格的表格,price_temp是本次更新的价格信息的表格,price_temp更新完成后,与price进行合并。

以上是爬取链家信息的方法,接下来的系列文章会讲如何爬取房天下的信息。

(未完待续...)

推荐 7
本文由 八公水鸭几 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册