两周前,签名图的访问量自从有记录以来突破了一万次,为纪念访问量突破一万次,重写了大部分代码,并写了这篇搭建教程。

基本原理

原理并不复杂,浏览器向服务器请求图片时,浏览器会向服务器提供诸如IP地址、浏览器版本和访问时间等基本信息,服务器可以利用这些信息,生成相关的图片返回给浏览器,这样浏览器会收到一张写有用户端信息的独一无二的图片。如果这张图片的url被放在论坛的签名档里面,所有看到签名档的用户就会看到写有自己IP地址及相关信息的签名图。

签名图样式
签名图样式

实现过程

要实现这个过程并不复杂,我就采用最熟悉的flask框架来做这件事。

首先是获取浏览器的基本信息(IP地址、UA等),在flask框架下获取相关信息很容易:

from flask import (Flask,request)
app = Flask(__name__)
@app.route('/')
def index():
    dog_ip = request.remote_addr#获取IP地址
    dog_ua = request.user_agent#获取浏览器UA
    return [dog_ip,dog_ua]

获取了这两个基本信息之后,需要分别解析出浏览器的版本,所在的操作系统与ip地址所对应的物理地址。获取ip地址相关信息有很多方法,这里采用,采用https://ip.zxinc.org/api.php 的 ip 地址 api。调用方法如下:

import requests

def get_ip_info(dogip):
    canshu = {'ip': dogip, 'type': 'json'}
    r = requests.get('https://ip.shanshan-business.com/api.php', params=canshu)
    resd = (r.content.decode('utf8'))
    res = json.loads(resd[1:])
    if res['code'] != 0:
        fu = [' ', ' ']
        return fu
    dog_location = res['data']['location']
    dog_ip_fw = '{} - {}'.format(res['data']['ip']['start'], res['data']['ip']['end'])
    return [dog_location, dog_ip_fw]

这个api会返回两个信息,分别是对应地理位置以及IP段范围。

另个关键点是解析解析浏览器的UA,即通过UA字段解析出客户端所用的浏览器、操作系统以及设备,这里采用的是user_agents这个库:

from user_agents import parse
dog_ua = request.user_agent
dog_uap = parse(str(dog_ua))
dog_os = dog_uap.get_os()#获取操作系统信息
dog_browser = dog_uap.get_browser()#获取浏览器信息
dog_device = dog_uap.get_device()#获取设备信息

获取这两个信息之后就可以开始画出图形了。

首先应该准备一个适当大小的背景图片,用来充当签名图的基底,打开这个文件应该在初始化阶段,将文件内容读入到内存中,当浏览器请求签名图的时候再把这个图片在内存中复制一遍,在副本的基础上进行画图,使得整个过程可以可持续进行。

为了配合flask输出文件,需要把这个文件以字符串的形式在python中保存与处理,这需要io这个库

basdog = Image.open('bd.png', 'r')
def dog_pic(ip, refer, ua):
    [dog_location, dog_ip_fw] = get_ip_info(ip)#获取ip地址相关信息
    dog_uap = parse(str(ua))
    dog_os = dog_uap.get_os()
    dog_browser = dog_uap.get_browser()
    dog_device = dog_uap.get_device()
    dog_num = get_number()
    #生成相关文本
    dog_text_1 = '只争朝夕,不负韶华。IP段 : {}'.format(dog_ip_fw)
    dog_text_2 = 'IP地址: {}  {}'.format(ip, dog_location)
    dog_text_3 = '{}'.format(refer)
    dog_text_4 = '浏览器: {} OS: {} 设备: {}'.format(
        dog_browser, dog_os, dog_device)
    dog_text_5 = '已被访问{}次  北京时间:{}'.format(dog_num, time.ctime())
    dog_text_6 = '为庆祝有记录以来访问量已经突破十万次,本签名图即将改版!'
    # 开始画图了
    image = basdog.copy()#复制基底片
    #选择字体
    font1 = ImageFont.truetype('/home/yu/.local/share/fonts/仿宋_GB2312.ttf', 14)
    font2 = ImageFont.truetype('/home/yu/.local/share/fonts/仿宋_GB2312.ttf', 20)
    draw = ImageDraw.Draw(image) #获取画笔
    #文字写入对应位置
    draw.text((10, 30), dog_text_1, font=font2, fill=(255, 0, 0))
    draw.text((10, 70), dog_text_2, font=font2, fill=(0, 0, 0))
    draw.text((10, 110), dog_text_3, font=font1, fill=rndColor2())
    draw.text((10, 150), dog_text_4, font=font2, fill=rndColor2())
    draw.text((10, 180), dog_text_5, font=font2, fill=rndColor2())
    draw.text((10, 210), dog_text_6, font=font1, fill=rndColor2())
    buf = io.BytesIO() # 字符串文件
    image.save(buf, 'jpeg') #将画好的图片保存
    image.close()
    # 转化成flask的返回格式
    buf_str = buf.getvalue()
    response = app.make_response(buf_str)
    response.headers['Content-Type'] = 'image/jpeg'
    return response

