Django笔记 —— 表单(form)

  最近在学习Django,打算玩玩网页后台方面的东西,因为一直很好奇但却没怎么接触过。Django对我来说是一个全新的内容,思路想来也是全新的,或许并不能写得很明白,所以大家就凑合着看吧~

  本篇笔记(其实我的所有笔记都是),并不会过于详细的讲解。因此如果有大家看不明白的地方,欢迎在我正版博客下留言,有时间的时候我很愿意来这里与大家探讨问题。(当然,不能是简简单单就可以百度到的问题-.-)

  我所选用的教材是《The Django Book 2.0》,本节是表单部分,对应书中第七章。

------------------------------------------------------------------------------------------------------------------------------------------------

0、阅读方法

  本节笔记,略去很多书中学习过程与讲解,建议在看完原书此节后,作总结复习之用。

  站点创建:django-admin.py startproject comeback

1、视图中的HttpResponse

  首先给我们的代码加上一个视图,网址是"http://127.0.0.1:8000/",网站内容就是一个Hello World。

  显然,其在"/comeback/views.py"中的代码内容是:

from django.http import HttpResponse

def hello(request):
    return HttpResponse("Hello World")

  其中,HttpResponse对象,即request变量,是有很多成员(属性和方法)的。通过他们,你可以知道很多信息,例如:正在加载这个页面的用户是谁,他用的是什么浏览器。

  这里列举一些属性:

成员 说明 举例
request.path 除域名以外的请求路径,以斜杠(即 /)开头 ”/hello/“
request.get_host() 主机名(例如:通常所说的域名) "127.0.0.1" or "www.example.com"
request.get_full_path() 请求路径,可能包含查询字符串 "/hello/?print=true"
request.is_secure() 如果通过https访问,返回True;否则,返回False True or False

  还有一个属性要重点说明,request.META,这是一个python字典,包含了所有本次HTTP请求的Header信息。这个信息是由用户的浏览器所提交的:

成员 说明 备注
HTTP_REFERER 进站前链接网页(如果有的话) 这是REFERRER的笔误-.-|||
HTTP_USER_AGENT 用户浏览器的user-agent字符串(如果有的话) 详见这一篇博文
REMOTE_ADDR 客户端IP 如果经过代理服务器,那么是逗号分割的多个IP地址

  应当注意,既然是用户浏览器提交的,这个信息也就不一定靠谱。因此,应当使用下列方式读取其中内容:

    1. 使用 try / except 语句

def ua_display_good1(request):
    try:
        ua = request.META['HTTP_USER_AGENT']
    except KeyError:
        ua = 'unknown'
    return HttpResponse("Your browser is %s" % ua)

    2. 使用 python字典的 get()方法(推荐)

def ua_display_good2(request):
    ua = request.META.get('HTTP_USER_AGENT', 'unknown')
    return HttpResponse("Your browser is %s" % ua)

  书中建议,你写一个函数,把request.META中所有数据打印出来看看,比如这样

1 def display_meta(request):
2     values = request.META.items()
3     values.sort()
4     html = []
5     for k, v in values:
6         html.append('<tr><td>%s</td><td>%s</td></tr>' % (k, v))
7     return HttpResponse('<table>%s</table>' % '\n'.join(html))

  request.META的内容太多了,我把其内容做了初步的整理和翻译,有兴趣的同学可以到本节末尾附录中看。

  当然,也可以用模板实现,而非手动输入代码,这里不多说。

  request中,还有两个属性,内含用户所提交的信息:

成员 说明
request.GET HTML中的<form>标签提交的 or URL中的查询字符串(the query string)
request.POST HTML中的<form>标签提交的

  这两个都是类字典对象,即其实现了字典的所有成员,另外还有些字典没有的成员。

2. 利用GET请求,查询一本书籍

  要做的事情很简单,做一个书籍查询页面,可以输入书名查书的信息。做法如下:

  1. 按照模型一节所讲,创建书籍的数据库。

  2. 在"/books/"下,创建几个文件

    search_form.html

 1 <html>
 2 <head>
 3     <title>Search</title>
 4 </head>
 5 <body>
 6     {% if errors %}
 7         <ul>
 8             {% for error in errors %}
 9             <li>{{ error }}</li>
