使用Flask构建网站流量分析应用

这篇文章我会展示怎样使用 Flask 来构建一个轻量的统计分析服务,由于原文使用的是peewee和SqliteDatabase,我是使用的是flask-sqlalchemy和mysql,数据库操作有所不同。

分析请求/响应流程

我们将要构建的分析服务有点类似 Google Analytics (更像是个简化版)

工作流程:

  • 每个要被跟踪的页面都会使用 <script> 标签引入一个 JavaScript 文件,这个文件由我们的应用(例如,放在被分析网站的基础模板中)
  • 当有人访问你的网站的时候,他们的浏览器会执行这个 JavaScript 文件
  • JavaScript 中的代码可以读取当前网页的标题,URL,还有其他感兴趣的元素。
  • 最酷的地方是,这个脚本会动态的创建一个 <img> 标签(一般来说是个1*1px的空白图片),这个标签的 src 属性的 URL 正式指向我们的分析应用
  • 当前页面中的信息收集完毕并且编码之后就会设置成图片的 src 属性,我们的分析服务端就会收到并解析这些信息
  • 分析服务解析完成,把这条信息存入数据库,然后返回一个1px的gif图片

下面是交互图:
pic1

设计考虑

因为要在资源有限的 VPS(可以认为是配置非常低的服务器) 上运行, 我的网站也没有多少流浪,所以要轻量,灵活。不管什么类型的项目我都喜欢使用 Flask,这个项目更是如此。 我们将使用 sqlalchemy ORM来存储PV(页面访问数据) 和查询分析数据。 告诉大家吧,我们的应用不会超过100行代码(包括注释)。

  • PV (page view)页面访问

数据库相关

为了能方面的查询数据,我们将使用关系型数据库存储PV数据。 我选择了 mysql5.7,因为它是个比较通用,方便生成环境 移植。

WSGI Server

虽然有非常多的选择,但是我还是比较喜欢使用 gevent。 Gevent 是一个基于协程的网络库,使用了 libev 的事件机制来实现轻线程(greenlets)。 通过使用 monkey-patching ,不需要特殊的API或者是语法,gevent 会把正常阻塞的 python 程序变成非阻塞的。 Gevent 的 WSGI server,尽管非常基础,但是有着非常高的性能和非常低的资源消耗。 和数据库一样,如果你使用别的库比较顺手,请自由选择。

创建 venv

我们先给这个分析应用创建一个隔离的环境,安装上 flaskpeewee(选择安装gevent).

➜  ~ mkdir analytics
➜  ~ cd analytics
➜  analytics python -V
Python 3.7.0
➜  analytics python -m venv venv
➜  analytics source venv/bin/activate
(venv) ➜  analytics pip install flask flask-sqlalchemy PyMySQL
...
...
Successfully installed Jinja2-2.10 MarkupSafe-1.0 PyMySQL-0.9.2 SQLAlchemy-1.2.10 Werkzeug-0.14.1 asn1crypto-0.24.0 cffi-1.11.5 click-6.7 cryptography-2.3 flask-1.0.2 flask-sqlalchemy-2.3.2 idna-2.7 itsdangerous-0.24 pycparser-2.18 six-1.11.0


(venv) ➜  analytics pip install gevent    # Optional.

实现Flask应用

让我们从整体代码框架开始吧。 正如前面讨论的,我们将会创建2个 view,一个用来返回 JavaScript 文件,另一个用来创建1px的GIF图片。 在 analytics目录(这是作者开发使用的目录,没有请自行创建),创建 analytics.py 文件,代码在下面列出。 这些代码包括了应用的代码结构,还有基本配置。

#coding:utf-8
import datetime
import os
from base64 import b64decode
from urllib.parse import parse_qsl, urlparse

from flask import Flask, Response, abort, request, render_template
from flask_sqlalchemy import SQLAlchemy


app = Flask(__name__)

# 1 pixel GIF, base64-encoded.
app.config['BEACON'] = b64decode('R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==')

