django 系列文章之分页
一、django中的分页
Django框架提供了内置的分页功能,使得在处理大量数据展示时,可以将结果分成多页显示,提高了用户体验和页面响应速度。主要使用django.core.paginator模块中的Paginator和Page类来实现分页。
1.1、Paginator类介绍
点击查看代码
class Paginator(object_list, per_page, orphans=0, allow_empty_first_page=True)
# 参数介绍:
object_list:必需参数。一个列表、元组、QuerySet 或其他具有 count() 或 __len__() 方法的可切片对象。为了实现一致的分页,QuerySet 应该是有序的,例如使用 order_by() 子句或使用模型上的默认 ordering。
per_page:必需参数。每页中包含的数据条数。
orphans:可选参数。当你不希望最后一页的项目数量很少时,使用这个选项。如果最后一页的数据条数小于或等于 orphans值,那么这些项目将被添加到前一页,而不是让这些项目单独留在一页上。默认为 0,这意味着最后一页不管有几条数据都不会合并到前一页里面。
allow_empty_first_page:可选参数。是否允许第一页为空。 如果值为False 并且 object_list 是空的,则会出现 EmptyPage 错误。
# 方法介绍:
get_page(number):获取指定页码的数据,返回值类型是一个Page 对象,同时处理超出范围(例如总页数是5,传值为6)和无效的页码(例如页码数不是整数或者为负数)。如果页数不是数字,它返回第一页。如果页码为负数或大于页数,则返回最后一页。
page(number):获取指定页码的数据,返回值类型是一个Page 对象,但不处理异常,如果页码无效(例如,页码不是整数或者为负数,page()方法会抛出PageNotAnInteger异常,如果页码超出有效范围,则抛出EmptyPage异常。需要手动处理异常。
get_elided_page_range(number, *, on_each_side=3, on_ends=2):当总页数很多时,这个方法可以帮助你只显示当前页附近的几页,以及首尾页码,中间的连续页码则用省略号(...)表示,以此来简化和美化分页导航栏。该方法在django3.2及以后版本新增。
该方法接受几个参数:
number: 当前页的页码。
on_each_side: 当前页码左右两边分别显示的页码数量,默认是3。
on_ends: 首尾两端除了当前页码外额外显示的页码数量,默认是1。
# 用法示例:
from django.core.paginator import Paginator
paginator = Paginator(queryset, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
# 使用 get_elided_page_range 来生成简洁的页码导航
page_range = paginator.get_elided_page_range(
number=page_obj.number,
on_each_side=2, # 每边显示2个页码
on_ends=1 # 首尾额外显示1个页码
)
然后可以在模板中遍历这个 page_range 来生成分页链接
# 属性
count:返回所有页面的数据总数量
num_pages:返回分页的总页数
page_range:返回一个表示分页后的所有页码的整数列表,例如[1,2,3,4,...].与 get_elided_page_range() 方法不同,page_range 不会对页码进行任何省略处理,它直接提供完整的页码序列.
使用示例:
点击查看代码
# django.core.paginator.Paginator 分页类
# 分页,每页10条记录
paginator = Paginator(queryset, 5)
total_pages = paginator.num_pages
page_obj = paginator.get_page(page_number)
ser = self.serializer_class(page_obj, many=True)
return Response({"data": ser.data, "total_pages": total_pages})
1.2、Page类介绍
在 Django 中,Page 类是 Paginator 类用来表示单个分页结果的一个实例。当你对查询结果进行分页时,Paginator 会根据指定的每页数量将查询结果切分为多个 Page 对象。每个 Page 对象代表了分页后的一个具体页面,包含了该页的数据以及一些分页相关的属性和方法。点击查看代码
class Page(object_list, number, paginator)
# 参数介绍:
object_list:当前页的对象列表。
number:当前页的页码
paginator:分配给这个页面的 Paginator 对象,可以用来获取关于整个分页信息,如总页数等。
注意:通常不会手工构建Page对象,而是使用Paginator中的page()方法或者get_page()方法获得。
# 方法介绍
Page.has_next()
如果有下一页,返回 True。
Page.has_previous()
如果有上一页,返回 True。
Page.has_other_pages()
如果有下一页 或 上一页,返回 True。
Page.next_page_number()
返回下一页的页码。如果下一页不存在,则引发 InvalidPage。
Page.previous_page_number()
返回上一页的页码。如果上一页不存在,则引发 InvalidPage。
Page.start_index()
返回页面上第一个对象,相对于分页器列表中所有对象的基于 1 的索引。例如,当对一个有 5 个对象的列表进行分页时,每页有 2 个对象,第二页的 start_index() 将返回 3。
Page.end_index()
返回页面上最后一个对象相对于分页器列表中所有对象的基于 1 的索引。例如,当对一个有 5 个对象的列表进行分页时,每页有 2 个对象,第二页的 end_index() 将返回 4。
1.3、异常类
exception InvalidPage 当分页器被传递一个无效的页码时引发异常的基类。Paginator.page() 方法在请求的页面无效(即不是整数)或不包含任何对象时引发异常。一般来说,只要捕获 InvalidPage 异常就够了,但如果你想要更细化,你可以捕获以下任何一种异常。
exception PageNotAnInteger
当 page() 的值不是整数时触发该异常。
exception EmptyPage
当 p传入的页码超过了总页数时,引发该异常。
这两个异常都是 InvalidPage 的子类,所以你可以用 except InvalidPage 处理这两个异常。
1.4、几个类的源码
点击查看代码
import collections.abc
import inspect
import warnings
from math import ceil
from django.utils.functional import cached_property
from django.utils.inspect import method_has_no_args
from django.utils.translation import gettext_lazy as _
class UnorderedObjectListWarning(RuntimeWarning):
pass
class InvalidPage(Exception):
pass
class PageNotAnInteger(InvalidPage):
pass
class EmptyPage(InvalidPage):
pass
class Paginator:
# Translators: String used to replace omitted page numbers in elided page
# range generated by paginators, e.g. [1, 2, '…', 5, 6, 7, '…', 9, 10].
ELLIPSIS = _('…')
def __init__(self, object_list, per_page, orphans=0,
allow_empty_first_page=True):
self.object_list = object_list
self._check_object_list_is_ordered()
self.per_page = int(per_page)
self.orphans = int(orphans)
self.allow_empty_first_page = allow_empty_first_page
def __iter__(self):
for page_number in self.page_range:
yield self.page(page_number)
def validate_number(self, number):
"""Validate the given 1-based page number."""
try:
if isinstance(number, float) and not number.is_integer():
raise ValueError
number = int(number)
except (TypeError, ValueError):
raise PageNotAnInteger(_('That page number is not an integer'))
if number < 1:
raise EmptyPage(_('That page number is less than 1'))
if number > self.num_pages:
if number == 1 and self.allow_empty_first_page:
pass
else:
raise EmptyPage(_('That page contains no results'))
return number
def get_page(self, number):
"""
Return a valid page, even if the page argument isn't a number or isn't
in range.
"""
try:
number = self.validate_number(number)
except PageNotAnInteger:
number = 1
except EmptyPage:
number = self.num_pages
return self.page(number)
def page(self, number):
"""Return a Page object for the given 1-based page number."""
number = self.validate_number(number)
bottom = (number - 1) * self.per_page
top = bottom + self.per_page
if top + self.orphans >= self.count:
top = self.count
return self._get_page(self.object_list[bottom:top], number, self)
def _get_page(self, *args, **kwargs):
"""
Return an instance of a single page.
This hook can be used by subclasses to use an alternative to the
standard :cls:`Page` object.
"""
return Page(*args, **kwargs)
@cached_property
def count(self):
"""Return the total number of objects, across all pages."""
c = getattr(self.object_list, 'count', None)
if callable(c) and not inspect.isbuiltin(c) and method_has_no_args(c):
return c()
return len(self.object_list)
@cached_property
def num_pages(self):
"""Return the total number of pages."""
if self.count == 0 and not self.allow_empty_first_page:
return 0
hits = max(1, self.count - self.orphans)
return ceil(hits / self.per_page)
@property
def page_range(self):
"""
Return a 1-based range of pages for iterating through within
a template for loop.
"""
return range(1, self.num_pages + 1)
def _check_object_list_is_ordered(self):
"""
Warn if self.object_list is unordered (typically a QuerySet).
"""
ordered = getattr(self.object_list, 'ordered', None)
if ordered is not None and not ordered:
obj_list_repr = (
'{} {}'.format(self.object_list.model, self.object_list.__class__.__name__)
if hasattr(self.object_list, 'model')
else '{!r}'.format(self.object_list)
)
warnings.warn(
'Pagination may yield inconsistent results with an unordered '
'object_list: {}.'.format(obj_list_repr),
UnorderedObjectListWarning,
stacklevel=3
)
def get_elided_page_range(self, number=1, *, on_each_side=3, on_ends=2):
"""
Return a 1-based range of pages with some values elided.
If the page range is larger than a given size, the whole range is not
provided and a compact form is returned instead, e.g. for a paginator
with 50 pages, if page 43 were the current page, the output, with the
default arguments, would be:
1, 2, …, 40, 41, 42, 43, 44, 45, 46, …, 49, 50.
"""
number = self.validate_number(number)
if self.num_pages <= (on_each_side + on_ends) * 2:
yield from self.page_range
return
if number > (1 + on_each_side + on_ends) + 1:
yield from range(1, on_ends + 1)
yield self.ELLIPSIS
yield from range(number - on_each_side, number + 1)
else:
yield from range(1, number + 1)
if number < (self.num_pages - on_each_side - on_ends) - 1:
yield from range(number + 1, number + on_each_side + 1)
yield self.ELLIPSIS
yield from range(self.num_pages - on_ends + 1, self.num_pages + 1)
else:
yield from range(number + 1, self.num_pages + 1)
class Page(collections.abc.Sequence):
def __init__(self, object_list, number, paginator):
self.object_list = object_list
self.number = number
self.paginator = paginator
def __repr__(self):
return '<Page %s of %s>' % (self.number, self.paginator.num_pages)
def __len__(self):
return len(self.object_list)
def __getitem__(self, index):
if not isinstance(index, (int, slice)):
raise TypeError(
'Page indices must be integers or slices, not %s.'
% type(index).__name__
)
# The object_list is converted to a list so that if it was a QuerySet
# it won't be a database hit per __getitem__.
if not isinstance(self.object_list, list):
self.object_list = list(self.object_list)
return self.object_list[index]
def has_next(self):
return self.number < self.paginator.num_pages
def has_previous(self):
return self.number > 1
def has_other_pages(self):
return self.has_previous() or self.has_next()
def next_page_number(self):
return self.paginator.validate_number(self.number + 1)
def previous_page_number(self):
return self.paginator.validate_number(self.number - 1)
def start_index(self):
"""
Return the 1-based index of the first object on this page,
relative to total objects in the paginator.
"""
# Special case, return zero if no items.
if self.paginator.count == 0:
return 0
return (self.paginator.per_page * (self.number - 1)) + 1
def end_index(self):
"""
Return the 1-based index of the last object on this page,
relative to total objects found (hits).
"""
# Special case for the last page because there can be orphans.
if self.number == self.paginator.num_pages:
return self.paginator.count
return self.number * self.paginator.per_page
二、django-restframework中的分页
Django REST Framework (DRF) 提供了内置的分页支持,允许我们在API中轻松地实现数据分页。它主要有三种分页器类,分别是:PageNumberPagination、LimitOffsetPagination、CursorPagination,本文只介绍PageNumberPagination分页器。
2.1、介绍
PageNumberPagination 类是 Django REST Framework (DRF) 提供的一个内置分页器,它实现了基于页码的分页逻辑。这种分页方式是最常见的,用户通过指定页码来浏览不同的数据子集。位置:
from rest_framework.pagination import PageNumberPagination核心特点:
基于页码分页:用户通过请求中的页码参数(如 ?page=2)来请求特定页面的数据。配置灵活:允许你自定义每页显示的项目数量、页码和每页大小的查询参数名称等。
自动链接:生成分页链接,如“上一页”、“下一页”、具体页码等,便于客户端导航。
可选的大小调整:允许客户端通过查询参数调整每页显示的项目数量(需开启并设置最大限制)。
2.2、方法和属性介绍
点击查看代码
# 属性
page_size: 指定每页显示的项目数量,默认值为 None,意味着不分页。可以通过配置文件或在自定义分页类中设置。
page_size_query_param: 定义客户端可以用来调整每页大小的查询参数名称,默认为 None,不启用此功能。如果设置,则客户端可以通过如 ?page_size=20 来请求每页20条数据。
max_page_size: 设置客户端通过 page_size_query_param 能够请求的最大每页项目数,默认为 None,表示不限制。这有助于防止因请求过大的分页大小而导致性能问题。
page_query_param: 定义用于指定页码的查询参数,默认为 'page'。例如,?page=3 会请求第三页的数据。
# 方法
paginate_queryset(queryset, request, view=None): 该方法负责根据请求中的页码参数和每页大小参数切片查询集。返回分页后的查询集,如果请求的页码无效,则返回 None。
参数解释:
queryset:就是queryset对象。
request:drf中http请求中的request。
view: 可选参数,这是当前处理请求的视图实例。一般高级用法中才能用到。
get_paginated_response(data): 将分页后的数据序列化并封装在一个具有分页元数据的响应中。元数据通常包含当前页码、每页数量、总页数、是否有上一页和下一页等信息。
参数解释:
data是序列化后的分页数据
get_page_number(request): 从请求中提取页码参数的值。
get_page_size(request): 如果配置允许动态改变每页大小,此方法将从请求中获取每页大小的参数值。
2.3、使用示例
点击查看代码
from rest_framework import generics
from rest_framework.pagination import PageNumberPagination
# 如果是想自定义分页类,继承PageNumberPagination即可,也可以使用默认分页类PageNumberPagination。
class LargeResultsSetPagination(PageNumberPagination):
page_size = 100 # 每页100条记录
page_size_query_param = 'page_size' # 允许客户端自定义每页大小
max_page_size = 1000 # 最大允许每页1000条记录
class PublisherView(ModelViewSet):
queryset = models.Publisher.objects.all()
serializer_class = serializers.PublisherSerializer
pagination_class = PageNumberPagination
pagination_class.page_size = 5
def list(self, request, *args, **kwargs):
publisher_name = request.GET.get('publisher_name')
page_number = request.GET.get('page')
if publisher_name:
queryset = self.get_queryset().filter(name__exact=publisher_name).order_by('id')
else:
queryset = self.get_queryset().order_by('id')
# 使用rest_framework.pagination.PageNumberPagination 分页类,
# 最好是继承GenericAPIView,这样就不用再初始化PageNumberPagination类的对象
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
return Response({"data": ser.data, "total_pages": total_pages})
2.4、PageNumberPagination 类源码
点击查看代码
class BasePagination:
display_page_controls = False
def paginate_queryset(self, queryset, request, view=None): # pragma: no cover
raise NotImplementedError('paginate_queryset() must be implemented.')
def get_paginated_response(self, data): # pragma: no cover
raise NotImplementedError('get_paginated_response() must be implemented.')
def get_paginated_response_schema(self, schema):
return schema
def to_html(self): # pragma: no cover
raise NotImplementedError('to_html() must be implemented to display page controls.')
def get_results(self, data):
return data['results']
def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
if coreapi is not None:
warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning)
return []
def get_schema_operation_parameters(self, view):
return []
class PageNumberPagination(BasePagination):
"""
A simple page number based style that supports page numbers as
query parameters. For example:
http://api.example.org/accounts/?page=4
http://api.example.org/accounts/?page=4&page_size=100
"""
# The default page size.
# Defaults to `None`, meaning pagination is disabled.
page_size = api_settings.PAGE_SIZE
django_paginator_class = DjangoPaginator
# Client can control the page using this query parameter.
page_query_param = 'page'
page_query_description = _('A page number within the paginated result set.')
# Client can control the page size using this query parameter.
# Default is 'None'. Set to eg 'page_size' to enable usage.
page_size_query_param = None
page_size_query_description = _('Number of results to return per page.')
# Set to an integer to limit the maximum page size the client may request.
# Only relevant if 'page_size_query_param' has also been set.
max_page_size = None
last_page_strings = ('last',)
template = 'rest_framework/pagination/numbers.html'
invalid_page_message = _('Invalid page.')
def paginate_queryset(self, queryset, request, view=None):
"""
Paginate a queryset if required, either returning a
page object, or `None` if pagination is not configured for this view.
"""
self.request = request
page_size = self.get_page_size(request)
if not page_size:
return None
paginator = self.django_paginator_class(queryset, page_size)
page_number = self.get_page_number(request, paginator)
try:
self.page = paginator.page(page_number)
except InvalidPage as exc:
msg = self.invalid_page_message.format(
page_number=page_number, message=str(exc)
)
raise NotFound(msg)
if paginator.num_pages > 1 and self.template is not None:
# The browsable API should display pagination controls.
self.display_page_controls = True
return list(self.page)
def get_page_number(self, request, paginator):
page_number = request.query_params.get(self.page_query_param) or 1
if page_number in self.last_page_strings:
page_number = paginator.num_pages
return page_number
def get_paginated_response(self, data):
return Response({
'count': self.page.paginator.count,
'next': self.get_next_link(),
'previous': self.get_previous_link(),
'results': data,
})
def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'required': ['count', 'results'],
'properties': {
'count': {
'type': 'integer',
'example': 123,
},
'next': {
'type': 'string',
'nullable': True,
'format': 'uri',
'example': 'http://api.example.org/accounts/?{page_query_param}=4'.format(
page_query_param=self.page_query_param)
},
'previous': {
'type': 'string',
'nullable': True,
'format': 'uri',
'example': 'http://api.example.org/accounts/?{page_query_param}=2'.format(
page_query_param=self.page_query_param)
},
'results': schema,
},
}
def get_page_size(self, request):
if self.page_size_query_param:
with contextlib.suppress(KeyError, ValueError):
return _positive_int(
request.query_params[self.page_size_query_param],
strict=True,
cutoff=self.max_page_size
)
return self.page_size
def get_next_link(self):
if not self.page.has_next():
return None
url = self.request.build_absolute_uri()
page_number = self.page.next_page_number()
return replace_query_param(url, self.page_query_param, page_number)
def get_previous_link(self):
if not self.page.has_previous():
return None
url = self.request.build_absolute_uri()
page_number = self.page.previous_page_number()
if page_number == 1:
return remove_query_param(url, self.page_query_param)
return replace_query_param(url, self.page_query_param, page_number)
def get_html_context(self):
base_url = self.request.build_absolute_uri()
def page_number_to_url(page_number):
if page_number == 1:
return remove_query_param(base_url, self.page_query_param)
else:
return replace_query_param(base_url, self.page_query_param, page_number)
current = self.page.number
final = self.page.paginator.num_pages
page_numbers = _get_displayed_page_numbers(current, final)
page_links = _get_page_links(page_numbers, current, page_number_to_url)
return {
'previous_url': self.get_previous_link(),
'next_url': self.get_next_link(),
'page_links': page_links
}
def to_html(self):
template = loader.get_template(self.template)
context = self.get_html_context()
return template.render(context)
def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
if coreapi is not None:
warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning)
assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
fields = [
coreapi.Field(
name=self.page_query_param,
required=False,
location='query',
schema=coreschema.Integer(
title='Page',
description=force_str(self.page_query_description)
)
)
]
if self.page_size_query_param is not None:
fields.append(
coreapi.Field(
name=self.page_size_query_param,
required=False,
location='query',
schema=coreschema.Integer(
title='Page size',
description=force_str(self.page_size_query_description)
)
)
)
return fields
def get_schema_operation_parameters(self, view):
parameters = [
{
'name': self.page_query_param,
'required': False,
'in': 'query',
'description': force_str(self.page_query_description),
'schema': {
'type': 'integer',
},
},
]
if self.page_size_query_param is not None:
parameters.append(
{
'name': self.page_size_query_param,
'required': False,
'in': 'query',
'description': force_str(self.page_size_query_description),
'schema': {
'type': 'integer',
},
},
)
return parameters
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix