Vue 2.0 起步(7) 大结局:公众号文章抓取 - 微信公众号RSS

上一篇:Vue 2.0 起步(6) 后台管理Flask-Admin - 微信公众号RSS
总算赶在2017年春节前把这个项目完结了!
第7篇新知识点不多,主要是综合应用Flask、Vue

本篇关键字编程式导航 编程式路由 vue-router Python爬虫 Flask

本篇完成功能:

  1. 上传订阅列表时,Python抓取公众号主页上的文章列表
  2. 点击右侧导航栏某一公众号,左侧显示它所包含的文章列表
  3. 点击顶部菜单(订阅文章),左侧显示所有公众号的文章列表,按更新时间排列
  4. 左侧显示某一公众号文章列表时,点击更新,可以检查是否有最新发表的公众号文章

演示网站:
DEMO: http://vue2.heroku.com

注:bootstrap v4 alpha6更新了,界面还没来及重新匹配,见谅!

最终完成图:

单个公众号文章列表.png
所有文章.png

下面依次介绍注意的知识点:

在此之前,先依照最新的models,更新数据库结构。Article模型有更新

/app/models.py

# cd C:\git\vue-tutorial
# python manage.py db migrate -m "Article"
# python manage.py db upgrade

1. 上传订阅列表时,Python抓取公众号主页上的文章列表

我们在 Vue 2.0 起步(5) 订阅列表上传和下载 - 微信公众号RSS 中,上传订阅列表时,服务器端是在mps.py里处理。我们添加:Flask异步调用函数fetchArticle(mp, 'async'),来爬虫抓取公众号主页上的文章

/app/api_1_0/mps.py

@api.route('/mps', methods=['POST'])
@auth_token_required
def new_mps():
    email = request.get_json()['email']
    user = User.query.filter_by(email=email).first()
    Mps = Mp.from_json(request.json)
    subscribed_mps_weixinhao = [i.weixinhao for i in user.subscribed_mps]
    rsp = []
    for mp in Mps:
        mp_sql = Mp.query.filter_by(weixinhao=mp.weixinhao).first()
    # 如果不存在这个订阅号,则添加到Mp,并订阅
        if mp_sql is None:
                db.session.add(mp)
                user.subscribe(mp)
                rsp.append(mp.to_json())
                db.session.commit()
    # aync update Articles
                mp_sql = Mp.query.filter_by(weixinhao=mp.weixinhao).first() # 此mp跟初始的 mp已经是不同对像
                [ok, return_str] = fetchArticle(mp_sql, 'async')

    # 如果用户没有订阅,则订阅
        elif not mp.weixinhao in subscribed_mps_weixinhao:
                user.subscribe(mp_sql)
                rsp.append(mp.to_json())
                db.session.commit()
    # aync update Articles
                [ok, return_str] = fetchArticle(mp, 'async')

    return jsonify(rsp), 201, \
        {'Location': url_for('api.get_mps', id=mp.id, _external=True)}

这个爬虫函数使用from threading import Thread来异步抓取:

/app/api_1_0/fetchArticles.py

注意:sogou.com搜索不能太频繁,不然会要求输入验证码。
看到服务器上有这个提示,要么等会再来,要么手动输入验证码来立即解除限制

2. 点击右侧导航栏某一公众号,左侧显示它所包含的文章列表

这里需要在vue-router里,添加新的路由,以显示公众号文章列表。
注意:新的路由path: '/article/:id'是动态的,可以匹配任意公众号文章的视图,比如/article/weixinhao1, /article/weixinhao2...
另外,取了个别名:name: 'article',在编程式路由跳转时会用到

/src/main.js

import Articles from './components/Articles'
import Article from './components/Article'


const routes = [{
    path: '/',
    component: Home
},{
    path: '/articles',
    component: Articles
},{
    path: '/article/:id',
    name: 'article',
    component: Article
},{
    path: '/home',
    component: Home
},{
    path: '/search',
    component: Search
}]

我们在导航栏的每个公众号上,添加@click="fetchArticles(mp.weixinhao, mp.mpName)",来触发获取文章的ajax请求。传给服务器的参数是:weixinhao、headers:token。获取到的数据,存入LocalStorage中。
注意:return this.$router.push({ name: 'article', params: { id: weixinhao, mpName: mpName }}),这是编程式路由跳转,观察浏览器的地址栏是不是变化了?而且带入了我们想要的参数,供Article.vue使用

