python实战博客

2018-10-31 更新Logging日志记录以及异常捕获

感谢廖大教程。Python实战

直接在闲置的服务器上开发。阿里云Centos 6.8 64位

1 搭建开发环境

Python 环境是Python 3.4, 在装aiohttp的时候报错,之前用pip3和系统本身的pip(Python 2.7)也有安装问题,索性下最新版Python 3.6.4,并安装virtualenv独立运行。

python3.3以后自带venv模块支持轻量级虚拟环境,virtualenv模块仍然支持,可安装。

1.创建虚拟环境
virtualenv --no-site-packages myvenv
等价于
virtualenv myvenv (目前新版默认不使用系统环境包)

python3自带venv
python -m venv myvenv
也是默认全新干净的环境,相反可选的参数
python -m venv --system-site-packages myvenv
使虚拟环境指向系统环境包目录(非复制),在系统环境pip新安装包,在虚拟环境就可以使用。

2.激活虚拟环境

Platform    Shell        Command to activate virtual environment
Posix        bash/zsh    $ source <venv>/bin/activate
            fish        $ . <venv>/bin/activate.fish
            csh/tcsh    $ source <venv>/bin/activate.csh
Windows        cmd.exe        C:> <venv>\Scripts\activate.bat
            PowerShell    PS C:> <venv>\Scripts\Activate.ps1

3.关闭虚拟环境
deactivate

4.删除虚拟环境
删除目录即可

-by 林er爱喝果汁Q  https://www.liaoxuefeng.com/discuss/001409195742008d822b26cf3de46aea14f2b7378a1ba91000/00150035599472221b683bc9ae245c4a08097bd0cc7866c000

所以直接运行:

> python3.6 myvenv
> source myvenv/bin/activate

之后就可以直接用Python命令而非Python3.6来指定使用版本了。

2 编写Web App骨架

原文中监听127.0.0.1:9000,本地测试直接打开即可,可是服务器怎么看页面呢。安装有nginx,但是配置太麻烦。想要快速查看页面。

服务器直接访问

阿里ECS管理页面中有公网和私网IP

公网私网IP

在代码中重新监听私有IP的9000端口,然后访问公网IP:9000,无效。

将端口加入到安全组

端口加入到安全组

之后,再次访问公网IP:9000。就成功了,不过是下载的形式。加上Content-Type即可:

return web.Response(body=b'Awesome', headers={'content-type':'text/html'})
ERROR: address already in use

Ctrl+Z结束正在运行的Python进程后,再次python app.py出错

error while attempting to bind on address ('172.*.*.*', 9000): address already in use
原因和解决办法
在ubuntu下,这个问题通常由于按ctrl+z结束程序造成。使用fg命令之后,按ctrl+c重新结束任务即可。

CTRL-Z和CTRL-C都是中断命令,但是他们的作用却不一样.
CTRL-C是强制中断程序的执行,
而CTRL-Z的是将任务中断,但是此任务并没有结束,他仍然在进程中他只是维持挂起的状态,用户可以使用fg/bg操作继续前台或后台的任务,fg命令重新启动前台被中断的任务,bg命令把被中断的任务放在后台执行.
例如:
当你vi一个文件是,如果需要用shell执行别的操作,但是你又不打算关闭vi,因为你得
存盘推出,你可以简单的按下CTRL-Z,shell会将vi进程挂起~,当你结束了那个shell操作之后,你可以用fg命令继续vi你的文件.

-by http://blog.csdn.net/helinbin/article/details/56015572

用了fg之后,确实又启动可以访问了:),用Ctrl+C结束后再次运行就没有地址占用错误提醒了。

##### 弃坑
3节ORM 和 5节的Web框架。都比较难。而且对比GIT中的多个不明所以的函数后面的继续不下去了。虽然很想粗略的过一遍。但是算了,承认自己比较弱也是和自己的一种妥协。协程和装饰器还是理解的慢。以后再来补,一个月后再来挑战。 2018-3-1 17:48:24

##### 再次开始
2018年9月17日

服务器再次清零(试过好多东西,内部git、laravel、练手的php框架),重新安装Python,只当作Python工具服务器用,只用Python实现,比如,想做一个距离懂你课程的倒计时来用退款鼓励自己。
`腾讯云Centos 7.5 64位`


总之,现在做什么事情,就是要快!

网上各种资料一查,就必须要尽快搞定。

我也总是目标不明确,本来做A,中途遇到问题牵扯到的其他问题就都看了,其实目标只是A。A才是主线,其他的都不重要。

3. 编写ORM