访问量统计

访问量统计功能并不十分复杂,核心思路就是在签名图被请求后,每调用一次一次生成图片的函数一次,就对统计访问变量加一。在固然可以在python里面用一个全局变量,但考虑到需要持久化的记录,使得图片生成与访问量统计进行解耦,以及后期方便进行升级改造,可以通过数据库来保存这个访问量统计。可以采用的数据库很多,但这里我们采用redis这个NoSQL的数据库。首先,访问量统计所存储的数据量很少,就用到一个整数,没有必要用关系数据库,其次redis是一个高性能的数据库,所有的数据都是放在内存之中,读写速度很快,占用资源又很少。(据说,某超大型的售票网站的部分后台数据就放在redis里)

redis是一个键值对数据库,关于redis的相关内容网上有很多资料与文档,这里就不再多说了。默认你们已经会了

在flask里面使用redis也非常方便,有专门flask_redis已经都安排好了,直接用就行了

from flask_redis import FlaskRedis
from flask import (Flask, flash, redirect, render_template, request,send_from_directory, url_for)
app = Flask(__name__)
app.config['REDIS_URL']="redis://:password@127.0.0.1:7098/0" 
#设置所连接的数据库参数,斜杠后面的数字是redis的数据库编号
rc=FlaskRedis(app,decode_responses=True)

def get_number():
    dog_number=rc.incr('dogg')
    #上面的函数是对dogg这个键的值进行+1操作,并返回这个键的数值
    return dog_number

在get_number()安排好之后,只需要在生产图片的函数里面调用一下,既可以返回当前的访问量,又可以对访问量进行+1操作,非常方便。

随机图API

这个功能是最近新加上的,涉及到flask的部分很简单,同时也需要redis的配合。在redis里面有一种数据结构是无序集合,redis有一种方法调用之后可以随机返回该集合之中的一个值。这样可以利用redis的集合存储一些图片的url,浏览器访问时,从集合之中随机取出一个图片的url,利用flask返回带有新的url的302重定向信息,这样就可以实现随机图片的功能了。代码并不复杂。

@app.route('/r/')
def rpic():
    dog_url=rc.srandmember('piclist')
    return redirect(dog_url,code=302)

@app.route('/r2/')
def rpic2():
    dog_url=rc.srandmember('piclist2')
    return redirect(dog_url,code=302)

其中piclist和piclist2是两个存储好图片的redis集合。

通过nginx进行部署

在进行开发时,固然可以用 python xx.py 的方式进行试用,但一旦需要投入到生产环境当中则需要nginx进行反向代理。为了实现flask框架与nginx等方向代理的互联互通,则需要WSGI作为中间的"接口"进行驳接,这里使用了通常采用的uwsgi来作为nginx与flask框架间的桥梁。uwsgi用起来很方便,既可以用命令行的形式,又可以通过配置文件。首先准备一个ini格式的配置文件。

[uwsgi]
module = tu:app
master = true
processes = 8
plugins = python3
pythonpath = /home/yu/signpic/
chdir = /home/yu/signpic/
socket = /home/yu/signpic/dog.sock
chmod-socket = 777

首先第一个module里面是python脚本文件与flask框架app的名称,如果按照flask文档来一般就叫app,如果改了其他名称就把冒号后面的做相应变化即可。后面的plugins是启动所用的程序,flask框架是基于python的,而且现在基本上就是python3了,直接写python3就行了。后面几个是执行脚本时的工作目录,一般填写脚本文件所在目录即可。socket文件是后面与nginx对接时所用到的插槽文件,后面的777是插槽文件的权限。

nginx的配置文件也不复杂,除去基本的通用配置之外也没别的了。

server {
    listen 443 ssl http2;
    ssl_certificate /root/key/dog.pem;
    ssl_certificate_key /root/key/dog.key;
    root /home/yu/signpic;
    server_name sig.dogcraft.top sigd.dogcraft.top;
    access_log /var/log/nginx/bbs.log;
    access_log /var/log/nginx/access.log vcombined;
    location / {
        include uwsgi_params;
        uwsgi_send_timeout 600;        # 指定向uWSGI传送请求的超时时间,完成握手后向uWSGI传送请求的超时时间。
        uwsgi_connect_timeout 600;   # 指定连接到后端uWSGI的超时时间。
        uwsgi_read_timeout 600;
        uwsgi_pass unix:/home/yu/signpic/dog.sock;
    }

    location /favicon.ico {
        rewrite ^(.*)$ https://www.dogcraft.top/favicon.ico;
    }
}

其中uwsgi_pass unix:/home/yu/signpic/dog.sock;要与uwsgi里面的一致,nginx的配置文件一般在/etc/nginx/sites-enabled/里面,安排好之后重启nginx即可。

然后再启动uwsgi

uwsgi -i conf.ini

