手把手教你如何用Crawlab構建技術文章聚合平臺(二)

MarvinZhang發表於2019-03-21

上一篇文章《手把手教你如何用Crawlab構建技術文章聚合平臺(一)》介紹瞭如何使用搭建Crawlab的執行環境,並且將Puppeteer與Crawlab整合,對掘金、SegmentFault、CSDN進行技術文章的抓取,最後可以檢視抓取結果。本篇文章將繼續講解如何利用Flask+Vue編寫一個精簡的聚合平臺,將抓取好的文章內容展示出來。

文章內容爬蟲

首先,我們需要對爬蟲部分做點小小的補充。上篇文章中我們只編寫了抓取文章URL的爬蟲,我們還需要抓取文章內容,因此還需要將這部分爬蟲編寫了。上次爬蟲的結果collection全部更改為results,文章的內容將以content欄位儲存在資料庫中。

經分析知道每個技術網站的文章頁都有一個固定標籤,將該標籤下的HTML全部抓取下來就OK了。具體程式碼分析就不展開了,這裡貼出具體程式碼。

const puppeteer = require('puppeteer');
const MongoClient = require('mongodb').MongoClient;

(async () => {
  // browser
  const browser = await (puppeteer.launch({
    headless: true
  }));

  // page
  const page = await browser.newPage();

  // open database connection
  const client = await MongoClient.connect('mongodb://192.168.99.100:27017');
  let db = await client.db('crawlab_test');
  const colName = process.env.CRAWLAB_COLLECTION || 'results';
  const col = db.collection(colName);
  const col_src = db.collection('results');

  const results = await col_src.find({content: {$exists: false}}).toArray();
  for (let i = 0; i < results.length; i++) {
    let item = results[i];

    // define article anchor
    let anchor;
    if (item.source === 'juejin') {
      anchor = '.article-content';
    } else if (item.source === 'segmentfault') {
      anchor = '.article';
    } else if (item.source === 'csdn') {
      anchor = '#content_views';
    } else {
      continue;
    }

    console.log(`anchor: ${anchor}`);

    // navigate to the article
    try {
      await page.goto(item.url, {waitUntil: 'domcontentloaded'});
      await page.waitFor(2000);
    } catch (e) {
      console.error(e);
      continue;
    }

    // scrape article content
    item.content = await page.$eval(anchor, el => el.innerHTML);

    // save to database
    await col.save(item);
    console.log(`saved item: ${JSON.stringify(item)}`)
  }

  // close mongodb
  client.close();

  // close browser
  browser.close();

})();
複製程式碼

然後將該爬蟲按照前一篇文章的步驟部署執行爬蟲,就可以採集到詳細的文章內容了。

文章內容爬蟲的程式碼已經更新到Github了。

接下來,我們可以開始對這些文章做文章了。

前後端分離

目前的技術發展來看,前後端分離已經是主流:一來前端技術越來越複雜,要求模組化、工程化;二來前後端分離可以讓前後端團隊分工協作,更加高效地開發應用。由於本文的聚合平臺是一個輕量級應用,後端介面編寫我們用Python的輕量級Web應用框架Flask,前端我們用近年來大紅大紫的上手容易的Vue。

Flask

Flask被稱為Micro Framework,可見其輕量級,幾行程式碼便可以編寫一個Web應用。它靠Extensions外掛來擴充套件其特定功能,例如登入驗證、RESTful、資料模型等等。這個小節中我們將搭建一個REST風格的後臺API應用。

安裝

首先安裝相關的依賴。

pip install flask flask_restful flask_cors pymongo
複製程式碼

基本應用

安裝完成後我們可以新建一個app.py檔案,輸入如下程式碼

from flask import Flask
from flask_cors import CORS
from flask_restful import Api

# 生成Flask App例項
app = Flask(__name__)

# 生成API例項
api = Api(app)

# 支援CORS跨域
CORS(app, supports_credentials=True)

if __name__ == '__main__':
    app.run()
複製程式碼

命令列中輸入python app.py就可以執行這個基礎的Flask應用了。

編寫API

接下來,我們需要編寫獲取文章的介面。首先我們簡單分析一下需求。

這個Flask應用要實現的功能為:

  1. 從資料庫中獲取抓取到的文章,將文章ID、標題、摘要、抓取時間返回給前端做文章列表使用;
  2. 對給定文章ID,從資料庫返回相應文章內容給前端做詳情頁使用。

