1. 程式人生 > >Django實戰: Python爬蟲爬取鏈家上海二手房資訊,存入資料庫並在前端顯示

Django實戰: Python爬蟲爬取鏈家上海二手房資訊,存入資料庫並在前端顯示

好久沒寫Django實戰教程了,小編我今天就帶你把它與Python爬蟲結合做出個有趣的東西吧。我們將開發這樣一個應用,前端使用者可以根據行政區劃,房廳數和價格區間選擇需要爬取的二手房房源資訊,後臺Python開始爬取資料。爬取資料完成後,通過Django將爬來的資料存入資料庫並通過網頁顯示給使用者。通過本文,你將學會:

  • Django如何與Python爬蟲結合與互動

  • 如何利用split方法和正則表示式從字串中提取我們所需要資訊

開發環境

使用venv或PyCharm新建一個專案,安裝本專案所需要的python第三方庫。後面3個庫都是python爬蟲常用的庫。我們將使用Django 2.1 + Python 3.X + SQLite開發。

  • pip install django

  • pip install bs4

  • pip install requests

  • pip install fake-useragent

新建專案與專案設定

使用django-admin startproject home_spider建立一個名為home_spider的django專案, 然後cd home_spider進入專案資料夾,使用python manage.py startapp homelink建立一個名為homelink的APP,讓後把它加入到settings.py的INSTALLED_APP裡去。

#home_spider/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'homelink',
]

因為美化我們的網需要用到靜態檔案如css和js,我們需要在settings.py裡設定STATIC_URL。

STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static"), ]

除此以外我們還要把app的urls加到專案裡去, 如下所示。

#home_spider/urls.py

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('allauth.urls')),
    path('homelink/', include('homelink.urls')),
] 

模型

我們的模型非常簡單,主要用於儲存二手房相關資訊,如小區,房廳,朝向,總價和單價。模型的各個欄位與我們即將從鏈家網上爬取的資訊是逐一對應的。

#homelink/models.py

from django.db import models

# Create your models here.


class HouseInfo(models.Model):
    title = models.CharField(max_length=256, verbose_name='標題')
    house = models.CharField(max_length=20, verbose_name='小區')
    bedroom = models.CharField(max_length=20, verbose_name='房廳')
    area = models.CharField(max_length=20, verbose_name='面積')
    direction = models.CharField(max_length=20, verbose_name='朝向')
    floor = models.CharField(max_length=60, verbose_name='朝向')
    year = models.CharField(max_length=10, verbose_name='年份')
    location = models.CharField(max_length=10, verbose_name='位置')
    total_price = models.IntegerField(verbose_name='總結(萬元)')
    unit_price = models.IntegerField(verbose_name='單價(元/平方米)')

    add_date = models.DateTimeField(auto_now_add=True, verbose_name="建立日期")
    mod_date = models.DateTimeField(auto_now=True, verbose_name="修改日期")

    def __str__(self):
        return "{}-{}-{}".format(self.house,self.bedroom, self.total_price)

    class Meta:
        verbose_name = "二手房"

在本例中我們對於面積和年份欄位使用了CharField,而不是IntegerField,這是因為房屋面積不一定是整數,而有些二手房年限未知。

URLConf與檢視

在homelink資料夾下新建一個python檔案urls.py,新增以下程式碼。該urls對應兩個檢視方法,一個(house_index)用於顯示查詢選項和爬取結果,一個(house_spider)用於後臺爬取資料並存入資料庫。我們接下來將分別編寫兩個檢視方法。

#homelink/urls.py

from django.urls import path
from . import views

app_name = 'homelink'

urlpatterns = [
    path('', views.house_index, name='house_index'),
    path('spider/', views.house_spider, name='house_spider'),
]

顯示首頁(house_index)

檢視中house_index方法對應首頁(index),其作用是渲染表單和顯示爬取結果。當資料庫已存有爬取資料時(house_list),分頁顯示爬取資料。當house_list不存在時,顯示帶有查詢選項的空表單。

#homelink/views.py

from .forms import HouseChoiceForm
from django.core.paginator import Paginator
from django.http import HttpResponseRedirect


def house_index(request):
    form = HouseChoiceForm()
    house_list = HouseInfo.objects.all()
    if house_list:
        paginator = Paginator(house_list, 10)
        page = request.GET.get('page')
        page_obj = paginator.get_page(page)

        return render(request, 'homelink/index.html',
                      {'page_obj': page_obj, 'paginator': paginator,
                       'is_paginated': True, 'form': form,})
    else:
        return render(request,'homelink/index.html', {'form': form,})

本例中我們使用到了帶有選項的表單HouseChoiceForm, 其程式碼如下。之所以這麼構建選項是因為鏈家上的二手房資訊連結是由不同選項拼接組成的。比如https://sh.lianjia.com/ershoufang/pudong/l2p3/查詢的是浦東價格是300-400萬之間的二房。

# homelink/forms.py

from django import forms


