Month: 六月 2017

Dockerfile 基础教程

Dockerfile 列出了构建一个docker image 的可复现步骤。比起一步一步通过 docker commit 来制作一个镜像,dockerfile更适用于 CI 自动测试等系统。

Dockerfile 命令

  • FROM,指定基础镜像
  • MAINTAINER,作者,建议格式(Jon Snow <jonsnow@westros.com>
  • EXPOSE,需要暴露的端口,但是一般也会使用 -p 来制定端口映射
  • USER,运行的用户
  • WORKDIR,进程的工作目录
  • COPY,复制文件到
  • RUN,运行shell命令
  • CMD,启动进程使用的命令
  • ENTRYPOINT,镜像启动的入口,默认是 bash -c
  • ENV,设定环境变量
  • VOLUME,卷

几个比较容易混淆的

COPY vs ADD

ADD 会自动解压压缩包,在不需要特殊操作的时候,最好使用COPY。

ENTRYPOINT vs CMD

entrypoint 指定了 Docker 镜像要运行的二进制文件(当然也包括参数),而 cmd 则指定了运行这个二进制文件的参数。不过因为默认 entrypoint 是 bash -c,所以实际上 CMD 指定的也是要运行的命令。

另外,docker run 时候包含命令行参数,会执行命令行参数,而不是 CMD 的内容。如果使用 /bin/bash 作为命令行的指令,这样便替换掉 CMD 的内容,从而进入镜像中查看编译出的镜像究竟是什么样的。

个人倾向于只使用 CMD,而不使用 ENTRYPOINT

如何理解 VOLUME 指令

Dockerfile 中的 volume 指定了一个匿名的 docker volume,也就是说在 docker run 的时候,docker 会把对应的目录mount 到一个匿名的卷。当然如果使用 -v 参数指定了 mount 到哪个目录,或者是指定了卷名,那就不会采用匿名的卷了。

使用Dockerfile 还是 commit 来构建镜像

如果可能的话,尽量使用 dockerfile,因为是可复现的。

I’ve been wondering the same thing, and my impression (which could be totally wrong) it that it’s really the same case as with VMs –> you don’t want to not know how to recreate the vm image. In my case I have regular .sh scripts to install, and am wondering why I can’t just maintain these, run docker and effectively call these, and create the golden version image that way. My scripts work to get it installed on a local PC, and the reason I want to use docker is to deal with conflicts of multiple instances of programs/have clean file system/etc
https://stackoverflow.com/questions/26110828/should-i-use-dockerfiles-or-image-commits

参考

  1. https://stackoverflow.com/a/34245657/1061155
  2. https://stackoverflow.com/questions/41935435/understanding-volume-instruction-in-dockerfile

systemd

YN:如何使安装的服务开机启动?是更改 wantedby 吗?如果是,wantedby 的值应该是什么? 对于 nginx 这样的 daemon 服务如何管理?

大多数的 Linux 系统已经选择了 systemd 来作为进程管理器。之前打算使用 supervisord 来部署服务,思考之后发现还不如直接使用 systemd 呢。这篇文章简单介绍下 systemd。 # 例子

我们从一个例子开始,比如说我们有如下的 go 程序:

<pre class="code">package main

import (
    fmt
    net/http
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, Hi there!)
}

func main() {
    http.HandleFunc(/, handler)
    http.ListenAndServe(:8181, nil)
}

编译到 /opt/listen/listen 这里。 首先我们添加一个用户,用来运行我们的服务:

<pre class="code">adduser -r -M -s /bin/false www-data

记下这条命令,如果需要添加用户来运行服务,可以使用这条。

Unit 文件

Unit 文件定义了一个 systemd 服务。/usr/lib/systemd/system/ 存放了系统安装的软件的 unit 文件,/etc/systemd/system/ 存放了系统自带的服务的 unit 文件。 我们编辑 /etc/systemd/system/listen.service 文件:

<pre class="code">[Unit]
Description=Listen

[Service]
User=www-data
Group=www-data
Restart=on-failure
ExecStart=/opt/listen/listen
WorkingDirectory=/opt/listen

Environment=VAR1=whatever VAR2=something else
EnvironmentFile=/path/to/file/with/variables

[Install]
WantedBy=multi-user.target

然后

<pre class="code">sudo systemctl enable listen
sudo systemctl status listen
sudo systemctl start listen

其他一些常用的操作还包括:

