MENU

小红书 视频图集解析 API 实现方法

May 5, 2025 • Read: 71 • Code

API仅供学习交流使用,禁止用于商业用途、违法用途等,否则后果自负!


其实都是大差不差的,首先获取到小红书的分享链接,然后重定向到原地址,然后直接请求这个地址,获取到网页HTML,直接从中提取,这里比抖音快手还要简单一些。支持图集/视频。

具体代码如下:

async getRedirectUrl(url) {
  try {
    const response = await this.curl(url, {
      method: "GET",
      headers: this.headers,
      followRedirect: false,
    });
    return this.safeGet(response, 'headers.location', url);
  } catch (error) {
    console.error("获取重定向URL时出错:", error);
    throw error;
  }
}

async getHtml(url) {
  try {
    const response = await this.curl(url, {
      headers: this.headers,
      dataType: "text",
    });
    return this.safeGet(response, 'data', null);
  } catch (error) {
    console.error("获取网页内容失败:", error);
    return null;
  }
}

parseHtml(html) {
  const jsonMatch = html.match(
    /<script>window\.__INITIAL_STATE__=(.*?)<\/script>/
  );

  if (!jsonMatch || jsonMatch.length < 2) {
    console.error("无法找到笔记信息");
    return null;
  }

  try {
    let jsonString = jsonMatch[1].replace(/undefined/g, "null");
    const data = JSON.parse(jsonString);

    const noteId = Object.keys(
      this.safeGet(data, 'note.noteDetailMap', {})
    )[0];
    
    if (!noteId) {
      console.error("无法找到笔记ID");
      return null;
    }

    const noteData = this.safeGet(
      data, 
      `note.noteDetailMap.${noteId}.note`, 
      null
    );
    
    if (!noteData) {
      console.error("无法获取笔记数据");
      return null;
    }

    const result = {
      title: this.safeGet(noteData, 'title', ''),
      desc: this.safeGet(noteData, 'desc', ''),
      type: this.safeGet(noteData, 'type', ''),
      user: {
        nickname: this.safeGet(noteData, 'user.nickname', ''),
        avatar: this.safeGet(noteData, 'user.avatar', ''),
        userId: this.safeGet(noteData, 'user.userId', ''),
      },
      time: this.safeGet(noteData, 'time', ''),
      likes: this.safeGet(noteData, 'interactInfo.likedCount', '0'),
      comments: this.safeGet(noteData, 'interactInfo.commentCount', '0'),
      collects: this.safeGet(noteData, 'interactInfo.collectedCount', '0'),
      view_count: this.safeGet(noteData, 'interactInfo.viewCount', '0'),
      share_count: this.safeGet(noteData, 'interactInfo.shareCount', '0'),
      platform: "xiaohongshu",
    };

    if (noteData.type === "video") {
      result.video = {
        url: this.safeGet(
          noteData, 
          'video.media.stream.h264.0.masterUrl', 
          ''
        ),
        cover: this.safeGet(noteData, 'video.cover.url', ''),
      };
    } else {
      result.images = this.safeGet(noteData, 'imageList', [])
        .map((img) => ({
          url: this.safeGet(img, 'urlDefault', '') || 
               this.safeGet(img, 'url', ''),
          width: this.safeGet(img, 'width', 0),
          height: this.safeGet(img, 'height', 0),
        }));
    }

    return result;
  } catch (error) {
    console.error("解析笔记信息失败:", error);
    return null;
  }
}

// 辅助方法:将字符串解析为数字
parseNumber(value) {
  if (typeof value === "number") return value;
  if (!value) return 0;
  const num = parseInt(value.replace(/[^0-9]/g, ""));
  return isNaN(num) ? 0 : num;
}

safeGet(obj, path, defaultValue = '') {
  return path.split('.').reduce((acc, part) => {
    if (acc && typeof acc === 'object' && part in acc) {
      return acc[part];
    }
    return defaultValue;
  }, obj);
}

编译后的php版:

<!--小红书解析-->
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");

class RedBookParser {
    private $headers = [
        'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
        'Referer: https://www.xiaohongshu.com/',
        'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
    ];

