别再用正则硬啃JS数据了!Python requests+json模块实战解析非标准API响应

当你用Python的requests库抓取网页时,是否经常遇到这样的尴尬场景:明明浏览器开发者工具里能看到清晰的JSON数据,但实际获取到的却是包裹在JavaScript变量中的"伪JSON"?这种数据既不是标准JSON格式,也不是常规HTML,用正则表达式处理既繁琐又脆弱。今天我们就来彻底解决这个痛点,用更优雅的方式处理这类混合数据。

1. 为什么JavaScript格式数据如此常见?

现代网页大量依赖前端渲染,数据通常以JavaScript变量形式嵌入HTML。以金融数据网站为例,实时行情、快讯等内容需要频繁更新,前端通过JavaScript动态加载是最佳实践。这种模式带来了几个典型特征:

  • 数据包裹在var data = {...}window.data = {...}这样的赋值语句中
  • 末尾常带有分号作为语句结束符
  • 键名可能不加引号(虽然这在标准JSON中不合法,但JavaScript允许)
  • 可能包含注释、函数调用等非数据内容
# 典型的混合数据示例
js_data = """
var flashData = {
    // 更新时间:2023-07-15
    news: [
        {time: "09:30", content: "美联储宣布维持利率不变"}, 
        {time: "10:15", content: "原油价格突破80美元关口"}
    ],
    updatedAt: new Date().getTime()
};
"""

2. 从正则表达式到结构化处理的思维转变

很多开发者的第一反应是写正则表达式提取大括号内的内容,比如:

import re

pattern = r"var flashData = ({.*?});"
match = re.search(pattern, js_data, re.DOTALL)
if match:
    json_str = match.group(1)

这种方法虽然能工作,但存在明显缺陷:

  • 脆弱性:对数据格式变化极其敏感,比如换行、空格、注释都会导致匹配失败
  • 维护成本:随着数据结构复杂化,正则表达式会变得难以理解和调试
  • 局限性:无法处理嵌套结构或特殊字符

更专业的做法是将问题分解为两个步骤:

  1. 定位数据边界:找到JSON部分的起始和结束位置
  2. 标准化处理:将非标准JSON转换为标准格式

3. 实战:处理金十快讯的JavaScript数据

让我们通过金十快讯的实际案例,演示如何系统性地解决这个问题。完整流程如下:

3.1 获取原始数据

首先使用requests获取网页内容:

import requests

url = "https://www.jin10.com/flash_newest.js"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
response = requests.get(url, headers=headers)
raw_data = response.text

3.2 数据预处理关键步骤

获得的数据可能如下形式:

var flash_data = {
    "news": [
        {"time": "09:30", "content": "美联储利率决议即将公布"},
        {"time": "10:15", "content": "欧洲央行维持宽松政策"}
    ]
};

处理流程:

def clean_js_data(js_string):
    # 步骤1:找到第一个{和最后一个}的位置
    start = js_string.find('{')
    end = js_string.rfind('}') + 1
    
    # 步骤2:提取JSON部分
    json_str = js_string[start:end]
    
    # 步骤3:修复常见非标准格式
    json_str = json_str.replace("'", '"')  # 单引号转双引号
    json_str = json_str.replace("True", "true")  # Python布尔值转JSON标准
    json_str = json_str.replace("False", "false")
    
    return json_str

3.3 完整解析代码

将上述步骤整合为可复用的工具函数:

import json
from typing import Any

def parse_js_object(js_string: str) -> Any:
    """解析JavaScript对象为Python数据结构
    
    Args:
        js_string: 包含JavaScript对象的字符串,如 'var data = {...};'
    
    Returns:
        解析后的Python对象(dict/list等)
    
    Raises:
        ValueError: 当无法找到有效JSON对象或解析失败时
    """
    # 定位对象边界
    start = js_string.find('{')
    end = js_string.rfind('}') + 1
    if start == -1 or end == 0:
        raise ValueError("No JavaScript object found in the string")
    
    # 提取并清理JSON字符串
    json_str = js_string[start:end]
    json_str = json_str.replace("'", '"')
    
    try:
        return json.loads(json_str)
    except json.JSONDecodeError as e:
        # 尝试修复常见的非标准JSON问题
        json_str = fix_common_json_issues(json_str)
        return json.loads(json_str)

