Learn Python 3 :Flask Web開發小記

最近看了Flask Web開發:基于Python的Web應用開發實戰,書中詳細介紹了Web程序的開發、測試、部署過程,值得一讀!我在書中例子的基礎上做了些更改,實現了一個簡單的個人博客:NiceBlog,僅作為個人學習,還有許多不足的地方待完善,這里做一些簡單的記錄,方便以后查閱,代碼放在了Github上:https://github.com/SheHuan/NiceBlog

一、功能

1、對于普通用戶,主要有如下功能:

  • 注冊、登錄、重置密碼(郵箱驗證)
  • 文章列表、詳情
  • 評論
  • 喜歡

2、對于管理員,除了有普通用戶的功能,主要有如下功能:

  • 寫文章(Markdown編輯)
  • 用戶權限管理(管理喜歡、評論的權限)
  • 評論管理(刪除、屏蔽)

3、為移動端提供相關api接口

二、項目結構

遵循了書中多文件Flask程序的基本結構,下邊是NiceBlog的項目結構:
|-NiceBlog
???|-app/ 主目錄
??????|-api/ 為移動端提供接口的藍本
??????|-auth/ 權限認證的藍本
??????|-main/ 主體功能的藍本
??????|-manage/ 管理相關功能的藍本
??????|-static/ 靜態資源目錄(icon、js、css)
??????|-templates/ html模板目錄
??????|-__init__.py 初始化項目的工廠函數
??????|-decorators.py 自定義的裝飾器
??????|-email.py 發送郵件功能
??????|-excepitions.py 自定義異常處理
??????|-models.py 數據模型
???|-migrations/ 數據庫遷移腳本目錄
???|-nb_env/ 虛擬環境
???|-tests/ 單元測試目錄
???|-config.py 配置文件
???|-manage.py 啟動程序以及其他的程序任務
???|-requirements.txt 項目的依賴包列表

三、實現

1、工廠函數

一個簡單的Flask Web程序可以寫在單文件中,test.py

app = Flask(__name__)

# 定義的路由
@app.route('/')
def index():
    return '<h1>Hello World!</h1>'

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

但是執行程序時,由于在全局作用域創建導致無法動態修改配置,也導致了單元測試時無法在不同配置環境運行程序。所以可以把程序的創建轉移到可顯示調用的工廠函數中,也就是前邊項目結構中的__init__.py,在工廠函數中導入需要的Flask擴展:

def create_app(config_name):
    app = Flask(__name__)
    # 導致指定的配置對象
    app.config.from_object(config[config_name])
    # 調用config.py的init_app()
    config[config_name].init_app(app)

    # 初始化擴展
    bootstrap.init_app(app)
    mail.init_app(app)
    moment.init_app(app)
    db.init_app(app)
    login_manager.init_app(app)
    pagedown.init_app(app)
    return app

2、藍本

新的問題來了,使用工廠函數后,程序在運行時創建,而不是在全局作用域,必須等到執行create_app()后才能使用@app.route()裝飾器,這時就要使用藍本了,在藍本中也可以定義路由,但是定義的路由處于休眠狀態直到藍本注冊到程序后在成為程序一部分,例如main藍本的目錄結構如下:
|-NiceBlog
???|-app/ 主目錄
??????|-main/ 主體功能的藍本
?????????|-__init__.py 創建藍本
?????????|-errors.py 藍本的錯誤處理
?????????|-forms.py 藍本的表單
?????????|-views.py 藍本的路由

首先看一下__init__.py

# 兩個參數分別指定藍本的名字、藍本所在的包或模塊(使用 __name__即可)
main = Blueprint('main', __name__)
# 導入路由模塊、錯誤處理模塊,將其和藍本關聯起來
# 在藍本的末尾導入在兩個模塊里還要導入藍本,防止循環導入依賴
from app.main import views, errors
2.1、表單

forms.py是當前藍本中的表單,項目中使用了FlaskForm,可以方便的完成表單校驗,例如創建、編輯文章的表單:

class BlogForm(FlaskForm):
    title = StringField('請輸入文章標題', validators=[DataRequired(), Length(1, 128)])
    labels = StringField('文章標簽(標簽之間用空格隔開)', validators=[DataRequired()])
    summary = TextAreaField('文章概要', validators=[DataRequired()])
    content = TextAreaField('文章內容', validators=[DataRequired()])
    preview = TextAreaField('文章預覽', validators=[DataRequired()])
    publish = SubmitField('發布')
    save = SubmitField('保存')
