向JSON API发送GET请求偶尔返回空白响应的问题排查
问题
我用Python写了个爬虫,请求https://www.saltybet.com/state.json接口获取JSON内容,代码如下:
import requests import time class SaltyJson(): def __init__(self): self.url = "https://www.saltybet.com/state.json" self.session = requests.Session() def get_json(self): try: self.response = self.session.get(self.url, headers={"User-Agent": "Mozilla/5.0", "Accept": "application/json"}) if self.response.status_code != 200: time.sleep(1) self.get_json() else: return self.response.json() except requests.exceptions.ConnectionError: time.sleep(1) self.get_json() except requests.exceptions.JSONDecodeError: time.sleep(1) self.get_json()
初始请求正常,但运行数小时后经常出现requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)错误,此时self.response.json()返回None,也就是收到了空白响应,而且这种情况在服务器返回200状态码时也会发生。
这个问题是偶发性的,大部分同类错误是因为JSON无效、缺少请求头或服务器返回错误码,但我的请求99%都正常,只有偶尔会收到空白响应且状态码为200。排查后发现错误出在char 0位置,说明是空白响应或者完全无效的JSON;捕获异常后返回的None在访问键值时会触发TypeError: 'NoneType' object is not subscriptable,调试显示整个字典都是None。
我已经尝试的解决措施:
- 添加指数退避机制排查服务器限流问题
- 增加200状态码检查
- 验证返回JSON的键数量是否符合预期(
len(list(self.response.json().keys())) == 8)
另外,我发现该接口响应的Content-Type是text/html,不是预期的application/json,就算请求里带了"Accept":"application/json"头也没改变;但网站的JS代码里指定了contentType: "application/json; charset=utf-8"。
我的疑问:
- GET请求的请求头设置是否有问题,导致偶尔返回空白响应?这个问题和响应Content-Type为text/html有关吗?
- 如果确实是空白响应,除了状态码检查外,还需要添加哪些代码检查?
回答
关于请求头与Content-Type的问题
首先,请求头的设置存在优化空间,但空白响应的核心原因大概率不是Content-Type为text/html——毕竟你99%的请求都能正常解析JSON,说明服务器返回的内容实际是JSON格式,只是Content-Type标错了。
可以补充更贴近真实浏览器的请求头,降低被服务器识别为爬虫的概率,比如:
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", "Accept": "application/json", "Accept-Language": "en-US,en;q=0.9", "Referer": "https://www.saltybet.com/", "Cache-Control": "no-cache" }
服务器偶尔返回空白200响应,更可能是它的限流机制触发、临时负载过高或者连接中断导致的不完整响应,和Content-Type的关系不大。
针对空白响应的额外检查
除了状态码,还需要做这些关键检查:
- 检查响应内容长度:直接判断
len(self.response.content) == 0,如果成立就是空白响应,触发重试 - 先解码再检查有效性:不要直接调用
response.json(),先把响应内容解码成字符串并去除首尾空白,确认非空后再解析:
raw_content = self.response.content.decode('utf-8').strip() if not raw_content: # 空白响应,进入重试逻辑 pass
- 修复递归重试的返回逻辑:你当前代码里递归调用
self.get_json()时没有写return,导致异常情况下递归的结果不会被返回,最终还是会得到None。所有递归调用都要加上return。
优化后的完整代码示例
import requests import time import json class SaltyJson(): def __init__(self): self.url = "https://www.saltybet.com/state.json" self.session = requests.Session() self.headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", "Accept": "application/json", "Accept-Language": "en-US,en;q=0.9", "Referer": "https://www.saltybet.com/", "Cache-Control": "no-cache" } self.retry_count = 0 self.max_retries = 5 def get_json(self): try: self.response = self.session.get(self.url, headers=self.headers) self.response.raise_for_status() # 主动抛出4xx/5xx类错误 # 检查内容是否为空 if len(self.response.content) == 0: raise ValueError("Empty response received") # 解码并验证非空 raw_content = self.response.content.decode('utf-8').strip() if not raw_content: raise ValueError("Blank decoded content") # 解析JSON并验证结构 data = json.loads(raw_content) if len(list(data.keys())) != 8: raise ValueError("Invalid JSON structure") # 重置重试计数 self.retry_count = 0 return data except (requests.exceptions.RequestException, ValueError, json.JSONDecodeError): if self.retry_count >= self.max_retries: self.retry_count = 0 return None # 指数退避重试 delay = 2 ** self.retry_count time.sleep(delay) self.retry_count += 1 return self.get_json()
内容的提问来源于stack exchange,提问作者Brian

