Flask应用

配置文件

flask中的配置文件是一个flask.config.Config对象(继承字典),默认配置为:

 default_config = ImmutableDict({
        'ENV':                                  None,
            # 环境配置,默认值:production(生产),不要在生产环境中将这个值设置为development。
        'DEBUG':                                None,
            # 启用/禁用调试模式
        'TESTING':                              False,
            # 启用/禁用测试模式
        'PROPAGATE_EXCEPTIONS':                 None,
            # 显式地允许或禁用异常的传播。如果没有设置或显式地设置为 None ,当 TESTING 或 DEBUG 为真时,这个值隐式地为 true.
        'PRESERVE_CONTEXT_ON_EXCEPTION':        None,
            # 默认情况下,如果应用工作在调试模式,请求上下文不会在异常时出栈来允许调试器内省。
            # 这可以通过这个键来禁用。你同样可以用这个设定来强制启用它,即使没有调试执行,这对调试生产应用很有用(但风险也很大)
        'SECRET_KEY':                           None,
            # 密钥
        'PERMANENT_SESSION_LIFETIME':           timedelta(days=31),
            # 以 datetime.timedelta 对象控制长期会话的生存时间。从 Flask 0.8 开始,也可以用整数来表示秒。
        'USE_X_SENDFILE':                       False,
            #     启用/禁用 x-sendfile
        'SERVER_NAME':                          None,
             # 服务器名和端口。需要这个选项来支持子域名 (例如: 'myapp.dev:5000' )。
             # 注意 localhost 不支持子域名,所以把这个选项设置为 “localhost” 没有意义。
             # 设置 SERVER_NAME 默认会允许在没有请求上下文而仅有应用上下文时生成 URL
        'APPLICATION_ROOT':                     '/',
            # 如果应用不占用完整的域名或子域名,这个选项可以被设置为应用所在的路径。
            # 这个路径也会用于会话 cookie 的路径值。如果直接使用域名,则留作 None
        'SESSION_COOKIE_NAME':                  'session',
            # 会话 cookie 的名称。
        'SESSION_COOKIE_DOMAIN':                None,
            # 会话 cookie 的域。如果不设置这个值,则 cookie 对 SERVER_NAME 的全部子域名有效
        'SESSION_COOKIE_PATH':                  None,
            # 会话 cookie 的域。如果不设置这个值,则 cookie 对 SERVER_NAME 的全部子域名有效
        'SESSION_COOKIE_HTTPONLY':              True,
            # 控制 cookie 是否应被设置 httponly 的标志, 默认为 True
        'SESSION_COOKIE_SECURE':                False,
            # 控制 cookie 是否应被设置安全标志,默认为 False
        'SESSION_COOKIE_SAMESITE':              None,
            # 限制cookie是如何随着外部网站请求发送的。该配置项能够被设置为‘Lax’(推荐)或者‘Strict’。请查阅Set-Cookie options(后续会讲到)。
        'SESSION_REFRESH_EACH_REQUEST':         True,
            # 这个标志控制永久会话如何刷新。如果被设置为 True (这是默认值),每一个请求 cookie 都会被刷新。
            # 如果设置为 False ,只有当 cookie 被修改后才会发送一个 set-cookie 的标头。
            # 非永久会话不会受到这个配置项的影响 。
        'MAX_CONTENT_LENGTH':                   None,
            # 如果设置为字节数, Flask 会拒绝内容长度大于此值的请求进入,并返回一个 413 状态码
        'SEND_FILE_MAX_AGE_DEFAULT':            timedelta(hours=12),
            # 默认缓存控制的最大期限,以秒计,在 flask.Flask.send_static_file() (默认的静态文件处理器)中使用。
             # 对于单个文件分别在 Flask 或 Blueprint 上使用 get_send_file_max_age() 来覆盖这个值。
             # 默认为 43200(12小时)。
        'TRAP_BAD_REQUEST_ERRORS':              None,
            # Werkzeug 处理请求中的特定数据的内部数据结构会抛出同样也是“错误的请求”异常的特殊的 key errors 。
            # 同样地,为了保持一致,许多操作可以显式地抛出 BadRequest 异常。
            # 因为在调试中,你希望准确地找出异常的原因,这个设置用于在这些情形下调试。
            # 如果这个值被设置为 True ,你只会得到常规的回溯。
        'TRAP_HTTP_EXCEPTIONS':                 False,
            # 如果这个值被设置为 True ,Flask不会执行 HTTP 异常的错误处理,而是像对待其它异常一样, 通过异常栈让它冒泡地抛出。
             # 这对于需要找出 HTTP 异常源头的可怕调试情形是有用的。
        'EXPLAIN_TEMPLATE_LOADING':             False,
            # 日志调试信息追踪如何加载一个模板文件。这可以被用来指出为什么一个模板没有被加载,或者为什么加载了一个错误的模板文件。
        'PREFERRED_URL_SCHEME':                 'http',
            # 生成URL的时候如果没有可用的 URL 模式话将使用这个值。默认为 http
        'JSON_AS_ASCII':                        True,
            # 默认情况下 Flask 使用 ascii 编码来序列化对象。
            # 如果这个值被设置为 False , Flask不会将其编码为 ASCII,并且按原样输出,返回它的 unicode 字符串。
            # 比如 jsonfiy 会自动地采用 utf-8 来编码它然后才进行传输。
        'JSON_SORT_KEYS':                       True,
            # 默认情况下 Flask 按照 JSON 对象的键的顺序来序来序列化它。
            # 这样做是为了确保键的顺序不会受到字典的哈希种子的影响,从而返回的值每次都是一致的,不会造成无用的额外 HTTP 缓存。
            # 你可以通过修改这个配置的值来覆盖默认的操作。但这是不被推荐的做法因为这个默认的行为可能会给你在性能的代价上带来改善。
        'JSONIFY_PRETTYPRINT_REGULAR':          False,
            # 如果这个配置项被 True ,
            # 如果不是 XMLHttpRequest 请求的话(由 X-Requested-With 标头控制) json 字符串的返回值会被漂亮地打印出来。
        'JSONIFY_MIMETYPE':                     'application/json',
            #  jsonify响应的mimetype。
        'TEMPLATES_AUTO_RELOAD':                None,
            # 当模板有更改时将会重新加载模板。如果没有设置,它会在调试模式下开启。
        'MAX_COOKIE_SIZE': 4093,
            # cookie头的最大字节数。默认为4093。较大的cookie可能会被浏览器静默地忽略。设置为0,用于关闭这个报警。
    })

 

