python3:request+BeautifuleSoup抓取房天下

开始之前

这篇代码的目标网站是房天下,使用python3编写,涉及到的核心库包含requests、BeautifulSoup等。抓取到的文件存储在本地的csv文件中。抓取了网站全国每一个城市对应的新房、二手房、租房下的房屋信息。

为什么选择这个网站

  1. 网站的反爬虫机制没有那么严格。
  2. 网上抓取这个网站的人不少,所以可以借鉴的资料很多。
  3. 网站的设计元素较为简单,就要抓取的内容而言不涉及Ajax、动态网页等。

目标数据

  1. 全国城市列表:省份-城市-城市链接
  2. 城市页面标题:城市-新房链接-二手房链接-租房链接
  3. 房屋信息列表:标题-房价-信息-位置-房屋链接

爬虫文件

  1. get_city.py
  2. get_type.py
  3. get_xf.py
  4. get_esf.py
  5. get_zf.py
  6. tool.py

存储的文件

  1. 全国城市链接.csv:省份-城市-城市链接
  2. 房屋类型链接.csv:城市-新房-二手房-租房
  3. **(城市名)-xf(esf/zf).csv:标题-房价-信息-位置-房屋链接
  4. xf(esf/zf)-失败记录.csv:城市-链接

tool.py

  1. 用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
  1. 将有问题的城市存入一个文件
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)

逻辑思路

以城市为单位,抓取城市下对应的新房、二手房、租房信息。

  1. 首先抓取全国每一个城市对应的链接。

在这里插入图片描述
打开首页可以看到全国那个按钮,通过浏览器分析可以看到更多城市对应的按钮,以及它所指向的链接。
下载解析出更多城市按钮对应的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("城市链接获取成功!")
  1. 抓取城市对应的新房、二手房、租房的链接
    接下来以北京新房为例进行抓取
    在这里插入图片描述
    在更多城市页面点击北京会进入到上面这个页面,位置显示为了北京,而我们的目标是标题的新房、二手房、租房对应的链接。
#解析出新房、二手房、租房的链接,放回字典类型
#{“城市”: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("成功保存房屋标题类型信息!")
  1. 抓取类型链接对应页面的房屋信息
    在这里插入图片描述
    以新房举例,点开新房标题进入这个页面,画圆圈的是要抓取的信息
# 获得一页面的新房信息,返回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
  1. 翻页
    在这里插入图片描述
    观察页面发现每一页的链接都是:区域的域名+”/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
  1. 解决了翻页之后开始抓取所有城市的新房
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()))

到这里爬虫程序就结束了,二手房和租房的数据获取方式和新房的获取方式大同小异,只是解析的选择器不太一样而已,根据网页分析修改就可以了。

运行结果

在这里插入图片描述
在这里插入图片描述

总结

  1. 没有做反爬虫的措施,针对失败的链接的策略就是存入失败文件,反复重新抓取,每次抓都会少一点失败的的链接,最后也能全都抓完,笨办法。
  2. 代码冗余问题比较严重,这个需要深思,修改。
  3. 耦合性严重,就拿下载页面出错来说,如果出错那么后面所有环节都会被影响。
  4. 没有将数据存入mysql。以前一直没觉得文件存储比数据库存储差在哪里,这次真的领悟到,存数据库是真的方便。查询什么的简直太轻松,使用数据库的话就不需要将新房、二手房、租房分开存不同文件,关系数据库一键查询。
  5. 可读性一般,这个代码我自己回头再看的时候都有点费劲 -.- ,之后考虑使用框架更新这个代码。

最后

这是第一次用python抓这么多的数据,希望有什么不对的地方,有问题的地方各位留言指出,或者加QQ:2060131328,互相交流进步。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值