因此,我們需要實現上述兩個API。下面開始編寫介面。

列表介面

app.py中新增如下程式碼,作為列表介面。

class ListApi(Resource):
    def get(self):
        # 查詢
        items = col.find({'content': {'$exists': True}}).sort('_id', DESCENDING).limit(40)

        data = []
        for item in items:
            # 將pymongo object轉化為python object
            _item = json.loads(json_util.dumps(item))

            data.append({
                '_id': _item['_id']['$oid'],
                'title': _item['title'],
                'source': _item['source'],
                'ts': item['_id'].generation_time.strftime('%Y-%m-%d %H:%M:%S')
            })
            
        return data

複製程式碼

詳情介面

同樣的,在app.py中輸入如下程式碼。

class DetailApi(Resource):
    def get(self, id):
        item = col.find_one({'_id': ObjectId(id)})
        
        # 將pymongo object轉化為python object
        _item = json.loads(json_util.dumps(item))
        
        return {
            '_id': _item['_id']['$oid'],
            'title': _item['title'],
            'source': _item['source'],
            'ts': item['_id'].generation_time.strftime('%Y-%m-%d %H:%M:%S'),
            'content': _item['content']
        }
複製程式碼

對映介面

編寫完介面,我們需要將它們對映到對應到URL中。

api.add_resource(ListApi, '/results')
api.add_resource(DetailApi, '/results/<string:id>')
複製程式碼

完整程式碼

以下是完整的Flask應用程式碼,很簡單,實現了文章列表和文章詳情兩個功能。接下來,我們將開始開發前端的部分。

import json

from bson import json_util, ObjectId
from flask import Flask, jsonify
from flask_cors import CORS
from flask_restful import Api, Resource
from pymongo import MongoClient, DESCENDING

# 生成Flask App例項
app = Flask(__name__)

# 生成MongoDB例項
mongo = MongoClient(host='192.168.99.100')
db = mongo['crawlab_test']
col = db['results']

# 生成API例項
api = Api(app)

# 支援CORS跨域
CORS(app, supports_credentials=True)


class ListApi(Resource):
    def get(self):
        # 查詢
        items = col.find({}).sort('_id', DESCENDING).limit(20)

        data = []
        for item in items:
            # 將pymongo object轉化為python object
            _item = json.loads(json_util.dumps(item))

            data.append({
                '_id': _item['_id']['$oid'],
                'title': _item['title'],
                'source': _item['source'],
                'ts': item['_id'].generation_time.strftime('%Y-%m-%d %H:%M:%S')
            })

        return data


class DetailApi(Resource):
    def get(self, id):
        item = col.find_one({'_id': ObjectId(id)})

        # 將pymongo object轉化為python object
        _item = json.loads(json_util.dumps(item))

        return {
            '_id': _item['_id']['$oid'],
            'title': _item['title'],
            'source': _item['source'],
            'ts': item['_id'].generation_time.strftime('%Y-%m-%d %H:%M:%S'),
            'content': _item['content']
        }


api.add_resource(ListApi, '/results')
api.add_resource(DetailApi, '/results/<string:id>')

if __name__ == '__main__':
    app.run()
複製程式碼

執行python app.py,將後臺介面伺服器跑起來。

Vue

Vue近年來是熱得發燙,在Github上已經超越React,成為三大開源框架(React,Vue,Angular)中star數最多的專案。相比於React和Angular,Vue非常容易上手,既可以雙向繫結資料快速開始構建簡單應用,又可以利用Vuex單向資料傳遞構建大型應用。這種靈活性是它受大多數開發者歡迎的原因之一。

為了構建一個簡單的Vue應用,我們將用到vue-cli3,一個vue專案的腳手架。首先,我們從npm上安裝腳手架。

安裝vue-cli3

yarn add @vue/cli
複製程式碼

如果你還沒有安裝yarn,執行下列命令安裝。

npm i -g yarn
複製程式碼

建立專案

接下來,我們需要用vue-cli3構建一個專案。執行以下命令。

vue create frontend
複製程式碼

命令列中會彈出下列選項,選擇default

? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint) 
  preset (vue-router, vuex, node-sass, babel, eslint, unit-jest) 
  Manually select features 
