DRF之过滤类源码分析
DRF之过滤类源码分析
【一】过滤类介绍及BaseFilterBackend
- Django REST framework(DRF)中的过滤类允许你在API视图中对查询进行过滤,以根据特定条件筛选结果集。
- 过滤类是DRF的一部分,它允许你轻松地添加各种过滤选项,以满足不同用例的需求。
class BaseFilterBackend: """ A base class from which all filter backend classes should inherit. """ def filter_queryset(self, request, queryset, view): """ Return a filtered queryset. """ raise NotImplementedError(".filter_queryset() must be overridden.") def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' return [] def get_schema_operation_parameters(self, view): return []
【二】内置过滤类SearchFilter
【1】使用
# 过滤:必须继承 GenericAPIView 及其子类,才能使用(如果继承APIView,则不能这么写) # DRF 给我们提供了一个排序类 from rest_framework.filters import SearchFilter class BookView(GenericViewSet, ListModelMixin): queryset = models.Book.objects.all() serializer_class = BookSerializer filter_backends = [SearchFilter] # 排序类 # SearchFilter:必须配合一个类属性 # 按照那些字段进行筛选 # http://127.0.0.1:8000/app01/v1/books/?search=梦 # 只要name/price中带 梦 都能被搜出来 search_fields = ['name']
【2】源码分析
class SearchFilter(BaseFilterBackend): # The URL query parameter used for the search. # 搜索参数的名称,默认为 api_settings.SEARCH_PARAM,通常是 search。 # 拼在路由尾部的关键字 http://127.0.0.1:8000/app01/v1/books/?search=梦 # 你可以通过在API视图中自定义该参数来更改搜索参数的名称。 search_param = api_settings.SEARCH_PARAM # 用于HTML渲染搜索部件的模板路径,默认为 'rest_framework/filters/search.html'。 template = 'rest_framework/filters/search.html' # 定义了搜索条件的前缀和它们对应的查询操作。 # 例如,^ 表示 "istartswith"(不区分大小写的开始于) # = 表示 "iexact"(不区分大小写的精确匹配) 使用居多 # @ 表示 "search"(全文搜索) # $ 表示 "iregex"(不区分大小写的正则表达式匹配)。 lookup_prefixes = { '^': 'istartswith', '=': 'iexact', '@': 'search', '$': 'iregex', } # search_title = _('Search') # search_description = _('A search term.') # 从视图中获取搜索字段列表 # 可以覆盖此方法以动态更改搜索字段 def get_search_fields(self, view, request): """ Search fields are obtained from the view, but the request is always passed to this method. Sub-classes can override this method to dynamically change the search fields based on request content. """ # 从视图类中中的 search_fields 列表中映射出所有的 过滤参数字段 return getattr(view, 'search_fields', None) # 解析查询参数中的搜索条件,将其分割成一个列表。 def get_search_terms(self, request): """ Search terms are set by a ?search=... query parameter, and may be comma and/or whitespace delimited. """ # 获取到路由尾部携带的参数 params = request.query_params.get(self.search_param, '') # 空格代替 空 params = params.replace('\x00', '') # strip null characters # 多字段进行切分 params = params.replace(',', ' ') # 返回切分后的参数 return params.split() # 构建搜索条件,根据前缀选择合适的查询操作。 # field_name 参数是传递给过滤器的搜索字段名,可能包含前缀 def construct_search(self, field_name): # 字典定义了搜索条件的前缀和它们对应的查询操作。 # 检查 field_name 的第一个字符(前缀),并使用 get 方法从 lookup_prefixes 字典中获取对应的查询操作。 lookup = self.lookup_prefixes.get(field_name[0]) # 如果前缀存在 if lookup: # 则将前缀从 field_name 中去除,并将查询操作存储在 lookup 变量中。 field_name = field_name[1:] else: # 如果前缀不存在,则默认使用 'icontains' 查询操作,这表示执行不区分大小写的部分匹配。 lookup = 'icontains' # 使用 LOOKUP_SEP 连接字段名和查询操作,返回最终的搜索条件字符串。 # LOOKUP_SEP 是Django中用于连接查询字段和操作的分隔符,通常是双下划线 __。 # 例如,如果传递的搜索字段名是 ^name,那么这个方法将返回 "name__istartswith",这表示要在 name 字段上执行不区分大小写的开始于匹配操作。 return LOOKUP_SEP.join([field_name, lookup]) # 返回一个布尔值,指示是否应该使用 distinct() 方法来查询结果集以避免重复项。 # queryset 参数是要进行过滤的查询集,通常是数据库查询的结果集。 # search_fields 参数是用于搜索的字段列表,这些字段可能包含前缀。 def must_call_distinct(self, queryset, search_fields): """ Return True if 'distinct()' should be used to query the given lookups. """ # 迭代 search_fields 中的每个搜索字段 for search_field in search_fields: # 对于每个搜索字段,它获取与查询集相关联的模型的元数据(opts = queryset.model._meta) opts = queryset.model._meta # 检查搜索字段的第一个字符是否在 lookup_prefixes 中 if search_field[0] in self.lookup_prefixes: # 如果前缀存在,则将前缀从搜索字段中去除(search_field = search_field[1:])。 search_field = search_field[1:] # Annotated fields do not need to be distinct # 检查搜索字段是否已经在查询集的注释中。 if isinstance(queryset, models.QuerySet) and search_field in queryset.query.annotations: # 如果字段已经在注释中,说明该字段已经被标记为注释字段,通常不需要使用 distinct()。 continue # 如果字段不在注释中,则进一步检查字段是否包含嵌套关系(例如,related_field__nested_field)。 parts = search_field.split(LOOKUP_SEP) # 如果字段是嵌套关系,它会更新模型的元数据以跟踪关系路径,并检查是否存在多对多关系(m2m 关系)。 for part in parts: field = opts.get_field(part) if hasattr(field, 'get_path_info'): # This field is a relation, update opts to follow the relation path_info = field.get_path_info() opts = path_info[-1].to_opts # # 如果字段是多对多关系,就需要调用 distinct() 方法,因为多对多关系通常会导致结果集中的重复项。 if any(path.m2m for path in path_info): # This field is a m2m relation so we know we need to call distinct # 如果需要调用 distinct(),它将返回 True return True else: # This field has a custom __ query transform but is not a relational field. break # 最后,方法返回一个布尔值,指示是否应该调用 distinct()。 # 如果需要调用 distinct(),它将返回 True,否则返回 False。 return False # 实际的查询过滤操作,根据查询参数中的搜索条件修改查询集 def filter_queryset(self, request, queryset, view): # 获取了视图中定义的搜索字段列表(search_fields)和查询参数中传递的搜索条件(search_terms)。 search_fields = self.get_search_fields(view, request) search_terms = self.get_search_terms(request) # 如果没有定义搜索字段或者没有传递搜索条件 if not search_fields or not search_terms: # 就直接返回原始的查询集 queryset,不进行任何过滤。 return queryset # 构建了一个包含了所有搜索字段的ORM查询操作列表 orm_lookups # 通过调用 construct_search 方法将搜索字段名转换为相应的查询操作。 orm_lookups = [ self.construct_search(str(search_field)) for search_field in search_fields ] # 始化了两个变量,base 和 conditions。 # base 是原始的查询集 # conditions 是用来存储过滤条件的列表。 base = queryset conditions = [] # 迭代每个搜索条件(search_term)和每个查询操作(orm_lookup),并创建一个 Q 对象,将查询操作和搜索条件传递给它。 # 这个 Q 对象表示了一个或多个查询条件的逻辑或关系。 for search_term in search_terms: # 创建的 Q 对象列表存储在 queries 中 queries = [ models.Q(**{orm_lookup: search_term}) for orm_lookup in orm_lookups ] # 然后使用 reduce(operator.or_, queries) 将它们组合成一个包含所有查询条件的 Q 对象。 # 最终,conditions 列表包含了一个或多个 Q 对象,每个 Q 对象代表一个搜索条件的查询操作。 # 使用 reduce(operator.and_, conditions) 将所有的 Q 对象组合成一个包含所有搜索条件的查询操作,这个查询操作表示了所有搜索条件之间的逻辑与关系。 conditions.append(reduce(operator.or_, queries)) # 最后,使用 queryset.filter() 方法,将这个复合查询操作应用于原始查询集,以便执行过滤操作。 queryset = queryset.filter(reduce(operator.and_, conditions)) # 如果必须调用 distinct() 方法(通过调用 must_call_distinct 方法来确定) if self.must_call_distinct(queryset, search_fields): # Filtering against a many-to-many field requires us to # call queryset.distinct() in order to avoid duplicate items # in the resulting queryset. # We try to avoid this if possible, for performance reasons. # 在过滤后的查询集上调用 distinct() 以确保结果集不包含重复项 queryset = distinct(queryset, base) # 返回过滤后的数据集 return queryset # 用于生成搜索框的HTML表示形式,以便在API的浏览器浏览界面中显示 def to_html(self, request, queryset, view): ''' 目的是生成一个包含搜索框的HTML表示,以便在API的浏览器浏览界面中让用户输入搜索条件。 搜索框的样式和布局通常由指定的模板文件定义,这使得可以根据需要自定义搜索框的外观。 搜索参数的名称和搜索条件的值也被传递到模板中,以便在HTML中动态生成搜索框。 ''' # 检查视图是否定义了搜索字段列表(search_fields)。 if not getattr(view, 'search_fields', None): # 如果没有定义搜索字段,就直接返回空字符串,表示不需要显示搜索框。 return '' # 获取查询参数中传递的搜索条件(search_terms)。 term = self.get_search_terms(request) # 如果搜索条件存在,就将第一个搜索条件(通常只支持一个搜索条件)存储在 term 变量中。 term = term[0] if term else '' # 创建一个名为 context 的字典,其中包含两个键值对: # 'param':用于指定搜索参数的名称,通常是 search。 # 'term':包含了搜索条件的值,即用户在搜索框中输入的文本。 context = { 'param': self.search_param, 'term': term } # 使用 Django 的 loader.get_template 函数获取指定模板文件(self.template)的模板对象。 # 这个模板通常包含了搜索框的HTML表示。 template = loader.get_template(self.template) # 使用 template.render(context) 渲染模板,将 context 字典中的数据传递给模板,生成包含搜索框的HTML表示 return template.render(context) def get_schema_fields(self, view): ''' 目的是生成一个包含搜索参数的字段定义,以便在API文档中显示搜索参数的相关信息。 这有助于API用户理解如何使用搜索功能,并构建正确的搜索请求。 ''' # 通过 assert 语句确保 coreapi 和 coreschema 这两个库已经安装。 # 这些库是用于构建API文档和描述API数据结构的工具。 assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' # 创建一个列表,其中包含一个 CoreAPI 字段定义对象。 # 这个字段定义对象用于描述搜索参数。 # 返回一个包含了搜索参数字段定义的列表。这个列表通常会与其他字段定义一起用于生成API文档,并且可以让API客户端了解如何构建搜索请求 return [ coreapi.Field( # name:搜索参数的名称,通常是 search。 name=self.search_param, # required:指定搜索参数是否是必需的,这里设置为 False 表示不是必需的。 required=False, # location:指定搜索参数在请求中的位置,这里设置为 query,表示在查询字符串中。 location='query', # schema:定义搜索参数的数据模式,这里使用 coreschema.String 来定义搜索参数的数据类型为字符串,并提供了标题(title)和描述(description)。 schema=coreschema.String( title=force_str(self.search_title), description=force_str(self.search_description) ) ) ] # 用于生成API操作的参数定义,这些参数用于API文档的生成 # 返回包含了参数定义的列表。这个列表通常会与API操作一起用于生成API文档,以帮助API用户了解如何构建请求和使用参数。 def get_schema_operation_parameters(self, view): ''' API文档中显示操作的参数信息。这有助于API用户理解如何构建API请求,并知道哪些参数是可选的、哪些是必需的 ''' # 创建了一个包含参数定义的列表。每个参数定义都是一个字典,描述了一个API操作的参数。 return [ { # 'name':指定参数的名称,通常是 search。 'name': self.search_param, # 'required':指定参数是否是必需的,这里设置为 False 表示不是必需的。 'required': False, # 'in':指定参数在请求中的位置,这里设置为 query,表示在查询字符串中。 'in': 'query', # 'description':提供参数的描述,使用了 force_str 函数来确保描述是字符串类型。 'description': force_str(self.search_description), # 'schema':定义参数的数据模式,这里指定参数的数据类型为字符串('type': 'string')。 'schema': { 'type': 'string', }, }, ]
【三】第三方过滤类DjangoFilterBackend
【1】使用
# (2) 过滤:必须继承 GenericAPIView 及其子类,才能使用(如果继承APIView,则不能这么写) # 第三方过滤类: pip3.9 install django-filter from django_filters.rest_framework import DjangoFilterBackend class BookView(GenericViewSet, ListModelMixin): queryset = models.Book.objects.all() serializer_class = BookSerializer filter_backends = [DjangoFilterBackend] # 排序类 # SearchFilter:必须配合一个类属性 # 按照那些字段进行筛选 # http://127.0.0.1:8000/app01/v1/books/?search=梦 # 根据指定字段进行筛选,按名字和价格精准匹配 filterset_fields = ['name', 'price']
【2】源码分析
class DjangoFilterBackend: # 用于指定用于过滤的过滤器集的基类,默认为filterset.FilterSet filterset_base = filterset.FilterSet # 指示在过滤器验证失败时是否引发异常,默认为True,表示引发异常。 raise_exception = True @property # 用于确定渲染过滤器表单时要使用的模板 def template(self): # 如果使用Crispy Forms if compat.is_crispy(): # 则为"django_filters/rest_framework/crispy_form.html" return "django_filters/rest_framework/crispy_form.html" # 否则为"django_filters/rest_framework/form.html"。 return "django_filters/rest_framework/form.html" # 返回过滤器集的实例 def get_filterset(self, request, queryset, view): # 使用get_filterset_class方法来获取用于过滤查询集的FilterSet类。 # 这个类可以在视图中通过filterset_class属性指定,如果未指定,则会自动创建一个基于查询集的FilterSet类。 filterset_class = self.get_filterset_class(view, queryset) # 检查过滤器集类:如果获取到了过滤器集类(filterset_class不为None),则继续执行下面的步骤。 # 否则,返回None,表示没有可用的过滤器集。 if filterset_class is None: return None # 获取过滤器集的关键字参数 # 调用get_filterset_kwargs方法,以获取传递给过滤器集构造函数的关键字参数 # 。这些参数通常包括请求数据、查询集和请求对象等。 kwargs = self.get_filterset_kwargs(request, queryset, view) # 创建过滤器集对象 # 最后,它使用获取到的过滤器集类和关键字参数来创建过滤器集对象,并将其返回。 return filterset_class(**kwargs) # 返回用于过滤查询集的FilterSet类 def get_filterset_class(self, view, queryset=None): """ # 这个方法会根据视图和查询集的情况来确定应该使用哪个 FilterSet 类 Return the `FilterSet` class used to filter the queryset. """ # 尝试从视图中获取 filterset_class 属性。这个属性是视图中定义的,可以指定要用于过滤的 FilterSet 类。 # 如果视图中有指定,则直接返回这个类。 filterset_class = getattr(view, "filterset_class", None) # 如果视图中没有指定 filterset_class,它会检查是否有 filterset_fields 属性。 # filterset_fields 属性是一组用于过滤的字段名称,通常与查询集的模型相关。 filterset_fields = getattr(view, "filterset_fields", None) if filterset_class: filterset_model = filterset_class._meta.model # FilterSets do not need to specify a Meta class if filterset_model and queryset is not None: assert issubclass( queryset.model, filterset_model ), "FilterSet model %s does not match queryset model %s" % ( filterset_model, queryset.model, ) return filterset_class # 如果视图中定义了这个属性 if filterset_fields and queryset is not None: # 它会自动创建一个临时的 FilterSet 类,该类的 Meta 类中包含了模型和字段信息。 MetaBase = getattr(self.filterset_base, "Meta", object) class AutoFilterSet(self.filterset_base): class Meta(MetaBase): model = queryset.model fields = filterset_fields return AutoFilterSet # 如果既没有指定 filterset_class 也没有 filterset_fields,则返回 None,表示没有可用的 FilterSet 类。 return None # 此方法返回传递给过滤器集构造函数的关键字参数,包括请求数据、查询集和请求对象。 def get_filterset_kwargs(self, request, queryset, view): return { "data": request.query_params, "queryset": queryset, "request": request, } # 此方法用于过滤查询集。,,。 def filter_queryset(self, request, queryset, view): # 通过调用 get_filterset 方法,获取与视图关联的过滤器集实例 filterset。 filterset = self.get_filterset(request, queryset, view) # 检查 filterset 是否为 None。 if filterset is None: # 如果 filterset 为 None,说明没有可用的过滤器集与视图关联,此时直接返回原始的查询集 queryset,不进行任何过滤。 return queryset # 如果 filterset 存在,方法会进一步验证过滤器集的有效性,即调用 is_valid 方法检查过滤器集是否通过了验证。 if not filterset.is_valid() and self.raise_exception: # 如果过滤器集无效(is_valid 返回 False)并且 self.raise_exception 属性为 True,则会抛出验证异常,该异常包含了过滤器集的错误信息。 raise utils.translate_validation(filterset.errors) # 然后将其应用于查询集 return filterset.qs # 此方法返回过滤器表单的HTML表示。 def to_html(self, request, queryset, view): # 调用 get_filterset 方法获取过滤器集实例 filterset。 filterset = self.get_filterset(request, queryset, view) # 如果 filterset 为 None,则返回 None,表示没有可用的过滤器集,因此无法生成过滤器表单的 HTML 表示。 if filterset is None: return None # 如果 filterset 存在,方法会根据指定的模板(self.template)渲染过滤器表单。 # 通常,模板会包含 HTML 表单元素,以显示过滤器字段和相应的输入框、复选框等表单组件。 template = loader.get_template(self.template) context = {"filter": filterset} # 最终,方法返回渲染后的 HTML 表示,以便在前端页面中显示过滤器表单 return template.render(context, request) # 此方法返回与过滤器字段相关的CoreAPI字段定义 def get_coreschema_field(self, field): # 首先,它根据过滤器字段的类型判断, if isinstance(field, filters.NumberFilter): # 如果字段类型是 filters.NumberFilter,则生成一个 compat.coreschema.Number 字段 field_cls = compat.coreschema.Number else: # 否则生成一个 compat.coreschema.String 字段。 field_cls = compat.coreschema.String # 字段的 description 属性通常设置为过滤器字段的帮助文本(help_text),以提供关于字段用途的描述信息。 # 最终,方法返回生成的 CoreAPI 字段定义,用于 API 文档的生成和展示 return field_cls(description=str(field.extra.get("help_text", ""))) # 此方法返回API操作的参数定义列表,用于生成API文档。它检查过滤器集的基本过滤器,并为每个字段创建一个参数定义。 def get_schema_fields(self, view): # This is not compatible with widgets where the query param differs from the # filter's attribute name. Notably, this includes `MultiWidget`, where query # params will be of the format `<name>_0`, `<name>_1`, etc... # 检查 Django 的 RemovedInDjangoFilter25Warning from django_filters import RemovedInDjangoFilter25Warning # 并发出警告,指出内置的模式生成已被弃用,建议使用 drf-spectacular。 warnings.warn( "Built-in schema generation is deprecated. Use drf-spectacular.", category=RemovedInDjangoFilter25Warning, ) # 断言 coreapi 和 coreschema 库已安装,因为这些库用于生成 API 文档的参数字段。 assert ( compat.coreapi is not None ), "coreapi must be installed to use `get_schema_fields()`" assert ( compat.coreschema is not None ), "coreschema must be installed to use `get_schema_fields()`" # 方法尝试从视图中获取查询集(queryset)对象。 try: # 这是通过调用视图的 get_queryset() 方法来完成的,以便获取与视图关联的查询集。 queryset = view.get_queryset() except Exception: # 如果无法获取查询集(通常是因为视图没有实现 get_queryset() 方法),则将 queryset 设置为 None queryset = None # 并发出警告,指出该视图不兼容模式生成。 warnings.warn( "{} is not compatible with schema generation".format(view.__class__) ) # 使用 get_filterset_class 方法来获取与视图关联的过滤器集类(filterset_class) filterset_class = self.get_filterset_class(view, queryset) return ( [] # 如果过滤器集类已经在视图中定义(通过 filterset_class 属性),则使用该类 if not filterset_class # 如果没有可用的过滤器集类,方法返回一个空列表 [],表示没有需要生成参数字段的过滤器。 else [ # 否则,如果视图定义了 filterset_fields 属性且查询集不为 None,则会动态创建一个自动生成的过滤器集类 AutoFilterSet,该类继承自 filterset_base,并使用查询集的模型和 filterset_fields 属性来定义过滤器集。 compat.coreapi.Field( # name 属性设置为字段的名称。 name=field_name, # required 属性根据字段的 required 属性设置。 required=field.extra["required"], # location 属性设置为 "query",表示这些参数位于查询字符串中 location="query", # schema 属性通过调用 get_coreschema_field 方法生成,该方法生成与字段相关的 CoreAPI 字段定义 schema=self.get_coreschema_field(field), ) # for field_name, field in filterset_class.base_filters.items() ] ) # 此方法返回API操作的参数列表,用于生成API文档。 # 它与get_schema_fields类似,但提供了更详细的参数信息,包括类型、是否必需等。 def get_schema_operation_parameters(self, view): # 检查 Django 的 RemovedInDjangoFilter25Warning from django_filters import RemovedInDjangoFilter25Warning # 并发出警告,指出内置的模式生成已被弃用,建议使用 drf-spectacular warnings.warn( "Built-in schema generation is deprecated. Use drf-spectacular.", category=RemovedInDjangoFilter25Warning, ) try: # 尝试从视图中获取查询集(queryset)对象,以便确定与视图关联的模型。 queryset = view.get_queryset() except Exception: # 如果无法获取查询集(通常是因为视图没有实现 get_queryset() 方法),则将 queryset 设置为 None queryset = None # 并发出警告,指出该视图不兼容模式生成。 warnings.warn( "{} is not compatible with schema generation".format(view.__class__) ) # 使用 get_filterset_class 方法来获取与视图关联的过滤器集类(filterset_class) # 如果过滤器集类已经在视图中定义(通过 filterset_class 属性),则使用该类 # 否则,如果视图定义了 filterset_fields 属性且查询集不为 None,则会动态创建一个自动生成的过滤器集类 AutoFilterSet,该类继承自 filterset_base,并使用查询集的模型和 filterset_fields 属性来定义过滤器集。 filterset_class = self.get_filterset_class(view, queryset) # 如果没有可用的过滤器集类,方法返回一个空列表 [],表示没有需要生成参数字段的过滤器。 if not filterset_class: return [] parameters = [] # # 遍历过滤器集类的基本过滤器字段(base_filters)并为每个字段创建一个参数定义 for field_name, field in filterset_class.base_filters.items(): # parameter = { # name 属性设置为字段的名称。 "name": field_name, # required 属性根据字段的 required 属性设置。 "required": field.extra["required"], # in 属性设置为 "query",表示这些参数位于查询字符串中。 "in": "query", # description 属性设置为字段的标签(label),如果字段没有标签,则设置为字段的名称。 "description": field.label if field.label is not None else field_name, # schema 属性是一个包含参数类型信息的字典。在这里,所有参数都被表示为字符串("type": "string")。 "schema": { "type": "string", }, } # 如果字段的 extra 属性包含了 "choices" 键(即字段具有选项) if field.extra and "choices" in field.extra: # 将 schema 字典的 "enum" 键设置为字段选项的列表。 parameter["schema"]["enum"] = [c[0] for c in field.extra["choices"]] parameters.append(parameter) return parameters
【四】自定义过滤类
【1】使用
from rest_framework import filters from django.db.models import Q class BookFilter(filters.BaseFilterBackend): ''' def filter_queryset(self, request, queryset, view): """ Return a filtered queryset. """ raise NotImplementedError(".filter_queryset() must be overridden.") ''' def filter_queryset(self, request, queryset, view): # 返回的数据,都是过滤后的数据 # http://127.0.0.1:8000/app01/v1/books/?price=99&name=追梦赤子心 # 需要将筛选条件变成price=99或name=追梦赤子心,而第三方写法中访问地址只能是上面的格式 price = request.query_params.get('price') name = request.query_params.get('name') queryset = queryset.filter(Q(name=name) | Q(price=price)) return queryset
【2】分析
- 根据地址栏中传入的数据进行过滤
- 将过滤完的 queryset 数据集返回
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通