app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://username:[email protected]:3306/database'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['DOMAIN'] = 'http://127.0.0.1:5000'  # TODO: change me.


# Simple JavaScript which will be included and executed on the client-side.
app.config['JAVASCRIPT'] = '' # TODO: add javascript implementation.

# Flask application settings.
app.config['DEBUG'] = bool(os.environ.get('DEBUG'))
app.config['SECRET_KEY'] = 'secret - change me'  # TODO: change me.

db = SQLAlchemy(app)


class PageView(db.Model):
    # TODO: add model definition.
    pass


@app.route('/a.gif')
def analyze():
    # TODO: implement 1pixel gif view.
    pass


@app.route('/a.js')
def script():
    # TODO: implement javascript view.
    pass


@app.errorhandler(404)
def not_found(e):
    return Response('Not found.')


if __name__ == '__main__':
    app.run()

从浏览器获取信息

我们开始写收集客户端信息的 JavaScript 文件, 主要就是抽取一些页面基本信息

  • URL信息,包括查询信息(document.location.href)
  • 页面的title (document.title)
  • 页面的feferring信息,如果有的话 (document.referrer)

下面的一些属性也可以提取出来,如果你感兴趣的话(我:其实可能比列出的多的多,特别是H5),例如:

  • cookie信息(document.cookie)
  • 文档的最后修改时间(document.lastModified)
  • 更多

提取了这些信息之后,我们通过url的查询字符串传给 analyze 这个view。 为了简单,这个 js 将在页面加载的时候立即触发,我们把所有的代码封装到一个匿名函数中。 最后使用 encodeURIComponent 方法对所有参数进行转义,确保所有参数安全:

(function() {
  var img = new Image,
      url = encodeURIComponent(document.location.href),
      title = encodeURIComponent(document.title),
      ref = encodeURIComponent(document.referrer);
  img.src = '%s/a.gif?url=' + url + '&t=' + title + '&ref=' + ref;
})();

我们预留Python中的占位符 %s,用来读取 DOMAIN 配置并插入到其中,组成完整的JS代码

在py文件中我们定义一个 JAVASCRIPT 变量来存储上面的 js代码:

# Simple JavaScript which will be included and executed on the client-side.
JAVASCRIPT = """(function(){
    var d=document,i=new Image,e=encodeURIComponent;
    i.src='%s/a.gif?url='+e(d.location.href)+'&ref='+e(d.referrer)+'&t='+e(d.title);
    })()""".replace('\n', '')

在view中这样处理

@app.route('/a.js')
def script():
    return Response(
        app.config['JAVASCRIPT'] % (app.config['DOMAIN']),
        mimetype='text/javascript')

保存PV信息

上面的脚本将会传3个值给 analyze 这个view, 包括页面的URL,title,和 referring page。 现在我们来定义一个 PageView 模型来存储这些数据。

在服务端,我们也可以读取到 访问者的IP和请求头信息,所以我们也为这些信息创建字段,还要加上请求的时间戳字段。

因为每个浏览器有着不同的请求头,每个页面请求的查询参数也不尽相同,我们将把他们用 JSON 格式存储到 JSON字段中。 如果你使用 Postgresql, 可以用 HStore 或者 native JSON data-type

下面是 PageView 模型的定义,JSON 用来存储 查询参数和请求头信息:

class PageView(db.Model):
    __tablename__ = 'page_views'

    id = db.Column(db.Integer, primary_key=True)
    domain = db.Column(db.String(255))
    url = db.Column(db.String(255))
    timestamp = db.Column(db.DateTime, default=datetime.datetime.utcnow)
    title = db.Column(db.String(255), default='')
    ip = db.Column(db.String(255), default='')
    referrer = db.Column(db.String(255), default='')
    user_agent = db.Column(db.String(255), default='')
    headers = db.Column(db.JSON)
    params = db.Column(db.JSON)

进入python创建表 

# python3
>>> from analytics import db
>>> db.create_all()