複製程式碼

然後vue-cli3會開始準備構建專案必要的依賴以及生成專案結構。

手把手教你如何用Crawlab構建技術文章聚合平臺(二)

此外,我們還需要安裝完成其他功能所需要的包。

yarn add axios
複製程式碼

文章列表頁面

views目錄中建立一個List.vue檔案,寫入下列內容。

<template>
  <div class="list">
    <div class="left"></div>
    <div class="center">
      <ul class="article-list">
        <li v-for="article in list" :key="article._id" class="article-item">
          <a href="javascript:" @click="showArticle(article._id)" class="title">
            {{article.title}}
          </a>
          <span class="time">
            {{article.ts}}
          </span>
        </li>
      </ul>
    </div>
    <div class="right"></div>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  name: 'List',
  data () {
    return {
      list: []
    }
  },
  methods: {
    showArticle (id) {
      this.$router.push(`/${id}`)
    }
  },
  created () {
    axios.get('http://localhost:5000/results')
      .then(response => {
        this.list = response.data
      })
  }
}
</script>

<style scoped>
  .list {
    display: flex;
  }

  .left {
    flex-basis: 20%;
  }

  .right {
    flex-basis: 20%;
  }

  .article-list {
    text-align: left;
    list-style: none;
  }

  .article-item {
    background: #c3edfb;
    border-radius: 5px;
    padding: 5px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
  }

  .title {
    flex-basis: auto;
    color: #58769d;
  }

  .time {
    font-size: 10px;
    text-align: right;
    flex-basis: 180px;
  }
</style>
複製程式碼

其中,引用了axios來與API進行ajax互動,這裡獲取的是列表介面。佈局用來經典的雙聖盃佈局。methods中的showArticle方法接收id引數,將頁面跳轉至詳情頁。

文章詳情頁面

views目錄中,建立Detail.vue檔案,並輸入如下內容。

<template>
  <div class="detail">
    <div class="left"></div>
    <div class="center">
      <h1 class="title">{{article.title}}</h1>
      <div class="content" v-html="article.content">
      </div>
    </div>
    <div class="right"></div>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  name: 'Detail',
  data () {
    return {
      article: {}
    }
  },
  computed: {
    id () {
      return this.$route.params.id
    }
  },
  created () {
    axios.get(`http://localhost:5000/results/${this.id}`)
      .then(response => {
        this.article = response.data
      })
  }
}
</script>

<style scoped>
  .detail {
    display: flex;
  }

  .left {
    flex-basis: 20%;
  }

  .right {
    flex-basis: 20%;
  }

  .center {
    flex-basis: 60%;
    text-align: left;
  }

  .title {

  }
</style>
複製程式碼

這個頁面也是經典的雙聖盃佈局,中間佔40%。由API獲取的文章內容輸出到content中,由v-html繫結。這裡其實可以做進一步的CSS優化,但作者太懶了,這個任務就交給讀者來實現吧。

新增路由

編輯router.js檔案,將其修改為以下內容。

import Vue from 'vue'
import Router from 'vue-router'
import List from './views/List'
import Detail from './views/Detail'

Vue.use(Router)

export default new Router({
  mode: 'hash',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'List',
      component: List
    },
    {
      path: '/:id',
      name: 'Detail',
      component: Detail
    }
  ]
})
複製程式碼

執行前端

在命令列中輸入以下命令,開啟http://localhost:8080就可以看到文章列表了。

npm run serve
複製程式碼

最終效果

最後的聚合平臺效果截圖如下,可以看到基本的樣式已經出來了。

手把手教你如何用Crawlab構建技術文章聚合平臺(二)

手把手教你如何用Crawlab構建技術文章聚合平臺(二)

總結

本文在上一篇文章《手把手教你如何用Crawlab構建技術文章聚合平臺(一)》的基礎上,介紹瞭如何利用Flask+Vue和之前抓取的文章資料,搭建一個簡易的技術文章聚合平臺。用到的技術很基礎,當然,肯定也還有很多需要優化和提升的空間,這個就留給讀者和各位大佬吧。

Github

如果感覺Crawlab還不錯的話,請加作者微信拉入開發交流群,大家一起交流關於Crawlab的使用和開發。

手把手教你如何用Crawlab構建技術文章聚合平臺(二)

相關文章