其中conf.ini是配置文件,这个时候访问对应的url应该没问题了。

最后需要把uwsgi安排到后台启动的系统服务。

到/etc/systemd/system下面新建一个sigpic.service

[Unit]
Description=Sigpic daemon

[Service]
Type=simple
User=yu     
ExecStart=/usr/bin/uwsgi -i /home/yu/signpic/si.ini    
WorkingDirectory=/home/yu/signpic/         
TimeoutSec=60
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=wx     
Restart=always

[Install]
WantedBy=multi-user.target

最后

sudo systemctl daemon-reload
sudo systemctl start sigpic

就可以了。

完整代码:


#!python3

import io
import json
import random
import time
import requests
from flask import (Flask, flash, redirect, render_template, request,send_from_directory, url_for)
from PIL import Image, ImageDraw, ImageFont
from flask_redis import FlaskRedis
from user_agents import parse
app = Flask(__name__)
app.config['REDIS_URL']="redis://:password@127.0.0.1:7091/0"
rc=FlaskRedis(app,decode_responses=True)
basdog=Image.open('/home/yu/signpic/bd.png','r')

def get_number():
    dog_number=rc.incr('dogg')
    return dog_number
    
def rndColor2():
    return (random.randint(1, 144), random.randint(1, 120), random.randint(1, 100))

def get_ip_info(dogip):
    canshu={'ip':dogip,'type':'json'}
    r=requests.get('https://ip.shanshan-business.com/api.php',params=canshu)
    resd=(r.content.decode('utf8'))
    res=json.loads(resd[1:])
    if res['code']!=0:
        fu=[' ',' ']
        return fu
    dog_location=res['data']['location']
    dog_ip_fw='{} - {}'.format(res['data']['ip']['start'],res['data']['ip']['end'])
    return [dog_location,dog_ip_fw]

def dog_pic(ip,refer,ua):
    """
    docstring
    """
    [dog_location,dog_ip_fw]=get_ip_info(ip)
    dog_uap=parse(str(ua))
    dog_os=dog_uap.get_os()
    dog_browser=dog_uap.get_browser()
    dog_device=dog_uap.get_device()
    dog_num=get_number()
    dog_text_1='只争朝夕,不负韶华。IP段 : {}'.format(dog_ip_fw)
    dog_text_2='IP地址: {}  {}'.format(ip,dog_location)
    dog_text_3='{}'.format(refer)
    dog_text_4='浏览器: {} OS: {} 设备: {}'.format(dog_browser,dog_os,dog_device)
    dog_text_5='已被访问{}次  北京时间:{}'.format(dog_num,time.ctime())
    dog_text_6='为庆祝有记录以来访问量已经突破十万次,本签名图即将改版!'
    #开始画图了
    image = basdog.copy()
    font1 = ImageFont.truetype('/home/yu/.local/share/fonts/仿宋_GB2312.ttf', 14)
    font2 = ImageFont.truetype('/home/yu/.local/share/fonts/仿宋_GB2312.ttf', 20)
    draw = ImageDraw.Draw(image)
    draw.text((10, 30), dog_text_1, font=font2, fill=(255,0,0))
    draw.text((10, 70), dog_text_2, font=font2, fill=(0,0,0))
    draw.text((10, 110), dog_text_3, font=font1, fill=rndColor2())
    draw.text((10, 150), dog_text_4, font=font2, fill=rndColor2())
    draw.text((10, 180), dog_text_5, font=font2, fill=rndColor2())
    draw.text((10, 210),dog_text_6, font=font1, fill=rndColor2())
    buf = io.BytesIO()
    image.save(buf, 'jpeg')
    image.close()
    buf_str = buf.getvalue()
    response = app.make_response(buf_str)
    response.headers['Content-Type'] = 'image/jpeg'
    return response

@app.route('/')
def index():
    dog_ip=request.remote_addr
    dog_ua=request.user_agent
    dog_uap=parse(str(dog_ua))
    dog_os=dog_uap.get_os()
    dog_browser=dog_uap.get_browser()
    dog_device=dog_uap.get_device()
    dog_refer=request.referrer
    [dog_location,dog_ip_fw]=get_ip_info(dog_ip)
    dog_num=get_number()
    dog_text='只争朝夕 不负韶华  <br>{}  {} <br>Hits: {}<br>浏览器: {} OS: {} 设备: {}<br>{}<br>{}\n'.format(dog_ip,dog_location,dog_num,dog_browser,dog_os,dog_device,dog_refer,dog_ip_fw)
    return dog_text

@app.route('/img.jpg')
def img():
    return dog_pic(str(request.remote_addr),str(request.referrer),request.user_agent)

@app.route('/r/')
def rpic():
    dog_url=rc.srandmember('piclist')
    return redirect(dog_url,code=302)

@app.route('/r2/')
def rpic2():
    dog_url=rc.srandmember('piclist2')
    return redirect(dog_url,code=302)


if __name__ == "__main__":
    app.run(debug=True)