方式一:
    app.config['DEBUG'] = True
 
    PS: 由于Config对象本质上是字典,所以还可以使用app.config.update(...)
 
方式二:
    app.config.from_pyfile("python文件名称")
        如:
            settings.py
                DEBUG = True
 
            app.config.from_pyfile("settings.py")
 
    app.config.from_envvar("环境变量名称")
        环境变量的值为python文件名称名称,内部调用from_pyfile方法
 
 
    app.config.from_json("json文件名称")
        JSON文件名称,必须是json格式,因为内部会执行json.loads
 
    app.config.from_mapping({'DEBUG':True})
        字典格式
 
    app.config.from_object("python类或类的路径")
 
        app.config.from_object('pro_flask.settings.TestingConfig')
 
        settings.py
 
            class Config(object):
                DEBUG = False
                TESTING = False
                DATABASE_URI = 'sqlite://:memory:'
 
            class ProductionConfig(Config):
                DATABASE_URI = 'mysql://user@localhost/foo'
 
            class DevelopmentConfig(Config):
                DEBUG = True
 
            class TestingConfig(Config):
                TESTING = True
 
        PS: 从sys.path中已经存在路径开始写
     
 
    PS: settings.py文件默认路径要放在程序root_path目录,如果instance_relative_config为True,则就是instance_path目录

Flask 实例化时__init__初始化配置:

 def __init__(
        self,
        import_name,
        static_url_path=None,
        static_folder='static',
        static_host=None,
        host_matching=False,
        subdomain_matching=False,
        template_folder='templates',
        instance_path=None,
        instance_relative_config=False,
        root_path=None
    ):   
#static_url_path:访问静态目录文件时的地址
#static_folder:静态文件的存放
 #template_folder:模板文件存放        