def fix_common_json_issues(json_str: str) -> str:
    """修复常见的非标准JSON问题"""
    # 处理未加引号的键名
    json_str = re.sub(r'([{,]\s*)(\w+)(\s*:)', r'\1"\2"\3', json_str)
    # 处理JavaScript的undefined/null
    json_str = json_str.replace("undefined", "null")
    return json_str

4. 高级技巧与边界情况处理

实际项目中,你可能会遇到更复杂的情况。以下是几种常见场景的解决方案:

4.1 处理多级嵌套的JavaScript对象

当数据包含多层嵌套时,简单的字符串处理可能不够:

var config = {
    api: {
        endpoint: "https://api.example.com",
        timeout: 5000
    },
    features: {
        analytics: true,
        logging: {
            level: "debug",
            format: "json"
        }
    }
};

解决方案是使用更精确的定位方法:

def extract_nested_js_object(js_string, variable_name):
    """提取特定变量名的JavaScript对象"""
    pattern = rf"var {variable_name} = ({{.*?}});"
    match = re.search(pattern, js_string, re.DOTALL)
    if not match:
        raise ValueError(f"Variable {variable_name} not found")
    return parse_js_object(match.group(1))

4.2 性能优化:处理大型数据集

当处理MB级别的JavaScript数据时,内存效率变得重要:

def parse_large_js_file(file_path):
    """流式处理大型JavaScript数据文件"""
    with open(file_path, 'r', encoding='utf-8') as f:
        buffer = ""
        in_object = False
        brace_count = 0
        
        for line in f:
            if not in_object:
                if '{' in line:
                    start = line.find('{')
                    buffer = line[start:]
                    in_object = True
                    brace_count = 1
                continue
            
            brace_count += line.count('{')
            brace_count -= line.count('}')
            buffer += line
            
            if brace_count == 0:
                yield parse_js_object(buffer)
                in_object = False
                buffer = ""

4.3 安全考虑:防范恶意数据

处理外部数据时,安全至关重要:

def safe_parse_js_object(js_string, max_size=10*1024*1024):
    """带安全检查的JavaScript对象解析"""
    if len(js_string) > max_size:
        raise ValueError("Input size exceeds maximum allowed")
    
    # 检查潜在的危险模式
    if "eval(" in js_string or "Function(" in js_string:
        raise ValueError("Dangerous JavaScript patterns detected")
    
    return parse_js_object(js_string)

5. 工程化实践:构建可维护的爬虫系统

将上述技术整合到完整项目中时,建议采用以下架构:

financial_news_crawler/
├── crawlers/          # 爬虫实现
│   ├── base.py        # 基础爬虫类
│   └── jin10.py       # 金十快讯专用爬虫
├── parsers/           # 数据解析器
│   └── js_parser.py   # JavaScript数据解析工具
├── storage/           # 存储后端
│   ├── json_file.py   # JSON文件存储
│   └── database.py    # 数据库存储
└── pipelines/         # 数据处理流水线
    └── news.py        # 新闻处理流水线

示例金十快讯爬虫实现:

from datetime import datetime
from typing import List, Dict
from .base import BaseCrawler
from ..parsers.js_parser import parse_js_object

class Jin10FlashNewsCrawler(BaseCrawler):
    """金十快讯爬虫"""
    
    def __init__(self):
        self.api_url = "https://www.jin10.com/flash_newest.js"
        self.headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
        }
    
    def fetch(self) -> List[Dict]:
        """获取并解析快讯数据"""
        response = self._request(self.api_url)
        js_data = response.text
        
        try:
            data = parse_js_object(js_data)
            return self._transform(data.get('news', []))
        except ValueError as e:
            self.logger.error(f"Failed to parse JS data: {e}")
            return []
    
    def _transform(self, raw_news: List) -> List[Dict]:
        """数据清洗转换"""
        return [{
            "time": self._parse_time(item['time']),
            "content": item['content'].strip(),
            "source": "jin10",
            "crawled_at": datetime.now()
        } for item in raw_news]
    
    def _parse_time(self, time_str: str) -> datetime:
        """解析时间字符串"""
        today = datetime.now().date()
        return datetime.strptime(f"{today} {time_str}", "%Y-%m-%d %H:%M")

这种架构的优势在于:

  1. 关注点分离:解析逻辑与爬虫实现解耦
  2. 可扩展性:轻松添加新的数据源或存储后端
  3. 可维护性:每个组件职责单一,易于测试和修改
  4. 复用性:JavaScript解析器可用于其他项目
Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