动态爬取链家二手房成交记录并保存至Excel

 

一、先观察网页结构

链家成交记录网址:https://bj.lianjia.com/chengjiao/

每页有30条成交记录,点击记录提示要下载APP才能查看详细信息。不管它,我们直接审查元素,找到成交记录的链接,点击打开。如下图

                                     https://bj.lianjia.com/chengjiao/101103150758.html

链接后面有一串数字,应该是这个成交记录的id号,由于记录每日更新,我们每次爬取完成之后用一个txt文本保存最新记录的id号,以便下次准确定位爬取结束的位置。

接下来回到成交记录页面,观察其翻页时网址的变化:

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

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

地址后加上:/pg+页数 就可以翻页了。

然后在详细信息页面观察要爬取的内容,如图:

包括基本属性、交易属性、成交额、成交单价和成交日期,全部爬取。

接下来老规矩,审查元素,找到这些内容所在的标签:

标签位置已经清楚,说一下爬取的基本思路:

先获取30个记录的链接,然后依次爬取信息,最后打开下一页,循环往复。

这是为了在第一次爬取的时候(有100页),一旦出现异常,可以将已爬取的先保存。

二、代码解析

需要的包有numpy,pandas,BeautifulSoup,re,urllib等

代码解析如下:

1、getbsobj函数

def getbsobj(url):
    try:
        html = urlopen(url,timeout=3)
    except (HTTPError,socket.timeout):
        return None
    return BeautifulSoup(html,'html.parser')

定义一个返回bs对象的函数,包括一些异常处理。

2、getLinksList函数

def getLinksList(url,n):
    '''

    :param url: 链接
    :param n: 第n页
    :return: 链接列表
    '''
    ls = []
    bsobj=getbsobj(url+'/pg%d'%n)
    if not bsobj:
        print('该页打不开')
        return None
    while True:
        aTagList = bsobj.find('ul', {'class': 'listContent'}).findAll('a', {'class': 'img'})
        if len(aTagList)>0:
            print('打开成功')
            break
        else:
            print('进入休眠')
            time.sleep(60)
            bsobj=getbsobj(url+'/pg%d'%n)
    for aTag in aTagList:
        if 'href' in aTag.attrs:
            ls.append(aTag['href'])
    return ls

定义获取30条成交记录链接的函数,以列表形式返回。

链接在ul标签下的a标签中。

中间有一个循环,原因是有的时候网页能打开,但是成交记录却没有显示,这个时候需要尝试重新打开,直到出现成交记录为止。没打开可能是因为访问过于频繁,所以打不开的时候进入休眠,60秒。

3、getInfo函数

infoArray = []

一条记录的信息由一个一维列表保存,而infoArray是保存获取的所有记录信息的二维列表。

tags = bsobj.find('div',{'class':'introContent'}).findAll('li')

获取保存基本属性和交易属性内容的标签列表。

info = list()
info.append(bsobj.head.title.get_text().split()[0])

info是保存一条记录的所有信息的临时列表。

这里添加的是小区名字。

for tag in tags:
     info.append(tag.get_text().strip()[4:])

这里添加的是基本属性和交易属性的内容

ul = bsobj.find('ul',{'class':'record_list'})
info.append(ul.li.span.get_text().strip())
text = ul.li.p.get_text().strip()
a = re.search(r'\d+元/平',text)
if a ==None:
    info.append('无')
else:
    info.append(a.group()[:-3])
b = re.search(r'\d+-\d+-\d+', text)
if b == None:
    info.append(text)
else:
    info.append(b.group())

这里添加的是成交额、单价和成交日期,因为和上面的属性不在一起,需要另外处理。

infoArray.append(info)

添加一条记录

arr = np.array(infoArray)

将最终的infoArray转换成矩阵arr,并返回。

4、checkid函数

在动态爬取的时候检查该id是否已经获取,若已获取,那么说明已经爬取到上一次运行时的最新记录,此时已经无需继续爬取。

def checkid(ls,id):
    '''

    :param ls: 链接列表
    :param id: 成交id
    :return: bool
    '''
    links = []
    tag = False
    for lk in ls:
        if lk[-17:-5]!=str(id):
            links.append(lk)
        else:break
    if len(links)!=len(ls):
        tag = True
    ls = links
    return tag,ls

这里对列表中的每一条链接进行检查,若该id与上一次保存的id相同,则该链接及其之后的链接全部丢弃,返回一个bool值和处理后的列表。

5、getIndex函数

用于获取保存至Excel时需要的属性标签

def getIndex(url):
    cols = ['小区名字']
    ls = getLinksList(url, 1)
    bs = getbsobj(ls[0])
    tags = bs.find('div', {'class': 'introContent'}).findAll('li')
    for tag in tags:
        cols.append(tag.span.get_text().strip())
    cols += ['成交额(万元)','单价(元/平)','日期']
    new_id = ls[0][-17:-5]
    return cols,new_id

6、download函数

主函数

def download(url,start,end):

从第start页开始,爬取到end页为止(包括end页)

cols,new_id = getIndex(url)

获取属性列表和最新记录的id号 