2.2、路由

views.py就是在藍本中定義的路由,例如主頁的路由:

@main.route('/create-blog', methods=['GET', 'POST'])
@admin_required
def create_blog():
    """
    寫新文章
    """
    form = BlogForm()
    if form.validate_on_submit():
        blog = None
        if form.publish.data:
            # 發布
        elif form.save.data:
            # 保存草稿
        return redirect(url_for('main.index'))
    return render_template('markdown_editor.html', form=form, type='create')

注意裝飾器為當前藍本的名字main,而不是之前的appcreate_blog()稱為視圖函數,一個路由保存了URL到視圖函數的映射關系。redirect(url_for('main.index'))代表重定向到主頁,url_for()的參數為要跳轉到的URL對應的視圖函數名,但需要加上視圖函數所在的藍本名,即main.indexrender_template()是Flask提供的函數,把Jinja2模板引擎集成到了程序中,第一個參數是模板名稱對應一個html文件,即執行該視圖函數后最終要渲染的頁面,后邊的參數為傳遞給模板的參數。

2.3、錯誤處理

errors.py是藍本中的錯誤處理程序,例如:

@main.app_errorhandler(404)
def page_not_found(e):
    if request.url.find('api') != -1:
        return jsonify({'error': '請求的資源不存在', 'code': '404', 'data': ''})
    return render_template('error/404.html'), 404

如果使用@main.errorhandler裝飾器只有當前藍本的錯誤才能觸發,為了使其它錯誤也能觸發所以使用了@main.app_errorhandler裝飾器

2.4、注冊藍本

其它藍本的定義也類似,最后需要在工廠函數中重注冊藍本,例如:

def create_app(config_name):
    # ......
    # 注冊main藍本
    from app.main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    # 注冊auth藍本
    from app.auth import auth as auth_blueprint
    # 使用url_prefix注冊后,藍本中定義的所有路由都會加上指定前綴,/login --> /auth/login
    app.register_blueprint(auth_blueprint, url_prefix='/auth')

    return app

3、前端

3.1、Jinja2

Flask使用Jinja2作為模板引擎,模板是一個包含響應文本的HTML文件,其中包含只有在請求的上下文才知道的動態占位變量。默認情況下,模板保存在templates目錄。

Jinja2模板中{{ 變量名 }}代表一個變量(注意變量名兩邊有一個空格,可以識別任意類型的變量),從渲染模板時使用的數據中獲取。如果變量的值是HTML,由于轉義的原因導致瀏覽器不能正常顯示HTML代碼,所以需要使用safe過濾器,例如文章詳情的HTML顯示就需要這樣處理,過濾器寫在變量名后用豎線隔開{{ 變量名|過濾器名 }}

Jinja2中用{% 控制語句 %}代表控制結構來改變模板的渲染流程,例如:

# 條件控制
{% if xxx %}
    <h1>Android</h1>
{% else %}
    <h1>iOS</h1>
{% endif %}
# for循環
{% for x in xs %}
    <li>{{ x }}</li>
{% endfor %}
# 導入
{% import 'xxx.html' %}
# 包含
{% include 'xxx.html' %}

導入、包含的目的都是為了復用,還可以通過繼承實現復用,類似于類的繼承:

# 繼承
{% extends "base.html" %}

通過繼承,模板中重復的代碼都可以寫在父模板里,例如導航條和頁面底部footer就可以放在父模板里。

3.2、Bootstrap

前端使用了Bootstrap框架,它提供了良好的CSS規范,可以幫助我們更好的美化界面,具體的可參考:
https://v3.bootcss.com/,要在項目中集成它可以使用Flask的Flask-Bootstrap擴展,直接在PyCharm安裝,并在工廠函數中初始化,還要讓項目的父模板繼承Bootstrap的基類模板:

# common_base.html
{% extends "bootstrap/base.html" %}

Bootstrap的基類模板base.html提供了一個網頁框架,包含了Bootstrap中的所有CSS和JS文件。除此之外基類模板還定義了許多可在其子類模板中重定義的塊,使用格式如下:

{% block 塊名稱 %}
{% endblock %}

常用的塊如下:

塊名稱 含義
head <head>標簽中的內容
title <title>標簽中的內容
body <body>標簽中的內容
styles css樣式單的定義
navbar 自定義的導航條
content 自定義的頁面內容
page_content 定義content在內部
scripts JS聲明,一般在模板尾部

