多线程抓取链家网数据

链家网是集房源信息搜索、产品研发、大数据处理、服务标准建立为一体的以数据驱动的全价值链房产服务平台。主营:二手房、租房、新房。通过链家网的数据可以很方便的获取商品房的市场信息
此次目的是抓取链家网广州地区二手房的数据
首先明确步骤:

  1. 分析网页
  2. 分析数据节点
  3. 编写爬虫程序
  4. 存储数据

首先分析网页
链家网网址在这 广州链家网二手房
链家网二手房首页
可以看到一共有25934套房源,数据是更新的,我爬取得时候是没有这么多数据的
接下来观察网页构造
网页构造
可以观察到一个网页是有30个< li >标签,一个标签对应一个房源信息,而且网页只有100页,我们通过观察发现网页的翻页只是在源地址后面加个pg的变量
在这里插入图片描述但是实测将pg填101会重新跳转到第一页,所以我们一共可以观察到3000的信息,后面的无法获取,需要重新观察
地区分类
通过观察发现可以按照地区的分类进去抓取数据,而且每个地区的成交数量都没有超过3000套。也就是我们可以在100页内将数据全部抓取。
列表页面
通过观察发现各个房源的标签是不一样的,而且实测无法在列表页就实现数据的全部抓取,所以我们只能到房源详情页进行抓取
详情页信息
在这里我们确定我们要抓的数据是售价,平方单价,挂牌价,关注人数,房屋户型,所在楼层等等
那么现在爬虫思路就是

  1. 先爬取所有地区链接
  2. 根据地区链接,在各地区链接下抓取所有列表房源的url
  3. 根据抓取到的房源url进行访问抓取数据
    接下来就是代码的构建了
    首先先抓取所有地区并存储在数据库中:
import requests
from urllib.parse import urljoin
from scrapy.selector import Selector
import pymongo
from fake_useragent import UserAgent


ua = UserAgent()
Mymongo = pymongo.MongoClient('localhost', 27017)  # 连接本地服务
lianjia = Mymongo['lianjia']   # 链接数据库
region_url_collection = lianjia['region_url']  # 集合对象
base_url = "https://gz.lianjia.com/chengjiao/"
region_list = ['tianhe', 'yuexiu', 'liwan', 'haizhu', 'panyu', 'baiyun', 'huangpugz', 'conghua', 'zengcheng', 'huadou', 'nansha']


def get_region_url():
    for i in range(0, len(region_list)):
        url = urljoin(base_url, region_list[i])  # 拼接URL
        response = requests.get(url, headers={'User-Agent': str(ua.random)}).text
        selector = Selector(text=response)
        area_url_list = selector.xpath('/html/body/div[3]/div[1]/dl[2]/dd/div/div[2]/a/@href').extract()  # 获取地址列表
        for url in area_url_list:
            region_url = urljoin(base_url, url)
            region_url_collection.insert_one({'region_url': region_url})  # 插入数据


get_region_url()

这里需要注意的是黄埔、花都在url里面的表示不是按照拼音来的,有点坑人,我找了一会才发现原因
地区数据如下:
地区链接
可以看到抓取的数据是有295个,但是实际是有重复的数据存在
例如点击越秀分类下的“同德围”会跳到白云区下面,所有后面需要进行去重处理
接下来是获取列表页中房源的地址ID

import requests
from bs4 import BeautifulSoup
import pymongo
from fake_useragent import UserAgent
import time
import pandas as pd


ua = UserAgent()
Mymongo = pymongo.MongoClient('localhost', 27017)
lianjia = Mymongo['lianjia']
house_id_collection = lianjia['house_id']
region_url_collection = lianjia['region_url']
missing_url_collection = lianjia['missing_url']
base_url = "https://gz.lianjia.com/chengjiao/"