/src/components/Sidebar.vue

     methods: {
            fetchArticles(weixinhao, mpName){
//              return this.$router.push({ name: 'article', params: { id: weixinhao }})
            this.isFetching = true;
            this.$nextTick(function () { });
            this.$http.get('/api/v1.0/articles', {
                    params: {
                        email: this.username,
                            weixinhao: weixinhao
                    },
                    headers: {
                        'Content-Type': 'application/json; charset=UTF-8',
                        'Authentication-Token': this.token
                    }
                }).then((response) => {
                    // 响应成功回调
                this.isFetching = false;
                this.$nextTick(function () { });
                   var data = response.body, article_data;
                    if (!data.status == 'ok') {
 //                   return alert('文章 from server:\n' + JSON.stringify(data));
                return alert('获取失败,请重新上传订阅列表!\n' +data.status)
                    }
            article_data = {
                'mpName': mpName,
                'weixinhao': weixinhao,
                'articles': data.articles,
                'sync_time': data.sync_time
                }
            window.localStorage.setItem('weixinhao_'+weixinhao, JSON.stringify(article_data));
            // 必须要命名route name,否则,地址会不停地往后加 /article/XXX, /article/article/XXX
            return this.$router.push({ name: 'article', params: { id: weixinhao, mpName: mpName }})
                }, (response) => {
                    // 响应错误回调
                    alert('同步出错了! ' + JSON.stringify(response))
                    if (response.status == 401) {
                        alert('登录超时,请重新登录');
                        this.is_login = false;
                        this.password = '';
                        window.localStorage.removeItem("user")
                    }
                });
            },

当然,服务器端需要对这个Ajax请求作出响应。检查这个公众号,如果不存在,则需要重新上传订阅列表。如果存在,则查询服务器端对应的文章集合,和上次同步文章列表的时间。这个动作,不会重新去sogou.com抓取新的文章

/app/api_1_0/mps.py


@api.route('/articles')
@auth_token_required
def get_articles():
    # request.args.items().__str__()
#   time.sleep(3)
    weixinhao = request.args.get('weixinhao')
    print 'fetch articles of ', weixinhao
    mp = Mp.query.filter_by(weixinhao=weixinhao).first()
    if mp is not None:
        if request.args.get('action') == 'sync':
            print '================sync'
            if datetime.utcnow() - mp.sync_time > timedelta(seconds=60*5):
                [ok, return_str] = fetchArticle(mp, 'sync')
                print ok, return_str
                # 需要重新获取mp对象,
                #DetachedInstanceError: Instance <Mp at 0x5d769b0> is not bound to a Session; attribute refresh operation cannot proceed
                mp = Mp.query.filter_by(weixinhao=weixinhao).first()
            else: 
                print '========== less than 5 mins, not to sync'
#           return jsonify(return_str)
        articles = Article.query.filter(Article.mp_id == mp.id)
        articles_list = [ a.to_json() for a in articles ]
        rsp = {
            'status': 'ok',
            'articles': articles_list,
            'sync_time': time.mktime(mp.sync_time.timetuple()) + 3600*8 # GMT+8 #建议用 time.time()代替!
        }
    #   print articles_list
        return jsonify(rsp)
    else:
        rsp = {
            'status': 'mp not found!'
        }
        return jsonify(rsp)

好了,数据取回来了,路由也跳转了,显示公众号文章吧。
articleList用计算属性,读取LocalStorage中的值。
TODO: use vuex, 从store中取出数据

/src/components/Article.vue

       computed : {
            articleList() {
                // TODO: use vuex, 从store中取出数据
                var data = JSON.parse(window.localStorage.getItem('weixinhao_'+this.$route.params.id));
            if (data == null) return {'mpName':'', 'articles': [] };
                else {
                        return data;
                    }
            }

3. 点击顶部菜单(订阅文章),左侧显示所有公众号的文章列表,按更新时间排列

如果想查看所有的公众号的文章列表,则先在顶部菜单条上添加路由

/src/App.vue

          <li class="nav-item">
            <router-link to="/articles" class="nav-link"><i class="fa fa-flag"></i>订阅文章</router-link>
          </li>

这个是总体显示,逻辑比较简单,也是用计算属性读取所有文章,再按发表时间,排一下序就行

/src/components/Articles.vue

computed : {
            articleList() {
                // TODO: use vuex, 从store中取出数据
                var storage = window.localStorage, data=[], mpName, articles;
             for(var i=0;i<storage.length;i++){
              //key(i)获得相应的键,再用getItem()方法获得对应的值
              if (storage.key(i).substr(0,10) == 'weixinhao_') {
                  mpName = JSON.parse(storage.getItem(storage.key(i))).mpName;
                  articles = JSON.parse(storage.getItem(storage.key(i))).articles
                for (let item of articles) {
                    item['mpName'] = mpName
                    data.push(item)
                }
                }
            }
            // 对所有文章按更新日期排序
            data.sort(function(a,b){
                    return b.timestamp-a.timestamp});
                    return data;
            }

4. 左侧显示某一公众号文章列表时,点击更新,可以检查是否有最新发表的公众号文章

大家注意到,我们的公众号文章,第一次是在上传订阅列表时更新的。后续再次更新的话,可以由用户来触发。
我们带入action: 'sync'参数,通知服务器,同步更新就行,不需要异步,本地LocalStorage里,已经有历史数据。

/src/components/Articles.vue

     methods:{
    updateArticle(weixinhao, mpName) {
            this.isFetching = true;
            this.$nextTick(function () { });
            this.$http.get('/api/v1.0/articles', {
                params: {
                            weixinhao: weixinhao,
                            action: 'sync'
                    },
                    headers: {
                        'Content-Type': 'application/json; charset=UTF-8',
                        'Authentication-Token': this.token
                    }
                }).then((response) => {
                    // 响应成功回调
                this.isFetching = false;
                   var data = response.body, article_data;
//                   alert('文章 from server:\n' + JSON.stringify(data));
                    if (! data.status == 'ok') {
                return alert('获取失败,请重新上传订阅列表!\n' +data.status)
                    }
            article_data = {
                'mpName': mpName,
                'weixinhao': weixinhao,
                'articles': data.articles,
                'sync_time': data.sync_time
                }
            window.localStorage.setItem('weixinhao_'+weixinhao, JSON.stringify(article_data));
            // TODO: 这里可能用 vuex更好一点
                }, (response) => {
                    // 响应错误回调
                    alert('同步出错了! ' + JSON.stringify(response))
                    if (response.status == 401) {
                        alert('登录超时,请重新登录');
                        this.is_login = false;
                        this.password = '';
                        window.localStorage.removeItem("user")
                    }
                });
           },      

服务器端,检查上次这个公众号更新时间,少于5分钟,则不更新,以免太频繁,给sogou.com封掉

/app/api_1_0/mps.py

       if request.args.get('action') == 'sync':
            print '================sync'
            if datetime.utcnow() - mp.sync_time > timedelta(seconds=60*5):
                [ok, return_str] = fetchArticle(mp, 'sync')
                print ok, return_str

爬虫函数,看到参数是sync的话,就不再使用Thead异步抓取了,而是同步等待爬虫结果。客户会看到动态的同步图标,直到更新完毕。

/app/api_1_0/fetchArticles.py

        if sync == 'async':
            thr = Thread(target=article_search, args=[app, db, mp.weixinhao])
            thr.start()
            return ['ok', return_str]
        else:
            article_search(app, db, mp.weixinhao)
            return ['ok', u'同步完成!']

好了,总算从头到尾,完整地做完一个项目了!
其实,这只是一个框架,把前端、后端、数据爬取等等,都跑通了一遍而已!如果是真正的项目,需要完善的东东很多!

大家踊跃评论哦!评论满100上源码 ;-)
(笑话而已,已经上传了,自己找找哦)

DEMO: http://vue2.heroku.com

部署时要注意:

  1. 数据库更新了,heroku run bash -> python manage.py clear_A -> python manage.py deploy
  2. heroku.com dashboard里,加上一个系统变量 WXUIN,转成文章永久链接用。如何计算,谷歌之
  3. requirement.txt里,加上lxml==3.6.4,给BeatuifulSoup用

TODO:

  • 用移动端UI来改写UI,适应手机访问
  • 同级组件的数据共享,比如articleList,最好统统用vuex来访问,LocalStorage有时会有不同步刷新的小bug
  • 添加功能:删除已订阅的公众号
  • 后台管理Flask-Admin,普通用户也可以查看部分内容(现在只有admin有权限)
  • Bootstrap v4 alpha6发布了,UI有些改变,需要更新 main.js,重新cnpm i
  • Articles页面,数据多了要分页
  • Copyright div,build之后不显示?需要手动放在 </body>之前?

祝大家在新的一年:红红火火!工作进步!学有所长!

http://www.jianshu.com/p/ab778fde3b99

https://github.com/kevinqqnj/vue-tutorial
请使用新的template: https://github.com/kevinqqnj/flask-template-advanced

2019延伸阅读推荐:带你进入异步Django+Vue的世界 - Didi打车实战(1)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容