DISTRICT_CHOICES = (('pudong', '浦東'), ('minhang', '閔行'), ('xuhui', '徐匯'))
PRICE_CHOICES = (('p3', '300-400萬'), ('p4', '400-500萬'), ('p5', '500-800萬'))
BEDROOM_CHOICES = (('l2', '二室'), ('l3', '三室'))


class HouseChoiceForm(forms.Form):
    district = forms.CharField(label="區域", 
    widget=forms.RadioSelect(choices=DISTRICT_CHOICES))
    price = forms.CharField(label="價格", 
    widget=forms.RadioSelect(choices=PRICE_CHOICES))
    bedroom = forms.CharField(label="庭室", 
    widget=forms.RadioSelect(choices=BEDROOM_CHOICES))

首頁對應模板如下。該模板使用者顯示錶單和爬取結果。如果使用者通過表單提交爬取選項,將交由檢視homelink:house_spider處理。

#homelink/templates/homelink/index.html

{% extends "homelink/base.html" %}

{% block content %}

<h3>爬取上海鏈家二手房資訊</h3>
<form method="POST" class="form-horizontal" role='form' 
action="{% url 'homelink:house_spider' %}">
  {% csrf_token %}
  {{ form.as_p }}
   <div class="form-group">
       <div class="col-md-12">
  <button type="submit" class="btn btn-primary form-control">開始爬取</button>
       </div>
   </div>
</form>

{% if page_obj %}
<h3>爬取二手房結果</h3>
<table class="table table-striped">
    <thead>
        <tr>            <th>標題</th>
            <th>小區</th>
            <th>房廳</th>
            <th>面積</th>
            <th>板塊</th>
            <th>總價(萬)</th>
            <th>單價(元/平方米)</th>
        </tr>
    </thead>
    <tbody>
     {% for house in page_obj %}
        <tr>
            <td>
            {{ house.title }}
            </td>
            <td>
            {{ house.house }}
            </td>
            <td>
            {{ house.bedroom }}
            </td>
             <td>
             {{ house.area }}
            </td>
            <td>
             {{ house.location }}
            </td>
             <td>
            {{ house.total_price }}
            </td>
            <td>
                {{ house.unit_price }}
            </td>
     {% endfor %}
        </tr>
    </tbody></table>