<pre class="code">systemctl start/stop/restart    
systemctl reload/reload-or-restart  
systemctl enable/disable    
systemctl status    
systemctl is-active 
systemctl is-enabled
systemctl is-failed
systemctl list-units [--all] [--state=…]    
systemctl list-unit-files
systemctl daemon-reload 
systemctl cat [unit-name]   
systemctl edit [uni-name]
systemctl list-dependencies [unit]

依赖管理

In that case add Requires=B and After=B to the [Unit] section of A. If the dependency is optional, add Wants=B and After=B instead. Note that Wants= and Requires= do not imply After=, meaning that if After= is not specified, the two units will be started in parallel. if you service depends on another service, use requires= + after= or wants= + after=

类型

Type: simple / forking 关于每个字段的含义,可以参考这篇文章

使用 journalctl 查看日志

首先吐槽一下, 为什么要使用 journal 这么一个拗口的单词, 叫做 logctl 不好么…

<pre class="code">journalctl -u service-name.service

还可以添加 -b 仅查看本次重启之后的日志.

启动多个实例

https://unix.stackexchange.com/questions/288236/have-systemd-spawn-n-processeshttp://0pointer.de/blog/projects/instances.html

SSL Pinning 与破解

什么是 SSL Pinning

To view https traffic, you could sign your own root CA, and perform mitm attack to view the traffic. HPKP (http public key pinning) stops this sniffing by only trust given CA, thus, your self-signed certs will be invalid. To let given app to trust your certs, you will have to modify the apk file.

How to break it?

Introducing Xposed

decompile, modify and then recompile the apk file can be very diffcult. so you’d better hook to some api to let the app you trying to intercept trust your certs. xposed offers this kind of ability. moreover, a xposed module called JustTrustMe have done the tedious work for you. just install xposed and JustTrustMe and you are off to go. Here are the detaild steps:

  1. Install Xposed Installer

for android 5.0 above, use the xposed installer.

NOTE: 对于 MIUI,需要搜索 Xposed 安装器 MIUI 专版。

  1. Install Xposed from xposed installer, note, you have to give root privilege to xposed installer

  2. Install JustTrustMe

常用的反爬虫封禁手段概览

一般的网站都不欢迎爬虫流量,消耗服务器资源不说,还会把自己的商业数据爬走,于是就诞生了各种各样的反爬虫手段。

从接口的角度来说,匿名的接口一定是可以滥用的,只是破解成本的问题,而有登录状态的接口一般不容易被滥用。客户端反爬一定是可以破解的,服务端反爬往往不一定能够破解。

这篇文章有点长,没时间看的同学可以直接拉到最后看总结的思维导图。

客户端反爬

通过在访问用户本地生成一些特征来作为区分真实用户和爬虫的做法称为客户端反爬。我们知道客户端大体可以分为两类:浏览器和 APP。其中由于浏览器只能使用 JavaScript,而 JavaScript 是明文的,所以浏览器的反爬比较简单一些。而 APP 一般会把加密代码写在 C 模块中,所以破解难度比较大。

浏览器反爬

浏览器反爬的第一个手段就是“验明正身”,也就是说验证是否是浏览器发出的请求

  1. 验证是否是浏览器,甚至于验证是否是自动控制的浏览器
  2. 前端通过 JS 生成 token

对于验证是否是浏览器来说,我们可以直接使用 selenium 或者 puppeteer 这种可以程序化控制的库来爬取网站。有一些网站还会检测是不是采用了这种自动化的手段,至于如何绕过这些限制又是一个大问题了,会在后面的文章中详细阐述。

有一些 API 访问必须通过Token,如果含有合法这个 token 就认为访问是合法的。一般来说在使用了 token 验证访问合法性的时候,服务端就不太会再对 IP 等做限制了。

Token 的计算过程往往有三个因素需要参与,分别是 key、secret 和签名算法。比如说下面的 API:

GET api.example.com/v1/search?q=XXX&type=XXX&limit=5&timestamp=1501230129&app_key=424242token=XXX

其中 app_key 等于 424242,表示请求方的唯一ID,secret是服务器授予请求方的密码,比如123456。而

secret = md5(sorted(["k=v" for k, v in params] + ["secret=123456"]).join("") + )

也就是把所有参数都排序之后,拼接成字符串然后再计算某个hash值,作为token附在参数后面。

