java 和 python 对链家二手房信息抓取思路和实现(已成交、在售)

作者:青山常在人不老   阅读 (2374)  |  收藏 (0)  |  点赞 (0)

摘要

本文将会向大家介绍链家二手房信息中的在售房源和历史成交房源信息的数据爬取和抓取,并且生成exal文件的思路和技术实现,技术实现部分采用Java 和Python两种方式实现,最终可抓取到链家3000个小区信息和58W已成交房源数据,以及几十万条在售房源数据。


原文链接:java 和 python 对链家二手房信息抓取思路和实现(已成交、在售)

首先声明:本文所讲内容均为从技术角度分析,注重的是技术分享,并不适用于商业运用,也不建议浏览者用于商业目的。

一、本文想要实现的目标:

1、实时查询北京链家所有二手房历史成交、在售房源数据

2、将查询到的数据生成exal表格,并支持保存到本地

二、本文实现思路

1、首先抓取本经所有小区信息(包含唯一标识和小区名称),保存到数据库中,以方便后续根据小区抓取房源信息

2、遍历所有的小区信息,封装请求URL,抓取每个小区的在售房源和历史成交房源(包含30天内的成交金额)信息

3、将生成的房源信息按照小区汇总生成exal表格数据,并下载到本地

三、技术实现问题分析:

1、历史成交房源中30日内的房源交易价格不可见

    解决方法:进入房源交易详情页面抓取交易价格

2、抓取的部分文字包含一些特殊字符或者空格

    解决办法:过滤掉此部分特殊字符或者空格

3、多次请求可能会造成IP被封掉

    解决办法:抓取时,注意要使用代理,否则请求次数过多会造成IP被链家网屏蔽掉

4、抓取的数据可能有重复的数据

    解决办法:抓取时,需要判断是否存在该信息,存在则更新

四、技术实现:

1、Java 版实现抓取房源信息的抓取代码

2、Python版实现抓取房源信息的抓取代码

五、最终成品

1、成功抓取小区和历史成交房源数据

2、成功下载抓取到的几十万的房源数据到exal中

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

六、正文

6.1抓取小区信息

访问北京链家网https://bj.lianjia.com/xiaoqu/?from=rec ,进入页面我们可以看到页面显示:共找到了11651个小区

java 和python实现抓取链家成交房源和在售房源

,但是该页面总共页面条数才30页


data

根据作者的实际测试发现,真实页数应该是在100页(100页之后的小区信息很多都是重复的),实际小区数应该在3000个左右。

那么我打算查询到这100页的小区,然后将这100页的小区信息同步到本地数据库中。

作者分析了这100页小区信息的URL,发现了如下共同点,那就是不同的页码,只是连接里面的数字发生变化,其余不变,例如下面展示了第3页和第4页的URL

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

第4页:https://bj.lianjia.com/xiaoqu/pg4/ 

这样分析下来,我们只需要从1遍历到100,分别解析这100页的html中的小区信息,即可得到2999条小区信息数据

通过使用F12调试模式,我们可以发现,小区有一个唯一标识 data-id,可以通过如下图的方式查看到:

image.png

6.1.1 首先,我们演示如何通过java来实现抓取小区信息:

通用的util类:

package com.beBianMin.beBianMin.util;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;

/**
 * 高级http工具(使用net.sourceforge.htmlunit获取完整的html页面,即完成后台js代码的运行)
 * 
 * @com https://www.blog-china.cn Http工具,包含:
 * 
 * @author Guopf at 2019/4/14 19:08.
 */
public class HttpUtils {
	/**
	 * 请求超时时间,默认20000ms
	 */
	private int timeout = 20000;
	/**
	 * 等待异步JS执行时间,默认20000ms
	 */
	private int waitForBackgroundJavaScript = 20000;

	private static HttpUtils httpUtils;

	private HttpUtils() {
	}

	/**
	 * 获取实例
	 *
	 * @return
	 */
	public static HttpUtils getInstance() {
		if (httpUtils == null)
			httpUtils = new HttpUtils();
		return httpUtils;
	}

	public int getTimeout() {
		return timeout;
	}

	/**
	 * 设置请求超时时间
	 *
	 * @param timeout
	 */
	public void setTimeout(int timeout) {
		this.timeout = timeout;
	}

	public int getWaitForBackgroundJavaScript() {
		return waitForBackgroundJavaScript;
	}

	/**
	 * 设置获取完整HTML页面时等待异步JS执行的时间
	 *
	 * @param waitForBackgroundJavaScript
	 */
	public void setWaitForBackgroundJavaScript(int waitForBackgroundJavaScript) {
		this.waitForBackgroundJavaScript = waitForBackgroundJavaScript;
	}

	/**
	 * 将网页返回为解析后的文档格式
	 * 
	 * @param html
	 * @return
	 * @throws Exception
	 */
	public static Document parseHtmlToDoc(String html) throws Exception {
		return removeHtmlSpace(html);
	}

	private static Document removeHtmlSpace(String str) {
		Document doc = Jsoup.parse(str);
		String result = doc.html().replace(" ", "");
		return Jsoup.parse(result);
	}

	/**
	 * 获取页面文档字串(等待异步JS执行)
	 *
	 * @param url      页面URL
	 * @param isLoadJs 是否加载js
	 * @return
	 * @throws Exception
	 */
	public String getHtmlPageResponse(String url, boolean isLoadJs) throws Exception {
		String result = "";

		final WebClient webClient = new WebClient(BrowserVersion.CHROME);

		webClient.getOptions().setThrowExceptionOnScriptError(false);// 当JS执行出错的时候是否抛出异常
		webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);// 当HTTP的状态非200时是否抛出异常
		webClient.getOptions().setActiveXNative(false);
		webClient.getOptions().setCssEnabled(false);// 是否启用CSS
		webClient.getOptions().setJavaScriptEnabled(isLoadJs); // 是否启用JS
		webClient.setAjaxController(new NicelyResynchronizingAjaxController());// 很重要,设置支持AJAX

