Python爬虫三:抓取链家已成交二手房信息(58W数据)

环境:Windows7+python3.6+Pycharm2017html

目标:抓取链家北京地区已成交二手房信息(无需登陆),以下图,户型、朝向、成交时间价格等,保存到csv。最后一共抓取约58W数据,程序运行8h。python

---所有文章: 京东爬虫 、链家爬虫美团爬虫微信公众号爬虫字体反爬Django笔记阿里云部署vi\vim入门----json

1、打开北京二手房网页https://bj.lianjia.com/ershoufang/,默认显示的是在售二手房信息,一共45634套,可是只显示了100页,每页30条,这3000条信息是没有任何反爬的,能够直接抓取,若是要抓取所有45634条,应该要按小区来。本文主要讨论已成交二手房信息,数据量更大,难度也要高一点。vim

 

2、点击页面右上角成交,切换到已成交二手房信息,显示一共有73W条数据,可是也只显示100页,每页30条共3000条信息。并且还有个问题就是近30天内成交的房源的成交时间、价格信息是不显示的。咱们能够右键检查进入开发者模式,在网页的html代码中找到房源的详情页面的url,而后进入详情页面抓取成交时间、价格。服务器

 

3、如何抓取尽量多的房源信息微信

如今问题就是73W已成交二手房信息,怎么能尽量多的抓下来。 办法就是这些房源经过分类来抓取,好比分不一样区域,价格,小区,这样能够抓到更多的数据。本文选用按小区抓取。点击页面上方小区,进入以下页面,再点击返回所有小区列表。显示一共有11435个小区,虽然下面翻页只有30页,可是咱们能够经过构造url来翻页,实测能够翻到100页,100页后都是重复的,共3000个小区。每页的url以下:多线程

第2页:https://bj.lianjia.com/xiaoqu/pg2/app

第3页:https://bj.lianjia.com/xiaoqu/pg3/函数

第100页:https://bj.lianjia.com/xiaoqu/pg100/字体

 

4、如何抓取每一个小区的已成交二手房信息

点击某个小区如北京新天地,进入小区详情页面,下拉找到北京新天地小区成交记录,点击下面的查看所有成交记录,便可获得该小区所有已成交房源信息。经过左上角房源总数2133套,除以每页30套,咱们能够获得该小区已成交房源一共有多少页。近30天内成交的进入详情页面抓取。观察页面的url  https://bj.lianjia.com/chengjiao/c1111027375945/ ,观察规律就是最后的一串数字是变化的,是每一个小区的id。翻页的规律以下:

第二页:https://bj.lianjia.com/chengjiao/pg2c1111027375945/

第三页:https://bj.lianjia.com/chengjiao/pg3c1111027375945/

    因此咱们的思路就是先抓取每一个小区的id,而后构造小区成交房源页面的url,经过房源总数来得知该小区一共有多少页,翻页抓取。近30天内成交的须要进入详情页面抓取,其余的直接在列表页面就能够。

 

5、抓取小区id

一共100页,每页的url以下,也很简单,直接每一个li标签中的data-id属性就是小区的id。注意的是该页面有对ip访问次数作限制,单ip连续访问好像是25页就会被封,因此须要采用代理ip,这里每一个ip只抓取20页。还有一点须要注意的就是抓取的id须要作排重,此处用的set。还有就是对于第一次访问没有获得数据的页面须要再次访问。

https://bj.lianjia.com/xiaoqu/pg2/

https://bj.lianjia.com/xiaoqu/pg3/ 

 实际一共抓取到2990个id,保存到本地csv,代码以下。

import requests
from lxml import etree
import csv
import time
import json
#单线程抓取小区id前100页信息
def get_xiaoqu(x,p):
    head = {'Host': 'bj.lianjia.com',
            'Referer': 'https://bj.lianjia.com/chengjiao/',
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36'
            }
    n=x*20+1
    l=list(range(n,n+20))
    for i in l:
        url = 'https://bj.lianjia.com/xiaoqu/pg' + str(i)
        try:
            r = requests.get(url, headers=head, proxies=p, timeout=3)
            html = etree.HTML(r.text)
            datas=html.xpath('//li[@class="clear xiaoquListItem"]/@data-id')
            title=html.xpath('//li[@class="clear xiaoquListItem"]/div[@class="info"]/div[@class="title"]/a/text()')
            print('No:' + str(x), 'page:' + str(i), len(s), len(datas), len(title))
            #若是当前页没有返回数据,将当前页数加到列表l末尾,再次抓取
            if len(datas)==0:
                print(url)
                l.append(i)
            else:
                for data in datas:
                    s.add(data)
        # 若是当前页访问出现异常,将当前页数加到列表l末尾,再次抓取
        except Exception as e:
            l.append(i)
            print(e)

    print('      ****No:'+str(x)+' finish')
