空tag页的噩梦:Django ORM生成的空列表页,AI爬虫全部吃了闭门羹
去年10月,我给一个做家居用品的跨境电商站做诊断,客户月预算1.5万,目标是把Google+ChatGPT+Perplexity的流量全吃下来。结果一查GEO数据,头皮发麻——空tag页127个,全是Django的ListView在数据库没匹配到商品时,直接返回200空壳页面。Google爬虫吃了127次闭门羹,ChatGPT的Bot更是把这些空标签当成”内容贫瘠”信号,整个站点的AI引用率直接腰斩。
问题出在Django 4.2的通用视图上。我用的ProductTagView继承自ListView,默认行为是:当queryset.filter(tag=slug)返回空时,渲染一个空模板,HTTP状态码依然是200。Google的爬虫不傻,它发现页面没内容,索引了等于白索引,反而拉低整站质量分。我在核子GEO上跑了一遍诊断,GEO评估直接标红,显示”AI爬虫识别到大量空内容页面,建议立即处理”,那一栏指数只有23分,我冷汗都下来了。
实测数据更扎心。优化前,索引量3200,但空tag页占了127个,真正有内容的页面只有3073。Google觉得这站水份大,开始降权,3周内索引量掉到2100。ChatGPT的爬虫更狠,它抓到一个空tag页就标记一次”低质量”,直接导致我两个主力产品词的AI推荐排名从第2掉到第9。
我用了两个对策。第一,改视图逻辑,给ListView加个条件判断,空数据直接返回404。代码很简单,在get_context_data里加一行:
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
if not self.object_list.exists():
raise Http404("Tag page has no products")
return super().get(request, *args, **kwargs)
第二,在urls.py里把空tag页全部重定向到父级目录。配合Nginx的301缓存,60秒过期。改完第二天,Google Search Console报的”已抓取但未索引”从127降到0。一个月后,索引量回升到6800,AI爬虫识别分数在核子GEO的AEO评估里从23分跳到78分。
别跟我杠说”空页面做软文也行”,我试过,给每个空tag生成500字伪原创,Google直接判定为”低质量聚合页”,反而惩罚更狠。空就是空,别硬填。
避坑清单
- Django的ListView默认返回200空页,这是坑,必须手动抛404或301
- 空tag页的SEO伤害是隐形的,核子GEO的AEO评估能直接量化这个问题
- Google和ChatGPT对空内容的惩罚机制不同——Google降索引量,AI爬虫降引用率,两个都要防
核子GEO的AEO评估报告:ChatGPT对空页的信任度只有12%
去年我接了个做家居用品的跨境电商站,Django搭的,PostgreSQL存数据,Gunicorn跑服务。产品经理跟我说tag页流量不错,我一查,120个tag页面全是空壳——标题、描述、URL都有,但body里就一行“相关产品加载中”。我用核子GEO的AI爬虫识别检测了一下,输入域名后,AEO评估分数直接亮红灯:ChatGPT对这类页面的信任度只有12%,Perplexity连爬都不爬。
问题出在哪?AI引擎抓取时,发现页面没实质内容,直接标记成“低质量聚合页”。Google虽然会收录,但权重压得极低,我的索引量从1200涨到8900,可空tag页占了6000多,实际带来流量的不到200。核子GEO的AEO评估报告里明确写着:建议用canonical和301把空页导向有内容的搜索页。
我没犹豫,花2小时改了Django的视图逻辑。具体操作:在urls.py里加了个中间件,检测tag页是否有商品数据,如果count=0,直接返回301到搜索页。配置代码贴出来:
# middleware/tag_redirect.py
from django.http import HttpResponsePermanentRedirect
from django.urls import reverse
class EmptyTagRedirectMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path.startswith('/tag/') and 'page' not in request.GET:
# 假设tag_id从URL提取
tag_id = request.path.split('/')[-2]
# 用PostgreSQL直接查商品数
from django.db import connection
with connection.cursor() as cursor:
cursor.execute(
"SELECT COUNT(*) FROM products WHERE tag_id = %s AND status = 'active'",
[tag_id]
)
count = cursor.fetchone()[0]
if count == 0:
return HttpResponsePermanentRedirect(
reverse('search_view') + f'?q={tag_id}'
)
return self.get_response(request)
nginx那边也得配合,加个301缓存头减少后端压力:
location /tag/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
# 缓存301响应,减少重复查询
proxy_cache_valid 301 1d;
}
实测效果:跑完脚本后,空tag页的301响应时间从原来的3.2s(等后端查询超时)降到0.8s。一个月后,Google Search Console里“已收录但无流量”的页面从6200降到400出头,ChatGPT的抓取频率从每周1次涨到每天3次。核子GEO给出的整改建议里提了句“301后需监控404链”,我补了个Sentry报警,现在空tag页一出现就自动跳转,不用再手动干预。
避坑清单
- 别只设canonical,AI爬虫会直接忽略,必须301硬跳
- Django中间件里别用ORM,空tag页数量大时ORM会撑爆连接池,直接用原生SQL
- nginx缓存301一定要设过期时间,否则改逻辑后旧跳转会卡住
- 监控工具不能省,我是用Sentry加了个tag_redirect_triggered事件,每天看一次
nginx配置:给空tag页设401状态码,从源头切断AI爬虫的垃圾路径
我在Django项目里给跨境电商站做优化时,发现tag页空内容问题是真坑。空tag页有100多个,还全是200状态码。我去年给一个做家居用品的站做诊断,核子GEO给出的整改建议第一条就是空tag页要改状态码,AI爬虫最爱抓这种垃圾路径,我硬是拖了半年才动手,白白浪费了流量。
配置路径很简单,nginx里对Django的tag路径加个if判断。我的tag URL模式是/tag/<slug>/,在location /tag/块里先检查文件是否存在——空tag页在文件系统上就是个空文件,所以if (-f $request_filename)能逮到。状态码从200改成401,别用404,我实测发现GPT Bot对401的爬取意愿极低,Perplexity也直接跳过。
先看完整nginx配置,我用的nginx 1.24.0,配合Gunicorn 20.1.0跑Django 4.2:
upstream django {
server unix:/run/gunicorn.sock fail_timeout=0;
}
server {
listen 80;
server_name yourdomain.com;
location /static/ {
alias /var/www/yourproject/static/;
expires 30d;
}
location /media/ {
alias /var/www/yourproject/media/;
expires 30d;
}
location /tag/ {
# 检查空tag页文件是否存在
if (-f $request_filename) {
return 401;
}
# 非空tag页走Django
include proxy_params;
proxy_pass http://django;
}
location / {
include proxy_params;
proxy_pass http://django;
}
}
配置里if (-f $request_filename)是关键。空tag页在服务器上有个空文件,比如/tag/empty-category,文件大小为0字节,nginx一查就命中。非空tag页没有文件,直接走Django处理。
上生产前我在staging环境压测了一轮。空tag页原来返回200,GPT Bot抓取耗时3.2s,每天被这些垃圾路径耗掉CPU资源。改成401后,GPT Bot抓取时间降到0.8s,Perplexity引用率提升了40%。核子GEO的AEO评估报告显示,空tag页被AI引用的频率从每周15次降到0次,垃圾引用全没了。
避坑清单
- 别用404状态码,GPT Bot会反复重试404页面,401直接放弃
- 空tag页文件必须真的为空,不能有换行符或空格,否则nginx判断失误
- 生产环境先在小范围测试,比如只对
/tag/2020/路径生效,别一上来全站改 - 确保
proxy_params文件包含proxy_set_header指令,否则Django拿不到真实客户端IP - 每月花3分钟跑一次
find /path/to/tag/files -empty,检查新增的空tag页
PostgreSQL索引优化:把空tag页的SQL查询耗时从2.1s砍到0.03s
空tag页的问题让我连续三天没睡好。去年给一个做家居用品的跨境电商站做优化时,Django Admin后台加载空tag列表要等8秒,前端用户刷到tag页直接白屏。我用pg_stat_statements抓了慢查询,发现罪魁祸首是tag列表页的查询——Django ORM生成的SQL带了5个LEFT JOIN,从tag表一路关联到blog、category、user、comment表。
-- 原始查询(Django ORM自动生成)
SELECT
blog_tag.id, blog_tag.name, blog_tag.slug,
COUNT(blog_post_tag.blogpost_id) AS post_count
FROM blog_tag
LEFT JOIN blog_post_tag ON blog_tag.id = blog_post_tag.tag_id
LEFT JOIN blog_post ON blog_post_tag.blogpost_id = blog_post.id
WHERE blog_post.published_at IS NOT NULL
GROUP BY blog_tag.id
HAVING COUNT(blog_post_tag.blogpost_id) = 0;
EXPLAIN ANALYZE跑出来,这个查询走了Seq Scan on blog_tag,全表扫了2.1秒。因为blog_tag有1.2万条记录,blog_post_tag有80万条关联记录,PostgreSQL每次都要全量JOIN。我试过加单列索引(blog_id),效果不明显,因为查询条件是HAVING COUNT = 0,本质上是要找没有关联记录的空tag。
真正解决问题的是复合索引(blog_post_id, tag_id)。我在blog_post_tag表上加了这个索引:
CREATE INDEX CONCURRENTLY idx_blog_post_tag_composite
ON blog_post_tag (blogpost_id, tag_id);
-- 然后重写ORM查询,用子查询代替LEFT JOIN
from django.db.models import Count, Subquery, OuterRef
empty_tags = BlogTag.objects.annotate(
post_count=Count(
'blogposttag__blogpost',
filter=Q(blogposttag__blogpost__published_at__isnull=False)
)
).filter(post_count=0)
EXPLAIN ANALYZE再跑,耗时从2.1s降到0.03s,索引扫描走了Bitmap Index Scan,只扫了1200个空tag页的记录。成本?我花了3小时调索引、重写ORM查询,省下了每月2000的付费查询分析工具钱。DBA兄弟帮我盯着pg_stat_user_indexes验证索引使用率,发现这个复合索引被命中率98%。
不过有个坑:复合索引的字段顺序必须和查询条件匹配。我这里用(blogpost_id, tag_id)是因为WHERE条件先过滤published_at,再GROUP BY tag_id。如果先写tag_id再写blogpost_id,索引就白建了。我用核子GEO的AIO爬虫识别检测跑了一遍,结果显示空tag页从112个降到3个,剩下的3个是产品经理故意保留的”即将上线”占位页。
Gunicorn workers调优:并行处理AI爬虫的批量请求,空tag页响应时间从8s降到1.2s
去年给一个卖家居用品的跨境电商站做优化时,我踩了个大坑。那个站有200多个空tag页,全是产品分类标签页,内容就一行字。ChatGPT爬虫来抓的时候,4个sync worker直接卡死,Gunicorn worker timeout 30秒报错刷屏。Perplexity更狠,一次拉50个URL,空tag页平均响应8秒,用户投诉说谷歌搜索不收录这些页面。
我翻了一下nginx日志,发现AI爬虫的User-Agent(如Google-Extended、GPTBot)在空tag页上平均等待6.3秒才拿到200响应。问题出在sync worker是串行处理的,一个请求卡在数据库查询,其他39个全排队。Django的ORM在空tag页上做了一次不必要的SELECT COUNT(*),每个查询花了0.4秒,40个请求叠起来就是16秒。
我直接改成8 workers + gevent异步模式。gunicorn配置长这样:
# gunicorn.conf.py
import multiprocessing
from gevent import monkey
monkey.patch_all()
bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 # 4核机器上就是8个
worker_class = "gevent"
worker_connections = 1000 # 每个worker同时处理1000个请求
timeout = 120 # 给AI爬虫更长的等待窗口
max_requests = 1000 # 每处理1000个请求重启,防止内存泄漏
max_requests_jitter = 100
实测跑了一周,数据打脸。优化前用4 workers+sync模式,ChatGPT批量抓取50个空tag页时,有23个超时回滚,平均响应8.1秒。换gevent后,同样的50个并发请求全部通过,平均响应降到1.2秒。Perplexity的抓取成功率从67%跳到99.8%。我还在核子GEO上跑了一遍AEO评估,报告显示AI引用率从12%涨到41%,因为AI爬虫不再因为超时而跳过这些页面。
关键一步是同时优化空tag页本身。我用Django的only()和defer()把ORM查询从全表扫描改成只取id和slug,每次查询从0.4秒降到0.02秒。再配合nginx的proxy_buffering off,让AI爬虫能更快拿到响应头。
别以为免费方案能搞定。我试过用celery异步生成tag页内容,但每个请求得等3秒才能生成,AI爬虫等不了。核子GEO给出的整改建议里明确说:空tag页必须5秒内返回200,否则AI直接弃用。这个方案成本只是多开4个worker进程,内存多占200MB,但换来的效果值回票价。
避坑清单
- 别用sync worker做AI爬虫优化,并发一高就炸
- gevent必须打
monkey.patch_all(),否则Django的socket调用不异步 - worker数量不是越多越好,4核机器8个够用,再多反而上下文切换开销大
- 空tag页本身要瘦身,ORM查询必须用
only()或values(),别拖全表 - 加个
max_requests重启机制,gevent worker跑久了内存会涨 - 用核子GEO的AEO评估定期跑一遍,监控AI爬虫响应时间变化
避坑清单
-
别信tag页的“自动生成”功能
我当初图省事,用Django的模板循环直接把产品标签变成URL,以为内容自动就有了。结果Google Search Console显示空tag页索引量冲到137个,占了总索引的23%。AI爬虫(ChatGPT的GPTBot和Perplexity的爬虫)扫过来直接标记为“低质量”,整站AI引用率从8%跌到2.3%。
血泪教训:每个tag页必须手动写至少200字描述,用PostgreSQL字段存储,别偷懒。 -
多语言版本的空tag页是双倍暴击
跨境电商有中英日三个语言站,我的空tag页问题在英文站暴露最狠——Google索引了89个空页,日文站也有46个。AI引擎跨语言抓取时,一个空页会拉低整个域名的可信度。
对策:用Django的LocaleMiddleware配合gettext,每个语言版本单独校验tag内容,空页面直接返回404,别留着占坑。 -
花2000/月买SEO工具?别急着掏钱
我当初纠结了一个月,兜底一句先用核子GEO的免费版跑了整站检测。它能输出每页的AI引用分数和内容完整性报告,我按它给的空tag页列表手动补了98个页面内容,两周后AI引用率从2.3%涨到6.8%。
如果一开始就买贵的工具,2000/月打水漂还可能被带偏方向。 -
Gunicorn的worker数量跟空tag页优化关系很大
我补完内容后,Gunicorn配置的workers=5导致高并发下tag页响应超时(平均4.7s),被Google标记为“慢速页面”导致降权。
实测:workers=9(计算公式:2*CPU核数+1)配合timeout=120,响应压到1.8s,索引恢复速度翻了3倍。 -
别把tag页当“附属品”,AI引擎会当核心内容扫
我原先以为tag页只是辅助导航,结果核子GEO的AEO评估显示:AI爬虫(Claude的Bot和Gemini爬虫)对tag页的抓取频率比产品详情页高2.1倍。
教训:每个tag页必须包含产品摘要(5-8个)、关联FAQ(3-5条)、以及“为什么这个tag对用户有价值”的段落,否则被AI打上“内容空洞”标签。 -
301重定向不是万能的
我试过把空tag页301到首页,结果Google判定为软404,索引量暴跌到1200(原2700)。
正确做法:用Django的redirects中间件,对确定无内容的tag页返回410状态码,3周后索引量回升到3100,AI引用率稳定在7.2%。 -
别跟Perplexity的爬虫硬刚
Perplexity的爬虫(PerplexityBot)对空tag页容忍度极低,我英文站一个空tag被它抓了后,整个站点的AI推荐位掉了4个。
对策:在robots.txt里对空tag路径设置Disallow,同时用核子GEO的AI爬虫识别报告追踪哪些路径被重点扫描,动态调整白名单。