前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >爬虫实战--拿下最全租房数据 | 附源码

爬虫实战--拿下最全租房数据 | 附源码

原创
作者头像
小一不二三
修改2020-01-08 10:13:50
2.2K1
修改2020-01-08 10:13:50
举报
爬虫实战封面.png
爬虫实战封面.png

点赞再看,养成好习惯Python版本3.8.0,开发工具:Pycharm

写在前面的话

老规矩,目前为止,你应该已经了解爬虫的三个基本小节:

不了解的自行点进去复习

上一篇的实战只是给大家作为一个练手,数据内容比较少,且官网也有对应的 API,难度不大。

但是“麻雀虽小,五脏俱全”,如果这一节看完感觉流程还不是很熟悉,建议去看上一节:

好了,前面的回顾就到此为止。这节开始带大家真正搞事情

准备工作

确定目标

今天我们的目标是某家网,官网链接:https://www.lianjia.com/

当你用浏览器访问这个网址的时候,可能会自动变成 https://sz.lianjia.com/ 这种。

sz 代表的是城市深圳

(哈哈,是的,小一我现在在深圳。)

某家网上有二手房、新房、租房等等,我们今天的目标是 https://sz.lianjia.com/zufang/

“你没看错,zufang租房 的拼音“

所以,今天我们要爬取某家网的租房数据,地点:深圳。

设定流程

因为官网的数据每天都在发生变化,你也不必说要和我截图中的数据一模一样。

首先,我们已经确定了目标是某家网在深圳的所有租房数据,看一下首页

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

截止2019-12-31号,深圳十个区共 32708 套深圳租房,好像还挺多的,不知道我们能不能全部爬下来。

按照官网每页30条数据来看,我们看一下翻页的显示:

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

问题来了,显示页码只有100页,是不是100页之后被隐藏了呢?

我们试着在 url 中修改页码为pg101,结果发现显示的还是第100页的内容。

那,如何解决网页只有前100页数据?

设置搜索条件,确保每个搜索条件下的数据不超过3000条,这样我们就可以通过100页拿到所有的数据。

通过设置区域进行搜索,试试看:

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

罗湖区 2792条数据 < 3000。

ok,我们再看看其他区

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

好像不太妙,福田区整租都有4002套(已经设置了整租条件的情况下)。

没关系,我们继续设置搜索条件:

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

新增居室搜索,可以看到福田区整租的一居有1621套,满足条件。

其他三个直接不用看了,肯定也满足。

继续查看剩余的几个区,发现也满足,搞定

那这样子的话,我们的步骤就是先检查记录数有没有超过3000条,超过了则继续增加新的条件,一直到不超过3000,然后分页遍历所有数据。

好,那我们稍微画一下流程图:

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

确定条件

大致流程基本没什么问题了,我们看一下具体需要注意的搜索条件。

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

首先是城市区域的获取,每个城市的区域都不一样,区域数据通过网页获取

其次是出租方式的获取,官网对应两种:整租和合租,观察 url 发现分别对应 rt200600000001、rt200600000002

然后是房屋居室的获取,官网对应四种:一居、二居、三居和四居,观察 url 发现分别对应 l0、l1、l2、l3(小写字母 L 不是1)

最后是分页的获取,官网 url 对应 pg+number

拼接成 url 之后是:

base_url+/区域/+pg+出租方式+居室

细节处理

  • 爬取的内容较多,每次爬取需要设置时间间隔
  • 需要增加浏览器标识,防止被封 ip
  • 需要增加检测机制,丢掉已经爬取过的数据
  • 数据需动态保存在文件中,防止被封后需要重头再来
  • 若要保存数据库,爬虫结束后再连接数据库

异常处理

官网中有一种类型的房屋,网页格式不标准,且拿不到具体数据。

对,就是公寓

可以看到,在房屋列表中公寓无论是在价格显示、房屋地址、朝向等都异于普通房屋。

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

且在详细界面的内容也是无法拿到标准信息的

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