10             {% endfor %}
11         </ul>
12     {% endif %}
13     <form action="" method="get">
14         <input type="text" name="q">
15         <input type="submit" value="Search">
16     </form>
17 </body>
18 </html>
View Code

    search_results.html 

 1 <p>You searched for: <strong>{{ query }}</strong></p>
 2 
 3 {% if books %}
 4     <p>Found {{ books|length }} book{{ books|pluralize }}.</p>
 5     <ul>
 6         {% for book in books %}
 7         <li>{{ book.title }}</li>
 8         {% endfor %}
 9     </ul>
10 {% else %}
11     <p>No books matched your search criteria.</p>
12 {% endif %}
View Code

    views.py

 1 from django.shortcuts import render_to_response
 2 from books.models import Book
 3 
 4 # Create your views here.
 5 
 6 def search(request):
 7     errors = []
 8     if 'q' in request.GET:
 9         q = request.GET['q']
10         if not q:
11             errors.append('Enter a search term.')
12         elif len(q)>20:
13             errors.append('Please enter at most 20 characters.')
14         else:
15             books = Book.objects.filter(title__icontains=q)
16             return render_to_response('search_results.html', {'books': books, 'query': q})
17     return render_to_response('search_form.html', {'errors': errors})
View Code

  3. 在url中的urlpatterns属性内,加入如下一条  url(r'^search/$', views.search),  并对应写出from...import语句

  4. 运行站点:python manage.py runserver

  5. 打开搜索页面:http://127.0.0.1:8000/search/

3. 表单简介

  在HTTP中,表单(form标签),是用来提交数据的,其action属性说明了其传输数据的方法:如何传、如何接收。

  访问网站时,表单可以实现客户端与服务器之间的通信。例如我们上面的查询书籍,就用到了表单(其属性中,action=get)。再比如说注册与登陆,也是要用到表单的。但这里由于涉及到隐私问题,需要保证数据传输的安全性,因此其传输方法就应当使用post而非get。

  总之,对客户端来说,表单就是用来向服务器提交数据的;而对服务器来说,表单就是你提供给客户端的发送信息的渠道,你需要对用户发送来的信息进行处理和响应,以达到页面的交互。

3+. get与post方法简介

  这里做一些扩展——介绍一下表单的传输方法。

  表单,一共有四种数据传输方法(即action的值):get、post、put、delete,即查、改、增、删。

  比如,上面查询书籍的 search_form.html 代码中,用的就是get方法。

  由于put和delete都可以用post实现,因此往往只使用get和post两种,甚至传统的Web MVC框架基本上都只支持这两种HTTP方法(-.-||)。这里,暂不介绍put和delete方法。

  首先给出一段百度知道上对于get和post的简介,原文作者是tawa08,原文在这里

    1. get是从服务器上获取数据,post是向服务器传送数据。
    2. get是把参数数据队列加到提交表单的action属性所指的url中,值和表单内各个字段一一对应,在url中可以看到。
        
post是通过http post机制,将表单内各个字段与其内容放置在html header内一起传送到action属性所指的url地址。
        
用户看不到这个过程。
    
3. 对于get方式,服务器端用Request.QueryString获取变量的值,对于post方式,服务器端用Request.Form获取提交的数据。
    
4. get传送的数据量较小,不能大于2KB。post传送的数据量较大,一般被默认为不受限制。
        
但理论上,IIS4中最大量为80KB,IIS5中为100KB。
    
5. get安全性非常低,post安全性较高。但是执行效率却比Post方法好。

    建议:
    
1、get方式的安全性较Post方式要差些,包含机密信息的话,建议用Post数据提交方式;
    
2、在做数据查询时,建议用Get方式;而在做数据添加、修改或删除时,建议用Post方式;

  如果希望对get和post进一步了解,那我这里推荐一篇文章,虽然长但却清晰而且很全:为什么大型网站都采用get方法,而非post方法

  至此,对于传输数据方法的介绍告一段落,咱们言归正传。

