技术学习,请勿用与非法用途!!!
成品图
用户作品列表接口
/api/sns/web/v1/user_posted?num=30&cursor=&user_id=642bf0850000000011022c4e&image_scenes=
http Get方式,请求头需要带上x-s x-t签名验证
笔记明细接口
/api/sns/web/v1/feed
data = {"source_note_id":"64356527000000001303282b", "image_scenes":["CRD_PRV_WEBP","CRD_WM_WEBP"]}
http Post方式,请求头需要带上x-s x-t签名验证
x-s算法部分是js+python完成
unit ApiXHS;
interface
uses
Windows, Messages, SysUtils, Classes, Forms, OverbyteIcsWSocket,
OverbyteIcsHttpProt, OverbyteIcsSuperObject, OverbyteIcsUtils, OverbyteIcsWndControl;
type
// 笔记信息
PNoteItem = ^TNoteItem;
TNoteItem = record
user_id: string; //用户id
nickname: string; //昵称
avatar: string; //头像
note_id: string;
collected_count: Integer; //收藏数
liked_count: Integer; //点赞数
comment_count: Integer; //评论数
share_count: Integer; //分享数
create_time: TDateTime; //创建时间
note_type: string; //类型 video -- 或 普通 normal
title: string; //标题
desc: string;
image_list: TStrings; //图片列表或视频封面
video_addr: string; //视频地址
tag_list: TStrings;
end;
type
// 小红书接口--仅学习交流 请勿用于非法用途
TXHS = class
private
function GetCookie: string;
procedure SetCookie(const Value: string);
protected
http: TSslHttpCli;
ssl: TSslContext;
_a1: string;
public
constructor Create;
destructor Destroy; override;
function get_xs( 算法\/: jeomoo168 ): Boolean;
function request(url, api, data: string; var json: ISuperObject; var s: string): Boolean; //请求
function note_info(p: PNoteItem; var s: string): Boolean; //笔记信息
function parse_noteId(url: string): string; //笔记链接->解析对于的笔记id
function parse_userId(url: string): string; //作者主页链接->解析对于的用户id
function user_posted(user_id: string;
var cursor, s: string; var has_more: Boolean; items: TList): Boolean; //获取作者作品列表
property cookie: string read GetCookie write SetCookie;
property a1: string read _a1 write _a1;
end;
procedure InitProfileItem(pi: PProfileItem);
procedure InitNoteItem(p: PNoteItem);
implementation
uses
ShellAPI;
procedure InitProfileItem(pi: PProfileItem);
begin
pi.user_id := '';
pi.nickname := '';
pi.avatar := '';
pi.desc := '';
pi.ipLocation := '';
pi.follows := 0;
pi.fans := 0;
pi.interaction := 0;
pi.gender := -1;
end;
procedure InitNoteItem(p: PNoteItem);
begin
if p = nil then exit;
p.user_id := '';
p.nickname := '';
p.avatar := '';
// p.note_id := '';
p.collected_count := 0;
p.liked_count := 0;
p.comment_count := 0;
p.share_count := 0;
p.create_time := Now;
p.note_type := '';
p.title := '';
p.desc := '';
p.video_addr := '';
p.image_list := TStringList.Create;
p.tag_list := TStringList.Create;
end;
//******************************************************************************
{ TXHS }
constructor TXHS.Create;
begin
inherited;
_a1 := '';
ssl := TSslContext.Create(nil);
http := TSslHttpCli.Create(nil);
http.SslContext := ssl;
http.Agent := 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188';
http.Accept := 'application/json, text/plain, */*';
http.AcceptLanguage := 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6';
http.Reference := 'https://www.xiaohongshu.com';
http.ContentTypePost := 'application/json;charset=UTF-8';
http.Timeout := 30; //默认30秒
end;
destructor TXHS.Destroy;
begin
ssl.Free;
http.Free;
inherited;
end;
function TXHS.GetCookie: string;
begin
Result := http.Cookie;
end;
procedure TXHS.SetCookie(const Value: string);
var
k: Integer;
begin
http.Cookie := Value;
k := 1;
_a1 := ParseTag(Value, 'a1=', ';', k);
end;
function TXHS.get_xs(api, data: string; var xs, xt, s: string): Boolean;
var
json: ISuperObject;
begin
Result := False;
s := '';
if a1 = '' then exit; //未设置cookie
Result := js-get-xs('x-s生成算法\/: jeomoo168', s);
try
json := SO(s);
except
json := SO('{}');
end;
xs := json.S['X-s'];
xt := json.S['X-t'];
end;
function TXHS.request(url, api, data: string; var json: ISuperObject; var s: string): Boolean;
var
xs, xt: string; // , v
buf: UTF8String;
begin
Result := False;
json := SO('{}');
s := '';
xs := '';
xt := '';
data := StringReplace(data, ' ', '', [rfReplaceAll]); //data字符串中不要有空格!!! 不然会请求失败
if api <> '' then //api不为空时 需要x-s x-t
begin
Result := get_xs(api, EscapeChars(data), xs, xt, s);
if not Result then exit;
end;
// xs:=''; xt:='';
http.URL := url + api;
http.ExtraHeaders.Clear;
//http.ExtraHeaders.Add('Accept-Encoding: gzip, deflate'); //!!! 不能加这个 加了 返回数据被压缩了
http.ExtraHeaders.Add('authority: edith.xiaohongshu.com');
http.ExtraHeaders.Add('origin: https://www.xiaohongshu.com');
http.ExtraHeaders.Add('Content-Type: application/json;charset=UTF-8');
http.ExtraHeaders.Add('x-s: ' + xs);
http.ExtraHeaders.Add('x-t: ' + xt);
//http.ExtraHeaders.Add('x-s-common: ');
http.RcvdStream := TMemoryStream.Create; // For answer
try
if data = '' then
http.Get
else
begin
//j2 := SO(data);
//v := j2.S['keyword']; //搜索关键词
//if v <> '' then
//begin
// v := utf8encode(v); //关键词utf8编码
// data := Format('{"keyword":"%s","note_type":0,"page":1,"page_size":20,"search_id":"2ci5066wl1gu6kr4q9apa","sort":"general","image_scenes":"FD_PRV_WEBP,FD_WM_WEBP"}', [v]);
//end;
data := utf8encode(data);
http.SendStream := TMemoryStream.Create;
http.SendStream.Write(PChar(data)^, Length(data));
http.SendStream.Position := 0; // Send from start!
http.Post;
end;
except
on e: Exception do
begin
s := Format('%s', [Trim(e.Message)]);
exit;
end;
end;
if http.StatusCode <> 200 then
if not SameText(http.RequestDoneErrorStr, 'No Error') then
begin
s := http.RequestDoneErrorStr;
exit;
end;
SetLength(buf, http.RcvdStream.Size);
Move((http.RcvdStream as TMemoryStream).Memory^, buf[1], http.RcvdStream.Size);
http.RcvdStream.Free;
http.RcvdStream := nil;
if http.SendStream <> nil then
begin
http.SendStream.Free;
http.SendStream := nil;
end;
s := Trim(Utf8ToStringA(buf)); //utf8解码 Utf8Decode
try
json := SO(s);
Result := json.B['success'];
except
json := SO('{}');
end;
// if Pos('Not Acceptable',s)>0 then s:=s+Format(' xs=%s',[xs]);
end;
function TXHS.note_info(p: PNoteItem; var s: string): Boolean;
var
url, api, data: string;
json, info, vi: ISuperObject;
va: TSuperArray;
i: Integer;
begin
Result := False;
s := '';
if p = nil then exit;
InitNoteItem(p);
url := 'https://edith.xiaohongshu.com';
api := '/api/sns/web/v1/feed';
data := '{"source_note_id":"' + p.note_id + '","image_scenes":["CRD_PRV_WEBP","CRD_WM_WEBP"]}';
Result := request(url, api, data, json, s); //'Not Acceptable'
if not Result then exit;
info := json.O['data'].A['items'][0].O['note_card'];
if info=nil then exit; //解析失败 有可能接口更新了
p.user_id := info.O['user'].S['user_id'];
p.nickname := info.O['user'].S['nickname'];
p.avatar := info.O['user'].S['avatar'];
p.collected_count := info.O['interact_info'].I['collected_count'];
p.comment_count := info.O['interact_info'].I['comment_count'];
p.share_count := info.O['interact_info'].I['share_count'];
p.liked_count := info.O['interact_info'].I['liked_count'];
p.desc := info.S['desc'];
p.create_time := GetTime_DateTime(info.I['time']);
p.title := info.S['title'];
p.note_type := info.S['type'];
// image_list
va := info.A['image_list'];
for i := 0 to va.Length - 1 do
begin
vi := va[i];
p.image_list.Add(vi.A['info_list'][1].S['url']);
end;
// tag_list
va := info.A['tag_list'];
for i := 0 to va.Length - 1 do
begin
vi := va[i];
p.tag_list.Add(vi.S['name']);
end;
if p.note_type = 'video' then
p.video_addr := 'https://sns-video-bd.xhscdn.com/' + info.O['video'].O['consumer'].S['origin_video_key'];
end;
// 笔记链接->解析对于的笔记id
function TXHS.parse_noteId(url: string): string;
var
s: string;
json: ISuperObject;
k: Integer;
begin
Result := '';
if url = '' then exit;
if Pos('xiaohongshu.com/explore/', url) > 0 then
begin
k := Pos('?', url);
if k > 0 then
url := Copy(url, 1, k - 1);
url := url + '/';
k := 1;
Result := ParseTag(url, '/explore/', '/', k);
exit;
end;
// http://xhslink.com/xxxxx 格式
try
http.FollowRelocation := False;
request(url, '', '', json, s);
finally
http.FollowRelocation := True;
end;
//<a href="https://www.xiaohongshu.com/discovery/item/6558d9300000000032xxxxx?app_platform=ios&app_version=8.14.3&share_from_user_hidden=true&type=normal&xhsshare=CopyLink&appuid=5beaa1f00ac0a40001f2d248&apptime=1700968043">Temporary Redirect</a>.
//或
//Redirecting to <a href="/discovery/item/6558d930000000003200698c">/discovery/item/6558d9300000000032xxxxx</a>.
k := Pos('?', s);
if k > 0 then
s := Copy(s, 1, k - 1) + '"';
k := 1;
Result := ParseTag(s, 'discovery/item/', '"', k);
end;
// 作者主页链接->解析对于的用户id
function TXHS.parse_userId(url: string): string;
var
k: Integer;
begin
// https://www.xiaohongshu.com/user/profile/5565692bb7ba2219xxxxx
// https://www.xiaohongshu.com/user/profile/5565692bb7ba221xxxxxx?xhsshare=CopyLink&appuid=5beaa1f00ac0a40001f2d248&apptime=1700990774
Result := '';
if url = '' then exit;
if Pos('xiaohongshu.com/user/profile/', url) = 0 then exit;
k := Pos('?', url);
if k > 0 then
url := Copy(url, 1, k - 1);
url := url + '/';
k := 1;
Result := ParseTag(url, '/profile/', '/', k);
exit;
end;
//获取作者作品列表
function TXHS.user_posted(user_id: string; var cursor, s: string; var has_more: Boolean; items: TList): Boolean;
var
api, data, url: string;
json, vi: ISuperObject;
va: TSuperArray;
p: PNoteItem;
i: Integer;
begin
items.Clear;
has_more := False;
url := 'https://edith.xiaohongshu.com';
api := Format('/api/sns/web/v1/user_posted?num=30&cursor=%s&user_id=%s&image_scenes=',
[cursor, user_id]);
data := '';
Result := request(url, api, data, json, s);
if not Result then exit;
if json.O['data'] = nil then exit;
cursor := json.O['data'].S['cursor'];
has_more := json.O['data'].B['has_more'];
va := json.O['data'].A['notes'];
for i := 0 to va.Length - 1 do
begin
Application.ProcessMessages;
vi := va[i];
New(p);
InitNoteItem(p);
Items.Add(p);
p.user_id := vi.O['user'].S['user_id'];
p.nickname := vi.O['user'].S['nickname'];
p.avatar := vi.O['user'].S['avatar'];
p.liked_count := vi.O['interact_info'].I['liked_count'];
p.note_id := vi.S['note_id'];
p.title := vi.S['display_title'];
p.note_type := vi.S['type'];
end;
end;
end.