python3:request+BeautifuleSoup抓取房天下
开始之前
这篇代码的目标网站是房天下,使用python3编写,涉及到的核心库包含requests、BeautifulSoup等。抓取到的文件存储在本地的csv文件中。抓取了网站全国每一个城市对应的新房、二手房、租房下的房屋信息。
为什么选择这个网站
- 网站的反爬虫机制没有那么严格。
- 网上抓取这个网站的人不少,所以可以借鉴的资料很多。
- 网站的设计元素较为简单,就要抓取的内容而言不涉及Ajax、动态网页等。
目标数据
- 全国城市列表:省份-城市-城市链接
- 城市页面标题:城市-新房链接-二手房链接-租房链接
- 房屋信息列表:标题-房价-信息-位置-房屋链接
爬虫文件
- get_city.py
- get_type.py
- get_xf.py
- get_esf.py
- get_zf.py
- tool.py
存储的文件
- 全国城市链接.csv:省份-城市-城市链接
- 房屋类型链接.csv:城市-新房-二手房-租房
- **(城市名)-xf(esf/zf).csv:标题-房价-信息-位置-房屋链接
- xf(esf/zf)-失败记录.csv:城市-链接
tool.py
- 用requests库发出请求,下载页面:
def get_page_soup(url, headers):
try:
#利用requests发出请求,设置url,header参数
response = requests.get(url, headers=headers, timeout=200, verify=False)
#设置网页编码为gbk
response.encoding = "gbk"
#将网页源码返回为BeautifulSoup类型
soup = bs4.BeautifulSoup(response.text, "html.parser")
except requests.exceptions.ConnectionError as e:
#如果请求发生异常,将异常url打印出来,并且返回’error‘
print(repr(e))
print("获取源码错误:""出错链接---"+url)
soup = "error"
return soup
verify参数是为了防止发生SSLRetyMax报错,设置之后似乎有改善,但偶尔依然会报这个错误,还不是很清楚这个SSL异常的原因,需要在深入了解。
2. 读本地csv文件内容
# 打开文件读取表格,返回字典列表
# 如 {"新房":href,"租房":href,"二手房":href,"城市":cityname}
def read_table(file_name):
type_url_list = list()
with open(file_name, encoding="utf-8-sig") as f:
reader = csv.reader(f)
#读csv文件的标题
header_row = next(reader)
#遍历每一行
for data in reader:
row_data = dict()
#遍历行中每一列对应元素
for num in range(len(data)):
#如果元素为空值就跳过
if data[num] == "":
continue
#将标题以及对应元素内容以键值对形式存入字典
row_data[header_row[num]] = row_data.get(header_row[num], data[num])
#将一行的内容放入列表
type_url_list.append(row_data)
return type_url_list
- 将有问题的城市存入一个文件
def write_error_city(file_name, city_list, header_list):
# 将有问题的城市另写入一个文件
with codecs.open(file_name, mode="w", encoding="utf-8-sig") as f:
#csv文件的标题
f_header = header_list
f_writer = csv.DictWriter(f=f, fieldnames=f_header)
f_writer.writeheader()
#将数据写入csv文件,writerows可一次写入多条数据,writerow一次只能写入一条数据
f_writer.writerows(city_list)
逻辑思路
以城市为单位,抓取城市下对应的新房、二手房、租房信息。
- 首先抓取全国每一个城市对应的链接。
打开首页可以看到全国那个按钮,通过浏览器分析可以看到更多城市对应的按钮,以及它所指向的链接。
下载解析出更多城市按钮对应的url
# 抓取首页更多城市对应的链接,返回链接
def parse_allcityspage_url(url, header):
#下载页面
soup = get_page_soup(url, header)
#如果下载页面出问题就直接传递下去
if soup == "error":
return soup
#解析链接
allcitys_soup = soup.select("#cityi010 > a:nth-child(34)")
allcitys_url = allcitys_soup[0].attrs["href"]
return allcitys_url
点开后将进入这个页面,也就是要重点抓取的目标页面之一。
#1:获得省份、城市、城市链接列表,形式为:
# [{"省份名":省份,"城市名":城市,"城市链接":链接}, ...]
def find_citys_url(url, headers):
province = ""
city_list = list()
#下载页面
soup = get_page_soup(url, headers)
#如果下载页面出错直接将错误传下去
if soup == "error":
return soup
#网页内容为表格形式,这一步解析出每一行的源码
lines_soup = soup.select("#c02 > table > tr")
# 循环处理每一行的源码
for line_soup in lines_soup:
# 获取省份名称
province_soup = line_soup.select("td > strong")
# 判断是否获取到province,也就是这一行中是否有对应的省份名称
#因为如果有一些行的第一个元素是空的,并不是省份
#对应的源码有两种情况:一是没有strong标签、二是strong标签内容为空格
#判断是否有strong标签
if province_soup:
# strong标签是否为空格
if province_soup[0].get_text() not in "\xa0":
# 提取省份名称
province = province_soup[0].get_text()
# 获取城市名及对应链接
citys_soup = line_soup.select("td > a")
#遍历一行中的每一列对应的元素
for city_soup in citys_soup:
city = city_soup.get_text()
city_url = city_soup.attrs["href"]
city_list.append(
{"省份": province,
"城市": city,
"城市链接": city_url}
)
return pro_city_url_list
if __name__ == “__main__”:
if __name__ == '__main__':
# 获得全国城市页面的链接
allcitys_url = parse_allcityspage_url(url, headers)
print("全国城市对应链接:"+allcitys_url)
#处理错误异常
if allcitys_url == "error":
# 城市链接获取失败
print("城市链接获取失败!")
#没有错误就正常抓取
else:
# 获得{省份-城市-城市链接}
pro_city_url_list = find_citys_url(allcitys_url, headers)
#写入文件
with codecs.open(filename="全国城市链接.csv", mode="w", encoding="utf-8-sig") as csv_file:
fieldnames = ["省份", "城市", "城市链接"]
writer = csv.DictWriter(f=csv_file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(pro_city_url_list)
print("城市链接获取成功!")
-
抓取城市对应的新房、二手房、租房的链接
接下来以北京新房为例进行抓取
在更多城市页面点击北京会进入到上面这个页面,位置显示为了北京,而我们的目标是标题的新房、二手房、租房对应的链接。
#解析出新房、二手房、租房的链接,放回字典类型
#{“城市”:cityname,“新房”:url,“二手房”:url,”租房“:url}
def pars_type_url(url, header, city_name):
type_url_dict = dict()
type_url_dict["城市"] = city_name
#下载网页
soup = get_page_soup(url, header)
#如果网页下载错误就返回错误
if soup == "error":
return soup
#定位到标题栏中的每一个标题
type_soup_list = soup.select(".newnav20141104 > div > div > div > a")
#遍历标题栏的每一个元素
for type_soup in type_soup_list:
#判断如果是新房、二手房、租房就把其title和href解析出来放入字典
title = type_soup.get_text()
if title in ["新房", "二手房", "租房"]:
href = type_soup.attrs["href"]
type_url_dict[title] = href
return type_url_dict
运行程序
if __name__ == '__main__':
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0'}
#将前面抓取的城市链接从文件中读出来,作为这里即将抓取的url
table = read_table("全国城市链接.csv")
with codecs.open("房屋类型链接.csv", mode="w", encoding="utf-8-sig") as csv_file:
#写csv文件的标题
fieldnames = ["城市", "新房", "二手房", "租房"]
writer = csv.DictWriter(f=csv_file, fieldnames=fieldnames)
writer.writeheader()
# 读取城市链接表格的每一行数据
for row in table: #row = {"城市":城市名,"城市链接":href,"省份":省份名}
city_name = row.get("城市")
city_href = row.get("城市链接")
# 经过测试无法访问台湾省的页面,并且台湾省往后就到了国外了,所以到台湾省后停止抓取
if city_name == "台湾":
break
#获得类型以及对应的链接的字典
type_url_dict = parse_type_url(city_href, headers, city_name)
#如果某一个城市因为网络原因或者其他原因导致下载错误,那么就跳过这个城市,继续下一个城市
if type_url_dict == "error":
print("type抓取失败"+"cityname:"+city_name+"cityurl:"+city_href)
continue
# 把一个城市对应的房屋类型链接存到文件中
writer.writerow(navigation_url_dict)
print("成功保存房屋标题类型信息!")
-
抓取类型链接对应页面的房屋信息
以新房举例,点开新房标题进入这个页面,画圆圈的是要抓取的信息
# 获得一页面的新房信息,返回list类型,由dict组成的列表{标题,价格,信息,位置,链接}
def get_xf_house_data(soup, xf_url):
xf_list = list()
#定位到所有的目标信息的位置
titles_soup = soup.find_all("div", attrs={"class": "nlcd_name"})
datas_soup = soup.find_all("div", attrs={"class": "house_type"})
prices_soup = soup.select("#newhouse_loupai_list > ul:nth-child(1) > li > div:nth-child(1) > div:nth-child(2) > div:nth-child(5)")
locations_soup = soup.find_all("div", attrs={"class": "address"})
#从每一个位置提取信息
for title_soup, price_soup, data_soup, location_soup in zip(titles_soup, prices_soup, datas_soup, locations_soup):
#因为直接提取到的信息并不是规范的信息,带有空格等内容,所以需要处理
title = title_soup.get_text().strip()
price = price_soup.get_text().strip()
data = data_soup.get_text().split()
data = "".join(data)
location = location_soup.find("a").attrs["title"]
house_url = xf_url+title_soup.find("a").attrs["href"]
#将信息构造称字典放入列表
xf_list.append({"标题": title, "信息": data, "位置": location, "价格": price,"房屋链接": house_url})
return xf_list
-
翻页
观察页面发现每一页的链接都是:区域的域名+”/house/s/b9+pagenum/,所以翻页采取链接拼接的方法。
"https://newhouse.fang.com" + "/house/s/b9" + pagenum + "/"
但是因为不同的城市的总页数不同,所以并不能确定要构造多少页这样的链接。解决策略是:尾页对应的数字,如果构造的链接和尾页的数字相同了就停止抓取。
def get_end_url(soup):
last_url_soup = soup.find("a", attrs={"class": "last"})
#分析发现有些城市的页面没有任何房屋信息,或者只有一页信息,就没有尾页这部分
#所以如果没有就返回no page
if last_url_soup is None:
return "no page"
end_url = last_url_soup.attrs["href"]
return end_url
- 解决了翻页之后开始抓取所有城市的新房
def main():
header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0'}
#将之前抓取的类型链接读出来作为接下来抓取的目标url
table = read_table("房屋类型链接.csv")
#存放出错的城市类型链接
xf_errors = list()
# 循环表格中每一个城市
for city in table:
# 拿到城市对应的新房链接和城市名
#分析得网站有些城市没有新房链接,所有如果没有就返回null
xf_href = city.get("新房", "null")
city_name = city.get("城市")
# 接下来开始抓取
print("正在抓取:" + city_name + "----------------------------------------------")
# 判断如果这个个城市没有新房链接就跳过这个城市
if xf_href == "null":
print(city_name + "没有新房链接,跳过")
continue
xf_href = xf_href.rstrip("/")
# 将每一个城市的房屋信息单独保存成一个csv文件,将城市的新房问价保存在一个xf文件夹
file_name = "D:\\Python\\PycharmProjects\\FangTianXia\\xf\\" + city_name + "-xf.csv"
# 创建存放对应城市新房数据的文件
with codecs.open(file_name, mode="w", encoding="utf-8-sig") as csv_file:
# 写csv文件的标题
file_header = ["标题", "价格", "信息", "位置", "房屋链接"]
writer = csv.DictWriter(f=csv_file, fieldnames=file_header)
writer.writeheader()
# 翻页抓取一个城市的每一页数据
i = 0
end_url = ""
#翻页,知道判断为尾页停止
while True:
# 获取每一页的信息
try:
# 翻页,链接拼接
i = i + 1
this_href = xf_href + "/house/s/b9" + str(i) + "/"
print("正在抓取页码链接:" + this_href)
# 下载源码
soup = get_page_soup(this_href, header)
# 如果源码获取失败就停止对这一城市的抓取,换下一个城市
if soup == "error":
#将失败的城市以及其链接放入一个失败列表,程序结束后放入一个文件
xf_errors.append({"城市": city_name, "新房": xf_href})
break
# 第一次循环时,获得有多少页数据,避免之后重复抓取
if i == 1:
end_url = get_end_url(soup)
end_url = end_url.strip("/").split("/")[-1]
print(city_name + "市一共有" + end_url + "页")
# 获取网页内容,并将数据字典存入列表
one_page_xf_list = get_xf_house_data(soup, xf_href)
# 写入文件
writer.writerows(one_page_xf_list)
# 如果判断为"no page",说明这一页网页中没有页码,也就是只有一页数据,或者没有数据,那么将这一页存入失败列表后换下一个城市
if end_url == "no page":
xf_errors.append({"城市": city_name, "新房": xf_href})
break
# 如果已经到了最后一页,就停止对这一城市的翻页,换下一个城市
if end_url in this_href:
print("到尾页!")
break
# 如果获取到的数据列表为空,就停止对这一城市的翻页,换下一个城市
if len(one_page_xf_list) == 0:
break
#设置延迟时间
time.sleep(1)
except Exception as e:
print(repr(e))
#如果在抓取这一页过程中报异常就停止循环,并将出错的链接打印出来,和存入失败列表
print("翻页报错:城市名字---" + city_name + "报错链接---" + this_href)
xf_errors.append({"城市": city_name, "新房": xf_href})
break
#最后程序结束的时候将失败列表存入文件
write_error_city("xf_失败记录.csv", xf_errors, ["城市", "新房"])
if __name__ == '__main__':
start_time = time.time()
print("开始时间:"+time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
main()
end_time = time.time()
seconds = end_time-start_time
m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
print("用时:"+"%02d:%02d:%02d" % (h, m, s))
print("结束时间:" + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
到这里爬虫程序就结束了,二手房和租房的数据获取方式和新房的获取方式大同小异,只是解析的选择器不太一样而已,根据网页分析修改就可以了。
运行结果
总结
- 没有做反爬虫的措施,针对失败的链接的策略就是存入失败文件,反复重新抓取,每次抓都会少一点失败的的链接,最后也能全都抓完,笨办法。
- 代码冗余问题比较严重,这个需要深思,修改。
- 耦合性严重,就拿下载页面出错来说,如果出错那么后面所有环节都会被影响。
- 没有将数据存入mysql。以前一直没觉得文件存储比数据库存储差在哪里,这次真的领悟到,存数据库是真的方便。查询什么的简直太轻松,使用数据库的话就不需要将新房、二手房、租房分开存不同文件,关系数据库一键查询。
- 可读性一般,这个代码我自己回头再看的时候都有点费劲 -.- ,之后考虑使用框架更新这个代码。
最后
这是第一次用python抓这么多的数据,希望有什么不对的地方,有问题的地方各位留言指出,或者加QQ:2060131328,互相交流进步。