4. CSRF简介

  由于我们要使用django中的form库,而且要用到post,所以便需要了解CSRF。

  CSRF,Cross Site Request Forgery,跨站请求伪造,这是一种黑客攻击方式,这里不过多介绍。你只需知道,当你使用表单传输数据时,有可能会接触这种攻击方式。因此,我们学习表单,最好知道这种攻击方式的存在。对于想深入了解的同学,我推荐一篇博文:浅谈CSRF攻击方式

  从之前对get和post的介绍中,大家可以了解到,在标准的用法中,get由于毫无安全性可言,因此只应用作数据的查询;而一旦涉及到数据的添加、修改、删除时,则一定要采用post方式。那么,只要网站设计的符合规范,针对get的CSRF攻击便无从谈起。

  因此,django则假设大家遵守这个标准,只在除了get之外那三种方法中才有针对CSRF的防御机制。

  综上,在django中,若你接触到form中的post,要么就只使用get,要么就关了CSRF防御机制,要么就正确打开并使用CSRF防御机制,若不正确设置则无法使用form库。

  这里给出官方的设置CSRF防御机制的步骤,若想进一步了解,参见官方文档

    1. 在所有使用post方法的模板(html)中,做如下修改:

      把表单开头的代码  <form action="." method="post"> 

      改成这样  <form action="." method="post">{% csrf_token %} 

    2. 在所有上面修改过的模板对应的视图(views.py)中,做如下修改:

      把原视图代码,比如这样的

from django.shortcuts import render_to_response

def my_view(request):
    return render_to_response("a_template.html")

      改成这样

from django.core.context_processors import csrf
from django.shortcuts import render_to_response

def my_view(request):
    c = {}
    c.update(csrf(request))
    return render_to_response("a_template.html", c)

5. email设置

  我们后面要发送邮件,因此还需要先设置好email相关的内容。

  先说一下后面具体要用django.form做什么:我们要做一个表单,效果如下图:

  点击Submit之后,网页后台会发送一封邮件。标题就是hello,内容是hi!,从邮箱A发送到邮箱B。这两个邮箱都是我们提前设置好的。

  我们设想的场景就是:这是一个网站,网站的访问者可以通过这里直接向网站的制作者发送邮件。

  这个过程,是需要用到邮箱A的SMTP服务的,这需要你的开通。比如我用的qq邮箱,开通方式就如教程所说。

  另外,这过程是django后台做的,那你自然需要告诉django你的邮箱用户名密码,还有SMTP服务的主机和端口,这需要在settings.py中添加以下参数:

EMAIL_HOST = 'smtp.qq.com'
EMAIL_PORT = 25
EMAIL_HOST_USER = '820645278@qq.com'
EMAIL_HOST_PASSWORD = 'nicai'

  这样一来,你便可以让django从后台帮你用你设置的这个邮箱(EMAIL_HOST_USER)发送邮件了。

  这个存两个疑问,希望高手予以解答:

    1. 为什么把EMAIL_PORT参数屏蔽了,仍可以正常运行?难道端口可以自动找?

    2. 本例原意是邮箱A由访问者输入,但我这种设置方法则锁定邮箱A必须是设置的这个邮箱,于是这个Email的输入框形同虚设。

    比如我就像下面代码中那样实现,send_mail()中的变量f若不是820645278@qq.com,则会报错:

      SMTPSenderRefused at /contact/ (501, 'mail from address must be same as authorization user', u'82064527@qq.com')

    如果我想达到访问者可以用任意邮箱访问的效果,那么我应当如何设置呢?

6. django中的form库:django.form  

  了解了表单、django中的CSRF防御机制、email的设置,下面我们终于可以介绍django.form了。

  django是框架,那么它的存在始终只有一个目的:让你写网站更加方便。django.form,就是一个可以让你快速写出表单的库。(不用form库的写法,原书中有,有兴趣的可以去看看,这里不写了)

  具体例子上面介绍email设置时已经说过,下面直接给出实现步骤:

    0. 把上面的email设置做好

    1. 创建"/contact/"目录

    2. 在其中创建一个空的__init__.py,以及下列文件

      contact_form.html

 1 <html>
 2 <head>
 3     <title>Contact us</title>
 4 </head>
 5 <body>
 6     <h1>Contact us</h1>
 7 
 8     {% if form.errors %}
 9         <p style="color: red;">
10             Please correct the error{{ form.errors|pluralize }} below.
11         </p>
12     {% endif %}
13 
14     {% csrf_token %}
15 
16     <form action="" method="post">{% csrf_token %}
17         <table>
18             {{ form.as_table }}
19         </table>
20         <input type="submit" value="Submit">
21     </form>
22 </body>
23 </html>
View Code

      forms.py

1 from django import forms
2 
3 class ContactForm(forms.Form):
4     subject = forms.CharField()
5     email = forms.EmailField(required=False)
6     message = forms.CharField()
View Code

      views.py

 1 from django.core.mail import send_mail
 2 from django.core.context_processors import csrf
 3 from django.shortcuts import render_to_response, RequestContext
 4 from contact.forms import ContactForm
 5 from django.http import HttpResponseRedirect
 6 
 7 def thanks(request):
 8     return render_to_response('thanks.html')
 9 
10 def contact(request):
11     c = {}
12     c.update(csrf(request))
13     if request.method == 'POST':
14         form = ContactForm(request.POST)
15         if form.is_valid():
16             cd = form.cleaned_data
17             f = cd.get('email', '820645278@qq.com')
18             if f == '': f = '820645278@qq.com'
19             send_mail(
20                 cd['subject'],
21                 cd['message'],
22                 f,
23                 ['icedream@sjtu.edu.cn'],
24             )
25             return HttpResponseRedirect('/contact/thanks/', {'method': request.method})
26     else:
27         form = ContactForm()
28     c['form'] = form
29     return render_to_response('contact_form.html', c, context_instance=RequestContext(request))
View Code

      thanks.html

1 <html>
2 <body>
3 Thanks!
4 </body>
5 </html>
View Code

    3. 在url的urlpatterns属性内,加入如下两条  url(r'^contact/$', contact),   url(r'^contact/thanks/$', thanks),  并对应写出from...import语句

    4. 运行站点:python manage.py runserver

    5. 打开站点联系表单页面:http://127.0.0.1:8000/contact/

    6. 如果成功,你应该能在邮箱B(即代码中icedream@sjtu.edu.cn)中收到你用邮箱A(即代码中820645278@qq.com)所发的邮件。

7. 总结:这一节都讲了些什么

  这一节首先介绍了之前一直在使用却并不了解的HttpResponse对象,

  然后介绍了表单、GET和POST方法、CSRF攻击方式、email设置,

  最后介绍了django中的表单(form)库,以及如何使用它做出网站的表单。

------------------------------------------------------------------------------------------------------------------------------------------------

  至此,“表单”一章笔记完成,django基础部分学习完毕。后面开始高级部分,下一章是“高级视图与URL配置”。

 


 

附录1+. HttpResponse.META内容

名称 值(断句方式仅供参考) 参考翻译
CLUTTER_IM_MODULE xim CLUTTER输入法模块
COLORTERM gnome-terminal 终端配色 
COMPIZ_CONFIG_PROFILE ubuntu 特效配置资料 
CONTENT_LENGTH   内容长度 
CONTENT_TYPE text/plain 内容类型 
CSRF_COOKIE R6SCXazGfl9QGZ2YsCI3VniLFiYNeOUj CSRFcookie 
DBUS_SESSION_BUS_ADDRESS unix:abstract=/tmp/dbus-HALHk0izgV

数据总线会话

总线地址 

DEFAULTS_PATH /usr/share/gconf/ubuntu.default.path 默认路径 
DESKTOP_SESSION ubuntu 桌面会话 
DISPLAY :0 展示 
DJANGO_SETTINGS_MODULE comeback.settings django设置模块 
GATEWAY_INTERFACE CGI/1.1 网关接口 
GDMSESSION ubuntu GDM会话 
GDM_LANG zh_CN GDM语言 
GNOME_DESKTOP_SESSION_ID this-is-deprecated GNOME桌面会话ID 
GNOME_KEYRING_CONTROL /run/user/1000/keyring-SkW2gT GNOME钥匙控制 
GNOME_KEYRING_PID 2176 GNOME钥匙PID 
GPG_AGENT_INFO /run/user/1000/keyring-SkW2gT/gpg:0:1 GPG代理信息 
GTK_IM_MODULE fcitx GTK输入法模块 
GTK_MODULES

overlay-scrollbar:

unity-gtk-module

GTK模块 
HOME /home/icedream 家 
HTTP_ACCEPT

text/html,

application/xhtml+xml,

application/xml;q=0.9,

image/webp,