接着我们给 PageView 添加一个方法,让它可以从请求中提取所有需要的值,并存到数据库。 urlparse 模块中包含了很多提取 request 信息的方法,我们用这些方法来获取 访问URL和请求参数:

class PageView(Model):
    # ... field definitions ...

    @classmethod
    def create_from_request(cls):
        page_view = PageView()
        parsed = urlparse(request.args['url'])
        params = dict(parse_qsl(parsed.query))
        user_agent = request.user_agent

        page_view.domain = parsed.netloc,
        page_view.url = parsed.path,
        page_view.title = request.args.get('t') or '',
        page_view.ip = request.headers.get('X-Forwarded-For', request.remote_addr),
        page_view.referrer = request.args.get('ref') or '',
        page_view.user_agent = request.user_agent.browser,
        page_view.headers = dict(request.headers),
        page_view.params = params

        db.session.add(page_view)
        db.session.commit()

先我们给 PageView 添加一个方法,让它可以从请求中提取所有需要的值,并存到数据库。 urlparse 模块中包含了很多提取 request 信息的方法,我们用这些方法来获取 访问URL和请求参数:

analyze view 最后一步是返回一个1px的GIF图片,为了安全起见,我们将检查下 URL是否存在,确保不会再数据库插入一个空白记录。

@app.route('/a.gif')
def analyze():
    if not request.args.get('url'):
        abort(404)

    PageView.create_from_request()

    response = Response(app.config['BEACON'], mimetype='image/gif')
    response.headers['Cache-Control'] = 'private, no-cache'
    return response

启动应用

这个时候如果你想测试下应用,可以先在命令行设置下 DEBUG=1 来启动debug模式

(venv) ➜  analytics DEBUG=1 python analytics.py
 * Serving Flask app "analytics" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 301-867-887

访问 http://127.0.0.1:5000/a.js 可以看到js文件。 如果本地有其他的web应用,可以把这段js嵌入到它的页面中,测试下 分析应用

<script src="http://127.0.0.1:5000/a.js" type="text/javascript"></script>

<!-- 如果是在自己后台布置 -->

<script src="{{ url_for('script') }}" type="text/javascript"></script>


查询数据

真正感觉到快乐的是收集了几天数据,查询的时候。这部分我们将看看怎样从收集的数据中查询出来一些有意思的信息。

下面使用我博客的数据,我们将会对最近7天的数据进行一些查询

>>> from analytics import *
>>> import datetime
>>> week_ago = datetime.date.today() - datetime.timedelta(days=7)
>>> base = db.session.query(PageView).filter(PageView.timestamp >= week_ago)

首先,我们来看看过去一周的PV

>>> base.count()
1150

有多少不同的IP访问过我的网站

>>> db.session.query(PageView.ip).filter(PageView.timestamp >= week_ago).group_by(PageView.ip).count()
850

访问最多的10个页面?

>>>  print(db.session\
     .query(PageView.title, func.Count(PageView.id))\
     .group_by(PageView.title)\
     .order_by(func.Count(PageView.id)\
     .desc()).limit(10).all())

# Prints...
[('Postgresql HStore, JSON data-type and Arrays with Peewee ORM',
  88),
 ("Describing Relationships: Django's ManyToMany Through",
  73),
 ('Using python and k-means to find the dominant colors in images',
  66),
 ('SQLite: Small. Fast. Reliable. Choose any three.', 58),
 ('Using python to generate awesome linux desktop themes',
  54),
 ("Don't sweat the small stuff - use flask blueprints", 51),
 ('Using SQLite Full-Text Search with Python', 48),
 ('Home', 47),
 ('Blog Entries', 46),
 ('Django Patterns: Model Inheritance', 44)]

哪些 user-agents 最流行呢?

