在脚本中使用 ORM:Faker 批量生成测试数据

2019-11-029080 阅读19 评论

为了防止博客首页展示的文章过多以及提升加载速度,可以对文章列表进行分页展示。不过这需要比较多的文章才能达到分页效果,但本地开发时一般都只有几篇测试文章,如果一篇篇手工添加将会非常麻烦。

解决方案是我们可以写一个脚本,自动生成任意数量的测试数据。脚本写好后,只需运行脚本就可以往数据库填充大量测试数据。脚本就是一段普通的 Python 代码,非常简单,但是通过这个脚本你将学会如何在 django 外使用 ORM,而不仅仅在 django 应用的内部模块使用。

脚本目录结构

一般习惯于将项目有关的脚本统一放在项目根目录的 scripts 包中,当然这只是一个惯例,你也可以采用自己觉得合理的目录结构,只要保证这个包所在目录能够被 Python 找到。

依据惯例,我们博客项目中脚本的目录结构如下:

HelloDjango-blog-tutorial\
    blog\
    blogproject\
    ...
    scripts\
        __init__.py
        fake.py
        md.sample

其中 fake.py 是生成测试数据的脚本,md.sample 是一个纯文本文件,内容是用于测试 Markdown 的文本。

使用 Faker 快速生成测试数据

博客文章包含丰富的内容元素,例如标题、正文、分类、标签。如果手工输入这些相关元素的文本会非常耗时,我们将借助一个 Python 的第三方库 Faker 来快速生成这些测试用的文本内容。Faker 意为造假工厂,顾名即可思义。

首先安装 Faker:

$ pipenv install Faker

Faker 通过不同的 Provider 来提供各种不同类型的假数据,我们将在下面的脚本中讲解它的部分用法,完整的用法可以参考其官方文档

批量生成测试数据

现在我们来编写一段 Python 脚本用于自动生成博客测试数据。思路非常简单,博客内容包括作者、分类、标签、文章等元素,只需依次生成这些元素的内容即可。当然为了使脚本能够正常运行,很多细节需要注意,我们会对需要注意的地方进行详细讲解。

先来看脚本 fake.py 开头的内容:

import os
import pathlib
import random
import sys
from datetime import timedelta

import django
import faker
from django.utils import timezone