一般来说常用的签名算法都是这样实现的:

  1. 参数中加上时间戳,同时附在请求上,这样服务器可以只接受当前时间附近的真实请求,从而避免某个请求被保存下来,用作重放攻击。
  2. 添加参数secret=123456到需要计算的参数中,但是secret并不会出现在请求中。
  3. 把所有需要加密的参数都按照字典排序,然后拼接成字符串,这样是为了计算出来的值唯一。
  4. 计算出的token也附在请求上,一起发给服务器。
  5. 服务器根据app_key,取出对应的app_secret,用同样的方法计算token,验证合法性。

app_key 的分配和含义

一般有两种理解,一种是把 app_key 作为某种类型客户端的标示,比如安卓客户端使用一个appkey,iOS客户端使用一个appkey。另一种是每个用户使用一个appkey,把appkey作为用户的标示。

对于网页中通过 ajax 请求 API 来说,因为 js 实际上相当于是源码公开的,所以隐藏secret和算法实际上是不现实的。这时候可以有两种做法,一种是把secret和加密算法等放到Flash里面去,flash是可以编译成二进制的,所以相对来说更安全一些,不过随着flash的死亡,这种做法应该是逐渐淘汰了。另一种做法是secret动态获取,控制secret的来提高破解难度,同时把加密算法做一些混淆。

对于 APP 中来说,简单一点的做法可直接把secret和算法都直接放到代码里面,但是一般来说因为通用的加密方法大家套路基本也都那么几样,通过反编译之后加上一些基于经验的猜测很容易才出来。所以进一步可以把加密算法写到native层,编译成so文件,这样就大大提高了反编译的难度,基本可以认为是安全的。

更严厉一点的话,可以限制只有登录用户可以访问某些敏感接口,这样就完全由服务端来控制接口的访问量了,只需要注意用户注册的接口不要被滥用即可。这就是服务端的验证了,后面会继续讨论。

关于 JS 的反编译和破解也是一个很大的话题了,有机会了再写。

一个例子

比如淘宝H5站的接口:

当请求不带任何cookie时,会返回一个_m_h5_tk_m_h5_tk_enc,通过下面的算法算出sign值再次请求

sign算法:_m_h5_tk值的'_'前部分+时间戳+appkey+data 中间用&分隔,如下

echo -n 'ddc882e0e69bb8babbfdecc479439252&1450260485494&12574478&{"platform":"8","asac":"D679AU6J95PHQT67G0B5","days":50,"cinemaid":"24053","showid":141207}'|md5sum|cut -d ' ' -f1

服务端反爬

既然客户端的信息都是可以伪造的,那么我们干脆不相信客户端的信息了,在服务端统计一些无法伪造的信息。比如来源 IP 和登录账户信息。

通过IP识别用户

这种方法简单粗暴,直接根据来源IP来判定是否是同一个用户,如果访问过快,屏蔽请求或者需要输入验证码。但是有一个问题,好多学校或者公司都是使用为数不多的几个IP地址来作为出口IP,方便管理,如果这种地方有一两个人在恶意请求,那么可能屏蔽会造成很多人访问异常。

有的大型网站甚至会对于民用IP和机房IP做出区别对待,比如 Google。

对于这种限制来说,可以放慢请求速度,或者使用多个代理IP来伪装自己。代理池的构建也是单独一篇文章才能讲清楚的,敬请期待。

通过验证码限制用户

不少网站往往不会直接把某个 IP 完全限制,而是在发现可疑访问时弹出验证码,这时候可以自己OCR识别,训练深度学习模型识别验证码(比如使用 CNN)或者直接对接打码平台。

关于不同类型的验证码和深度学习后面有时间再写。

通过Cookies来识别用户

上面说过直接通过 IP 来识别用户的话比较暴力,可能误伤,另一种方法就是通过 Cookie 来标示用户,如果有一个用户访问过多的话,就对这个Cookie做限制。

对于这种限制来说,可以直接每次请求不带 Cookie,或者预先多申请一些 Cookie,然后负载均衡一下。

除了在服务端生成 cookie 之外,网站还可以选择在客户端通过复杂的算法来生成 cookie,不过这就是客户端反爬的情况了,对于这种还是要看懂对方的 JS 还好。或者不在乎效率的话,有的时候可以直接用控制浏览器访问解决。

对于传统的静态页面的限制和破解基本上就是这些方法。不过现在很多页面都是操作丰富的动态页面,也就是我们感兴趣的消息可能是通过ajax加载的,我们只需要访问这个api就可以了。

通过登录状态

