python實戰博客

2018-10-31 更新Logging日誌記錄以及異常捕獲

感謝廖大教程。Python實戰php

直接在閒置的服務器上開發。阿里雲Centos 6.8 64位css

1 搭建開發環境

Python 環境是Python 3.4, 在裝aiohttp的時候報錯,以前用pip3和系統自己的pip(Python 2.7)也有安裝問題,索性下最新版Python 3.6.4,並安裝virtualenv獨立運行。html

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來指定使用版本了。vue

2 編寫Web App骨架

原文中監聽127.0.0.1:9000,本地測試直接打開便可,但是服務器怎麼看頁面呢。安裝有nginx,可是配置太麻煩。想要快速查看頁面。python

服務器直接訪問

阿里ECS管理頁面中有公網和私網IPmysql

公網私網IP

在代碼中從新監聽私有IP的9000端口,而後訪問公網IP:9000,無效。jquery

將端口加入到安全組nginx

端口加入到安全組

以後,再次訪問公網IP:9000。就成功了,不過是下載的形式。加上Content-Type便可:laravel

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. 完善功能

  • [x] 完善添加大日誌錯誤(next_id打成了next_id()),編輯日誌錯誤(此處日誌指文章)
  • [ ] 評論
  • [x] markdown ``` 解析失敗,換了mistune, 添加了markdown樣式Python下Markdnown解析踩的小坑
  • [x] 配置logging文件記錄日誌並滾動2018-10-31
  • [ ] markdown mistune + task list
  • markdown 代碼顏色
  • logging 完善
  • 發佈jenkins
  • 優化路由/handlers, 單獨出類
  • [x] 圖片管理。寫md確定須要解決圖片問題,找到markdown-helper 經過七牛雲傳圖獲取URL,完美。p.s. 有之前建的一個存儲空間,刪掉以後再建了一個提醒須要認證,可是認證須要手持身份證太麻煩,轉又拍雲了。
  • 用戶名登陸
  • 稍微學一點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
相關文章
相關標籤/搜索