链家网是集房源信息搜索、产品研发、大数据处理、服务标准建立为一体的以数据驱动的全价值链房产服务平台。主营:二手房、租房、新房。通过链家网的数据可以很方便的获取商品房的市场信息
此次目的是抓取链家网广州地区二手房的数据
首先明确步骤:
- 分析网页
- 分析数据节点
- 编写爬虫程序
- 存储数据
首先分析网页
链家网网址在这 广州链家网二手房
可以看到一共有25934套房源,数据是更新的,我爬取得时候是没有这么多数据的
接下来观察网页构造
可以观察到一个网页是有30个< li >标签,一个标签对应一个房源信息,而且网页只有100页,我们通过观察发现网页的翻页只是在源地址后面加个pg的变量
但是实测将pg填101会重新跳转到第一页,所以我们一共可以观察到3000的信息,后面的无法获取,需要重新观察
通过观察发现可以按照地区的分类进去抓取数据,而且每个地区的成交数量都没有超过3000套。也就是我们可以在100页内将数据全部抓取。
通过观察发现各个房源的标签是不一样的,而且实测无法在列表页就实现数据的全部抓取,所以我们只能到房源详情页进行抓取
在这里我们确定我们要抓的数据是售价,平方单价,挂牌价,关注人数,房屋户型,所在楼层等等
那么现在爬虫思路就是
- 先爬取所有地区链接
- 根据地区链接,在各地区链接下抓取所有列表房源的url
- 根据抓取到的房源url进行访问抓取数据
接下来就是代码的构建了
首先先抓取所有地区并存储在数据库中:
import requests
from urllib.parse import urljoin
from scrapy.selector import Selector
import pymongo
from fake_useragent import UserAgent
ua = UserAgent()
Mymongo = pymongo.MongoClient('localhost', 27017) # 连接本地服务
lianjia = Mymongo['lianjia'] # 链接数据库
region_url_collection = lianjia['region_url'] # 集合对象
base_url = "https://gz.lianjia.com/chengjiao/"
region_list = ['tianhe', 'yuexiu', 'liwan', 'haizhu', 'panyu', 'baiyun', 'huangpugz', 'conghua', 'zengcheng', 'huadou', 'nansha']
def get_region_url():
for i in range(0, len(region_list)):
url = urljoin(base_url, region_list[i]) # 拼接URL
response = requests.get(url, headers={'User-Agent': str(ua.random)}).text
selector = Selector(text=response)
area_url_list = selector.xpath('/html/body/div[3]/div[1]/dl[2]/dd/div/div[2]/a/@href').extract() # 获取地址列表
for url in area_url_list:
region_url = urljoin(base_url, url)
region_url_collection.insert_one({'region_url': region_url}) # 插入数据
get_region_url()
这里需要注意的是黄埔、花都在url里面的表示不是按照拼音来的,有点坑人,我找了一会才发现原因
地区数据如下:
可以看到抓取的数据是有295个,但是实际是有重复的数据存在
例如点击越秀分类下的“同德围”会跳到白云区下面,所有后面需要进行去重处理
接下来是获取列表页中房源的地址ID
import requests
from bs4 import BeautifulSoup
import pymongo
from fake_useragent import UserAgent
import time
import pandas as pd
ua = UserAgent()
Mymongo = pymongo.MongoClient('localhost', 27017)
lianjia = Mymongo['lianjia']
house_id_collection = lianjia['house_id']
region_url_collection = lianjia['region_url']
missing_url_collection = lianjia['missing_url']
base_url = "https://gz.lianjia.com/chengjiao/"
def get_house_id(region_url):
response = requests.get(region_url, headers={'User-Agent': str(ua.random)}).text
soup = BeautifulSoup(response, 'lxml')
house_num = soup.select('div.total.fl > span')
# page_num = int(int(house_num[0].get_text())/30)
page_num = int(int(house_num[0].get_text())/30)+1 if int(house_num[0].get_text()) > 30 else 1 # 获取页码
for i in range(1, page_num+1):
time.sleep(1)
current_url = region_url + 'pg{}'.format(i) # 拼接URL
response = requests.get(current_url, headers={'User-Agent': str(ua.random)}).text
soup = BeautifulSoup(response, 'lxml')
house_num_test = soup.select('div.resultDes.clear > div.total.fl > span')
if int(house_num_test[0].get_text()) != 0:
for house_url in soup.select("div.info > div.title > a"):
house_id = house_url['href'].split('.html')[0].split('/')[-1]
house_id_collection.insert_one({'house_id': house_id})
else:
print(current_url)
# missing_url_collection.insert_one(current_url)
def get_all_house_id():
data = pd.DataFrame(list(region_url_collection.find()))
for region_url in list(set(data['region_url'])): # 经过观察发现有重复的地区地址,用set集合去重后再转化为list
get_house_id(region_url)
get_all_house_id()
这里我们使用set()集合来对整个列表进行去重,实测只有243个地区
这里的翻页我们是先抓取地区链接下的房源总数,通过一页有30套房子的模式,与30整除向上取整就可以得到页码的数量了
如图我们发现天河-车陂下的房源总数是101,所以它的页数应该是101/30+1=4,一共四页
获取到的ID如下:
接下来就是获取详细信息页的数据啦
import requests
from bs4 import BeautifulSoup
from scrapy.selector import Selector
import pymongo
from fake_useragent import UserAgent
import pandas as pd
import time
from multiprocessing import Pool
ua = UserAgent()
cilent = pymongo.MongoClient('localhost', 27017) # 数据库连接
lianjia = cilent['lianjia'] # 数据库连接对象
house_info_collection = lianjia['house_info'] # 数据集合对象
house_url_collection = lianjia['house_url']
missing_house_info = lianjia['missing_house_info']
house_url_collection_success = lianjia['house_url_collection_success']
data = pd.DataFrame(list(house_url_collection.find())) # 使用pandas的DataFrame结构提取出来
proxy = [
'HTTPS://111.176.28.176:9999',
'HTTPS://119.101.117.114:9999',
'HTTPS://119.101.118.115:9999',
'HTTPS://119.101.116.219:9999',
'HTTPS://119.101.113.185:9999',
'HTTPS://119.101.113.25:9999',
'HTTPS://119.101.117.143:9999',
'HTTPS://114.116.10.21:3128',
'HTTPS://60.6.241.72:808',
'HTTPS://113.105.170.139:3128',
'HTTPS://114.99.2.201:9999',
'HTTPS://119.101.113.213:9999',
]
def get_house_info(url):
wb_data = requests.get(url, headers={'User-Agent': str(ua.random)})
# wb_data = requests.get(url, headers={'User-Agent': str(ua.random)},proxies={'https': random.choice(proxy)})
if wb_data.status_code == 200:
# 实测不用睡眠也可以实现抓取
# time.sleep(1) # 睡眠1秒
soup = BeautifulSoup(wb_data.text, 'lxml')
selector = Selector(text=wb_data.text)
title = soup.select('h1')[0].get_text() # 标题
deal_date = soup.select('body > div.house-title > div > span')[0].get_text() # 成交时间
house_position = soup.select('div.myAgent > div.name > a')[0].get_text() # 所处区域
dealTotalPrice = soup.select('div.price > span > i')[0].get_text() # 成交价格
unit_price = soup.select('div.price > b')[0].get_text() # 单价
list_price = soup.select('div.info.fr > div.msg > span:nth-of-type(1) > label')[0].get_text() # 挂牌价
focus_num = soup.select('div.info.fr > div.msg > span:nth-of-type(5) > label')[0].get_text() # 关注人数
# floor = soup.select('div.base > div.content > ul > li:nth-of-type(2)')
# 使用css selector会将li标签下的所有文字全都抓取过来,此处使用xpath
floor = selector.xpath('//div[@class="base"]/div[2]/ul/li[2]/text()').extract() # 楼层
house_orientation = selector.xpath('//div[@class="base"]/div[2]/ul/li[7]/text()').extract() # 房屋朝向
built_date = selector.xpath('//div[@class="base"]/div[2]/ul/li[8]/text()').extract() # 房屋建成年限
elevator = selector.xpath('//div[@class="base"]/div[2]/ul/li[14]/text()').extract() # 有无电梯
subway = '有' if soup.find_all('a', 'tag is_near_subway') else '无'
data = {
'house_type': title.split(' ')[1], # 户型
'area': title.split(' ')[-1].split('平')[0], # 房屋面积
'deal_date': deal_date.split(' ')[0], # 成交日期
'house_position': house_position, # 所属区域
'dealTotalPrice': dealTotalPrice, # 成交价格
'unit_price': unit_price, # 单价
'list_price': list_price, # 挂牌价
'focus_num': focus_num, # 关注人数
'floor': floor[0].split('(')[0], # 楼层数
'house_orientation': house_orientation[0].split(' ')[0], # 房屋朝向
'built_date': built_date[0], # 建成年限
'elevator': elevator[0].split(' ')[0], # 有无电梯
'subway': subway # 有无地铁
}
house_info_collection.insert_one(data)
house_url_collection_success.insert_one({'house_id': url}) # 收集已抓取的url,ru
else:
missing_house_info.insert_one({'missing_house_url': url}) # 将抓取错误的url收集起来,如果出现错误就可以根据url重新抓取
if __name__ == '__main__':
pool = Pool(processes=8)
pool.map(get_house_info, list(data['house_url'])) # 使用pool的map函数
pool.close() # 关闭进程池,不再接受新的进程
pool.join() # 主进程阻塞等待子进程的退出
首先考虑到网站的反爬,我们使用对各IP进行抓取,考虑到数量也很大,我们使用多进程进行爬取,IP最好是使用付费代理IP,这样子会稳些,我的IP是在西刺爬取的,具体爬取可以观看这篇文章大批量抓取西刺代理,在这里我们将IP验证地址设置为链家网,用链家网进行IP验证。获取到IP后就可以进行爬取了。
PS:我在这次爬取,虽然过滤了IP,可是爬取一阵子后IP就会挂掉,非常不友好,所以我试了不用IP,使用单个IP,然后睡眠1秒来试试网站怎么封禁IP,结果我发现我开了多线程,不设置睡眠,居然也可以将所有数据抓取下来,这里要表白链家网,不过大家还是设置睡眠吧,不要给服务器过多压力
这里我们开了8个进程,在跑数据分过程发现程序会报出如下错误
上了Google,发现因为是多进程,无法得知索引信息,也就是说不知道哪些ID已经删除了,哪些还没删除。所以其实在抓取页面的代码那里,我们应该设置每次抓取url,就将他从数据库中delete掉,之前没想到,是另外用了一个爬取成功表,每次使用所有的url减去成功的url,剩下的就是未爬取的,但错误还是会发生,后来发现是有些房子缺少数据,导致网页的构造不一样,无法获取数据
如图,暂无数据这个标签与本应该的房价在网页的结构不一样,无法判断,所以只能每次程序中断后继续跑,慢慢缩小,后面手动删除暂无数据的url,毕竟只是少数,这里就是我们获取的数据啦
其实如果没有异常页的出现,在8个进程以及没有设置睡眠的情况下,我们可以在15分钟内获取到所有数据。多进程真是个好东西。
这里是全部地址列表页删除爬取成功页的url的代码:
import pymongo
import pandas as pd
cilent = pymongo.MongoClient('localhost', 27017)
lianjia = cilent['lianjia']
house_url_collection = lianjia['house_url']
house_url_collection_succcess = lianjia['house_url_collection_success']
data = pd.DataFrame(list(house_url_collection_succcess.find()))
data1 = pd.DataFrame(list(house_url_collection.find()))
for url in list(data['house_id']):
if url in list(data1['house_url']):
house_url_collection.delete_one({'house_url': url})
else:
pass
至此就全部结束啦,接下来就拿着数据去分析吧。