基於joint-spider爬蟲資料的Web端資料視覺化平台,線上訪問
資料來源來自專案Joint-spider
後端: django-restframework
前端: Vue
# 环境配置需要 python3.7 redis mysql vue vue-cli3.x
#
# 项目启动
# 1. 后端服务启动,进入项目HouseAnalysis
python manage.py runserver
# 2. 前端项目启动,进入项目目录HouseAnalysisWeb
npm run dev
# 3. 前端项目打包
npm run build-prod
# 4. 访问地址:http://localhost:9527/
# 5. 后端接口地址: http://localhost:8000/
小區房源列表詳情
搜尋房源頁面
具體房源詳情頁面
房屋週邊
# 项目依赖
Django==2.1.5
django-cors-headers==2.4.0
django-crispy-forms==1.7.2
django-filter==2.0.0
django-formtools==2.1
django-guardian==1.4.9
django-import-export==1.1.0
django-redis==4.10.0
django-reversion==3.0.2
djangorestframework==3.9.0
djangorestframework-jwt==1.11.0
drf-extensions==0.4.0
PyMySQL==0.9.3
redis==3.0.1
SQLAlchemy==1.3.2
基本流程
Django生命週期:
前端發送請求-->Django的wsgi-->中間件-->路由系統-->視圖-->ORM資料庫操作-->模板-->返回資料給用戶
rest framework生命週期:
前端傳送請求-->Django的wsgi-->中間件-->路由系統_執行CBV的as_view(),就是執行內部的dispath方法-->在執行dispath之前,有版本分析與渲染器-->在dispath內,對request封裝-->版本-->認證-->權限-->限流-->視圖-->如果視圖用到快取( request.data or request.query_params )就用到了解析器-->視圖處理數據,用到了序列化(對數據進行序列化或驗證) -->視圖返回數據可以用到分頁
專案文件樹:
# 1. 初始化项目
django-admin startproject HouseAnalysis
# 2. 创建相关应用
cd HouseAnalysis
django-admin startapp house
# 项目根目录下创建extra_app 文件夹,存放xadmin压缩文件,运行
pip3 install xadmin
# 1. 配置settings.py 文件 位于\HouseAnalysisHouseAnalysissettings.py
# 找到INSTALLED_APPS,加入配置项
' corsheaders ' ,
' xadmin ' ,
' crispy_forms ' ,
' rest_framework ' ,
' django_filters ' ,
' house '
# 上述配置向是用于将app注册到项目中
# 2. 增加跨域支持
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST = (
' * '
)
CORS_ALLOW_METHODS = (
' DELETE ' ,
' GET ' ,
' OPTIONS ' ,
' PATCH ' ,
' POST ' ,
' PUT ' ,
' VIEW ' ,
)
CORS_ALLOW_HEADERS = (
' XMLHttpRequest ' ,
' X_FILENAME ' ,
' accept-encoding ' ,
' authorization ' ,
' content-type ' ,
' dnt ' ,
' origin ' ,
' user-agent ' ,
' x-csrftoken ' ,
' x-requested-with ' ,
' Pragma ' ,
)
# 3. 在DATABASES中配置MySQL数据库信息
DATABASES = {
' default ' : {
' ENGINE ' : ' django.db.backends.mysql ' ,
' NAME ' : ' house ' ,
' USER ' : ' root ' ,
' PASSWORD ' : ' 123456 ' ,
' HOST ' : ' 127.0.0.1 ' ,
' PORT ' : ' 3306 ' ,
' OPTIONS ' :{
' init_command ' : " SET sql_mode='STRICT_TRANS_TABLES' "
},
}
}
# 4. 配置drf缓存
REST_FRAMEWORK_EXTENSIONS = {
# 10s后失效
' DEFAULT_CACHE_RESPONSE_TIMEOUT ' : 10
}
# 5. 配置redis缓存
CACHES = {
" default " : {
" BACKEND " : " django_redis.cache.RedisCache " ,
" LOCATION " : " redis://127.0.0.1:6379/3 " ,
" OPTIONS " : {
" CLIENT_CLASS " : " django_redis.client.DefaultClient " ,
}
}
}
註:本項目為介面開發不涉及template模板及static靜態資源,所以不用對這兩項進行配置
進入HouseAnalysisHouseAnalysisurls.py,設定路由資訊:
將path('xadmin/', xadmin.site.urls)
, re_path(r'docs/', include_docs_urls(title="HA"))
,到urlpatterns
列表,xadmin為管理頁路由,
docs為介面文件路由,透過http://localhost:8000/docs/可進行訪問,如下圖:
Django Model設計是Django五個基礎核心設計之一(Model設計,URL配置,View編寫,Template設計,From使用),也是MVC(MVT)模式中重要的環節。
Model是資料模型,不是資料庫,它描述了資料的構成和他們之間的邏輯關係,在Django中,Model設計實質上就是一個類別。每一個模型都映射一張資料庫表。
進入HouseAnalysishousemodels.py ,添加Api,Elevator,Floor,Layout,Region,Decortion,Purposes,Orientation,Constructure,Community,CommunityRange
模型,用以提供數據進行序列化。
model創建完成後進行資料遷移:
# 进入项目根目录
python manage.py makemigrations
# 同步至数据库
python manage.py migrate
在house資料夾下新建檔案serializer.py 文件,用來進行資料序列化。
定義序列化器(本質就是一個類別),一般包括模型類別的字段,有自己的字段類型規則。實作了序列化器後,就可以建立序列化物件以及查詢集進行序列化操作,透過序列化物件.data來取得資料(不用自己建構字典,再傳回Json資料)
用於序列化時,將模型類別物件傳入instance參數
用於反序列化時,將要被反序列化的資料傳入data參數
除了instance和data參數外,在建構Serializer物件時,還可透過context參數額外添加數據
這裡我們使用模型序列化器(ModelSerializer),ModelSerializer與常規的Serializer相同,但提供了:
例如:利用簡單程式碼就可序列化house內容
class HouseSerializer ( serializers . ModelSerializer ):
class Meta :
# 需要序列化的model
model = Api
# 需要序列化的字段
fields = "__all__"
同理,將剩餘的model序列化,包括ElevatorSerializer,FloorSerializer,LayoutSerializer,RegionSerializer,RegionS,CommunitySerializer,CommunityRangeSerializer,DecortionSerializer,PurposesSerializer,ConstructureSerializer,OrientationSerializer
,其中,ient RegionS
序列化器作為CommunitySerializer
和CommunityRangeSerializer
的上級序列化器。
視圖的功能:說穿了就是接收前端請求,進行資料處理
(這裡的處理包括:如果前端是GET請求,則構造查詢集,將結果返回,這個過程為序列化;如果前端是POST請求,假如要對數據庫進行改動,則需要拿到前端發來的數據,進行校驗,將資料寫入資料庫,這個過程稱為反序列化)
最原始的視圖可以實現這樣的邏輯處理,但是針對不同的請求,需要在類別視圖中定義多個方法實現各自的處理,這樣是可以解決問題,但是存在一個缺陷,那就是每個函數中一般的邏輯都差不多:讀取請求,從資料庫拿數據,寫東西到資料庫,回傳結果給前端。這樣就會產生大量的重複程式碼。
在開發REST API的視圖中,雖然每個視圖具體操作的資料不同,但增、刪、改、查的實作流程基本上套路化,所以這部分程式碼也是可以重複使用簡化編寫的:
增:校驗請求資料-> 執行反序列化過程-> 儲存資料庫-> 將已儲存的物件序列化並傳回
刪除:判斷要刪除的資料是否存在-> 執行資料庫刪除
改:判斷要修改的資料是否存在-> 校驗請求的資料-> 執行反序列化過程-> 儲存資料庫-> 將已儲存的物件序列化並傳回
查:查詢資料庫-> 將資料序列化並傳回
視圖編寫在views.py 檔案中,這裡我們使用類識圖的方式來開發。
http://127.0.0.1/api/v1/allhouse
,我們需要將所有房源資訊的序列化結果傳回暴露出去。http://127.0.0.1/api/v1/allhouse
/1,我們需要將房源id(此時的id可設定為指定需求參數)為1的序列化結果返回暴露出去。{id:xxx,values:xxx...}
,我們需要當前信息做檢驗,合法後將當前數據添加到資料庫中。例:
# CacheResponseMixin: 缓存,内存级别
# ListModelMixin处理GET(all)请求
# RetrieveModelMixin处理(id)请求
# 基础类GenericViewSet,提供分页,过滤,排序等功能
class HouseListViewSet ( CacheResponseMixin , mixins . ListModelMixin , mixins . RetrieveModelMixin , GenericViewSet ):
queryset = Api . objects . all ()
# 房源序列化
serializer_class = HouseSerializer
# 房源分页
pagination_class = HousePagination
# 自定义排序,过滤类
filter_backends = ( DjangoFilterBackend , filters . SearchFilter , filters . OrderingFilter )
# 过滤信息字段
filter_fields = ( 'price' ,)
# 搜搜字段
search_fields = ( 'title' , 'region' , 'community_name' )
# 排序字段
ordering_fields = ( 'price' , 'unit_price' , 'construction_area' )
# 重写retrieve方法,实现获取单个序列化对象信息
def retrieve ( self , request , * args , ** kwargs ):
instance = self . get_object ()
serializer = self . get_serializer ( instance )
return Response ( serializer . data )
# 分页配置项
class HousePagination ( PageNumberPagination ):
page_size = 30
page_size_query_param = 'page_size'
page_query_param = "page"
max_page_size = 100
其他視圖類似,包括: ElevaorViewSet,FloorViewSet,LayoutViewSet,RegionViewSet,CommunityViewSet,CommunityPagination,AllCommunityViewSet,CommunityRangeViewSet,DecortionViewSet,PurposesViewSet,ConstructureViewSet,OrientationViewSet
其中對於CommunityViewSet
類,我們需要根據特定的community_id來傳回其所有訊息,這時就不能用RetrieveModelMixin
類別的retrieve
方法了,需要重寫get_queryset
方法,傳回特定的model。
def get_queryset ( self ):
if self . request . GET [ 'rid' ] == '0' :
# 获取所有
return Community . objects . all ()
else :
return Community . objects . filter ( region = self . request . GET [ 'rid' ])
其他參數
使用rest_framework
的DefaultRouter
進行路由註冊:
from rest_framework . routers import DefaultRouter
router = DefaultRouter ()
router . register ( r'v1/api/all_house' , HouseListViewSet , base_name = "house" )
......
將router加入urlpatterns:
path ( '' , include ( router . urls )),
這時當有請求存取到存在的路由後,路由會將請求轉送到視圖類,在視圖類別中將請求處理後,返回序列化結果,即後端暴露的介面資料。
[外鏈圖片轉存失敗,來源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JmH9EULX-1588257851195)(imgs/介面資料.png)]
數據描述
HTTP狀態碼:
# 将所有房源数,小区数,总体单价价均价,总体总价均价,存入redis
def insertIndexInfo ():
all_number = house . count ()
com_number = Api . objects . values ( 'community_name' ). distinct (). count ()
mean_price = format ( house . aggregate ( Avg ( 'price' ))[ 'price__avg' ], '.3f' )
mean_unit_price = format ( house . aggregate ( Avg ( 'unit_price' ))[ 'unit_price__avg' ], '.3f' )
data = {
"all_number" : all_number ,
"com_number" : com_number ,
"mean_price" : mean_price ,
"mean_unit_price" : mean_unit_price
}
redis_conn . mset ( data )
# 1. 定义专门数据库操作类InsertIntoMysql
class InsertIntoMysql ():
# 属性中定义sql语句获取数据
def __init__ ( self ):
self . elevator_sql = 'select unit_price, price, elevator from house_api'
# ......
# 定义数据插入方法
def insertElevator ( self ):
index , values , price_mean , unit_price_mean = multiple ( self . elevator_sql , engine , 'elevator' )
for i in range ( len ( index )):
elevator = Elevator ()
elevator . version = 'v1'
elevator . title = '电梯分布情况'
elevator . has_ele = index [ i ]
elevator . el_num = values [ i ]
elevator . mean_price = price_mean [ i ]
elevator . mean_unit_price = unit_price_mean [ i ]
elevator . save ()
# 其他剩余数据插入方法
# ......
# 2. 数据处理模块,包括单因子和对因子处理
# 利用数据处理模块pandas
def single ( sql , engine , feature ):
"""
:param sql: sql语句
:param engine: mysql引擎
:param feature: 数据特征
:return: 特征,数量
"""
df_sql = sql
df = pd . read_sql ( df_sql , engine )
df [ feature ] = df [ feature ]. astype ( str )
if feature == 'floor' :
df [ df [ feature ] == '暂无数据' ] = '18'
df [ feature ] = df [ feature ]. apply ( lambda x : re . findall ( 'd+' , x )[ 0 ])
feature_res = df [ feature ]. value_counts ()
index = feature_res . index
values = feature_res . values
return index , values
def multiple ( sql , engine , feature ):
"""
:param sql: sql语句
:param engine: mysql引擎
:param feature: 数据特征
:return: 特征,数量,总价均价,单价均价
"""
df_sql = sql
df = pd . read_sql ( df_sql , engine )
df [ feature ] = df [ feature ]. astype ( str )
if feature == 'region' :
df [ feature ] = df [ feature ]. apply ( lambda x : re . sub ( r"[|]|'" , '' , x ). split ( ',' )[ 0 ])
feature_res = df [ feature ]. value_counts ()
index = feature_res . index
values = feature_res . values
price_mean = []
unit_price_mean = []
max_unit_price = []
min_unit_price = []
for inx , val in zip ( index , values ):
price_mean . append ( format ( df [ df [ feature ] == inx ][ 'price' ]. astype ( float ). mean (), '.3f' ))
unit_price_mean . append ( format ( df [ df [ feature ] == inx ][ 'unit_price' ]. astype ( float ). mean (), '.3f' ))
max_unit_price . append ( format ( df [ df [ feature ] == inx ][ 'unit_price' ]. astype ( float ). max (), '.3f' ))
min_unit_price . append ( format ( df [ df [ feature ] == inx ][ 'unit_price' ]. astype ( float ). min (), '.3f' ))
return index , values , price_mean , unit_price_mean , max_unit_price , min_unit_price
前端使用基本框架Vue搭建,基於vue-admin-template開發。前端主要業務用於圖表展示,地圖信息,主要用於做成都二手房房價數據及相關信息可視化,包括總體信息展示,各項特徵-如房屋裝修情況等的圖表信息可視化,房源搜索,基本房來源資訊視覺化,地圖展示等功能。
項目說明
前端專案文件樹:
具體流程
# 安装到项目
npm install -S element-ui
# 全局加载: 在main.js中
import ElementUI from ' element-ui '
import ' element-ui/lib/theme-chalk/index.css '
import locale from ' element-ui/lib/locale/lang/en ' // i18n
Vue.use(ElementUI, { locale })
# 这样我们据可以在vue文件中使用UI组件,如: 按钮组件<Button />
# 安装到项目
npm install -S vue-baidu-map
# 局部加载: 在任意vue文件中
import BaiduMap from ' vue-baidu-map/components/map/Map.vue '
# 地图标注组件
import { BmMarker } from ' vue-baidu-map '
# 使用,例如:
< baidu-map ref= " baidumap "
class= " bm-view "
style= " width:100%;height:93vh "
ak= " YOUR AK "
:center= " {lng: 104.07, lat: 30.67} "
:scroll-wheel-zoom= " true "
:zoom= " 16 " >
< bm-navigation anchor= " BMAP_ANCHOR_TOP_LEFT " ></bm-navigation >
< bm-marker v-for= " spoi in searchResults " -- >
< ! --:position= " {lng: spoi.lng, lat: spoi.lat} " -- >
< ! --:title= " spoi.community_name " > -- >
< /bm-marker >
< /baidu-map >
在src目錄下新建資料夾utils,用於存放我們自訂的工具文件,新建requests.js,重新封裝axios方法,滿足我們請求介面時的操作。此文件將暴露一個用作非同步請求的對象,做為更高級的介面存取方法,在介面文件設計中將使用此方法。
# 新建一个servie对象
const service = axios . create ( {
// 项目基本配置文件中定义的VUE_APP_BASE_API,开发和发布环境各部相同,在.env.devolopment和.env.product文件中进行配置
baseURL : process . env . VUE_APP_BASE_API ,
// 直接写死的url,无论环境如何都不会改变
// baseURL: 'http://127.0.0.1:8000/v1/api',
timeout : 8000 // 最大请求时间
} )
service . interceptors . request . use ( config => {
// 请求方法为POST时,配置请求头
config . method === 'post'
? config . data = qs . stringify ( { ... config . data } )
: config . params = { ... config . params }
config . headers [ 'Content-Type' ] = 'application/x-www-form-urlencoded'
return config
} , error => {
// 错误时提示信息
Message ( {
message : error ,
type : 'error' ,
duration : 3 * 1000
} )
Promise . reject ( error )
} )
service . interceptors . response . use (
response => {
if ( response . statusText === 'OK' ) {
// 正确响应时,直接返回数据
return response . data
} else {
Notification ( {
title : '错误' ,
message : '访问api错误' , // TODO 后端拦截错误请求
type : 'error' ,
duration : 3 * 1000
} )
}
} ,
// 错误处理
error => {
let dtext = JSON . parse ( JSON . stringify ( error ) )
try {
if ( dtext . response . status === 404 ) {
Notification ( {
type : 'error' ,
title : '出问题404' ,
message : '访问api错误或服务器忙' ,
duration : 3 * 1000
} )
}
} catch ( err ) {
Notification ( {
title : '错误' ,
message : '请求超时,请稍后再试或刷新页面' ,
type : 'error' ,
duration : 3 * 1000
} )
}
return Promise . reject ( error )
}
)
export default service
使用vue-router實作路由功能,在新建router.js,進行路由項目設定。
// 定义路由列表
export const constantRoutes = [
{
path : '/404' ,
component : ( ) => import ( '@/views/404' ) ,
hidden : true
} ,
{
path : '/' ,
component : Layout ,
redirect : '/dashboard' ,
children : [ {
path : 'dashboard' ,
name : 'Dashboard' ,
// 采用路由懒加载的方式
component : ( ) => import ( '@/views/dashboard/index' ) ,
meta : { title : '房源信息' , icon : 'house' }
} ]
} ,
// ......
]
// 构造路由对象
const createRouter = ( ) => new Router ( {
// mode: 'history', // require service support
scrollBehavior : ( ) => ( { y : 0 } ) ,
routes : constantRoutes
} )
// 在main.js 中引入并挂载
import router from './router'
new Vue ( {
el : '#app' ,
router ,
render : h => h ( App )
} )
介面api.js檔案中定義了所有與專案相關的介面存取請求,並封裝成函數暴露出去,供元件使用。
// 例如获取区划信息接口
export function getRegionInfo ( id , params ) {
return request ( {
url : '/region/' + id ,
method : 'get' ,
params
} )
}
其中request為上面封裝好的介面存取對象,使用前需引入,元件中使用介面的時候,按需匯入,例如:
// charts.js是接口函数文件
import { getRegionInfo } from '@/api/charts'
// 使用
// 第一种方式,由于它本来就是一个promise对象,所以直接在then中获取响应
getRegionInfo ( 1 ) . then ( ( res , err ) => {
// 相关操作
} )
// 第二种方式,使用async await方式,包含await的方法中需要加上async 作为方法前缀
let res = await getRegionInfo ( 1 )