#本人购买的代理获取方式,须要根据大家本身的修改。函数功能获取n个ip,并以列表形式返回,每一个元素为字典:{'https':'https://118.120.228.202:4286'}
def get_ip(n):
    url='XXXXXXXXXXXXXXXXXXXXXXXX'
    r=requests.get(url)
    html=json.loads(r.text)
    proxies=[]
    for i in range(n):
        a=html['data'][i]['ip']
        b=html['data'][i]['port']
        val='https://'+str(a)+':'+str(b)
        p={'https':val}
        proxies.append(p)
    return(proxies)

if __name__=='__main__':
    global s
    #将id保存在set中,达到排重效果
    s = set()
    #该页面网站会禁ip,因此每一个ip只访问20页
    for x in range(5):
        now=time.time()
        ls = get_ip(1)
        p=ls[0]
        get_xiaoqu(x,p)
        print(time.time()-now)
    print('******************')
    print('抓取完成')
    #将抓取的id保存到本地csv
    with open('xiaoqu_id.csv', 'a', newline='', encoding='gb18030')as f:
        write = csv.writer(f)
        for data in s:
            write.writerow([data])
        f.close()

 

6、对每一个小区已成交房源信息进行抓取 

本文没有开多线程(开5个线程跑了几分钟好像也没遇到问题),也没遇到封ip,就没加代理。逻辑很简单,parse_xiaoqu(url,pa) 函数用于爬取小区具体一页的房源信息,先抓取第一页数据,获取房源信息的同时得到该小区房源总数,而后肯定该小区一共有多少页。而后就是对每一页调用parse_xiaoqu(url,pa)进行抓取。主要注意点有:

一、对于第一次抓取失败的页面,包括timeout这种异常和无异常可是返回0条房源信息两种状况,都须要对这些页面进行第二次的抓取。parse_xiaoqu(url,pa)返回值中有一个1或0就是用以标记本次抓取是否成功。第一次抓取失败的,第二次抓取成功的数据还挺多的。

二、爬取过程当中遇到报错中断,能够经过已经抓取的小区数量,修改range(0,2990)函数的第一个参数达到断点后续抓。

代码以下,代码应该是把小区id导入就能够直接运行的。

import requests
from lxml import etree
import csv
import time
import threading
#小区具体一页房源信息的抓取,输入为当前页面url,当前爬取页数pa。返回数据为小区房源总数num,该页抓取的房源信息home_list,状态码1或0(1表示成功)
def parse_xiaoqu(url,pa):
    head = {'Host': 'bj.lianjia.com',
            'Referer': 'https://bj.lianjia.com/chengjiao/',
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36'

            }
    r = requests.get(url, headers=head,timeout=5)
    html = etree.HTML(r.text)
    num = html.xpath('//div[@class="content"]//div[@class="total fl"]/span/text()')[0]
    num = int(num)
    datas = html.xpath('//li/div[@class="info"]')
    print('小区房源总数:', num,'第%d页房源数:'%pa,len(datas))
    print(url)
    if len(datas)==0:
        return(num,[],0)   #服务器无返回数据,状态码返回0
    house_list=[]
    for html1 in datas:
        title = html1.xpath('div[@class="title"]/a/text()')
        info = html1.xpath('div[@class="address"]/div[@class="houseInfo"]/text()')
        floor = html1.xpath('div[@class="flood"]/div[@class="positionInfo"]/text()')
        info[0] = info[0].replace('\xa0','')  #该条信息中有个html语言的空格符号 须要去掉,否则gbk编码会报错,gb18030显示问号
        date = html1.xpath('div[@class="address"]/div[@class="dealDate"]/text()')
        #30天内成交的进入详情页面抓取
        if date[0] == '近30天内成交':
            p_url = html1.xpath('div[@class="title"]/a/@href')
            r = requests.get(p_url[0], headers=head,timeout=5)
            html = etree.HTML(r.text)
            price = html.xpath('//div[@class="overview"]/div[@class="info fr"]/div[@class="price"]/span/i/text()')
            unitprice = html.xpath('//div[@class="overview"]/div[@class="info fr"]/div[@class="price"]/b/text()')
            date = html.xpath('//div[@class="house-title LOGVIEWDATA LOGVIEW"]/div[@class="wrapper"]/span/text()')
            #有的房源信息没有价格信息,显示暂无价格
            if len(price)==0:
                price.append('暂无价格')
            if len(unitprice)==0:
                unitprice.append('暂无单价')
            date[0] = date[0].replace('链家成交', '')
            a = [title[0], info[0], floor[0], date[0], price[0], unitprice[0]]
            house_list.append(a)
            print(title[0], info[0], floor[0], date[0], price[0], unitprice[0])
        else:
            price = html1.xpath('div[@class="address"]/div[@class="totalPrice"]/span/text()')
            unitprice = html1.xpath('div[@class="flood"]/div[@class="unitPrice"]/span/text()')
            if len(price) == 0:
                price = ['暂无价格']
            if len(unitprice) == 0:
                unitprice = ['暂无单价']
            a = [title[0], info[0], floor[0], date[0], price[0], unitprice[0]]
            house_list.append(a)
            print(title[0], info[0], floor[0], date[0], price[0], unitprice[0])
    print('                *********************         ','第%d页完成!'%pa)
    return (num,house_list,1)