路由

  • from flask import Flask
    app=Flask(__name__)
    @app.route('/<name>')  #设置url传参数 http://127.0.0.1:5000/lijie
    def first_flask(name):  #视图必须有对应接收参数
        print(name)
        return 'Hello World'  #response
    if __name__ == '__main__':
        app.run()
    
    
    #接收整型数字参数@app.route('/post/<int:age>')
    #接收浮点型型数字参数@app.route('/post/<float:salary>')
    # 接收URL链接类型参数@app.route('/post/<path:path>')
    # 指定请求方法@app.route('/<path:url>/',methods=['get','post'])#也可以是元祖

    2. 通过别名反向生成url

  • @app.route('/<path:url>',endpoint='name1')
    def first_flask(url):
        print(url_for('name1',url=url)) #如果设置了url参数,url_for(别名,加参数)
        return 'Hello World'

    3. 通过app.add_url_rule()调用路由器

  • app.add_url_rule(rule='/index/',endpoint='name1',view_func=first_flask,methods=['GET'])
    #app.add_url_rule(rule=访问的url,endpoint=路由别名,view_func=视图名称,methods=[允许访问的方法])

    注册url

    @app.route和app.add_url_rule参数:
                rule,                       URL规则
                view_func,                  视图函数名称
                defaults=None,              默认值,当URL中无参数,函数需要参数时,使用defaults={'k':'v'}为函数提供参数
                endpoint=None,              名称,用于反向生成URL,即: url_for('名称')
                methods=None,               允许的请求方式,如:["GET","POST"]
                
    
                strict_slashes=None,        对URL最后的 / 符号是否严格要求,
                                            如:
                                                @app.route('/index',strict_slashes=False),
                                                    访问 http://www.xx.com/index/ 或 http://www.xx.com/index均可
                                                @app.route('/index',strict_slashes=True)
                                                    仅访问 http://www.xx.com/index 
                redirect_to=None,           重定向到指定地址
                                            如:
                                                @app.route('/index/<int:nid>', redirect_to='/home/<nid>')

    自定制路由正则匹配

    from flask import Flask, render_template, request,url_for,views
    from werkzeug.routing import BaseConverter
    
    
    #BaseConverter所有转换器的基类
    app = Flask(import_name=__name__) #实例化名字
    
    
    class RegexConverter(BaseConverter):
        """
        自定义URL匹配正则表达式
        """
        def __init__(self, map, regex):                         #初始化
            #regex = '[^/]+' 正则
            super(RegexConverter, self).__init__(map)
            self.regex = regex
    
        def to_python(self, value):
            """
            路由匹配时,匹配成功后传递给视图函数中参数的值
            :param value:
            :return:
            """
            return int(value)
    
        def to_url(self, value):
            """
            使用url_for反向生成URL时,传递的参数经过该方法处理,返回的值用于生成URL中的参数
            :param value:
            :return:
            """
            val = super(RegexConverter, self).to_url(value)
            return val
    
    # 添加到flask中
    app.url_map.converters['regex'] = RegexConverter
    
    
    @app.route('/index/<regex("\d+"):nid>')
    def index(nid):
        print(url_for('index', nid = nid))
        return 'Index'
    
    
    if __name__ == '__main__':
        app.run()

 

视图

  #注意!如果要给视图函数用装饰器添加新功能

  1.请求数据相关信息

  

  2.响应相关数据

  

     return jsonify(dict) #返回json 数据

 

    

  3.CBV视图

#CBV视图
from flask import Flask,url_for,views
#-----------------------------------------------------
app=Flask(__name__)               #装饰器
def auth(func):

    def inner(*args,**kwargs):
        return func(*args,**kwargs)
    return inner
#--------------------------------------------------------
class IndexView(views.MethodView):  #CBV视图
    
    methods=['GET']                #允许的http请求方法(改CBV只允许GET方法)
    decorators = [auth,]            #每次请求过来都加auth装饰器
    
    def get(self):
            return 'Index.GET'
    
    def post(self):
        return 'Index.POST'

app.add_url_rule('/index/',view_func=IndexView.as_view(name='name1')) #(name='name1'反向生成url别名
if __name__ == '__main__':
    app.run()

 

模板

Flask使用的是Jinja2模板,所以其语法和Django无差别