对于这种数据,我们直接丢掉就好。

开始实战

根据流程图,步骤已经很清楚了:

  1. 确定城市,获取目标主页网址
  2. 针对数据,确定目标查询条件
  3. 针对总数,确定目标页码划分
  4. 针对内容,确定目标对象字段

你准备好了吗?

确定要获取的数据字段:

代码语言:txt
复制
city: 城市
house_id:房源编号
house_rental_method:房租出租方式:整租/合租/不限
house_address:房屋地址:城市/区/小区/地址
house_longitude:经度
house_latitude:纬度
house_layout:房屋格局
house_rental_area:房屋出租面积
house_orientation:房屋朝向
house_rental_price:房屋出租价格
house_update_time:房源维护时间
house_tag:房屋标签
house_floor:房屋楼层
house_elevator:是否有电梯
house_parking:房屋车位
house_water:房屋用水
house_electricity:房屋用电
house_gas:房屋燃气
house_heating:房屋采暖
create_time:创建时间
house_note:房屋备注
# 额外字段
house_payment_method:房屋付款方式:季付/月付
housing_lease:房屋租期

第一件事,设置城市、网址和爬虫头部

代码语言:txt
复制
# 通过城市缩写确定url
city_number = 'sz'
url = 'https://{0}.lianjia.com/zufang/'.format(city_number)

爬虫头部我们只需要设置一个 User-Agent 就行了

User-Agent 尽可能多的设置。(篇幅有限,这里只放一部分,更多设置请在文末获取源码查看)

代码语言:txt
复制
# 主起始页
self.base_url = url
# 当前筛选条件下的页面
self.current_url = url
# 设置爬虫头部
self.headers = {
	'User-Agent': self.get_ua(),
}

def get_ua(self):
    """
    在UA库中随机选择一个UA
    :return: 返回一个库中的随机UA
    """
    ua_list = [
        "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 9.50",
        "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0"
    ]

    return random.choice(ua_list)

接下来,获取当前城市的总记录数。

想一想,万一有的城市出租房总记录数都不大于3000,那我们岂不是连搜索条件都不用设置了?

每个城市的区域数据都不一样,如果要手动输入的话那太麻烦了。

我们直接通过网页获取到要查询城市的区域数据。

代码语言:txt
复制
def get_house_count(self):
    """
    获取当前筛选条件下的房屋数据个数
    @param text:
    @return:
    """
    # 爬取区域起始页面的数据
    response = requests.get(url=self.current_url, headers=self.headers)
    # 通过 BeautifulSoup 进行页面解析
    soup = BeautifulSoup(response.text, 'html.parser')
    # 获取数据总条数
    count = soup.find_all(class_='content__title--hl')[0].string
    
    return soup, count     

获取到总记录数之后,就需要拿 3000 对它衡量一下了。

超过3000,则进行二次划分;不超过,则直接遍历获取数据

代码语言:txt
复制
# 获取当前筛选条件下数据总条数
soup, count_main = self.get_house_count()

# 如果当前当前筛选条件下的数据个数大于最大可查询个数,则设置第一次查询条件
if int(count_main) > self.page_size*self.max_pages:
    # 获取当前城市的所有区域,当做第一个查询条件
    pass
else:
    # 直接遍历获取数据
    pass

第二步,添加条件

首先获取当前城市的所有区域

可以看到,深圳市的所有区域都在页面上

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

多谢某家整理的整整齐齐,以后租房就去你家了

直接获取到所有符合要求的 li 标签,拿到区域数据

需要注意我们拿到的区域数据,我们只需要它的拼音,即 href 中后面的部分

代码语言:txt
复制
# 拿到所有符合要求的 li 标签
soup_uls = soup.find_all('li', class_='filter__item--level2', attrs={'data-type': 'district'})
self.area = self.get_area_list(soup_uls)

def get_area_list(self, soup_uls):
    """
    获取城市的所有区域信息,并保存
    """
    area_list = []
    for soup_ul in soup_uls:
        # 获取 ul 中的 a 标签的 href 信息中的区域属性
        href = soup_ul.a.get('href')
        # 跳过第一条数据
        if href.endswith('/zufang/'):
            continue
		else:
            # 获取区域数据,保存到列表中
            area_list.append(href.replace('/zufang/', '').replace('/', ''))

	return area_list

拿到之后,直接遍历每个区域,将区域当做我们第一个查询条件

在第一个查询条件下,同样需要获取该条件下的总记录数

是不是有点熟悉,又重复第一步的工作了。体会到我为什么刚才把获取总记录数这个功能封装在函数里了吧,后面也还会再用到!

代码语言:txt
复制
# 遍历区域,重新生成筛选条件
for area in self.area:
	self.get_area_page(area)
    
def get_area_page(self, area):
    """
    当前搜索条件:区域
    @param area:
    @return:
    """
    # 重新拼接区域访问的 url
    self.current_url = self.base_url + area + '/'
    # 获取当前筛选条件下数据总条数
    soup, count_area = self.get_house_count()

在当前条件下,同样需要判断是否超过 3000条。

如果超过,同样进行条件划分

代码语言:txt
复制
'''如果当前当前筛选条件下的数据个数大于最大可查询个数,则设置第二次查询条件'''
if int(count_area) > self.page_size * self.max_pages:
	# 遍历出租方式,重新生成筛选条件
    for rental_method in self.rental_method:
    	pass
else:
	# 直接遍历获取数据
    pass

这里我们在初始化函数中定义了出租方式和居室情况,所以不需要再从网页上获取,可以直接 for 循环了。

每个城市的出租方式和居室数据都是固定的,直接定义好会更方便。

代码语言:txt
复制
# 出租方式:整租+合租
self.rental_method = ['rt200600000001', 'rt200600000002']
# 居室:一居、二居、三居、四居+
self.rooms_number = ['l0', 'l1', 'l2', 'l3']

同样我们需要获取出租方式条件下的总记录数

代码语言:txt
复制
# 重新拼接区域 + 出租方式访问的 url
self.current_url = self.base_url + area + '/' + rental_method + '/'
# 获取当前筛选条件下数据总条数
soup, count_area_rental = self.get_house_count()

同理,继续往下添加房屋居室数量

代码语言:txt
复制
# 重新拼接区域 + 出租方式 + 居室 访问的 url
self.current_url = self.base_url + area + '/' + rental_method + room_number + '/'
# 获取当前筛选条件下数据总条数
soup, count_area_rental_room = self.get_house_count()

第三步,确定页数,并开始遍历每一页

设置相应的页码初始化数据,方便进行遍历

代码语言:txt
复制
# 起始页码默认为0
self.start_page = 0
# 当前条件下的总数据页数
self.pages = 0
# 每一页的出租房屋个数,默认page_szie=30
self.page_size = page_size
# 最大页数
self.max_pages = 100

当我们最终条件确定的记录数不足3000时

就可以通过遍历页码获取所有数据。

代码语言:txt
复制
# 确定页数
# count_number是当前搜索条件下的总记录数
self.pages = int(count_number/self.page_size) \
if (count_number%self.page_size) == 0 else int(count_number/self.page_size)+1

'''遍历每一页'''
for page_index in range(1, self.pages+1):
	self.current_url = self.base_url + area + '/' + 'pg' + str(page_index) + rental_method + room_number + '/'

	# 解析当前页的房屋信息,获取到每一个房屋的详细链接
	self.get_per_house()
	page_index += 1

第四步,访问每个房屋的详细页面

上一步已经定位到整个页面了,我们来看看定位的页面

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

这个页面已经包含详细页面的跳转 url以及当前房屋的部分主要数据

并且这部分主要数据比详细页面的主要数据更好拿到,格式更规整。

好,那就选它了。