#抓取某小区全部已成交二手房信息,排重后存入本地csv,输入为小区id,返回抓取到的该小区的房源总数
def crow_xiaoqu(id):
    url='https://bj.lianjia.com/chengjiao/c%d/'%int(id)
    h_list=[]      #保存该小区抓取的全部房源信息
    fail_list=[]   #保存第一次抓取失败的页数,第一遍抓取完成后对这些页数再次抓取
    try:
        #爬取小区第一页信息
        result=parse_xiaoqu(url,1)
    except:
        #若是第一页信息第一次爬取失败,sleep2秒再次爬取
        time.sleep(2)
        result=parse_xiaoqu(url,1)
    #获取该小区房源总数num
    num = result[0]
    #若是无数据返回,sleep2秒再爬取一次
    if num == 0:
        time.sleep(2)
        result=parse_xiaoqu(url,1)
        num = result[0]
    new_list = result[1]
    pages=1
    for data in new_list:
        if data not in h_list:
            h_list.append(data)
    # 肯定当前小区房源页数pages
    if num > 30:
        if num % 30 == 0:
            pages = num // 30
        else:
            pages = num // 30 + 1
    for pa in range(2,pages+1):
        new_url = 'https://bj.lianjia.com/chengjiao/pg'+str(pa)+'c'+str(id)
        try:
            result=parse_xiaoqu(new_url,pa)
            status=result[2]
            if status==1:
                new_list=result[1]
                #排重后存入h_list
                for data in new_list:
                    if data not in h_list:
                        h_list.append(data)
            else:
                fail_list.append(pa)
        except Exception as e:
            fail_list.append(pa)
            print(e)
    print('   开始抓取第一次失败页面')
    for pa in fail_list:
        new_url = 'https://bj.lianjia.com/chengjiao/pg' + str(pa) + 'c' + str(id)
        print(new_url)
        try:
            result = parse_xiaoqu(new_url,pa)
            status = result[2]
            if status == 1:
                new_list = result[1]
                for data in new_list:
                    if data not in h_list:
                        h_list.append(data)
            else:
                pass
        except Exception as e:
            print(e)
    print('    抓取完成,开始保存数据')
    #一个小区的数据所有抓完后存入csv
    with open('lianjia_123.csv','a',newline='',encoding='gb18030')as f:
        write=csv.writer(f)
        for data in h_list:
            write.writerow(data)
    #返回抓取到的该小区房源总数
    return(len(h_list))
if __name__=='__main__':
    counts=0    #记录爬取到的房源总数
    now=time.time()
    id_list=[]
    with open('xiaoqu_id.csv','r')as f:
        read=csv.reader(f)
        for id in read:
            id_list.append(id[0])
    m=0
    #能够经过修改range函数的起始参数来达到断点续抓
    for x in range(0,2990):
        m+=1    #记录一共抓取了多少个小区
        print('    开始抓取第'+str(m)+'个小区')
        time.sleep(1)
        count=crow_xiaoqu(id_list[x])
        counts=counts+count
        #打印已经抓取的小区数量,房源数量,所花的时间
        print('     已经抓取'+str(m)+'个小区  '+str(counts)+'条房源信息',time.time()-now)

7、总结

    一共抓取了 58W条数据,程序跑了8h,速度通常。主要缺点就是程序不够顽强,中途中断了不少次,须要人工的再次启动,修改range参数才能断点续抓。还有就是1.1W个小区也就抓了3000个,可是从房源数据来看73W数据抓了58w,大概80%。

水平有限,但愿你们指正。

---所有文章: 京东爬虫 、链家爬虫美团爬虫微信公众号爬虫字体反爬Django笔记阿里云部署vi\vim入门----                                    

欢迎关注我的公众号,更多案例持续更新!