def get_house_id(region_url):
    response = requests.get(region_url, headers={'User-Agent': str(ua.random)}).text
    soup = BeautifulSoup(response, 'lxml')
    house_num = soup.select('div.total.fl > span')
    # page_num = int(int(house_num[0].get_text())/30)
    page_num = int(int(house_num[0].get_text())/30)+1 if int(house_num[0].get_text()) > 30 else 1  # 获取页码
    for i in range(1, page_num+1):
        time.sleep(1)
        current_url = region_url + 'pg{}'.format(i)  # 拼接URL
        response = requests.get(current_url, headers={'User-Agent': str(ua.random)}).text
        soup = BeautifulSoup(response, 'lxml')
        house_num_test = soup.select('div.resultDes.clear > div.total.fl > span')
        if int(house_num_test[0].get_text()) != 0:
            for house_url in soup.select("div.info > div.title > a"):
                house_id = house_url['href'].split('.html')[0].split('/')[-1]
                house_id_collection.insert_one({'house_id': house_id})
        else:
            print(current_url)
            # missing_url_collection.insert_one(current_url)


def get_all_house_id():
    data = pd.DataFrame(list(region_url_collection.find()))
    for region_url in list(set(data['region_url'])):   # 经过观察发现有重复的地区地址,用set集合去重后再转化为list
        get_house_id(region_url)


get_all_house_id()

这里我们使用set()集合来对整个列表进行去重,实测只有243个地区
这里的翻页我们是先抓取地区链接下的房源总数,通过一页有30套房子的模式,与30整除向上取整就可以得到页码的数量了
在这里插入图片描述
如图我们发现天河-车陂下的房源总数是101,所以它的页数应该是101/30+1=4,一共四页
获取到的ID如下:
可以看到有24110个id
接下来就是获取详细信息页的数据啦

import requests
from bs4 import BeautifulSoup
from scrapy.selector import Selector
import pymongo
from fake_useragent import UserAgent
import pandas as pd
import time
from multiprocessing import Pool


ua = UserAgent()
cilent = pymongo.MongoClient('localhost', 27017)  # 数据库连接
lianjia = cilent['lianjia']   # 数据库连接对象
house_info_collection = lianjia['house_info']  # 数据集合对象
house_url_collection = lianjia['house_url']
missing_house_info = lianjia['missing_house_info']
house_url_collection_success = lianjia['house_url_collection_success']
data = pd.DataFrame(list(house_url_collection.find()))  # 使用pandas的DataFrame结构提取出来
proxy = [
    'HTTPS://111.176.28.176:9999',
    'HTTPS://119.101.117.114:9999',
    'HTTPS://119.101.118.115:9999',
    'HTTPS://119.101.116.219:9999',
    'HTTPS://119.101.113.185:9999',
    'HTTPS://119.101.113.25:9999',
    'HTTPS://119.101.117.143:9999',
    'HTTPS://114.116.10.21:3128',
    'HTTPS://60.6.241.72:808',
    'HTTPS://113.105.170.139:3128',
    'HTTPS://114.99.2.201:9999',
    'HTTPS://119.101.113.213:9999',
]


def get_house_info(url):
    wb_data = requests.get(url, headers={'User-Agent': str(ua.random)})
    #  wb_data = requests.get(url, headers={'User-Agent': str(ua.random)},proxies={'https': random.choice(proxy)})
    if wb_data.status_code == 200:
        # 实测不用睡眠也可以实现抓取
        # time.sleep(1)  # 睡眠1秒
        soup = BeautifulSoup(wb_data.text, 'lxml')
        selector = Selector(text=wb_data.text)
        title = soup.select('h1')[0].get_text()  # 标题
        deal_date = soup.select('body > div.house-title > div > span')[0].get_text()  # 成交时间
        house_position = soup.select('div.myAgent > div.name > a')[0].get_text()  # 所处区域
        dealTotalPrice = soup.select('div.price > span > i')[0].get_text()   # 成交价格
        unit_price = soup.select('div.price > b')[0].get_text()  # 单价
        list_price = soup.select('div.info.fr > div.msg > span:nth-of-type(1) > label')[0].get_text()  # 挂牌价
        focus_num = soup.select('div.info.fr > div.msg > span:nth-of-type(5) > label')[0].get_text()  # 关注人数
        # floor = soup.select('div.base > div.content > ul > li:nth-of-type(2)')
        # 使用css selector会将li标签下的所有文字全都抓取过来,此处使用xpath
        floor = selector.xpath('//div[@class="base"]/div[2]/ul/li[2]/text()').extract()   # 楼层
        house_orientation = selector.xpath('//div[@class="base"]/div[2]/ul/li[7]/text()').extract()   # 房屋朝向
        built_date = selector.xpath('//div[@class="base"]/div[2]/ul/li[8]/text()').extract()  # 房屋建成年限
        elevator = selector.xpath('//div[@class="base"]/div[2]/ul/li[14]/text()').extract()  # 有无电梯
        subway = '有' if soup.find_all('a', 'tag is_near_subway') else '无'
        data = {
            'house_type': title.split(' ')[1],   # 户型
            'area': title.split(' ')[-1].split('平')[0],   # 房屋面积
            'deal_date': deal_date.split(' ')[0],   # 成交日期
            'house_position': house_position,   # 所属区域
            'dealTotalPrice': dealTotalPrice,   # 成交价格
            'unit_price': unit_price,  # 单价
            'list_price': list_price,  # 挂牌价
            'focus_num': focus_num,   # 关注人数
            'floor': floor[0].split('(')[0],  # 楼层数
            'house_orientation': house_orientation[0].split(' ')[0],   # 房屋朝向
            'built_date': built_date[0],   # 建成年限
            'elevator': elevator[0].split(' ')[0],   # 有无电梯
            'subway': subway  # 有无地铁
        }
        house_info_collection.insert_one(data)
        house_url_collection_success.insert_one({'house_id': url})   # 收集已抓取的url,ru

    else:
        missing_house_info.insert_one({'missing_house_url': url})  # 将抓取错误的url收集起来,如果出现错误就可以根据url重新抓取