代码语言:txt
复制
def get_per_house(self):
    """
    解析每一页中的每一个房屋的详细链接
    @return:
    """
    # 爬取当前页码的数据
    response = requests.get(url=self.current_url, headers=self.headers)
    soup = BeautifulSoup(response.text, 'html.parser')

    # 定位到每一个房屋的 div (pic 标记的 div)
    soup_div_list = soup.find_all(class_='content__list--item--main')
    # 遍历获取每一个 div 的房屋详情链接和房屋地址
    for soup_div in soup_div_list:
        # 定位并获取每一个房屋的详情链接
        detail_info = soup_div.find_all('p', class_='content__list--item--title twoline')[0].a.get('href')
        detail_href = 'https://sz.lianjia.com/' + detail_info

        # 获取详细链接的编号作为房屋唯一id
        house_id = detail_info.split('/')[2].replace('.html', '')
        '''解析部分数据'''
        # 获取该页面中房屋的地址信息和其他详细信息
        detail_text = soup_div.find_all('p', class_='content__list--item--des')[0].get_text()
        info_list = detail_text.replace('\n', '').replace(' ', '').split('/')
        # 获取房屋租金数据
        price_text = soup_div.find_all('span', class_='content__list--item-price')[0].get_text()

这里面我们需要注意开头说到的一点:公寓

公寓的 content__list--item--des 没有地址信息,所以我们通过长度去判断

代码语言:txt
复制
# 如果地址信息为空,可以确定是公寓,而我们并不能在公寓详情界面拿到数据,所以,丢掉
if len(info_list) == 5:
    # 解析当前房屋的详细数据
    self.get_house_content(detail_href, house_id, info_list, price_text)

第五步,获取每个房屋的详细数据

上一步已经获取部分主要数据,这一步我们取剩下的数据。

首先先来看一下详细页面长啥样:

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

最上边的维护时间显示房源的更新状态,要它!

最右边的房屋标签数据也有用,要它一部分!

最下边的基本信息太有用了吧,肯定要它!

代码语言:txt
复制
# 生成一个有序字典,保存房屋结果
house_info = OrderedDict()
    
'''爬取页面,获得详细数据'''
response = requests.get(url=href, headers=self.headers, timeout=10)
soup = BeautifulSoup(response.text, 'html.parser')

'''解析房源维护时间'''
soup_div_text = soup.find_all('div', class_='content__subtitle')[0].get_text()
house_info['house_update_time'] = re.findall(r'\d{4}-\d{2}-\d{2}', soup_div_text)[0]

'''解析房屋出租方式(整租/合租/不限)'''
house_info['house_rental_method'] = soup.find_all('ul', class_='content__aside__list')[0].find_all('li')[0].get_text().replace('租赁方式:', '')

'''解析房屋的标签'''
house_info['house_tag'] = soup.find_all('p', class_='content__aside--tags')[0].get_text().replace('\n', '/').replace(' ', '')