# 将项目根目录添加到 Python 的模块搜索路径中
back = os.path.dirname
BASE_DIR = back(back(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)

这一段很简单,只是导入一些会用到的模块,然后通过脚本所在文件找到项目根目录,将根目录添加到 Python 的模块搜索路径中,这样在运行脚本时 Python 才能够找到相应的模块并执行。

接下来是脚本的逻辑,先看第一段:

if __name__ == '__main__':
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blogproject.settings.local")
    django.setup()

    from blog.models import Category, Post, Tag
    from comments.models import Comment
    from django.contrib.auth.models import User

这是整个脚本最为重要的部分。首先设置 DJANGO_SETTINGS_MODULE 环境变量,这将指定 django 启动时使用的配置文件,然后运行 django.setup() 启动 django。这是关键步骤,只有在 django 启动后,我们才能使用 django 的 ORM 系统。django 启动后,就可以导入各个模型,以便创建数据。

接下来的逻辑就很简单了,不断生成所需的测试数据即可,我们来一段一段地看:

    print('clean database')
    Post.objects.all().delete()
    Category.objects.all().delete()
    Tag.objects.all().delete()
    Comment.objects.all().delete()
    User.objects.all().delete()

这一段脚本用于清除旧数据,因此每次运行脚本,都会清除原有数据,然后重新生成。

    print('create a blog user')
    user = User.objects.create_superuser('admin', 'admin@hellogithub.com', 'admin')

    category_list = ['Python学习笔记', '开源项目', '工具资源', '程序员生活感悟', 'test category']
    tag_list = ['django', 'Python', 'Pipenv', 'Docker', 'Nginx', 'Elasticsearch', 'Gunicorn', 'Supervisor', 'test tag']
    a_year_ago = timezone.now() - timedelta(days=365)

    print('create categories and tags')
    for cate in category_list:
        Category.objects.create(name=cate)

    for tag in tag_list:
        Tag.objects.create(name=tag)

    print('create a markdown sample post')
    Post.objects.create(
        title='Markdown 与代码高亮测试',
        body=pathlib.Path(BASE_DIR).joinpath('scripts', 'md.sample').read_text(encoding='utf-8'),
        category=Category.objects.create(name='Markdown测试'),
        author=user,
    )

这个脚本没什么说的,简单地使用 django 的 ORM API 生成博客用户、分类、标签以及一篇 Markdown 测试文章。

    print('create some faked posts published within the past year')
    fake = faker.Faker()  # English
    for _ in range(100):
        tags = Tag.objects.order_by('?')
        tag1 = tags.first()
        tag2 = tags.last()
        cate = Category.objects.order_by('?').first()
        created_time = fake.date_time_between(start_date='-1y', end_date="now",
                                              tzinfo=timezone.get_current_timezone())
        post = Post.objects.create(
            title=fake.sentence().rstrip('.'),
            body='\n\n'.join(fake.paragraphs(10)),
            created_time=created_time,
            category=cate,
            author=user,
        )
        post.tags.add(tag1, tag2)
        post.save()

这段脚本用于生成 100 篇英文博客文章。博客文章通常内容比较长,因此我们使用了之前提及的 Faker 库来自动生成文本内容。脚本逻辑很清晰,只对其中涉及的几个知识点进行讲解:

  • fake = faker.Faker(),要使用 Faker 自动生成数据,首先实例化一个 Faker 对象,然后我们可以在脚本中使用这个实例的一些方法生成需要的数据。Faker 默认生成英文数据,但也支持国际化。至于如何生成中文数据在下一段脚本中会看到。

  • order_by('?') 将返回随机排序的结果,脚本中这块代码的作用是达到随机选择标签(Tag) 和分类(Category) 的效果。

  • 然后就是 2 个 Faker 的 API 了:

  • fake.date_time_between

    这个方法将返回 2 个指定日期间的随机日期。三个参数分别是起始日期,终止日期和时区。我们在这里设置起始日期为 1 年前(-1y),终止日期为当下(now),时区为 get_current_timezone 返回的时区,这个函数是 django.utils.timezone 模块的辅助函数,它会根据 django 设置文件中 TIME_ZONE 的值返回对应的时区对象。

  • '\n\n'.join(fake.paragraphs(10))

    fake.paragraphs(10) 用于生成 10 个段落文本,以列表形式返回,列表的每个元素即为一个段落。要注意使用 2 个换行符连起来是为了符合 Markdown 语法,Markdown 中只有 2 个换行符分隔的文本才会被解析为段落。

    fake = faker.Faker('zh_CN')
    for _ in range(100):  # Chinese
        tags = Tag.objects.order_by('?')
        tag1 = tags.first()
        tag2 = tags.last()
        cate = Category.objects.order_by('?').first()
        created_time = fake.date_time_between(start_date='-1y', end_date="now",
                                              tzinfo=timezone.get_current_timezone())
        post = Post.objects.create(
            title=fake.sentence().rstrip('.'),
            body='\n\n'.join(fake.paragraphs(10)),
            created_time=created_time,
            category=cate,
            author=user,
        )
        post.tags.add(tag1, tag2)
        post.save()

这一段脚本和上一段几乎完全一样,唯一不同的是构造 Faker 实例时,传入了一个语言代码 zh_CN,这将生成中文的虚拟数据,而不是默认的英文。

    print('create some comments')
    for post in Post.objects.all()[:20]:
        post_created_time = post.created_time
        delta_in_days = '-' + str((timezone.now() - post_created_time).days) + 'd'
        for _ in range(random.randrange(3, 15)):
            Comment.objects.create(
                name=fake.name(),
                email=fake.email(),
                url=fake.uri(),
                text=fake.paragraph(),
                created_time=fake.date_time_between(
                     start_date=delta_in_days, 
                     end_date="now", 
                     tzinfo=timezone.get_current_timezone()),
                post=post,
            )

    print('done!')

最后依葫芦画瓢,给前 20 篇文章(Post) 生成评论数据。要注意的是评论的发布时间必须位于被评论文章的发布时间和当前时间之间,这就是 delta_in_days = '-' + str((timezone.now() - post_created_time).days) + 'd' 这句代码的作用。

执行脚本

脚本写好了,在项目根目录执行下面的命令运行整个脚本:

$ pipenv run python -m scripts.fake

看到如下的输出说明脚本执行成功了。

clean database
create a blog user
create categories and tags
create a markdown sample post
create some faked posts published within the past year
create some comments
done!

运行开发服务器,访问博客首页可以看到生成的测试数据,是不是有点以假乱真的感觉?

博客测试数据页面

现在,我们有了 200 多篇测试文章,用来测试分页效果就十分简单了,接下来让我们来实现功能完整的分页效果。

-- EOF --

19 评论
登录后回复
gaopfEditer
2021-04-08 11:32:21

级联删除设置了,清空表的操作 怎么不行呢
清空表操作
print("clean database")
Post.objects.all().delete()
Category.objects.all().delete()
报错:IntegrityError: FOREIGN KEY constraint failed
在python manage.py shell中也是报这个错(get一条删除没问题,删除全部就不行),django版本2.2.19在别处搜删除就应该是这个啊

回复
gaopfEditer gaopfEditer
2021-04-08 16:14:38

搞了半天,最后这条post里的数据删不掉,最初创建的时候不知道除了什么问题,不跟他外键关联的可以用delete的方法删除全部,暂记下,后边再说吧

回复
Fan()
2020-10-22 11:25:45

太强了

回复
LeechenLove
2020-07-12 16:45:20

谢谢博主,太棒了

回复
YAO ZELIANG
2020-04-08 04:09:28

打卡

回复
MirrorNight201611
2020-02-23 17:27:02

多级评论学习 感谢zmrenwu~

回复
啦啦啦啦啦啦0000
2020-01-30 14:29:54

image.png
文章作者这里没有随机

回复
冰加可乐__
2020-01-03 09:54:47

sqlite3.IntegrityError: UNIQUE constraint failed: blog_post.id
django.db.utils.IntegrityError: UNIQUE constraint failed: blog_post.id
报这两个错误,没有生成文章

回复
冰加可乐__ 冰加可乐__
2020-01-03 11:05:51

我删除数据库重建也不行,这个要怎么改,什么原因呢,在线等

回复
冰加可乐__ 冰加可乐__
2020-01-03 11:25:07

文章有了,就是markdown的body内容没有

回复
MirrorNight201611 冰加可乐__
2020-02-23 17:36:22

多级评论测试

回复
runiccolon 冰加可乐__
2020-07-05 10:04:56

那是因为你md.sample没内容啊

回复
BigYoungs
2019-11-29 15:16:39

报错代码:

delta_in_days = '-' + str((timezone.now() - post_created_time).days) + 'd'

报错内容:
TypeError: unsupported operand type(s) for -: 'datetime.datetime' and 'datetime.date'

原因:
post_created_time  <class 'datetime.date'> ,内容是:2019-11-29
timezone.now()  <class 'datetime.datetime'> ,内容:2019-11-29 07:08:27.043319+00:00

两者类型不一样,一直是天,一个具体到秒。不能直接操作。

个人改进方法:

delta_in_days = '-' + str((datetime.date.today() - post_created_time).days) + 'd'

回复
追梦人物 BigYoungs
2020-02-25 11:46:41

post_created_time 类型是 <class 'datetime.date'> 是不是因为你的 created_time 字段使用的是 DateField 类型?使用 DateTimeField 的话类型应该是正确的。

回复
BigYoungs
2019-11-29 11:38:16

本人实机操作,此行代码导致报错:

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blogproject.settings.local")

正确代码:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blogproject.settings")

教程代码会提示:
ModuleNotFoundError: No module named 'blogproject.settings.local'; 'blogproject.settings' is not a package

回复
追梦人物 BigYoungs
2020-01-31 11:38:26

settings 已升级为一个包,local 配置本地开放环境的。

回复
lingnickTang BigYoungs
2021-04-18 09:51:02

博主在“使用 Fabric 自动化部署”这一章对settings.py进行了拆分, 现在settings已经是个python包了, 如果没有拆分的话把local去掉就行了

回复
boyang
2019-11-12 21:41:40

花了几天时间把博主的博客差不多看了一遍,只能说,看的很爽,非常感谢。学到很多东西。期待博主什么时候能出一套vue+restful framework的教程?

回复
丶叶
2019-11-09 19:59:53
def test():
    print('hello world')
回复