1、模板的基本数据类型使用

  render_templatetemplate_name_or_list** context 

参数:

  • template_name_or_list - 要呈现的模板的名称,或具有模板名称的可迭代项,将呈现现有的第一个模板。
  • context - 模板上下文中应该可用的变量。
    @app.route("/tpl")
    def tpl():
        context = {
            'users':['longtai','liusong','zhoahuhu'],
        }
        return render_template('tpl.html',**context)

    html模板调用 #可以执行python语法,如:dict.get()  list['xx']

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        {{users.0}}
        {{users[0]}}
    </body>
    </html>

    注意:Markup等价django的mark_safe 渲染html标签。

  • render_template_string来源**上下文

    使用给定的上下文从给定的模板源字符串呈现模板。模板变量将被自动调整。

    参数:
    • source - 要呈现的模板的源代码
    • context - 模板上下文中应该可用的变量。
  • get_template_attributetemplate_nameattribute 

    加载模板导出的宏(或变量)。这可以用于从Python代码中调用宏。例如,如果您有一个名为_cider.html以下内容的模板

    {% macro hello(name) %}Hello {{ name }}!{% endmacro %}
    

    您可以从Python代码访问此代码,如下所示:

    hello = get_template_attribute('_cider.html', 'hello')
    return hello('World')
    
    更新日志
    参数:
    • template_name - 模板的名称
    • attribute - 要访问的宏变量的名称

 

  - 传入函数
  - django,自动执行
  - flask,不自动执行

 

全局模板传参

@app.template_global()
def sb(a1, a2):
# {{sb(1,9)}}
return a1 + a2

@app.template_filter()
def db(a1, a2, a3):
# {{ 1|db(2,3) }}
return a1 + a2 + a3

- 模板继承

layout.html

<!DOCTYPE html>
                <html lang="zh-CN">
                <head>
                    <meta charset="UTF-8">
                    <title>Title</title>
                    <meta name="viewport" content="width=device-width, initial-scale=1">
                </head>
                <body>
                    <h1>模板</h1>
                    {% block content %}{% endblock %}
                </body>
                </html>

tpl.html

{% extends "layout.html"%}


{% block content %}
    {{users.0}}
    {{users[0]}}
    {{txt}}
    <!--{{txt|safe}}-->
    {{func(6)}}
 
{% endblock %}

 

2、自定义模板方法

Flask中自定义模板方法的方式和Bottle相似,创建一个函数并通过参数的形式传入render_template,如:

html

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <h1>自定义函数</h1>
    {{ww()|safe}}

</body>
</html>

py文件

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from flask import Flask,render_template
app = Flask(__name__)
 
 
def wupeiqi():
    return '<h1>Wupeiqi</h1>'
 
@app.route('/login', methods=['GET', 'POST'])
def login():
    return render_template('login.html', ww=wupeiqi)
 
app.run()
 3、宏

  模板内定义一些常用的模板类似一个函数,不调用不显示,{{函数名(‘c1’)}}调用就显示。

 {% macro ccc(name, type='text',value='') %}
        <h1>函数模板宏</h1>
        <input type="{{ type }}" name="{{ name }} "value="{{ value }}">
        <input type="submit" value="提交">
    {% endmacro %}

    {{ ccc("n1") }}
    {{ ccc("n2") }}

模板安全传参:

    - 前端: {{u|safe}}

    - 后端 : MarkUp('sdfs')

Session

除请求对象之外,还有一个 session 对象。它允许你在不同请求间存储特定用户的信息。它是在 Cookies 的基础上实现的,并且对 Cookies 进行密钥签名要使用会话,你需要设置一个密钥。

session内部继承了字典所以他有字典的所有操作方法

 

  • 设置:session['username'] = 'xxx'

  • 删除:session.pop('username', None)

  Session 使用


        #当请求刚到来:flask读取cookie中session对应的值:eyJrMiI6NTY3fQ.DuJIBA.ak5d21YrFHw0FVBIyfEJJaIOuuo,
        #将该值解密并反序列化成字典,放入内存以便视图函数使用。

     @app.route('/ses')
     def ses():
      session['k1'] = 123
      session['k2'] = 456
      del session['k1']

      return "Session"

         #当请求结束时,flask会读取内存中字典的值,进行序列化+加密,写入到用户cookie中。