安装MariaDB、连接以及系统一些问题花费了小半天,遇到问题不少,如果刚开始就从手册走的话,就会清晰便捷很多。MariaDB使用MySQL连接器。

1). 非异步的ORM
#! /usr/bin/python

import pymysql as DB
import datetime
from hashlib import md5


class Sql(object):
    def __init__(self):
        pass

    def sqlInit(self, dbInfo, mode='tuple'):
        """ sql初始化 """
        try:
            dbHost = dbInfo.get('host')
            dbUser = dbInfo.get('user')
            dbPasswd = dbInfo.get('passwd')
            dbName = dbInfo.get('db')
            conn = DB.connect(host=dbHost, user=dbUser, passwd=dbPasswd, db=dbName)

            if mode == 'tuple':
                cur = conn.cursor()
            else:
                cur = conn.cursor(DB.cursors.DictCursor)
            return conn, cur
        except Exception as e:
            self.log(e)
            print("connect failed.")
            exit()

    @classmethod
    def query(cls, sql, mode='tuple', **dbInfo):
        """ 查询 """
        conn, cur = cls().sqlInit(dbInfo, mode)
        try:
            cur.execute(sql)
            data = cur.fetchall()
        except Exception as e:
            Sql.log(e)
            Sql.log("error sql: " + str(sql))
            data = None
        conn.close()
        return data

    @classmethod
    def insert(cls, sql, param, **dbInfo):
        """ 插入 """
        conn, cur = cls().sqlInit(dbInfo)
        try:
            cur.executemany(sql, param)
            conn.commit()
            result = True
        except Exception as e:
            conn.rollback()
            Sql.log(e)
            Sql.log("error sql: " + str(sql))
            result = False
        conn.close()
        return result

    @classmethod
    def delete(cls, sql, param, **dbInfo):
        """ 删除 """
        conn, cur = cls().sqlInit(dbInfo)
        try:
            cur.execute(sql, param)
            conn.commit()
            result = True
        except Exception as e:
            Sql.log(e)
            Sql.log('error sql: ' + str(sql))
            conn.rollback()
            result = False
        conn.close()
        return result

    @classmethod
    def update(cls, sql, param, **dbInfo):
        """ 更新 """
        conn, cur = cls().sqlInit(dbInfo)
        try:
            cur.execute(sql, param)
            conn.commit()
            result = True
        except Exception as e:
            Sql.log(e)
            Sql.log('error sql: ' + str(sql))
            conn.rollback()
            result = False
        conn.close()
        return result

    @classmethod
    def run(cls, sql, **dbInfo):
        """ 执行其他语句 """
        conn, cur = cls().sqlInit(dbInfo)
        try:
            cur.execute(sql)
            result = True
        except Exception as e:
            Sql.log(e)
            Sql.log('error sql: ' + str(sql))
            result = False
        conn.close()
        return result

    @staticmethod
    def log(msg):
        """ 记录日志 """
        nowStr = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        fileName = datetime.datetime.now().strftime('%Y%m%d') + '.log'
        with open('./' + fileName, 'a') as f:
            f.write(str(nowStr) + '\n')
            if isinstance(msg, Exception):
                f.write("\tFile: %s, Line: %s.\n" % (msg.__traceback__.tb_frame.f_globals['__file__'], msg.__traceback__.tb_lineno))
            f.write('\t' + str(msg) + '\n')


dbInfo = {
    'host': '127.0.0.1',
    'user': 'root',
    'passwd': '123456',
    'db': 'test_xxxx'
}

try:
    print("1. 建表")
    if not Sql.run("DROP TABLE IF EXISTS `user`", **dbInfo):
        raise Exception('drop error')

    sql = '''
        CREATE TABLE `user`(
            `id` int(11) NOT NULL AUTO_INCREMENT,
            `name` varchar(255) NOT NULL,
            `password` varchar(64) NOT NULL,
            `created` varchar(64) NULL,
            `updated` varchar(64) NULL,
            `deleted` varchar(64) NULL,
            `status` int(1) NOT NULL DEFAULT 1,
            PRIMARY KEY (`id`)
        ) ENGINE = InnoDB;
    '''
    if not Sql.run(sql, **dbInfo):
        raise Exception('建表失败!')

    print('2. 插入')
    sql = "INSERT INTO `user`(name, password, created, status) VALUES (%s, %s, %s, %s)"
    param = [['admin', md5(b'123456').hexdigest(), datetime.datetime.now(), 1]]
    Sql.insert(sql, param, **dbInfo)

    print('3. 查询')
    data = Sql.query("select * from user", 'tuple', **dbInfo)
    print(data)

    print('4. 更新')
    Sql.update("UPDATE `user` set name = %s, updated = %s where id = %s", ('admin2', datetime.datetime.now(), 1), **dbInfo)
    data = Sql.query("select * from user", 'dict', **dbInfo)
    print(data)

    print('5. 删除')
    Sql.delete("DELETE FROM `user` where id = %s", 1, **dbInfo)
    data = Sql.query("select * from user", 'tuple', **dbInfo)
    print(data)