上面说的通过 cookies 来识别客户其实指的是匿名账户的 cookies。好多网站的资源都是需要登录账户来访问的,这时候首先要考虑的是能不能大批量伪造账户,可以借助于匿名邮箱和手机验证码接码平台等。

如果不能大规模的注册账户,那么还需要的是购买账户了,这个就看抓取的 ROI(投资回报比)是什么了。

最简单的情况下,网站对登录用户没有限制,那么买一个账户总是值的。

对于账户除了整体的频次控制以外,往往还会限制登录的 IP。比如说账户获得的 cookie 是和 IP 绑定的,某个 IP 每天只能登录若干个账户等等。

总结

上面的反爬和反反爬手段可以总结如下:

方便爬虫利用的设计缺陷

除了上述措施之外,还要避免一些设计上的缺陷被爬虫滥用。下面举几个例子。

第一个例子,自增ID被滥用

什么值得买的评测页面,https://test.smzdm.com/pingce/p/40205/ ,这个链接的最后一个数字就是评测的问题ID,大小才不过几万,也就是说,我只要遍历一下这个数字,就可以把“什么值得买”这个网站的所有评测都爬取一遍,这个是在太容易被利用了。

对于这个问题,可以不要直接使用数据库的主键作为页面的ID,而是尽量使用没有规律的数字(比如UUID)或者至少大一点的数字作为ID,避免被穷举遍历。

第二个例子,列表页面被滥用

链家的二手房页面,https://bj.lianjia.com/ershoufang/101102279987.html ,这个页面的ID就比较大了,但是我们没办法去遍历这样一个数字。这时候可以从列表页入手,https://bj.lianjia.com/ershoufang/rs/ ,只要从页面上找到所有二手房的页面地址就可以了。

第三个例子,API 的攻防

有一些 API 没有任何防护,对于这种 API 直接刷就好了,不过可能有的 API 会有根据 IP 的频次限制。

MySQL 备份与恢复

基础使用

帮助命令很简单

Usage: mysqldump [OPTIONS] database [tables]
OR     mysqldump [OPTIONS] --databases [OPTIONS] DB1 [DB2 DB3...]
OR     mysqldump [OPTIONS] --all-databases [OPTIONS]

备份

mysqldump -u root --password=xxx DB_NAME [TABLE_NAME] > backup.sql

--password 可以直接在命令中使用密码

可以选择只 dump 一个数据库或者一个表。

恢复

mysql -u root -p DB_NAME < backup.sql

一行操作

mysqldump -u root -pPassword --all-databases | ssh user@new_host.host.com 'cat - | mysql -u root -pPassword'

问题

如果直接备份所有数据库并恢复会更改 root 密码, 并且导致内部数据库不一致, 可以使用如下命令修复:

mysql_upgrade --force -uroot -p

参考:

  1. https://stackoverflow.com/questions/43846950/column-count-of-mysql-user-is-wrong-expected-42-found-44-the-table-is-probabl

学习 Django ORM

定义模型

继承 models.Model 并使用 models.XXXField

from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=100, blank=True)
    authors = models.ManyToManyField(Author)  # 定义了多对多外键
    publisher = models.ForeignKey(Publisher, on_delete=models.DO_NOTHING)  # 定义了多对一外键
    publication_date = models.DateField(null=True, blank=True)

    def __str__(self):
        return f"<Book {self.title}>"

    class Meta:
        ordering = ["name"]
        db_table = ''

外键-多对多映射和多对一映射

注意其中的 ManToManyField and ForeignKey 字段。关于多对多关系,还需要详细参考文档

注意 ForeignKey 字段必须添加 on_delete 参数,参考这里

个人认为,on_delete 最好使用 models.DO_NOTHING,虽然会造成数据库的完整性缺失,但是没有丢失任何信息。如果需要在删除的时候删除其他关联模型的话,还是自己实现比较稳妥。另外,对于数据库来说,尽量少删除数据,而是用一个字段标记为已删除。

当使用 manytomanyfield 的时候,django 会自动创建一张关联表,可以通过 through 来指定关联关系对应的模型,在其中指定对应的表。

manytomanyfield 和 foreinkey 其中的第一个参数(也就是模型)也可以使用对应模型的名字的字符串,避免引用未定义的类型。

如果需要指向自己的外键,可以用 models.ForeignKey('self')

