1. 基础 URL:
通过观察房产链接,可以看到其基础 URL 格式如下:
https://[city].fang.lianjia.com/loupan/
其中 city 为城市拼音首字母,例如重庆为 ‘cq’
2. 楼盘列表页:
楼盘列表页包含了楼盘名称和对应的楼盘代码,楼盘代码是楼盘详情页链接的组成部分,所以我们要先提取列表页的信息
观察可以发现,列表页的 URL 格式如下:
base_url + house_class + page
其中:
于是我们的列表页链接为:
pages = range(1, 51)
my_list_url = base_url + my_house_class + page, page in pages
如:https://cq.fang.lianjia.com/loupan/nht1pg1
3. 楼盘详情页
观察可以发现,楼盘详情页的 URL 格式如下:
base_url + 'p_'+ 楼盘代码 + '/xiangqing/'
可见关键部分为楼盘代码,这部分通过爬取列表页得到。通过观察列表页的源码,如下截图,我们可以在爬取楼盘列表页的时候使用 BeautifulSoup,利用 CSS 选择器,在楼盘列表页提取对应节点和楼盘代码。
selected = soup.select('ul.resblock-list-wrapper div.resblock-name a')
1. 网页源码分析:
楼盘列表页中,我们只需要提取楼盘名称及其对应的 URL 代码即可,在 Chrome 中查看楼盘名称的源码,可以发现该文字链接在一个 a 节点中,完整的节点结构如下,在爬取的时候可以适当简化:
body > div.resblock-list-container.clearfix > ul.resblock-list-wrapper > li:nth-child(1) > div > div.resblock-name > a
2. 提取信息:
def get_name_dic(url):
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, 'lxml')
selected = soup.select('ul.resblock-list-wrapper div.resblock-name a')
name_dic = {
}
for item in selected:
name_dic[item.string] = item['href'][8:]
print('code of {} getted.'.format(item.string))
return name_dic
1. 网页源码分析
2. 提取信息
# 获取楼盘中需要的信息
def get_info(url):
soup = get_soup(url)
lis = soup.select('ul.x-box li')
infos = {
}
my_keys = ['参考价格:', '区域位置:', '绿化率:', '容积率:', '产权年限:', '开发商:', '物业公司:', '物业费:']
for li in lis:
if li.select('.label')[0].text in my_keys:
label = li.select('.label')[0].text.replace(':', '')
value = li.select('.label-val')[0].text.strip()
else:
continue
infos[label] = value
print('info getted')
return infos
def save_to_json(data, name='data'):
results = json.dumps(data, ensure_ascii=False)
with open(name + '.json', 'a+', encoding='utf-8') as f:
f.write(results)
f.write('\n')
return None
1. 第一版,效率低下
'''效率低下版'''
CITY = 'cq'
MY_HOUSE_CLASS = 'nht1'
PAGES = range(1, 51)
def main():
# 文件初始化
with open(CITY + '.json', 'w', encoding='utf-8') as f:
pass
base_url = 'https://{}.fang.lianjia.com/loupan/'.format(CITY)
houses = []
name_dic = {
}
start = ctime()
# 获取楼盘名称及代码
for page in PAGES:
print(page)
list_url = '{base}{house}pg{page}'.format(base=base_url, house=MY_HOUSE_CLASS, page=page)
name_dic.update(get_name_dic(list_url))
# 爬取各楼盘信息
for name, code in name_dic.items():
detail_url = base_url + code + 'xiangqing/'
my_info = get_info(detail_url)
my_info['楼盘名称'] = name
houses.append(my_info)
# 保存文件
save_to_json(houses)
end = ctime()
print('All done\nStarted at {}, done at {}'.format(start, end))
2. 多线程版本
# 定义每页的爬取与保存函数
def get_and_save(page, base_url, filename):
page_info = []
print(page)
list_url = '{base}{house}pg{page}'.format(base=base_url, house=MY_HOUSE_CLASS, page=page)
name_dic = get_name_dic(list_url)
for name, code in name_dic.items():
detail_url = base_url + code + 'xiangqing/'
my_info = get_info(detail_url)
my_info['楼盘名称'] = name
page_info.append(my_info)
save_to_json(page_info, name=filename)
print('Page {} saved'.format(page))
# 主函数
def main():
# 文件初始化
with open(CITY + '.json', 'w', encoding='utf-8') as f:
pass
base_url = 'https://{}.fang.lianjia.com/loupan/'.format(CITY)
print('Started at:{}'.format(ctime()))
for page in PAGES:
threading.Thread(target=get_and_save, args=(page, base_url, CITY)).start()
@register
def _atexit():
print('All done at {}'.format(ctime()))
1. 读取数据
# 读取文件
with open('D://Code/House Analysis/{}.json'.format(city), 'r', encoding='utf-8') as f:
for line in f.readlines():
data.extend(json.loads(line))
# 转换为 DataFrame
df = pd.DataFrame(data, index=(range(1, len(data) + 1)))
2. 索引处理
# 索引处理
df.index.name = 'No'
new_col = ['楼盘名称', '区域位置', '参考价格', '产权年限', '开发商', '物业公司', '物业费', '容积率', '绿化率']
col_name = {
'参考价格': '价格 (元/平)', '产权年限': '产权 (年)', '物业费': '物业费 (元/平/月)', '绿化率': '绿化率(%)'}
df = df.reindex(columns=new_col).rename(columns=col_name)
3. 区域位置处理
# 区域位置数据处理,去掉城市,只保留区县
df['区域位置'] = df['区域位置'].map(lambda x: str(x).split('-')[1])
4. 均价处理
price_re = re.compile(r'均价 (\d+)元/平')
# 先函数映射正则匹配,再剔除未匹配到的,再利用函数映射提取数据,这里使用了 pandas 的方法链,一步到位
df['价格 (元/平)'] = df['价格 (元/平)'].map(lambda x: price_re.match(x)).dropna().map(lambda x: int(x.group(1)))
5. 产权处理
# 产权修改,产权有些有多种,但数量不多,为了节省资源,以第一个数字为准
df['产权 (年)'] = df['产权 (年)'].map(lambda x: int(x[:2]))
6. 物业费处理
# 先提取数字部分,以 '~' 分隔,并剔除缺失值
no_na = df['物业费 (元/平/月)'].map(lambda x: x[:-6].split('~') if x[:-6] != '' else None).dropna()
# 转换为 array 数组并求平均值
df['物业费 (元/平/月)'] = no_na.map(lambda x: np.array(list(map(float, x))).mean())
7. 容积率、绿化率处理
# 将容积率、绿化率转换为数字
df['容积率'] = df['容积率'].map(lambda x: float(x))
df['绿化率(%)'] = df['绿化率(%)'].map(lambda x: float(x[:-1]) if x != '暂无信息' else None)
8. 保存数据
df.to_csv('Processed_{}.csv'.format(city.upper()), encoding='utf-8-sig')
1. 各区县楼盘数量与均价对比
# 显示中文
from pylab import *
mpl.rcParams['font.sans-serif']=['SimHei']
# 计算区域楼盘数
area = df['区域位置'].value_counts()
# 计算各区均价
price = df.groupby(df['区域位置'])[['价格 (元/平)']].mean().reindex(area.index)
# 计算全市均价
average_price = df['价格 (元/平)'].mean()
# 设置图片参数
plt.rc('figure', figsize=(8, 6))
plt.rc('font', size=10)
# 新建绘图区
fig, axes1 = plt.subplots(1, 1)
# 设置图片标题
axes1.set_title('{}各区楼盘数量及均价图', fontsize=16)
# 绘制各区域楼盘数柱状图
axes1.bar(x=area.index, height=area.values, label='各区楼盘数', color='k', alpha=0.3)
# x 轴标签旋转 45 度,设置 x, y 轴名称
axes1.set_xticklabels(area.index, rotation=45)
axes1.set_xlabel('区域')
axes1.set_ylabel('楼盘数量')
# 绘制各区均价,与 axes1 共用 x 轴
axes2 = axes1.twinx()
axes2.plot(price.values, label='各区均价', color='darkorange', marker='o', markersize='5', markerfacecolor='white')
axes2.set_ylabel('均价(元/平)')
# 设置均价值标签
x = np.arange(len(price))
y = np.array(price.values)
for a,b in zip(x,y1):
plt.text(a, b+0.1, '%.0f' % b, fontsize=8, horizontalalignment='center', verticalalignment='bottom')
# 绘制全市均价线,与 axes2 共用 y 轴
axes3 = axes2.twiny()
axes3.plot(np.ones(len(area)) * average_price, 'r--', label='全市均价:%d' % average_price)
axes3.set_xticks([]) # 关闭均价线的 x 轴
# 设置图例
handles1, labels1 = axes1.get_legend_handles_labels()
handles2, labels2 = axes2.get_legend_handles_labels()
handles3, labels3 = axes3.get_legend_handles_labels()
plt.legend(handles1 + handles2 + handles3, labels1 + labels2 + labels3, loc='best')
# 保存图片
plt.savefig('{}各区域楼盘数量及均价.png'.format(cities[city]), dpi=400, bbox_inches='tight')
# 关闭绘图区
plt.close()
2. 各区县楼盘平均产权、物业费、绿化率、容积率对比
# 计算各指标,并封装到列表中,以便遍历调用
grouped = df.groupby(df['区域位置'])
property_years = grouped[['产权 (年)']].mean().reindex(area.index)
property_costs = grouped[['物业费 (元/平/月)']].mean().reindex(area.index)
volume_rate = grouped[['容积率']].mean().reindex(area.index)
greening_rate = grouped[['绿化率(%)']].mean().reindex(area.index)
values = [property_years, property_costs, volume_rate, greening_rate]
# 设置图片参数
plt.rc('figure', figsize=(10, 10))
plt.rc('font', size=8)
# 新建绘图区
fig, axes = subplots(2, 2)
# 指标序号
k = 0
keys = ['产权', '物业费', '容积率', '绿化率']
# 开始绘图
for i in range(2):
for j in range(2):
# 绘制各指标
axes[i, j].bar(x=values[k].index, height=values[k].values.T[0], color='k', alpha=0.3)
# x 轴标签旋转 45 度
axes[i, j].set_xticklabels(values[k].index, rotation=45)
axes[i, j].set_xlabel('区域')
axes[i, j].set_ylabel(values[k].columns[0])
axes[i, j].set_title('重庆各区域楼盘平均{}对比'.format(keys[k]))
# 设置值标签
x = np.arange(len(values[k]))
y = np.array(values[k].values)
for a,b in zip(x,y):
axes[i, j].text(a, b+0.05, '%.0f' % b, ha='center', va= 'bottom',fontsize=8)
k += 1
# 调整子图周围间距
plt.subplots_adjust(hspace = 0.3)
# 保存图片
plt.savefig('{}各区域楼盘产权、物业费、容积率及绿化率.png'.format(cities[city]), dpi=400, bbox_inches='tight')
# 关闭绘图区
plt.close()
本项目源代码放到了 Github 上,如有需要可查阅,地址如下
https://github.com/Raymone23/House-Analysis
----------------------------------------------8/28 更新------------------------------------------------------
1. 城市代码:
import requests
from bs4 import BeautifulSoup
url = 'https://cq.fang.lianjia.com/loupan'
response = requests.get(url)
soup = BeautifulSoup(response.text, 'lxml')
selected = soup.select('li.clear a')
cities = {
}
for item in selected:
cities[item.text] = item['href'].split('.')[0][2:]
2. 其他部分代码修改
CITIES = {
'保定': 'bd',
'保亭': 'bt',
'北京': 'bj',
'承德': 'chengde',
...
'镇江': 'zj',
'漳州': 'zhangzhou',
'郑州': 'zz',
'珠海': 'zh',
'中山': 'zs',
}
# 选择城市
while True:
try:
inputs = input('选择你的城市:')
city = CITIES[inputs]
except KeyError:
print('没有该城市数据,请重新输入!')
else:
break
1. MySQL
# 连接数据库
try:
db = pymysql.connect(host='localhost', user='root', password='xxxxxx', port=3306, db='house')
cursor = db.cursor()
except pymysql.err.InternalError:
db = pymysql.connect(host='localhost', user='root', password='xxxxxx', port=3306)
cursor = db.cursor()
cursor.execute('CREATE DATABASE house DEFAULT CHARACTER SET utf8')
# 数据表表头,为 main 模块的全局变量
LIST = ["参考价格", "区域位置", "开发商", "绿化率", "容积率",
"产权年限", "物业公司", "物业费", "楼盘名称"]
HEAD = ' VARCHAR(255), '.join(LIST) + ' VARCHAR(255), PRIMARY KEY(楼盘名称)'
# 创建城市表
sql = 'CREATE TABLE IF NOT EXISTS {} ({})'.format(name, head)
cursor.execute(sql)
db.close()
def save_to_db(data, name='data'):
# 表头键名
keys = ', '.join(data.keys())
# 构造插入的占位符,使用 , 分隔,数量等于字典的长度
values = ','.join(['%s'] * len(data))
# 连接数据库
db = pymysql.connect(host='localhost', user='root', password='yeswedid631,,', port=3306, db='house')
cursor = db.cursor()
# 加上 ON DUPLICATE KEY,表明如果主键已经存在,则执行更新操作
sql = 'INSERT INTO {table}({keys}) VALUES({values}) ON DUPLICATE KEY UPDATE'.format(table=name, keys=keys, values=values)
# update = 'id = %s, name = %s, age = %s'
update = ','.join([" {key} = %s".format(key=key) for key in data])
# 完整的 SQL 语句
sql += update
try:
cursor.execute(sql, tuple(data.values()) * 2)
print('Data saved')
db.commit()
except:
print('Failed to save data')
db.rollback()
db.close()
return None
db = pymysql.connect(host='localhost', user='root', password='yeswedid631,,', port=3306, db='house')
cursor = db.cursor()
cursor.execute('SELECT * FROM {}'.format(city))
rows = cursor.fetchall()
df = pd.DataFrame(list(rows), columns=[x[0] for x in cursor.description])
2. MongoDB
def save_to_db(data, name='data'):
# 连接数据库
client = pymongo.MongoClient(host='localhost', port=27017)
db = client.house
collection = db[name]
collection.insert_many(data)
print('Data saved')
return None
# 链接数据库
client = pymongo.MongoClient(host='localhost', port=27017)
db = client.house
collection = db[city]
data = []
for item in collection.find():
del item['_id']
data.append(item)
df = pd.DataFrame(data)