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
posted @   有形无形  阅读(24)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· 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
点击右上角即可分享
微信分享提示