字段的选项

  • null 是否可以为 nullable. 默认 False.
  • blank 是否可以留空, 注意不是 null,这个是 django 的验证,不会反应在数据库 DDL 中, 默认 False.
  • db_index 是否为该字段建立索引, default False.
  • choices 用来在 django admin 中限制字段的选项,必须是回一个 list of tuple choices = ((1, 'male'), (0, 'female'))
  • default 默认值,可以为值或 callable,如果默认要是动态的值,最好是一个 callable.
  • help_text 帮助文本
  • verbose 长名字
  • unique 是否应该是 unique 字段
  • primary_key 是否设置为主键

null 与可以留空的字段

to make string field optional, just add blank = True if you want to allow blank values in a date field (e.g., DateField, TimeField, DateTimeField) or numeric field (e.g.,IntegerField, DecimalField, FloatField), you’ll need to use both null=True and blank=True.

自动生成的 ID 字段

by default, django gives each model a primary key field. if primary_key=True is set on any other field, django will not generate this.

class Meta

  • db_table, 建表对应的表名,默认是 <APPNAME>_<MODEL>
  • ordering, 数组,admin 中用来排序的依据
  • unique_together, 字段组合作为 unique 索引: unique_together = (("driver", "restaurant"),)
  • index_together, 字段组合作为索引, index_together = [["pub_date", "deadline"],]

抽象基类

比如说有时候我们对于每一个模型都需要创建 create_time, modify_time 字段,那么可以把这些字段定义成一个抽象基类,就不需要在每个模型里面定义了,在基类的 meta 中设定 abstract 为 true 即可。

from django.db import models

class CommonInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True  # 注意这里

class Student(CommonInfo):
    home_group = models.CharField(max_length=5)
    # 这个模型就自动拥有了 name 和 age 了

为什么要尽量不用外键

当应用变大以后以后,你的数据表可能要分库,这时候就没法再使用外键了,毕竟要跨库。所以可以在 django 中使用 ForeignKey,但是最好不要再数据库定义中使用数据库的外键。

其他

如果你想覆盖 init 方法, 记得调用父类的方法

def __init__(self, *args, **kwargs):
    super().__init__(self, *args, **kwargs)
    # your code here

使用模型

上面讲完了模型的定义,下面我们来看下模型的使用。

创建

Model.objects.create(**kwargs)

保存模型

model = Model()
model.save() # note that all of the fields will be updated, not just the ones that have been changed
model/queryset.update()
model/queryset.delete()

查询

查询会返回一个 QuerySet 对象,也就是查询的结果,表现出来类似于一个模型实例的数组。

Model.objects.get(**kwargs)  # returns one object, may raise DoesNotExist or MultiOjbectsReturned
Model.objects.all()
Model.objects.filter(**kwargs) # returns a query set
Model.objects.order_by(*colnames) # 可以使用 - 表示反向排序

查询条件

查询语法是:

<field>__<lookuptype>=value

其中 lookuptype 可以是:

empty/exact/iexact  # 是否为空、是否是某个值
contains/icontains  # 包含、忽略大消息包含
(i)startswith/(i)endswith  # 开头结尾
range  # 在某个范围内
in  # 在给定的元素中,相当于 sql 的 in
gt/gte/lt/lte  # 大于小于
year/month/day/week_day/hour/minute/second  # 时间
isnull  # 是否为 null
regex/iregex  # 正则匹配

复杂查询,使用 F 和 Q

https://docs.djangoproject.com/en/2.2/topics/db/queries/#complex-lookups-with-q

上面提到的查询条件在 SQL 层面会形成“与”的关系,那么怎么表示“或”的查询呢?可以使用 Q 对象

F 表达式

what if you want to compare the value of a model field with another field on the same model?
use F(colname) to reference the column value

Q 表达式

Q 表达式封装了一个查询条件,可以是会用 |& ~组合来表达“或”和“与”“非”的关系,通过两个运算符会得到一个新的 Q 表达式。

比如说,以 Who 或者 What 开头的问题:

Q(question__startswith='Who') | Q(question__startswith='What')

可以在查询中这样使用:

Poll.objects.get(
    Q(question__startswith='Who'),
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)

QuerySet 分片

查询的分片会对应到数据库的 limit 和 offset 语句,所以需要注意的是不要使用较大的索引,另外不支持负数索引,也就是不能直接访问最后一个元素。

通常来说,对于 QuerySet 的切片会返回一个新的切片,使用切片的过程并不会去从数据库加载数据,只有到最后需要访问数据的时候才会去真正访问数据库。

>>> Entry.objects.all()[:10:2]

上面的这个查询可能会引起 IndexError