except Exception as e:
    print('error: '+str(e))

执行结果

1. 建表                             
2. 插入                              
3. 查询                              
((1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', '2018-10-04 21:52:13.543246', No
ne, None, 1),)                            
4. 更新                             
[{'id': 1, 'name': 'admin2', 'password': 'e10adc3949ba59abbe56e057f20f883e', 'crea
ted': '2018-10-04 21:52:13.543246', 'updated': '2018-10-04 21:52:13.553830', 'dele
ted': None, 'status': 1}]   
5. 删除                             
()

用了两种查询方法tuple dict.

def cursor(self, cursor=None):
    """
    Create a new cursor to execute queries with.

    :param cursor: The type of cursor to create; one of :py:class:`Cursor`,
        :py:class:`SSCursor`, :py:class:`DictCursor`, or :py:class:`SSDictCursor`.
        None means use Cursor.
    """
    if cursor:
        return cursor(self)
    return self.cursorclass(self)

有一个简单日志记录错误。整体很简单粗糙,不能称之为ORM,只是简单的增删改查操作。

2).异步的ORM

照着廖大的抄了一遍,再看了justoneliu的ORM注释理解了一下没懂的地方。

还有需要修改的地方,比如Logging.info输出如何输出log文件,输出中args丢失,更新数据未指定列全部成了默认值。不影响主进度日志待定,先完善一下输出。

4. 编写Model

Model小节也简单过了一遍,应该有个生成脚本直接通过各表Model去生成sql。

5. 编写WEB框架

这里感觉也好难理解呢。python之aiohttp源码解析——add_route和middleware的工作方式 ,aiohttp的middleware为啥有两个参数,倒序的包含handle又是怎么回事?还是没有理解。官方文档aiohttp middlewares中都用了@web.middleware修饰了,大概看一下流程,以后再来搞懂吧。真头大,每次看到这些很难懂的都感觉自己很笨。

6. 配置

配置我直接复制重命名.bak了一份,开发时自己去掉就好了。

7. 编写MVC

希望可以顺利启动哈哈。orm webframe app 都很紧张呢。
这里终于要建handlers.py了,之前app.py运行会报错

INFO:root:Model into metaclass ,yeeeeeeeeee
DEBUG:asyncio:Using selector: EpollSelector
INFO:root:create database connection pool...
./app.py:107: DeprecationWarning: loop argument is deprecated
  logger_factory, response_factory
INFO:root:init jinja2...
INFO:root:set jinja2 template path: /data/webapp/templates
Traceback (most recent call last):
  File "./app.py", line 118, in <module>
    loop.run_until_complete(init(loop))
  File "/usr/local/lib/python3.7/asyncio/base_events.py", line 568, in run_until_complete
    return future.result()
  File "./app.py", line 111, in init
    add_routes(app, 'handlers')
  File "/data/webapp/webFrame.py", line 169, in add_routes
    mod = __import__(module_name, globals(), locals())
ModuleNotFoundError: No module named 'handlers'

Ok

8. 构建前端

blogs = [
        Blog(id='1', name='Test Blog', summary=summary, created_at=time.time()-120),
        Blog(id='2', name='Something New', summary=summary, created_at=time.time()-3600),
        Blog(id='3', name='Learn Swift', summary=summary, created_at=time.time()-7200)
    ]

这块没理解,调了几次发现只是因为Blog(dict)继承自dict, 就直接转为dict了。Ohhhhhhh

本节没有按照原文用uikit css框架,直接写一个最简单的html,用一个jquery cdn就够用了。类似这个很像文档的文章,我想要的博客就是这样的,简单不做作,有干货,虽然我还差好多 😃

9. 编写API

这一节很简单

10. 用户注册和登录

缺少vue 引入vue.js, 以及awesome.js 中定义的方法。又碰到了CryptoJS,肯定再引入sha1.min.js。

测试POST时,报错405: Method Not Allowed, 因为已经存在一个同名的get请求(90%)。

一个__init__参数错误卡了很久,

INFO:root:server started at http://127.0.0.1:9000
INFO:root:Request:POST /api/users
INFO:root:Response handler...
Unsupported Content-Type: application/octet-stream
ERROR:aiohttp.server:Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/aiohttp/web_protocol.py", line 390, in start
    resp = await self._request_handler(request)
  File "/usr/local/lib/python3.7/site-packages/aiohttp/web_app.py", line 366, in _handle
    resp = await handler(request)
  File "/usr/local/lib/python3.7/site-packages/aiohttp/web_middlewares.py", line 106, in impl
    return await handler(request)
  File "./app.py", line 43, in logger
    return (await handler(request))
  File "./app.py", line 61, in response
    r = await handler(request)
  File "/usr/local/lib/python3.7/site-packages/aiohttp/web_urldispatcher.py", line 120, in handler_wrapper
    result = await result
  File "/data/webapp/webFrame.py", line 114, in __call__
    return web.HTTPBadRequest('Unsupported Content-Type: {0}'.format(request.content_type))
TypeError: __init__() takes 1 positional argument but 2 were given

需要一个参数,给了两个。检查再三,以为抄代码时候哪里遗漏了,对比再对比,发现都一样的啊。调试一下
此处为调试pdb图片
发现确实是这句的错,去掉参数后就好了。 归根溯源,看看真身,
此处有HTTPBadRequest函数

class HTTPBadRequest(HTTPClientError)
 |  HTTPBadRequest(*, headers=None, reason=None, body=None, text=None, content_type=None)

原来只有命名关键字参数的啊。再次验证一下命名关键字参数前的*是不能接受参数么,来试一下:
此处为测试命名关键参数
原来如此。

那为啥大伙的HTTPBadRequest中加str的参数可以呢。不管了,修改为
return web.HTTPBadRequest(reason = 'Unsupported Content-Type: {0}'.format(request.content_type), content_type = request.content_type) Okay.

现在,问题来了,我的为啥会抛错。怎么就得到了这个错,content-type是application/octet-stream。 在app.pyresponse_factory中响应到字节流的时候会带这个type

if isinstance(r, bytes):
   		resp = web.Response(body = r)
   		resp.content_type = 'application/octet-stream'
   		return resp

用浏览器提交则正常,我是通过postman模拟的就错误,设置header的Content-Type:application/x-www-form-urlencoded即好。

另:
之后其实用postman模拟参数name, email, passed也提交不了,因为passwd在客户端就用sha1加密了。像之前那个tapd登录一样,也得模拟一下sha1加密才可提交成功。这里sha1加密直接查看源代码就可以看到,很简单的用sha1加密, passwd: CryptoJS.SHA1(email + ':' + this.password1).toString(),而那次逆向学习到的是加AES的CBC加密。有时间了可以好好研究一下。

又搞了两个小时,修复了错误,加了cookie和登录,时间过得真快。

11. 完善功能

  • markdown 代码颜色
  • logging 完善
  • 发布jenkins
  • 优化路由/handlers, 单独出类
  • 用户名登录
  • 稍微学一点VUE, 添加功能
  • 简洁页面
  • 添加工具箱功能
  • 同步发布到博客园

-1. 附录

专门开一块,这里放不懂和学习到的,防止思绪飞扬

1. 复习多进程

import os

print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
if pid == 0:
    print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
    print('I (%s) just created a child process (%s).' % (os.getpid(), pid))

这段代码,输出中os.getppid()为1,和预期不符

[root@centos py]# ./process.py
process 21289 start
I (21289) just created a child process (21290).
[root@centos py]# I am child process (21290) and my parent is 1.

参考getpid,getppid中说:“getppid返回父进程的pid,但是如果父进程已经结束了,那个子进程得到的getppid永远是1。”“阻止父进程消亡,加上sleep”

#! /usr/bin/python
import os
import time
print("process %s start"% os.getpid())

pid = os.fork()
if pid==0:
        print("I am child process (%s) and my parent is %s." % (os.getpid(), os.getppid() ) )
else:
        print("I (%s) just created a child process (%s)."% (os.getpid(), pid))

time.sleep(1)
[root@centos py]# ./process.py
process 21754 start
I (21754) just created a child process (21755).
I am child process (21755) and my parent is 21754.

2. vim 进入默认为 REPLACE

ssh登录主机后,使用vim打开文件默认为REPLACE 模式,好烦人。
搜索一阵怀疑是ConEmu的问题,在下面启用MINGW登录ssh启动vim则是正常的。发现win下的sshpowershell都会如此。

3. 加解密

AES五种加密模式(CBC、ECB、CTR、OCF、CFB)

还搜到一个好玩的N1CTF 2018:RSA_Padding,关于m^3>n,我本来胡乱猜想的是

(a+b)^3

二项式展开后第一项a^3==n的话,取模就会为0,balabalabala胡扯,之后突然发现这个公式

(a+b)^p ≡ a^p+b^p(modp)

这不就是答案么,第一项a^p<=n的话,取模则为a自身或0。可能加密没用了吧。

这里有从费马小定理的公式推导,又扯太远了,况且我现在又看不懂。

4. 日志

加了日志记录,终于摆脱了logging.basicConfig(level=logging.DEBUG)的简单记录。放到配置文件去读取

logging.config.fileConfig('log.conf')
logger = logging.getLogger('root')

文件中配置了日志滚动,设置之后直接使用logger.info()/warn()/debug()感觉很方便。

配置文件参考:

[loggers]
keys=root

[handlers]
keys=hand01,hand02

[formatters]
keys=form01,form02

[logger_root]
level=NOTSET
handlers=hand01

###################################
[handler_hand01]
class=handlers.TimedRotatingFileHandler
level=DEBUG
formatter=form01
args=('/srv/web/log/run.log','h', 2, 12*10)
# 2h一次滚动,共备份十天的

[handler_hand02]
class=StreamHandler
level=DEBUG
formatter=form02
#args=(sys.stdout,)
args=tuple()

###################################
[formatter_form01]
format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s
datefmt=[%Y-%m-%d %H:%M:%S]

[formatter_form02]
format=%(asctime)s %(name)s %(levelname)s %(message)s
class=logging.Formatter    

hand02为屏幕输出,方便调试时用。

有缺点是只能获取到自己手动打的日志,未捕获的异常则没记录很不方便。
sys.excepthook进行异常捕获。

def my_exception_hook(exctype, value, traceback):
    """ 捕获异常 """
    logger.warn(' !! exception hook !!')
    logger.warn("type:{},value:{}\n traceback:{}".format(exctype, value, traceback))
    
 def exception_set(flag):
    """ 设置捕获异常 """
    if flag:
        sys.excepthook = my_exception_hook

在其他文件使用1/0测试触发异常。
app.py中运行exception_set(True)则开启异常捕获。


图:上部分未开启捕获,异常报错输出,下部分开启捕获信息写入日志。

日志信息为:

  [2018-10-31 12:06:19] functions.py[line:82] WARNING  !! exception hook !!
  [2018-10-31 12:06:19] functions.py[line:83] WARNING type:<class 'ZeroDivisionError'>,value:division by zero
   traceback:<traceback object at 0x7f6dbe43b448>

traceback只输出了对象信息。
最终修改为:

import traceback
...
...

 81 def my_exception_hook(exctype, value, tb):
 82     """ 捕获异常 """
 83     err_msg = ' '.join(traceback.format_exception(exctype, value, tb))
 84     logger.warn(' !! exception hook !!')
 85     #logger.warn("type:{},value:{}\n traceback:{}".format(exctype, value, traceback.print_exc()))
 86     logger.warn(err_msg)
 87
 88 def exception_set(flag):
 89     """ 设置捕获异常 """
 90     if flag:
 91         sys.excepthook = my_exception_hook
 92     else:
 93         sys.excepthook = sys.__excepthook__
 94

日志查看

 4 [2018-10-31 12:33:18] functions.py[line:84] WARNING  !! exception hook !!
 5 [2018-10-31 12:33:18] functions.py[line:86] WARNING Traceback (most recent call last):
 6    File "app.py", line 148, in <module>
 7     loop.run_until_complete(init(loop))
 8    File "/usr/local/lib/python3.7/asyncio/base_events.py", line 568, in run_until_complete
 9     return future.result()
10    File "app.py", line 138, in init
11     add_routes(app, 'handlers')
12    File "/srv/web/www/webFrame.py", line 174, in add_routes
13     mod = __import__(module_name, globals(), locals())
14    File "/srv/web/www/handlers.py", line 20, in <module>
15     1/0
16  ZeroDivisionError: division by zero

虽然不是那么完美,够用了。


参考资料

    Centos7中没有Mysql, 所以mysql-server mysql-devel都不存在,用yum install mysql安装一次后再次调用会有
     
    # yum install mysql
    Package 1:mariadb-5.5.60-1.el7_5.x86_64 already installed and latest version
    
    证明了默认安装mysql则为`mariadb`, 故安装`mariadb-server` `mariadb-devel` 即可。

PyMySQL模块有如下要求:
Python解释器(满足下列条件之一): 
    Cpython解释器 >= 2.6 或 >= 3.3
    PyPy >= 4.0
    IronPython = 2.7
MySQL服务(满足下列条件之一): 
    MySQL >= 4.1
    MariaDB >= 5.1
posted @ 2018-10-28 11:11  姜小豆  阅读(594)  评论(0编辑  收藏  举报