>>> from collections import Counter
>>> c = Counter(pv.headers[0]['User-Agent'] for pv in base)
>>> print(c.most_common(5))
[(u'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36',
  81),
 (u'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36',
  70),
 (u'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:32.0) Gecko/20100101 Firefox/32.0',
  50),
 (u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.78.2 (KHTML, like Gecko) Version/7.0.6 Safari/537.78.2',
  37),

直接获得浏览器

>>> c = Counter(pv.query.get('user_agent') for pv in base)
>>> print(c.most_common(5))
[(u'Chrome', 151), (u'Firefox',50),(u'Safari',37),]

你想要什么数据全看你。

提升应用的想法

  • 建立一个web接口或者是API来查询pv数据
  • 使用表或者是类似 Postgresql HStore 来标准化请求头数据
  • 收集用户cookies 跟踪用户访问路径
  • 使用 GeoIP 来确定用户的地理位置
  • 使用 canvas 指纹来更好的确定用户的唯一性
  • 更多更酷的查询来研究数据

原文Saturday morning hacks: Building an Analytics App with Flask 

附录

#coding:utf-8
import datetime
import os
from base64 import b64decode
from urllib.parse import parse_qsl, urlparse

from flask import Flask, Response, abort, request, render_template
from flask_sqlalchemy import SQLAlchemy


app = Flask(__name__)

# 1 pixel GIF, base64-encoded.
app.config['BEACON'] = b64decode('R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==')

app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://username:[email protected]:3306/database'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['DOMAIN'] = 'http://127.0.0.1:5000'  # TODO: change me.

# Simple JavaScript which will be included and executed on the client-side.
app.config['JAVASCRIPT'] = """(function(){
    var d=document,i=new Image,e=encodeURIComponent;
    i.src='%s/a.gif?url='+e(d.location.href)+'&ref='+e(d.referrer)+'&t='+e(d.title);
    })()""".replace('\n', '')

# Flask application settings.
app.config['DEBUG'] = bool(os.environ.get('DEBUG'))
app.config['SECRET_KEY'] = 'secret - change me'  # TODO: change me.

db = SQLAlchemy(app)


class PageView(db.Model):
    __tablename__ = 'page_views'

    id = db.Column(db.Integer, primary_key=True)
    domain = db.Column(db.String(255))
    url = db.Column(db.String(255))
    timestamp = db.Column(db.DateTime, default=datetime.datetime.utcnow)
    title = db.Column(db.String(255), default='')
    ip = db.Column(db.String(255), default='')
    referrer = db.Column(db.String(255), default='')
    user_agent = db.Column(db.String(255), default='')
    headers = db.Column(db.JSON)
    params = db.Column(db.JSON)

    def __repr__(self):
        return "<Model PageView `{}`>".format(self.domain)

    @classmethod
    def create_from_request(cls):
        page_view = PageView()

        parsed = urlparse(request.args['url'])
        params = dict(parse_qsl(parsed.query))
        print(request.user_agent.browser)

        page_view.domain = parsed.netloc,
        page_view.url = parsed.path,
        page_view.title = request.args.get('t') or '',
        page_view.ip = request.headers.get('X-Forwarded-For', request.remote_addr),
        page_view.referrer = request.args.get('ref') or '',
        page_view.headers = dict(request.headers),
        page_view.user_agent = request.user_agent.browser,
        page_view.params = params

        db.session.add(page_view)
        db.session.commit()


@app.route('/')
def index():
    return render_template('index.html')


@app.route('/a.gif')
def analyze():
    if not request.args.get('url'):
        abort(404)

    PageView.create_from_request()

    response = Response(app.config['BEACON'], mimetype='image/gif')
    response.headers['Cache-Control'] = 'private, no-cache'
    return response


@app.route('/a.js')
def script():
    return Response(
        app.config['JAVASCRIPT'] % (app.config['DOMAIN']),
        mimetype='text/javascript')


@app.errorhandler(404)
def not_found(e):
    return Response('Not found.')


if __name__ == '__main__':
    app.run()

猜你喜欢

转载自blog.csdn.net/gh254172840/article/details/81291613