注意如在子模板在模板已有的塊中添加新內容,需要使用super()函數:

{% block scripts %}
    {{ super() }}
    <!-- 新加的內容 -->
{% endblock %}
3.3、Flask-WTF

2.1中我們已經看到了用Flask-WTF定義表單的方式,即自定義的表單類繼承FlaskForm類,并添加需要的類變量,Flask-WTF定義了許多標準字段可以被渲染成指定的表單類HTML標簽,例如:

字段名 對應的H5標簽
StringField 文本框
TextAreaField 多行文本框
PasswordField 密碼輸入框
BooleanField 復選框
SubmitField 表單提交按鈕

同時Flask-WTF還提供了許多常用的表單校驗函數,例如:Email()EqualTo()DataRequired()Length()等等,當點擊提交按鈕時,會自動校驗表單是否滿足預定義的條件。

2.2中,我們通過參數把表單類的實例同步form參數傳入模板:

render_template('markdown_editor.html', form=form, type='create')

在模板中可以通過如下方式生表單(只保留了部分核心代碼):

<form method="post" role="form" class="height-full">
        {{ form.hidden_tag() }}
        {{ form.title(id="title", class="form-control editor-blog-title", placeholder=form.title.label.text) }}
        {{ form.labels(class="form-control editor-blog-area", placeholder=form.labels.label.text) }}
        {{ form.summary(class="form-control editor-blog-area", placeholder=form.summary.label.text, rows=3) }}
        {{ form.publish(class="btn btn-info") }}
        {{ form.save(class="btn btn-success") }}
    </form>

這樣的好處是我們能自定義表單的樣式等等,但是工作量蠻大的,如果對表單樣式沒有特殊的需求,Bootstrap中的表單樣式可以滿足需求,可以通過Flask-Bootstrap提供的輔助函數快速的渲染表單,只需要如下兩步:

{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}

例如登錄的H5模板就是這樣做的。
form.hidden_tag() 模板參數將被替換為一個隱藏字段,用來實現在配置中激活的 CSRF 保護。如果你已經激活了CSRF,這個字段需要出現在你所有的表單中。

2.2中,如果點擊表單提交按鈕,所有的表單都能成功通過校驗,則form.validate_on_submit()的值為True,否則校驗失敗,網頁會出現對應提示。如果有兩個提交按鈕,那么在校驗成功后,還需要判斷點擊的是哪個按鈕,否則所有的按鈕都執行了同一個操作。例如我們的文章發布和保存按鈕,當表單類中的按鈕字段的data屬性為True則代表該按鈕被點擊,例如:

if form.publish.data:
    # 發布
elif form.save.data:
    # 保存草稿
3.4、jQuery

有些頁面需要在相關操作后修改控件的CSS樣式,例如文章詳情的喜歡取消喜歡按鈕,最簡單的方式是操作成功后直接刷新整個頁面,但這樣體驗并不好,更好的方式是局部刷新。這里直接使用jQuery(Bootstrap也提供了類似的操作,同時包含了jQuery,不需要單獨導入jQuery)來實現。使用jQuery強大的選擇器功能可以方便的得到要操作的DOM節點,按鈕的點擊也是發起一個請求,jQuery也集成了ajax,可以方便的處理請求,在請求完成后根據響應結果來更改DOM節點的樣式。看下按鈕的點擊事件:

favourite = function (id) {
        if ($('.blog-favourite-btn').length > 0) {//取消喜歡
            $.get('/manage/blog/cancel_favourite', {
                id: id
            }).done(function (data) {
                $('.blog-favourite-btn span').removeClass('glyphicon-heart').addClass('glyphicon-heart-empty');
                $('.blog-favourite-btn').removeClass('blog-favourite-btn').addClass('blog-unfavourite-btn');
            })
        } else if ($('.blog-unfavourite-btn').length > 0) {//喜歡
            $.get('/manage/blog/favourite', {
                id: id
            }).done(function (data) {
                if ('200' === data) {
                    $('.blog-unfavourite-btn span').removeClass('glyphicon-heart-empty').addClass('glyphicon-heart');
                    $('.blog-unfavourite-btn').removeClass('blog-unfavourite-btn').addClass('blog-favourite-btn');
                }

                if ('403' === data) {
                    alert('沒有操作權限');
                }
            })
        }
    }

4、Markdown