if __name__ == '__main__':
    pool = Pool(processes=8)
    pool.map(get_house_info, list(data['house_url']))  # 使用pool的map函数
    pool.close()  # 关闭进程池,不再接受新的进程
    pool.join()  # 主进程阻塞等待子进程的退出


首先考虑到网站的反爬,我们使用对各IP进行抓取,考虑到数量也很大,我们使用多进程进行爬取,IP最好是使用付费代理IP,这样子会稳些,我的IP是在西刺爬取的,具体爬取可以观看这篇文章大批量抓取西刺代理,在这里我们将IP验证地址设置为链家网,用链家网进行IP验证。获取到IP后就可以进行爬取了。
PS:我在这次爬取,虽然过滤了IP,可是爬取一阵子后IP就会挂掉,非常不友好,所以我试了不用IP,使用单个IP,然后睡眠1秒来试试网站怎么封禁IP,结果我发现我开了多线程,不设置睡眠,居然也可以将所有数据抓取下来,这里要表白链家网,不过大家还是设置睡眠吧,不要给服务器过多压力
这里我们开了8个进程,在跑数据分过程发现程序会报出如下错误
无从下手的数据
上了Google,发现因为是多进程,无法得知索引信息,也就是说不知道哪些ID已经删除了,哪些还没删除。所以其实在抓取页面的代码那里,我们应该设置每次抓取url,就将他从数据库中delete掉,之前没想到,是另外用了一个爬取成功表,每次使用所有的url减去成功的url,剩下的就是未爬取的,但错误还是会发生,后来发现是有些房子缺少数据,导致网页的构造不一样,无法获取数据
异常房源
如图,暂无数据这个标签与本应该的房价在网页的结构不一样,无法判断,所以只能每次程序中断后继续跑,慢慢缩小,后面手动删除暂无数据的url,毕竟只是少数,这里就是我们获取的数据啦
mongodb的数据
其实如果没有异常页的出现,在8个进程以及没有设置睡眠的情况下,我们可以在15分钟内获取到所有数据。多进程真是个好东西。
这里是全部地址列表页删除爬取成功页的url的代码:

import pymongo
import pandas as pd


cilent = pymongo.MongoClient('localhost', 27017)
lianjia = cilent['lianjia']
house_url_collection = lianjia['house_url']
house_url_collection_succcess = lianjia['house_url_collection_success']


data = pd.DataFrame(list(house_url_collection_succcess.find()))
data1 = pd.DataFrame(list(house_url_collection.find()))
for url in list(data['house_id']):
    if url in list(data1['house_url']):
        house_url_collection.delete_one({'house_url': url})
    else:
        pass

至此就全部结束啦,接下来就拿着数据去分析吧。

猜你喜欢

转载自blog.csdn.net/weixin_36637463/article/details/86494534