*/*;q=0.8

HTTP接收 
HTTP_ACCEPT_ENCODING

gzip,

deflate,

sdch

HTTP接收编码 
HTTP_ACCEPT_LANGUAGE

zh-CN,

zh;q=0.8,

en;q=0.6,

en-US;q=0.4,

en-GB;q=0.2

HTTP接收语言 
HTTP_CONNECTION keep-alive HTTP连接 
HTTP_COOKIE

sessionid=8ifnqpfwvuh0pm04kq24zz4djw3lx4fp;

csrftoken=R6SCXazGfl9QGZ2YsCI3VniLFiYNeOUj

HTTPcookie

会话id & CSRF令牌 

HTTP_HOST 127.0.0.1:8000 HTTP主机 
HTTP_USER_AGENT

Mozilla/5.0 (X11; Linux i686)

AppleWebKit/537.36 (KHTML, like Gecko)

Chrome/43.0.2357.134

Safari/537.36

HTTP用户代理 
IM_CONFIG_PHASE 1 输入法配置阶段 
INFOPATH :/usr/local/texlive/2015/texmf-dist/doc/info 信息路径 
INSTANCE Unity 实例 
JOB gnome-session 工作 
LANG zh_CN.UTF-8 语言 
LANGUAGE

zh_CN:

zh

语言 
LESSCLOSE /usr/bin/lesspipe %s %s LESS关闭 
LESSOPEN | /usr/bin/lesspipe %s LESS打开 
LOGNAME icedream 登陆用户名 
LS_COLORS

rs=0:
di=01;34:
ln=01;36:
mh=00:
pi=40;33:
so=01;35:
do=01;35:
bd=40;33;01:
cd=40;33;01:
or=40;31;01:
su=37;41:
sg=30;43:
ca=30;41:
tw=30;42:
ow=34;42:
st=37;44:
ex=01;32:
*.tar=01;31:
*.tgz=01;31:
*.arc=01;31:
*.arj=01;31:
*.taz=01;31:
*.lha=01;31:
*.lz4=01;31:
*.lzh=01;31:
*.lzma=01;31:
*.tlz=01;31:
*.txz=01;31:
*.tzo=01;31:
*.t7z=01;31:
*.zip=01;31:
*.z=01;31:
*.Z=01;31:
*.dz=01;31:
*.gz=01;31:
*.lrz=01;31:
*.lz=01;31:
*.lzo=01;31:
*.xz=01;31:
*.bz2=01;31:
*.bz=01;31:
*.tbz=01;31:
*.tbz2=01;31:
*.tz=01;31:
*.deb=01;31:
*.rpm=01;31:
*.jar=01;31:
*.war=01;31:
*.ear=01;31:
*.sar=01;31:
*.rar=01;31:
*.alz=01;31:
*.ace=01;31:
*.zoo=01;31:
*.cpio=01;31:
*.7z=01;31:
*.rz=01;31:
*.cab=01;31:
*.jpg=01;35:
*.jpeg=01;35:
*.gif=01;35:
*.bmp=01;35:
*.pbm=01;35:
*.pgm=01;35:
*.ppm=01;35:
*.tga=01;35:
*.xbm=01;35:
*.xpm=01;35:
*.tif=01;35:
*.tiff=01;35:
*.png=01;35:
*.svg=01;35:
*.svgz=01;35:
*.mng=01;35:
*.pcx=01;35:
*.mov=01;35:
*.mpg=01;35:
*.mpeg=01;35:
*.m2v=01;35:
*.mkv=01;35:
*.webm=01;35:
*.ogm=01;35:
*.mp4=01;35:
*.m4v=01;35:
*.mp4v=01;35:
*.vob=01;35:
*.qt=01;35:
*.nuv=01;35:
*.wmv=01;35:
*.asf=01;35:
*.rm=01;35:
*.rmvb=01;35:
*.flc=01;35:
*.avi=01;35:
*.fli=01;35:
*.flv=01;35:
*.gl=01;35:
*.dl=01;35:
*.xcf=01;35:
*.xwd=01;35:
*.yuv=01;35:
*.cgm=01;35:
*.emf=01;35:
*.axv=01;35:
*.anx=01;35:
*.ogv=01;35:
*.ogx=01;35:
*.aac=00;36:
*.au=00;36:
*.flac=00;36:
*.m4a=00;36:
*.mid=00;36:
*.midi=00;36:
*.mka=00;36:
*.mp3=00;36:
*.mpc=00;36:
*.ogg=00;36:
*.ra=00;36:
*.wav=00;36:
*.axa=00;36:
*.oga=00;36:
*.spx=00;36:
*.xspf=00;36:

LS颜色 
MANDATORY_PATH /usr/share/gconf/ubuntu.mandatory.path MANDATORY(托管)路径 
MANPATH :/usr/local/texlive/2015/texmf-dist/doc/man MAN路径 
OLDPWD /home/icedream 旧的工作目录 
PATH

/usr/local/sbin:

/usr/local/bin:

/usr/sbin:

/usr/bin:

/sbin:

/bin:

/usr/games:

/usr/local/games:

/usr/local/texlive/2015/bin/i386-linux

路径 
PATH_INFO /cookie/ 路径信息 
PWD /home/icedream/workspace/django/comeback 当前目录 
QT4_IM_MODULE fcitx QT4输入法模块 
QT_IM_MODULE fcitx QT输入法模块 
QT_QPA_PLATFORMTHEME appmenu-qt5 QT_QPA平台主题 
QUERY_STRING   查询字符串 
REMOTE_ADDR 127.0.0.1 远程地址 
REMOTE_HOST   远程主机 
REQUEST_METHOD GET 请求方法 
RUN_MAIN true 运行MAIN 
SCRIPT_NAME   脚本名称 
SERVER_NAME localhost 服务器名称 
SERVER_PORT 8000 服务器端口 
SERVER_PROTOCOL HTTP/1.1 服务器协议 
SERVER_SOFTWARE WSGIServer/0.1 Python/2.7.8 服务器软件 
SESSIONTYPE gnome-session 会话类型 
SHELL /bin/bash 命令行 
SHLVL 1 命令行层次 
SSH_AUTH_SOCK /run/user/1000/keyring-SkW2gT/ssh SSH_AUTH_SOCK 
TERM xterm TERM 
TEXTDOMAIN im-config 文本域 
TEXTDOMAINDIR /usr/share/locale/ 文本域目录 
TZ UTC 时区 
UPSTART_EVENTS started starting UPSTART事件 
UPSTART_INSTANCE   UPSTART距离 
UPSTART_JOB unity-settings-daemon UPSTART作业 
UPSTART_SESSION unix:abstract=/com/ubuntu/upstart-session/1000/2178 UPSTART会话 
USER icedream 用户 
VTE_VERSION 3603 VTE版本 
WINDOWID 69206028 窗口ID 
XAUTHORITY /home/icedream/.Xauthority X权威 
XDG_CONFIG_DIRS

/etc/xdg/xdg-ubuntu:

/usr/share/upstart/xdg:

/etc/xdg

XDG配置路径 
XDG_CURRENT_DESKTOP Unity XDG当前桌面
XDG_DATA_DIRS

/usr/share/ubuntu:

/usr/share/gnome:

/usr/local/share/:

/usr/share/

XDG数据路径 
XDG_GREETER_DATA_DIR /var/lib/lightdm-data/icedream XDG_GREETER数据路径 
XDG_RUNTIME_DIR /run/user/1000 XDG运行时路径 
XDG_SEAT seat0 XDG椅子 
XDG_SEAT_PATH /org/freedesktop/DisplayManager/Seat0 XDG椅子路径 
XDG_SESSION_DESKTOP ubuntu XDG会话桌面 
XDG_SESSION_ID c2 XDG会话ID 
XDG_SESSION_PATH /org/freedesktop/DisplayManager/Session0 XDG会话路径 
XDG_SESSION_TYPE x11 XDG会话类型 
XDG_VTNR 7 XDG_VTNR 
XMODIFIERS @im=fcitx XMODIFIERS 
_ /usr/bin/python
wsgi.errors ', mode 'w' at 0xb74d20d0> WSGI错误 
wsgi.file_wrapper wsgiref.util.FileWrapper WSGI文件包装 
wsgi.input <socket._fileobject object="" at="" 0xb5a726ec=""> WSGI输入 
wsgi.multiprocess False WSGI多进程 
wsgi.multithread True WSGI多线程 
wsgi.run_once False WSGI运行一次 
wsgi.url_scheme http WSGI网址类型 
wsgi.version (1, 0) WSGI版本 

posted @ 2015-07-17 21:58 IceDream61 阅读(...) 评论(...) 编辑 收藏