对于需要访问最后一个元素来说,通常可以采用反向排序,访问第一个元素解决。

>>> Publisher.objects.order_by('name')[-1]
Traceback (most recent call last):
  ...
AssertionError: Negative indexing is not supported.

>>> # This is easy to get around, though. Just change the order_by() statement, like this:
>>> Publisher.objects.order_by('-name')[0]

删除一个QuerySet

Model.objects.get().delete()

QuerySet 操作

filter(**kwargs)
exclude(**kwargs)
count
create
get_or_create
update
delete
iterate
exists
iterator

模型的懒加载

Each QuerySet contains a cache to minimize database access. Understanding how it works will allow you to write the most efficient code. In a newly created QuerySet, the cache is empty. The first time a QuerySet is evaluated – and, hence, a database query happens – Django saves the query results in the QuerySet’s cache and returns the results that have been explicitly requested (e.g., the next element, if the QuerySet is being iterated over). Subsequent evaluations of the QuerySet reuse the cached results.

Using iterator vs directly

A QuerySet typically caches its results internally so that repeated evaluations do not result in additional queries. In contrast, iterator() will read results directly, without doing any caching at the QuerySet level (internally, the default iterator calls iterator() and caches the return value). Using iterator would probably save your memory.

Keep this caching behavior in mind, because it may bite you if you don’t use your QuerySets correctly. For example, the following will create two QuerySets, evaluate them, and throw them away:

>>> print([e.headline for e in Entry.objects.all()]) # two querysets created and evaluated and thrown
>>> print([e.pub_date for e in Entry.objects.all()])

That means the same database query will be executed twice, effectively doubling your database load. Also, there’s a possibility the two lists may not include the same database records, because an Entry may have been added or deleted in the split second between the two requests.
To avoid this problem, simply save the QuerySet and reuse it:

>>> queryset = Entry.objects.all()        # store the queryset to a variable
>>> print([p.headline for p in queryset]) # Evaluate the query set.
>>> print([p.pub_date for p in queryset]) # Re-use the cache from the evaluation.

When querysets are not cached?

Querysets do not always cache their results. When evaluating only part of the queryset, the cache is checked, but if it is not populated then the items returned by the subsequent query are not cached. Specifically, this means that limiting the queryset using an array slice or an index will not populate the cache.

For example, repeatedly getting a certain index in a queryset object will query the database each time:

>>> queryset = Entry.objects.all()
>>> print queryset[5] # Queries the database
>>> print queryset[5] # Queries the database again 

However, if the entire queryset has already been evaluated, the cache will be checked instead:

>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # Queries the database
>>> print queryset[5] # Uses cache
>>> print queryset[5] # Uses cache 
Here are some examples of other actions that will result in the entire queryset being evaluated and therefore populate the cache:
>>> [entry for entry in queryset]
>>> bool(queryset)
>>> entry in queryset
>>> list(queryset)

if you want to add extra check in model save, just override the defualt save method aorr add post save handlers

fat models is not all that good, it may cause god object problem

使用 only 来指定需要的字段。

如果只需要一个或者几个值,可以使用 values_list 方法

In [6]: authors = Author.objects.values_list('name', 'qq')

In [7]: authors

Out[7]: <QuerySet [(u'WeizhongTu', u'336643078'), (u'twz915', u'915792575'), (u'wangdachui', u'353506297'), (u'xiaoming', u'004466315')]>

In [8]: list(authors)

Out[8]: [(u'WeizhongTu', u'336643078'),

 (u'twz915', u'915792575'),

 (u'wangdachui', u'353506297'),

 (u'xiaoming', u'004466315')]

如果只需要 1 个字段,可以指定 flat=True

In [9]: Author.objects.values_list('name', flat=True)

Out[9]: <QuerySet [u'WeizhongTu', u'twz915', u'wangdachui', u'xiaoming']>

In [10]: list(Author.objects.values_list('name', flat=True))

Out[10]: [u'WeizhongTu', u'twz915', u'wangdachui', u'xiaoming']

其他技巧

查看执行的 sql 语句和执行时间

from django.db import connection
print(connection.queries)

如何在 django 外部单独使用模型

import os
from django.conf import settings
from django.apps import apps

conf = {
    'INSTALLED_APPS': [
        'Demo'
    ],
    'DATABASES': {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': os.path.join('.', 'db.sqlite3'),
        }
    }
}

settings.configure(**conf)
apps.populate(settings.INSTALLED_APPS)

https://stackoverflow.com/a/46050808/1061155