V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
tikazyq
V2EX  ›  Python

手把手教你如何用 Crawlab 构建技术文章聚合平台(二)

  •  
  •   tikazyq ·
    tikazyq · 2019-03-21 21:19:36 +08:00 · 1429 次点击
    这是一个创建于 2108 天前的主题,其中的信息可能已经有所发展或是发生改变。

    上一篇文章《手把手教你如何用 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 会开始准备构建项目必要的依赖以及生成项目结构。

    此外,我们还需要安装完成其他功能所需要的包。

    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 构建技术文章聚合平台(一)》的基础上,介绍了如何利用 Flask + Vue 和之前抓取的文章数据,搭建一个简易的技术文章聚合平台。用到的技术很基础,当然,肯定也还有很多需要优化和提升的空间,这个就留给读者和各位大佬吧。

    Github

    如果感觉 Crawlab 还不错的话,请加作者微信拉入开发交流群,大家一起交流关于 Crawlab 的使用和开发。

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   954 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 22:01 · PVG 06:01 · LAX 14:01 · JFK 17:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.