{% else %}
{# 註釋: 這裡可以換成自己的物件 #}
    <p>尚無二手房資訊。</p>
{% endif %}


{# 註釋: 下面程式碼實現分頁 #}
{% if is_paginated %}
     <ul class="pagination">
    {% if page_obj.has_previous %}
      <li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a></li>
    {% else %}
      <li class="page-item disabled"><span class="page-link">Previous</span></li>
    {% endif %}

    {% for i in paginator.page_range %}
        {% if page_obj.number == i %}
      <li class="page-item active"><span class="page-link"> {{ i }} <span class="sr-only">(current)</span></span></li>
       {% else %}
        <li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li>
       {% endif %}
    {% endfor %}

         {% if page_obj.has_next %}
      <li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a></li>
    {% else %}
      <li class="page-item disabled"><span class="page-link">Next</span></li>
    {% endif %}
    </ul>

{% endif %}

{% endblock 

前端展示效果如下圖所示:

爬取資料存入資料庫(house_spider)

本章內容是本文最重要的部分。house_spider方法將負責爬取資料並通過Django存入資料庫。完整程式碼如下:

#homelink/views.py

from django.shortcuts import render
from .models import HouseInfo
from .forms import HouseChoiceForm
from django.core.paginator import Paginator
from django.http import HttpResponseRedirect


from fake_useragent import UserAgent
import requests
from bs4 import BeautifulSoup
import re

# Create your views here.


def house_spider(request):
    if request.method == 'POST':
        form = HouseChoiceForm(request.POST)
        if form.is_valid():
            district = form.cleaned_data.get('district')
            price = form.cleaned_data.get('price')
            bedroom = form.cleaned_data.get('bedroom')
            url = 'https://sh.lianjia.com/ershoufang/{}/{}{}'.format(district, price, bedroom)

            home_spider = HomeLinkSpider(url)
            home_spider.get_max_page()
            home_spider.parse_page()
            home_spider.save_data_to_model()
            return HttpResponseRedirect('/homelink/')
    else:
        return HttpResponseRedirect('/homelink/')


class HomeLinkSpider(object):
    def __init__(self, url):
        self.ua = UserAgent()
        self.headers = {"User-Agent": self.ua.random}
        self.data = list()
        self.url = url

    def get_max_page(self):
        response = requests.get(self.url, headers=self.headers)
        if response.status_code == 200:
            soup = BeautifulSoup(response.text, 'html.parser')
            a = soup.select('div[class="page-box house-lst-page-box"]')
            max_page = eval(a[0].attrs["page-data"])["totalPage"] # 使用eval是字串轉化為字典格式
            return max_page
        else:
            return None

    def parse_page(self):
        max_page = self.get_max_page()
        for i in range(1, max_page + 1):
            url = "{}pg{}/".format(self.url, i)
            response = requests.get(url, headers=self.headers)
            soup = BeautifulSoup(response.text, 'html.parser')
            ul = soup.find_all("ul", class_="sellListContent")
            li_list = ul[0].select("li")
            for li in li_list:
                detail = dict()
                detail['title'] = li.select('div[class="title"]')[0].get_text()

                # 大華錦繡華城(九街區)  | 3室2廳 | 76.9平米 | 南 | 其他 | 無電梯
                house_info = li.select('div[class="houseInfo"]')[0].get_text()
                house_info_list = house_info.split(" | ")

                detail['house'] = house_info_list[0]
                detail['bedroom'] = house_info_list[1]
                detail['area'] = house_info_list[2]
                detail['direction'] = house_info_list[3]

                # 低樓層(共7層)2006年建板樓  -  張江. 提取樓層,年份和板塊
                position_info = li.select('div[class="positionInfo"]')[0].get_text().split(' - ')

                floor_pattern = re.compile(r'.+\)')
                match1 = re.search(floor_pattern, position_info[0])  # 從字串任意位置匹配
                if match1:
                    detail['floor'] = match1.group()
                else:
                    detail['floor'] = "未知"

                detail['floor'] = re.search(floor_pattern, position_info[0]).group()  # 從字串頭部開始匹配

                year_pattern = re.compile(r'\d{4}')
                match2 = re.search(year_pattern, position_info[0])  # 從字串任意位置匹配
                if match2:
                    detail['year'] = match2.group()
                else:
                    detail['year'] = "未知"
                detail['location'] = position_info[1]

                # 650萬,匹配650
                price_pattern = re.compile(r'\d+')
                total_price = li.select('div[class="totalPrice"]')[0].get_text()
                detail['total_price'] = re.search(price_pattern, total_price).group()

                # 單價64182元/平米, 匹配64182
                unit_price = li.select('div[class="unitPrice"]')[0].get_text()
                detail['unit_price'] = re.search(price_pattern, unit_price).group()
                self.data.append(detail)

    def save_data_to_model(self):
        for item in self.data:
            new_item = HouseInfo()
            new_item.title = item['title']
            new_item.house = item['house']
            new_item.bedroom = item['bedroom']
            new_item.area = item['area']
            new_item.direction = item['direction']
            new_item.floor = item['floor']
            new_item.year = item['year']
            new_item.location = item['location']
            new_item.total_price = item['total_price']
            new_item.unit_price = item['unit_price']
            new_item.save()

我們現在來看下上面這段程式碼如何工作的:

  • 當用戶以POST提交查詢表單,我們構建需要爬取的連結,然後交由HomeLinkSpider類處理。處理完成後返回index頁面。

  • 我們構建HomeLinkSpider類,該類接收需要爬取的url作為引數,並具體包含了三個方法。get_max_page方法可獲取目標url的分頁最大頁數。parse_page方法可以迴圈爬取每個分頁上的資料,並將其存入self.data列表。save_to_model方法可以將self.data遍歷並存入Django資料庫。

  • 我們使用fake-useragent構造請求頭headers防止爬蟲被封。

實戰效果

當你在首頁選擇爬取選項,點選開始爬取,等待2分鐘,你就可以看到爬取的資料以表格形式分頁顯示在同一頁面上啦。

使用split方法和正則表示式提取資料

在整個專案中,從爬取的資料(通常是字串)中提取我們所需要的資訊是最重要的。下例展示了我們如何使用split方法從一長串字串中提取小區的名字,房廳,面積和朝向資訊。

# 大華錦繡華城(九街區)  | 3室2廳 | 76.9平米 | 南 | 其他 | 無電梯
house_info = li.select('div[class="houseInfo"]')[0].get_text()
house_info_list = house_info.split(" | ")

detail['house'] = house_info_list[0]
detail['bedroom'] = house_info_list[1]
detail['area'] = house_info_list[2]
detail['direction'] = house_info_list[3]

下例展示了我們如何使用正則表示式的re.search方法從一長串字串中提取樓層,年份和板塊資訊。

# 低樓層(共7層)2006年建板樓  -  張江. 提取樓層,年份和板塊
position_info = li.select('div[class="positionInfo"]')[0].get_text().split(' - ')

floor_pattern = re.compile(r'.+\)')
match1 = re.search(floor_pattern, position_info[0])  # 從字串任意位置匹配
if match1:
    detail['floor'] = match1.group()
else:
    detail['floor'] = "未知"

detail['floor'] = re.search(floor_pattern, position_info[0]).group()  # 從字串頭部開始匹配

year_pattern = re.compile(r'\d{4}')
match2 = re.search(year_pattern, position_info[0])  # 從字串任意位置匹配
if match2:
    detail['year'] = match2.group()
else:
    detail['year'] = "未知"
detail['location'] = position_info[1]

GitHub原始碼

對於程式碼看不全的同學們,可以去github上看完整程式碼,是個不錯的爬蟲和Django練習哦。原始碼地址如下所示:

https://github.com/shiyunbo/django-homelink-spider

大江狗

2018.11.7