if __name__ == '__main__': app.run()

第三方使用

#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
pip3 install redis
pip3 install flask-session

"""


from flask import Flask, session, redirect
from flask.ext.session import Session


app = Flask(__name__)
app.debug = True
app.secret_key = 'asdfasdfasd'


app.config['SESSION_TYPE'] = 'redis'
from redis import Redis
app.config['SESSION_REDIS'] = Redis(host='192.168.0.94',port='6379')
Session(app)


@app.route('/login')
def login():
    session['username'] = 'alex'
    return redirect('/index')


@app.route('/index')
def index():
    name = session['username']
    return name


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

 闪现


闪现,在session中存储一个数据,读取时通过pop将数据移除。

源码:_request_ctx_stack.top.flashes = flashes = session.pop('_flashes') if '_flashes' in session else []  #移除数据后再读取值就是一个[ ]。

from flask import Flask,request,flash,get_flashed_messages
app = Flask(__name__)
app.secret_key = 'some_secret'
@app.route('/page1')
def page1():
    flash('eric')    #在message中设置1个个值
    return 'ok'
#----------------------------------------------------------------------
@app.route('/page2')
def page2():
    #获取message中设置的值,只能获取1次,再次访问值为[]。(1次性)
    print(get_flashed_messages())
    return "page2"
if __name__ == "__main__":
    app.run()

闪现消息类别

闪现消息还可以指定类别,如果没有指定,默认类别为 'message' 。不同的 类别可以给用户提供更好的反馈。

from flask import Flask,flash,get_flashed_messages
        @app.route('/page1')
        def page1():

            flash('临时数据存储','error')
            flash('sdfsdf234234','error')
            flash('adasdfasdf','info')

            return "Session"

        @app.route('/page2')
        def page2():
            print(get_flashed_messages(category_filter=['error']))
            return "page2"

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

 中间件

flask项目启动的简单流程:     run_simple(host, port, self, **options)#首先执行run_simple,里面有ip,端口self是app对象。

            对象加()执行__call__方法.         def __call__(self, environ, start_response):    return self.wsgi_app(environ, start_response)

            所有请求到来都先__call__方法,再执行wsgi_app方法。

方式1:  自定义一个中间件

from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
    print('index')
    return 'Index'

class Middleware(object):
    def __init__(self,old):
        self.old = old
    def __call__(self, *args, **kwargs):
        print('')
        ret =  self.old(*args,**kwargs)
        print('')
        return ret

if __name__ == '__main__':
    app.wsgi_app = Middleware(app.wsgi_app) #把源码的app.wsgi_app方法以传参的形式传入自定义的类Middleware,先调用自己的call方法,再调用源码call方法。
    app.run(port=8888)

方式2:

flask也有中间件功能和Django类似,不同的是使用的是使用3个装饰器来实现的;

  1.@app.before_first_request :请求第1次到来执行1次,之后都不执行;
  2.@app.before_request:请求到达视图之前执行;(改函数不能有返回值,否则直接在当前返回)
  3.@app.after_request:请求 经过视图之后执行;(最下面的先执行)

特殊装饰器

@app.before_request #谁先定义,谁先执行。

@app.after_request #谁后定义,谁先执行。

flask特殊装饰器中间件与1.0版本后django 的中间件请求遇到return后的 response流程有不同:

from flask import Flask
                app = Flask(__name__)


                @app.before_request
                def x1():
                    print('before:x1')
                    return ''

                @app.before_request
                def xx1():
                    print('before:xx1')


                @app.after_request
                def x2(response):
                    print('after:x2')
                    return response

                @app.after_request
                def xx2(response):
                    print('after:xx2')
                    return response



                @app.route('/index')
                def index():
                    print('index')
                    return "Index"


                @app.route('/order')
                def order():
                    print('order')
                    return "order"


                if __name__ == '__main__':

                    app.run()

 

 @app.before_first_request    #请求到来时只执行1次,内部原理是一个标识第一次请求为True,第一次请求结束后改为False

            from flask import Flask
            app = Flask(__name__)

            @app.before_first_request
            def x1():
                print('123123')


            @app.route('/index')
            def index():
                print('index')
                return "Index"


            @app.route('/order')
            def order():
                print('order')
                return "order"


            if __name__ == '__main__':

                app.run()

 @app.template_global() #模板渲染  定义全局函数

 @app.template_filter()     #模板使用

 @app.errorhandler()        #经常使用,可以自定义返回报错页面如404 错误。

@app.errorhandler(404)
def not_found(arg):
    print(arg)
    return "没找到"

蓝图

目标:给开发者提供目录结构

蓝图可以极大地简化大型应用并为扩展提供集中的注册入口。

Flask 中蓝图有以下用途:

  • 把一个应用分解为一套蓝图。这是针对大型应用的理想方案:一个项目可以实例化 一个应用,初始化多个扩展,并注册许多蓝图。
  • 在一个应用的 URL 前缀和(或)子域上注册一个蓝图。 URL 前缀和(或)子域的 参数成为蓝图中所有视图的通用视图参数(缺省情况下)。
    •   可以为某一类蓝图加上一个统一的前缀,例:app.register_blueprint(uc,url_prefix='/api') 注册后访问uc类试图必须/api/视图名
  • 使用不同的 URL 规则在应用中多次注册蓝图。
  • 通过蓝图提供模板过滤器、静态文件、模板和其他工具。蓝图不必执行应用或视图 函数。
  • 当初始化一个 Flask 扩展时,为以上任意一种用途注册一个蓝图。

当分配请求 时, Flask 会把蓝图和视图函数关联起来,并生成两个端点之前的 URL 。

创建蓝图:

from flask import Blueprint,render_template         #导入蓝图模块

ac = Blueprint('ac',__name__,template_folder='templates')#创建蓝图

@ac.route('/login')
def login():
    print('Login')
    return render_template('login.html')

@ac.route('/logout')
def logout():
    return 'Logout'

注册蓝图:

from flask import Flask

#用户面对app 再由此分发蓝图app
from .views.user import uc
from .views.account import ac


def create_app():
    
    app = Flask(__name__)
    #注册蓝图
    app.register_blueprint(ac)
    app.register_blueprint(uc)
    return app
 蓝图静态文件

蓝图的第三个参数是 static_folder 。这个参数用以指定蓝图的静态文件所在的 文件夹,它可以是一个绝对路径也可以是相对路径。:

  admin = Blueprint('admin', __name__, static_folder='static')

蓝图模板

如果你想使用蓝图来暴露模板,那么可以使用 Blueprint 的 template_folder 参数:

  admin = Blueprint('admin', __name__, template_folder='templates')

创建URL

如果要创建页面链接,可以和通常一样使用 url_for() 函数,只是要把蓝图 名称作为端点的前缀,并且用一个点( . )来分隔:

url_for('admin.index')

另外,如果在一个蓝图的视图函数或者被渲染的模板中需要链接同一个蓝图中的其他 端点,那么使用相对重定向,只使用一个点使用为前缀:

url_for('.index')

如果当前请求被分配到 admin 蓝图端点时,上例会链接到 admin.index 。

错误处理器

蓝图像 Flask 应用对象一样支持错误处理装饰器,所以很容易使用蓝图特 定的自定义错误页面。

下面是 “404 Page Not Found” 异常的例子:

@simple_page.errorhandler(404)
def page_not_found(e):
    return render_template('pages/404.html')

 

大多数错误处理器会按预期工作。然而,有一个涉及 404 和 405 例外处理器的警示。 这些错误处理器只会由一个适当的 raise 语句引发或者调用在另一个蓝图视图 中调用 abort 引发。它们不会引发于无效的 URL 访问。这是因为蓝图不“拥有” 特定的 URL 空间,在发生无效 URL 访问时,应用实例无法知道应该运行哪个蓝图错 误处理器。 如果你想基于 URL 前缀执行不同的错误处理策略,那么可以 在应用层使用 request 代理对象定义它们:

@app.errorhandler(404)
@app.errorhandler(405)
def _handle_api_error(ex):
    if request.path.startswith('/api/'):
        return jsonify_error(ex)
    else:
        return ex

 

posted @ 2018-12-06 11:38  蛇夫座  阅读(205)  评论(0)    收藏  举报