ls = getLinksList(url,i)

获取链接列表

tag,ls = checkid(ls,id)

检查id

arr=getInfo(ls)

获取数据矩阵

df_new = pd.DataFrame(arr,columns=cols)
df = df.append(df_new,ignore_index=True)

转化为DataFrame格式。

df_old = pd.read_excel('链家成交数据.xlsx')
df_old = df.append(df_old, ignore_index=True)
df_old.to_excel('链家成交数据.xlsx')

保存为Excel。

三、输出结果

四、完整代码

from bs4 import BeautifulSoup
from urllib.request import urlopen
from urllib.error import HTTPError
import pandas as pd
import numpy as np
import re
import socket,time

url = 'https://bj.lianjia.com/chengjiao'

def getbsobj(url):
    try:
        html = urlopen(url,timeout=3)
    except (HTTPError,socket.timeout):
        return None
    return BeautifulSoup(html,'html.parser')

def getLinksList(url,n):
    '''

    :param url: 链接
    :param n: 第n页
    :return: 链接列表
    '''
    ls = []
    bsobj=getbsobj(url+'/pg%d'%n)
    if not bsobj:
        print('该页打不开')
        return None
    while True:
        aTagList = bsobj.find('ul', {'class': 'listContent'}).findAll('a', {'class': 'img'})
        if len(aTagList)>0:
            print('打开成功')
            break
        else:
            print('进入休眠')
            time.sleep(60)
            bsobj=getbsobj(url+'/pg%d'%n)
    for aTag in aTagList:
        if 'href' in aTag.attrs:
            ls.append(aTag['href'])
    return ls

def getInfo(ls):
    '''

    :param ls:链接列表
    :return: 数组
    '''
    infoArray = []
    i = 1
    for lk in ls:
        print('正在获取第%d条信息'%i)
        bsobj = getbsobj(lk)
        if not bsobj:continue
        tags = bsobj.find('div',{'class':'introContent'}).findAll('li')
        info = list()
        info.append(bsobj.head.title.get_text().split()[0])
        for tag in tags:
            info.append(tag.get_text().strip()[4:])
        ul = bsobj.find('ul',{'class':'record_list'})
        info.append(ul.li.span.get_text().strip())
        text = ul.li.p.get_text().strip()
        a = re.search(r'\d+元/平',text)
        if a ==None:
            info.append('无')
        else:
            info.append(a.group()[:-3])
        b = re.search(r'\d+-\d+-\d+', text)
        if b == None:
            info.append(text)
        else:
            info.append(b.group())
        infoArray.append(info)
        i += 1
    print('-'*20)
    arr = np.array(infoArray)
    return arr

def checkid(ls,id):
    '''

    :param ls: 链接列表
    :param id: 成交id
    :return: bool
    '''
    links = []
    tag = False
    for lk in ls:
        if lk[-17:-5]!=str(id):
            links.append(lk)
        else:break
    if len(links)!=len(ls):
        tag = True
    ls = links
    return tag,ls

def getIndex(url):
    cols = ['小区名字']
    ls = getLinksList(url, 1)
    bs = getbsobj(ls[0])
    tags = bs.find('div', {'class': 'introContent'}).findAll('li')
    for tag in tags:
        cols.append(tag.span.get_text().strip())
    cols += ['成交额(万元)','单价(元/平)','日期']
    new_id = ls[0][-17:-5]
    return cols,new_id

def download(url,start,end):
    i = start
    fn = open('id.txt','r')
    id = fn.readline()
    fn.close()
    cols,new_id = getIndex(url)
    df = pd.DataFrame()

    try:
        while True:
            print('正在获取第%d页链接'%i)
            ls = getLinksList(url,i)
            if not ls:
                print(ls)
                print(1)
                break
            #print(ls)
            tag,ls = checkid(ls,id)
            #print(ls)
            arr=getInfo(ls)
            df_new = pd.DataFrame(arr,columns=cols)
            df = df.append(df_new,ignore_index=True)

            if tag:
                print(2)
                break
            i += 1
            if i>end:
                print(3)
                break
    except Exception as e:
        print(e)
    else:
        df_old = pd.read_excel('链家成交数据.xlsx')
        df_old = df.append(df_old, ignore_index=True)
        df_old.to_excel('链家成交数据.xlsx')
        fn = open('id.txt', 'w')
        fn.write(new_id)
        fn.close()
        print('程序执行完毕!')





if __name__ == '__main__':
    download(url,1,20)

       进行第一次爬取的时候,上述代码需要稍作修改,try的后面加个finally,将已爬取的内容保存,防止出现异常带来的损失。之后的爬取就可以用上述代码了。根据观察,链家数据每天的更新大约有3至16页不等,因此download函数参数取1和20。

        正常结束应该是先print(2),然后print(程序执行完毕),假如某天更新了21页,那么会先print(3),然后print(程序执行完毕),这时需要修改参数再次执行,当然,也可以直接把参数20改为100,这样除非出现莫名异常,一般都可以正常执行完。

猜你喜欢

转载自blog.csdn.net/qq_37768140/article/details/81835241