    public function getRedirectUrl($url) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, true);
        curl_setopt($ch, CURLOPT_NOBODY, true);
        curl_exec($ch);
        
        $location = '';
        if (curl_getinfo($ch, CURLINFO_HTTP_CODE) === 302) {
            $location = curl_getinfo($ch, CURLINFO_REDIRECT_URL);
        }
        curl_close($ch);
        
        return $location ?: $url;
    }

    public function getHtml($url) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        
        $html = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode !== 200) {
            throw new Exception("获取网页失败,状态码:{$httpCode}");
        }
        return $html;
    }

    public function parseHtml($html) {
        if (!preg_match('/<script>window\.__INITIAL_STATE__=(.*?)<\/script>/s', $html, $matches)) {
            throw new Exception("无法找到笔记信息");
        }

        $jsonString = str_replace('undefined', 'null', $matches[1]);
        $data = json_decode($jsonString, true);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new Exception("JSON解析失败:" . json_last_error_msg());
        }

        $noteDetailMap = $this->safeGet($data, 'note.noteDetailMap', []);
        if (empty($noteDetailMap)) {
            throw new Exception("无法找到笔记ID");
        }
        reset($noteDetailMap);
        $noteId = key($noteDetailMap);

        $noteData = $this->safeGet($data, "note.noteDetailMap.{$noteId}.note", []);
        if (empty($noteData)) {
            throw new Exception("无法获取笔记数据");
        }

        $result = [
            'title' => $this->safeGet($noteData, 'title', ''),
            'desc' => $this->safeGet($noteData, 'desc', ''),
            'type' => $this->safeGet($noteData, 'type', ''),
            'user' => [
                'nickname' => $this->safeGet($noteData, 'user.nickname', ''),
                'avatar' => $this->safeGet($noteData, 'user.avatar', ''),
                'userId' => $this->safeGet($noteData, 'user.userId', ''),
            ],
            'time' => $this->safeGet($noteData, 'time', ''),
            'likes' => $this->parseNumber($this->safeGet($noteData, 'interactInfo.likedCount', '0')),
            'comments' => $this->parseNumber($this->safeGet($noteData, 'interactInfo.commentCount', '0')),
            'collects' => $this->parseNumber($this->safeGet($noteData, 'interactInfo.collectedCount', '0')),
            'view_count' => $this->parseNumber($this->safeGet($noteData, 'interactInfo.viewCount', '0')),
            'share_count' => $this->parseNumber($this->safeGet($noteData, 'interactInfo.shareCount', '0')),
            'platform' => 'xiaohongshu'
        ];

        if ($noteData['type'] === 'video') {
            $result['video'] = [
                'url' => $this->safeGet($noteData, 'video.media.stream.h264.0.masterUrl', ''),
                'cover' => $this->safeGet($noteData, 'video.cover.url', '')
            ];
        } else {
            $result['images'] = array_map(function($img) {
                return [
                    'url' => $this->safeGet($img, 'urlDefault', '') ?: $this->safeGet($img, 'url', ''),
                    'width' => $this->parseNumber($this->safeGet($img, 'width', 0)),
                    'height' => $this->parseNumber($this->safeGet($img, 'height', 0))
                ];
            }, $this->safeGet($noteData, 'imageList', []));
        }

        return $result;
    }

    private function safeGet($obj, $path, $default = '') {
        $keys = explode('.', $path);
        $current = $obj;
        
        foreach ($keys as $key) {
            if (is_array($current) && array_key_exists($key, $current)) {
                $current = $current[$key];
            } else {
                return $default;
            }
        }
        return $current ?: $default;
    }

    private function parseNumber($value) {
        if (is_numeric($value)) return (int)$value;
        if (empty($value)) return 0;
        $num = preg_replace('/[^0-9]/', '', $value);
        return is_numeric($num) ? (int)$num : 0;
    }
}

try {
    if (!isset($_GET['url'])) {
        throw new Exception("请提供分享链接参数(?url=链接)");
    }
    
    $parser = new RedBookParser();
    $redirectUrl = $parser->getRedirectUrl($_GET['url']);
    $html = $parser->getHtml($redirectUrl);
    $result = $parser->parseHtml($html);

    // 关键修改:添加JSON_UNESCAPED_SLASHES禁止斜杠转义
    echo json_encode([
        'code' => 200,
        'data' => $result
    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
    echo json_encode([
        'code' => 500,
        'message' => $e->getMessage()
    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
?>


Last Modified: May 7, 2025
Archives QR Code Tip
QR Code for this page
Tipping QR Code