書中使用的是Flask-PageDownMarkdown兩個庫來實現對Markdown功能的支持,但是不夠理想,有些Markdown語法并不能很好的支持,例如Flask-PageDown實時預覽時并不支持代碼塊和表格等。最后使用了marked這個庫,它是一個全功能的Markdown解析器和編譯器,用JavaScript編寫,構建速度快,其實就是實時將用Markdown語法編輯的內容轉換成對應的HTML預覽,但是沒有CSS樣式的HTML還是有點丑,github-markdown-css是一個不錯的選擇,可以幫助我們實現github風格的Markdwon預覽。既然是要編輯文章那么直接使用HTML里的<textarea>肯定難以實現理想的效果,這里使用了ace,它是一個用JavaScript編寫的獨立代碼編輯器,下載ace-builds/arc-min即可。核心的幫助工具就這些了,接下來就是把他們組合起來,首先看HTML界面主要有編輯和預覽兩部分:

<!--編輯-->
<div class="col-md-6 markdown-panel">
     <div id="markdown-edit"></div>
</div>
<!--預覽-->
<div class="col-md-6 markdown-panel">
     <div id="markdown-preview" class="markdown-body"></div>
</div>

接下來就是編輯器的初始化了:

<script>
    //編輯器配置
    var ace_edit = ace.edit('markdown-edit');
    ace_edit.setTheme('ace/theme/chrome');
    ace_edit.getSession().setMode('ace/mode/markdown');
    ace_edit.renderer.setShowPrintMargin(false);
    //字體大小
    ace_edit.setFontSize(15);
    //自動換行
    ace_edit.setOption('wrap', 'free');

    $("#markdown-edit").keyup(function () {
        // 實現Markdown到HTML的預覽
        $("#preview").text(marked(ace_edit.getValue()));
    });
</script>

更多細節可參考markdown_editor.html,看一下效果:

Markdwon

5、數據庫

數據庫使用的是MySql,同時使用了ORM框架SQLAlchemy把關系數據庫的表結構映射到對象上,來簡化數據庫的操作,Flask有一個Flask-SQLAlchemy擴展可以方便的在程序中使用SQLAlchemy,首先需要指定數據庫URL,這一步在config.py完成:

SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:123456@127.0.0.1:3306/niceblog_dev'

然后在工廠函數完成配置。之后就是定義數據模型了,項目中一共定義了6個數據模型:UserRoleBlogCommentFavouriteLabel,都繼承db.Model,在數據模型中指定表名稱、列名稱等信息,例如保存文章信息的Blog

class Blog(db.Model):
    """
    博客數據Model
    """
    __tablename__ = 'blogs'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(128))
    summary = db.Column(db.Text)
    content = db.Column(db.Text)
    content_html = db.Column(db.Text)
    # 發布日期
    publish_date = db.Column(db.DateTime, index=True)
    # 最后的編輯日期
    edit_date = db.Column(db.DateTime, index=True)
    # 外鍵,和User表對應
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    # 是否是草稿
    draft = db.Column(db.Boolean)
    # 是否禁用評論
    disable_comment = db.Column(db.Boolean, default=False)
    # 被瀏覽的次數
    views = db.Column(db.Integer, default=0)
    comments = db.relationship('Comment', backref='blog', lazy='dynamic')
    favourites = db.relationship('Favourite', backref='blog', lazy='dynamic')

配置好了數據庫、定義好了數據模型,就可以通過如下命令來操作數據庫了:

  • db.create_all():創建表
  • db.session.add():插入行、修改行,最后需要執行db.session.commit()
  • db.session.delete():刪除行,最后需要執行db.session.commit()
  • 數據模型名.query().查詢過濾器.查詢執行函數:查詢行

常用的查詢過濾器有:filter()filter_by()limitoffset()order_by()group_by()
常用的查詢執行函數有:all()first()first_or_404()get()get_or_404()count()paginate()

如果在shell中操作數據庫,每次都要導入數據庫實例和數據模型,如何避免這個問題呢?由于項目使用了Flask-Script命令行解釋器,支持自定義命令,可以讓Flask-Script的shell命令自動導入特定對象即可:

def make_shell_context():
    return dict(app=app, db=db, User=User, Role=Role, Blog=Blog, Comment=Comment, Favourite=Favourite, Label=Label,
                Permission=Permission)
manager.add_command('shell', Shell(make_context=make_shell_context))

開發中修改數據模型是不可避免的,為了不發生刪表重建導致數據丟失的問題,我們需要使用數據庫遷移框架,增量式的把數據模型的改變應用到數據庫中,我們可以直接使用Flask-Migrate來完成,在Flask-Script集成數據庫遷移功能:

migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)

數據庫遷移只要有如下三個命令:

  1. python manage.py db init:創建遷移倉庫,初始執行一次即可
  2. python manage.py db migrate --message "initial migration":創建遷移腳本
  3. python manage.py db upgrade:更新數據庫

每次修改數據模型后需要更新數據庫時執行命令2、3即可。

6、接口開發

api藍本的目錄結構如下:
|-NiceBlog
???|-app/ 主目錄
??????|-api/ 為移動端提供接口的藍本
?????????|-__init__.py 創建藍本
?????????|-authentication.py 登錄、注冊、token檢驗
?????????|-blogs.py 文章列表、詳情的接口
?????????|-comments.py 評論相關接口
?????????|-decorators.py 自定義裝飾器
?????????|-favourites.py 喜歡操作相關的接口
?????????|-labels.py 文章分類標簽接口
?????????|-responses.py 幫助返回JSON數據

Flask提供的jsonify()函數可以方便的把一個字典轉換成JSON串返,例如返回文章分類標簽的路由可以這么寫:

@api.route('/labels/')
def get_labels():
    labels = Label.query.all()
    data = {'labels': [label.to_json() for label in labels]}
    return jsonify({'error': '', 'code': '200', 'data': data})

to_json()方法是數據模型中Label類的方法,完成數據模型到JSON格式化的序列化字典轉換:

    def to_json(self):
        json_label = {
            'id': self.id,
            'name': self.name,
        }
        return json_label

為了保證接口有一定的安全性,不被隨意訪問,除了登錄、注冊、以及文章預覽的html頁面外其他接口都需要一個token參數,token可以在登錄后得到,token過期后需要重新請求。由于要對請求攜帶的token參數校驗,可以定義一個before_request鉤子,在每次請求前統一完成token的校驗:

@api.before_request
def before_request():
    url = request.url
    if url.find('login') == -1 and url.find('register') == -1 and url.find('preview') == -1:
        token = request.args.get('token')
        if token is None:
            return unauthorized('token缺失')
        user = User.verify_auth_token(token)
        if user is None:
            return forbidden('token過期,請重新登錄')
        else:
            # g是程序上下文,用作臨時存儲對象,
            # 保存當前的請求對應的user,每次請求都會更新
            g.current_user = user

測試接口可以使用HTTPie,通過PyCharm在虛擬環境安裝HTTPie后,啟動Web服務,windows下通過cmd進入虛擬環境目錄,執行Scripts\activate激活虛擬環境(退出虛擬環境執行deactivate):

激活虛擬環境

執行登錄請求,命令如下:

http POST http://127.0.0.1:5000/api/login/ email==shehuan320@163.com password==123456

響應如下:


登錄

使用登錄的得到的token請求文章分類標簽接口:

http GET http://127.0.0.1:5000/api/labels/ token==eyJhbGciOiJIUzI1NiIsImlhdCI6MTUxODEzOTQwNiwiZXhwIjoxNTE4NzQ0MjA2fQ.eyJpZCI6MX0.EujL1Pb4lg20Bb2QWngop1N79os0LdFWniA8bL4JQHo

響應如下:


文章分類標簽

四、安裝

以下的安裝步驟是基于Windows環境的!

  1. 從Guthub clone NiceBlog到本地
  2. 安裝Python 3 的開發環境
  3. 安裝PyCharm開發工具,導入項目,建議使用虛擬環境,可直接在 PyCharm 中創建一個虛擬環境,或者使用命令行創建。
  4. 在虛擬環境中安裝requestments.txt中的擴展包,直接在 PyCharm 的 Terminal 執行如下命令:
    pip install -r requirements.txt
  5. 安裝MySql,創建數據庫,并在config.py中替換為自己創建的數據名,并修改用戶名和密碼
  6. 在 Terminal 執行python manage.py shell切換到 shell 環境,再執行db.create_all()創建數據表
  7. 由于注冊賬號使用了qq郵箱驗證,請在config.py中替換自己的qq郵箱和授權登錄密碼,并更改管理員郵箱為自己的郵箱。
  8. 執行exit()退出 shell 環境,再執行python manage.py runserver就可以啟動 Web 服務了,默認運行在http://127.0.0.0.1:5000
  9. 在瀏覽器訪問http://127.0.0.0.1:5000,你就可以進行注冊賬號,創建文章等操作了,希望一切順利吧!

最后附上幾張截圖:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容