		webClient.getOptions().setTimeout(timeout);// 设置“浏览器”的请求超时时间
		webClient.setJavaScriptTimeout(timeout);// 设置JS执行的超时时间

		HtmlPage page;
		try {
			page = webClient.getPage(url);
		} catch (Exception e) {
			webClient.close();
			throw e;
		}
		webClient.waitForBackgroundJavaScript(waitForBackgroundJavaScript);// 该方法阻塞线程

		result = page.asXml();
		webClient.close();

		return result;
	}

	/**
	 * 获取页面文档Document对象(等待异步JS执行)
	 *
	 * @param url      页面URL
	 * @param isLoadJs 是否加载js
	 * @return
	 * @throws Exception
	 */
	public Document getHtmlPageResponseAsDocument(String url, boolean isLoadJs) throws Exception {
		return parseHtmlToDoc(getHtmlPageResponse(url, isLoadJs));
	}
}

抓取每页小区信息的代码

/**
	 * 同步所有小区的id和名称信息
	 * @author Guopf
	 * @param pageNo
	 *            页码
	 * @com   https://www.blog-china.cn           
	 *            
	 */
	public void getXiaoQu4EvPage(Integer pageNo) {
		String url = "https://bj.lianjia.com/xiaoqu/pg" + pageNo + "/";
		HttpUtils httpUtils = HttpUtils.getInstance();
		httpUtils.setTimeout(30000);
		httpUtils.setWaitForBackgroundJavaScript(30000);
		try {
			String htmlPageStr = httpUtils.getHtmlPageResponse(url, false);
			JXDocument jxDocument = JXDocument.create(htmlPageStr);
			List<Object> ids = jxDocument.sel("//li[@class=\"clear xiaoquListItem\"]/@data-id");
			List<Object> titles = jxDocument
					.sel("//li[@class=\"clear xiaoquListItem\"]/div[@class=\"info\"]/div[@class=\"title\"]/a/text()");
			if (CollectionUtils.isEmpty(ids) || CollectionUtils.isEmpty(titles)) {
				logger.info("第[{}]页数据没有查询到", pageNo);
				return;
			}
			if (ids.size() != titles.size()) {
				logger.error("查询到的信息不一致");
				return;
			}
			logger.info("查询到的数据为:[{}]", ids.size());
			List<LinkXiaoQuInfo> linkXiaoQuInfos = new ArrayList<LinkXiaoQuInfo>();
			LinkXiaoQuInfo linkXiaoQuInfo = null;
			for (int i = 0; i < ids.size(); i++) {
				linkXiaoQuInfo = new LinkXiaoQuInfo();
				Object object = ids.get(i);
				String id = object.toString();
				String name = titles.get(i).toString();
				linkXiaoQuInfo.setXiaoQuId(id);
				linkXiaoQuInfo.setXiaoQuName(name);
				linkXiaoQuInfos.add(linkXiaoQuInfo);
				logger.info("要新增的小区信息为:[{}],[{}]", linkXiaoQuInfo.getXiaoQuId(), linkXiaoQuInfo.getXiaoQuName());
				// linkXiaoQuDao.insertXiaoQu(linkXiaoQuInfo);
			}
			linkXiaoQuDao.insertXiaoQuBatch(linkXiaoQuInfos);
			logger.info("第[{}]页小区信息同步完成", pageNo);
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

调用代码如下:

for (int i = 1; i <= 100; i++) {
    getXiaoQu4EvPage(i);
}
6.1.2 其次,我们演示如何使用Python来实现抓取小区信息:
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保存到本地

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

点击某个小区如太阳公元,进入小区详情页面,下拉找到太阳公元小区成交记录

java 和python实现抓取链家成交房源和在售房源

点击下面的查看全部成交记录,即可得到该小区全部已成交房源信息。通过左上角房源总数530套,除以每页30套,我们可以得到该小区已成交房源一共有多少页。近30天内成交的进入详情页面抓取。观察页面的url  https://bj.lianjia.com/chengjiao/c1111027380101/,观察规律就是最后的一串数字是变化的,是每个小区的id。翻页的规律如下: 

第一页:https://bj.lianjia.com/chengjiao/pg1c1111027380101/

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

这时我们第一步抓取到的小区id就在这里起到了重要的作用

遍历我们刚刚抓取到的小区id,以及每个小区的成交房源的页码,我们就可以获取每个小区的历史成交房源信息,由于30天内的成交数据中的成交金额在抓取到的列表中并不显示,因此对于这类数据,我们需要进入该房源详情页面进行获取交易金额

java 和python实现抓取链家成交房源和在售房源

30天内的成交数据中的成交金额抓取


由于需要请求3000个小区,并且每个小区有多套房源,因此在代码中实现时需要请求几十万次甚至上百万次,在这个请求过程中,极有可能会被链家网后台屏蔽掉你的IP,因此强烈建议使用代理IP。并且考虑到数据量多,建议使用多线程处理

6.2.1 Python实现抓取小区中每页的房源信息:

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

2、爬取过程中遇到报错中断,可以通过已经抓取的小区数量,修改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语言的空格符号&nbsp;需要去掉,不然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)
6.2.2 Java实现抓取小区中每页的房源信息

此部分代码作者正在努力开发中,本文将及时更新此部分代码。

分类   项目开发逻辑
字数   14230

博客标签    java抓取链家成交记录   链家成交记录抓取  

评论