'''房屋其他基本信息'''
# 定位到当前div并获取所有基本信息的 li 标签
soup_li = soup.find_all('div', class_='content__article__info', attrs={'id': 'info'})[0]. 
find_all('ul')[0].find_all('li', class_='fl oneline')
# 赋值房屋信息
house_info['house_elevator'] = soup_li[8].get_text().replace('电梯:', '')
house_info['house_parking'] = soup_li[10].get_text().replace('车位:', '')
house_info['house_water'] = soup_li[11].get_text().replace('用水:', '')
house_info['house_electricity'] = soup_li[13].get_text().replace('用电:', '')
house_info['house_gas'] = soup_li[14].get_text().replace('燃气:', '')
house_info['house_heating'] = soup_li[16].get_text().replace('采暖:', '')
house_info['create_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
house_info['city'] = self.city

# 保存当前影片信息
self.data_info.append(house_info)

应该该拿的数据都拿到了。

不对,好像还有经纬度没有拿到。

检查一下,在 js 代码中发现了一个坐标

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

看着很可疑,我们通过坐标反查看一看到底是不是这个房屋地址

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

ok,没问题,正是我们要的,那把它也拿下吧!

代码语言:txt
复制
'''解析经纬度数据'''
# 获取到经纬度的 script定义数据
location_str = response.text[re.search(r'(g_conf.coord)+', response.text).span()[0]:
                             re.search(r'(g_conf.subway)+', response.text).span()[0]]
# 字符串清洗,并在键上添加引号,方便转化成字典
location_str=location_str.replace('\n','').replace('','').replace("longitude","'longitude'").replace("latitude", "'latitude'")
# 获取完整经纬度数据,转换成字典,并保存
location_dict = eval(location_str[location_str.index('{'): location_str.index('}')+1])
house_info['house_longitude'] = location_dict['longitude']
house_info['house_latitude'] = location_dict['latitude']

第六步,保存数据

每 50 条数据追加保存到本地文件中当所有记录都爬完之后,将本地文件保存到数据库中。

数据需要保存到本地文件和数据库中。

其中本地文件每爬取50条追加保存记录,数据库只需要爬取结束后保存一次

代码语言:txt
复制
def data_to_sql(self):
    """
    保存/追加数据到数据库中
    @return:
    """
    # 连接数据库
    self.pymysql_engine, self.pymysql_session = connection_to_mysql()
    # 读取数据并保存到数据库中
    df_data = pd.read_csv(self.save_file_path, encoding='utf-8')
    # 导入数据到 mysql 中
    df_data.to_sql('t_lianjia_rent_info', self.pymysql_engine, index=False, if_exists='append')

def data_to_csv(self):
    """
    保存/追加数据到本地
    @return:
    """
    # 获取数据并保存成 DataFrame
    df_data = pd.DataFrame(self.data_info)

    if os.path.exists(self.save_file_path) and os.path.getsize(self.save_file_path):
        # 追加写入文件
        df_data.to_csv(self.save_file_path, mode='a', encoding='utf-8', header=False, index=False)
	else:
        # 写入文件,带表头
        df_data.to_csv(self.save_file_path, mode='a', encoding='utf-8', index=False)
        
	# 清空当前数据集
    self.data_info = []

到此我们的流程就已经结束了。

小一我最终花了一天多的时间,爬取到了27000+数据。(公寓数据在爬取过程中已经丢掉了)

自行设置每次的休眠间隔,上面流程中我并没有贴出来,需要的在源代码中查看。

贴一下最终数据截图:

文章首发:公众号『知秋小梦』
文章首发:公众号『知秋小梦』

总结一下

主要流程

  • 确定目标:爬取的网站网址以及要爬取的数据
  • 设定流程:详细说明了我们每一步如何进行,以及整体的流程图
  • 确定条件:在搜索过程中确定每个层级的搜索条件
  • 细节处理:爬取数据较多,增加必要的细节处理,提高代码健壮性
  • 异常处理:异常房屋类型的处理,在这里我们直接丢掉。

日常思考:

比起第一个项目,这个项目流程会复杂一些,但是本质上没有区别

可以看到爬虫的核心代码其实就是那几句。

思考以下几点:

  • 如果本次的网站需要登录,应该怎么办?
  • 如果你要租房,你应该怎么分析?

必要提醒

  • 上述方法仅针对当前的官网源代码
  • 本次爬虫内容仅用作交流学习

源码获取

公众号后台回复 某家租房 获取 爬取某家网租房信息源码

本次爬虫的结果数据不对外公开,有需要的交流学习的可以加群获取。(后台回复加群

写在后面的话

发现最近几篇文章都是5000字的长文,是我太啰嗦了吗(真的怀疑自己了)?

坚持读到这的晚上记得给自己加个鸡腿,你已经很棒了。

我、我、我也想要加个鸡腿

呸呸呸,说好的不拿人民群众一针一线。

那,点个在看总行吧?

原创不易,欢迎点赞噢

文章首发:公众号【知秋小梦】 文章同步:掘金,简书,csdn

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面的话
  • 准备工作
  • 开始实战
  • 总结一下
  • 源码获取
  • 写在后面的话
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档