一、小红书评论原理及步骤
1、评论接口
https://edith.xiaohongshu.com/api/sns/web/v2/comment/page?note_id=%s&cursor=%s
cursor起始为空。
2、展开更多回复评论接口
https://edith.xiaohongshu.com/api/sns/web/v2/comment/sub/page?note_id=%s&root_comment_id=%s&num=10&cursor=%s
3、请求数据头
headers: {
'Content-Type': 'application/json',
'Accept':'application/json',
'accept-language':"zh-CN,zh;q=0.9",
'Referer': 'https://www.xiaohongshu.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
'X-s': xs ,
'X-t': xt,
'Cookie':cookie,
}
发送https get请求,X-s为签名验证;X-t为签名验证时间戳,必须有。需要帮助的加v:byc6352。
Cookie可以从浏览器复制出来。
二、获取流程及源代码
procedure TComment.working();
var
bRet:boolean;
js,apiurl,relativeurl:string;
i:integer;
cursor,comment_id,line:string;
begin
try
Ffilename:=Fsavedir+'\'+Ftitle+'.txt'; //保存评论数据的文件名
apiurl:=format(XHS_COMMON_API,[Fnoteid,'']); //格式化请求url
relativeurl:=getrelativeurl(apiurl); //相对链接url
bRet:=getXsXt(relativeurl); //对相对链接url签名
if(bRet=false)then begin log('getXsXt=失败');exit;end;
js:=getRequestResult(apiurl,Fcookie,Fxs,Fxt,XHS_COMMON_REFER); //发送https请求接口数据
if(js='')then begin log('js=空');exit;end;
if(pos('成功',js)<=0)then
begin
log(js); //记录返回的json日志数据
exit;
end;
log(js);
parseData(js); //解析json数据,取得评论数据
while Fhasmore='true' do //如果还有更多评论,继续请求数据
begin
apiurl:=format(XHS_COMMON_API,[Fnoteid,Fcursor]); //格式化请求接口url,传入笔记id,cursor参数
relativeurl:=GetRelativeUrl(apiUrl); //相对链接签名
bRet:=getXsXt(relativeurl); //获取签名字段x-s,x-t
if(not bRet)then begin log('getXsXt=失败');continue;end;
js:=getRequestResult(apiurl,uConfig.cookie,Fxs,Fxt,XHS_COMMON_REFER); //发送https get
if(js='')then begin log('js=空');continue;end;
if(pos('成功',js)<=0)then begin log('js=失败'+js);exit;end;
log(js);
parseData(js); //解析json评论数据
end;
Fmorecomment.SaveToFile(Fsavedir+'\morecomment.txt');
for I := 0 to Fmorecomment.Count-1 do //如果有展开更多评论,继续请求更多评论接口
begin
line:=Fmorecomment[i];
comment_id:=leftstr(line,24);//root_comment_id=640b7dad000000000700ded2
cursor:=rightstr(line,24);//cursor=640bc4e4000000001500e610
apiurl:=format(XHS_MORE_COMMON_API,[Fnoteid,comment_id,cursor]);//更多评论接口
relativeurl:=GetRelativeUrl(apiUrl);
bRet:=getXsXt(relativeurl);
if(not bRet)then begin log('getXsXt=失败');continue;end;
js:=getRequestResult(apiurl,uConfig.cookie,Fxs,Fxt,XHS_COMMON_REFER);//请求更多评论接口数据
if(js='')then begin log('js=空');continue;end;
if(pos('成功',js)<=0)then begin log('js=失败'+js);exit;end;
log('morecomment='+js);
parseMoreData(js); //解析更多评论json数据
end;
finally
Fcomment.SaveToFile(Ffilename,Tencoding.UTF8); //保存完整的评论数据
SendMessage(Fform,wm_downfile,3,integer(self)); //发送完成消息
end;
end;
三、完整源代码
unit uComment;
interface
uses
windows,classes,System.Net.URLClient, System.Net.HttpClient, System.Net.HttpClientComponent,
System.SysUtils,strutils,uLog,System.RegularExpressions,uFuncs,system.JSON,uConfig,
uVideoInfo,uDownVideo,NetEncoding,ComObj,ActiveX;
const
wm_user=$0400;
wm_downfile=wm_user+100+1;
XHS_COMMON_API:string='https://edith.xiaohongshu.com/api/sns/web/v2/comment/page?note_id=%s&cursor=%s';
XHS_MORE_COMMON_API:string='https://edith.xiaohongshu.com/api/sns/web/v2/comment/sub/page?note_id=%s&root_comment_id=%s&num=10&cursor=%s';
XHS_COMMON_REFER:string='https://www.xiaohongshu.com/';
type
TComment=class(TThread)
private
FId:cardinal;
Fnoteid,Ftitle:string;
Fhasmore,Fcursor:string;
Fxs,Fxt:string;
Fsavedir,Ffilename:string;
Fcomment,FmoreComment:tstrings;
class var Fform: HWND;
class var Fcookie: string;
class procedure SetForm(const hForm: HWND); static;
class procedure SetCookie(const cookie: string); static;
procedure SetSaveDir(dir:string);
procedure parseData(data:string);
procedure parseMoreData(data:string);
function JsonExist(parent:TJSONObject;child:string):boolean;
protected
procedure Execute; override;
public
constructor Create(id:cardinal;note_id,title:string);
destructor Destroy;
property id:cardinal read FId;
class property form: HWND read Fform write SetForm;
class property cookie: string read Fcookie write SetCookie;
property savedir:string read Fsavedir write SetSaveDir;
procedure working();
function getDataFromQuery(note_id,cursor:string):string;
function getXsXt(url:string):boolean;
function GetRelativeUrl(url:string):string;
function getRequestResult(apiurl:string;Cookie:string;xs,xt,refer:string):string;
function getSubCommentCount(sub_Comment_cout:string):integer;
function getIndex(s:string;ss:tstrings):integer;
end;
implementation
//传入线程id号,笔记id,笔记标题
constructor TComment.Create(id:cardinal;note_id,title:string);
var
line:string;
begin
//inherited;
//FreeOnTerminate := True;
inherited Create(True);
FId:=id;
Fnoteid:=note_id;
Ftitle:=uFuncs.formatFilename(title);
Fhasmore:='false';
Fcomment:=tstringlist.Create;
line:='笔记ID='+note_id+' 标题:'+title;
Fcomment.Add(line);
Fmorecomment:=tstringlist.Create;
end;
destructor TComment.Destroy;
begin
inherited Destroy;
Fcomment.Free;
Fmorecomment.free;
end;
//在子线程中运行
procedure TComment.Execute;
begin
working();
end;
//主要流程
procedure TComment.working();
var
bRet:boolean;
js,apiurl,relativeurl:string;
i:integer;
cursor,comment_id,line:string;
begin
try
Ffilename:=Fsavedir+'\'+Ftitle+'.txt';
apiurl:=format(XHS_COMMON_API,[Fnoteid,'']);
relativeurl:=getrelativeurl(apiurl);
bRet:=getXsXt(relativeurl);
if(bRet=false)then begin log('getXsXt=失败');exit;end;
js:=getRequestResult(apiurl,Fcookie,Fxs,Fxt,XHS_COMMON_REFER);
if(js='')then begin log('js=空');exit;end;
if(pos('成功',js)<=0)then
begin
log(js);
exit;
end;
log(js);
parseData(js);
while Fhasmore='true' do
begin
apiurl:=format(XHS_COMMON_API,[Fnoteid,Fcursor]);
relativeurl:=GetRelativeUrl(apiUrl);
bRet:=getXsXt(relativeurl);
if(not bRet)then begin log('getXsXt=失败');continue;end;
js:=getRequestResult(apiurl,uConfig.cookie,Fxs,Fxt,XHS_COMMON_REFER);
if(js='')then begin log('js=空');continue;end;
if(pos('成功',js)<=0)then begin log('js=失败'+js);exit;end;
log(js);
parseData(js);
end;
Fmorecomment.SaveToFile(Fsavedir+'\morecomment.txt');
for I := 0 to Fmorecomment.Count-1 do
begin
line:=Fmorecomment[i];
comment_id:=leftstr(line,24);//root_comment_id=640b7dad000000000700ded2
cursor:=rightstr(line,24);//cursor=640bc4e4000000001500e610
apiurl:=format(XHS_MORE_COMMON_API,[Fnoteid,comment_id,cursor]);
relativeurl:=GetRelativeUrl(apiUrl);
bRet:=getXsXt(relativeurl);
if(not bRet)then begin log('getXsXt=失败');continue;end;
js:=getRequestResult(apiurl,uConfig.cookie,Fxs,Fxt,XHS_COMMON_REFER);
if(js='')then begin log('js=空');continue;end;
if(pos('成功',js)<=0)then begin log('js=失败'+js);exit;end;
log('morecomment='+js);
parseMoreData(js);
end;
finally
Fcomment.SaveToFile(Ffilename,Tencoding.UTF8);
SendMessage(Fform,wm_downfile,3,integer(self));
end;
end;
//获取相对链接
function TComment.GetRelativeUrl(url:string):string;
var
i:integer;
s:string;
begin
result:='';
if(url='')then exit;
i:=pos('//',url);
if(i<=0)then exit;
s:=rightstr(url,length(url)-i-2);
i:=pos('/',s);
if(i<=0)then exit;
s:=rightstr(s,length(s)-i+1);
result:=s;
end;
//根据评论id号,获取评论序号
function TComment.getIndex(s:string;ss:tstrings):integer;
var
i:integer;
line:string;
begin
result:=-1;
for i := 0 to ss.Count-1 do
begin
line:=ss[i];
if(s=line)then
begin
result:=i;
exit;
end;
end;
end;
//子评论数量
function TComment.getSubCommentCount(sub_Comment_cout:string):integer;
var
s:string;
begin
s:=sub_Comment_cout;
s:=StringReplace(s,'"','',[rfReplaceAll]);
result:=strtoint(s);
end;
//解析展开更多评论 数据
procedure TComment.parseMoreData(data:string);
var
json,j1,j2,j3,j4,j5:TJSONObject;
ja:TJSONArray;
nickname:string;
i,index:integer;
user_id,content,line:string;
comment_id:string;
ss:tstrings;
begin
try
ss:=tstringlist.Create;
json := TJSONObject.ParseJSONValue(data) as TJSONObject;
if json = nil then exit;
j1:=json.GetValue('data') as TJSONObject;
//Fhasmore:=j1.GetValue('has_more').Value;
//if(Fhasmore='false')then exit;
//Fcursor:=j1.GetValue('cursor').Value;
ja:=j1.GetValue('comments') as TJSONArray;
for I := 0 to ja.Size-1 do
begin
j2:=ja.Get(i) as TJSONObject;
content:=j2.GetValue('content').Value;
j3:=j2.GetValue('user_info') as TJSONObject;
user_id:=j3.GetValue('user_id').Value;
nickname:=j3.GetValue('nickname').Value;
//comment_id:=j2.GetValue('id').Value;
//fcomment.Add(line);
line:=' '+user_id+' '+nickname+' '+content;
ss.Add(line);
j3:=j2.GetValue('target_comment') as TJSONObject;
comment_id:=j3.GetValue('id').Value;
end;
index:=getIndex(comment_id,Fcomment);
if(index>0)then
begin
Log(ss.Text);
Fcomment.Insert(index+5,ss.Text);
end;
finally
if(json<>nil)then json.Free;
ss.Free;
end;
end;
//解析评论json数据
procedure TComment.parseData(data:string);
var
json,j1,j2,j3,j4,j5:TJSONObject;
ja,ja1,sub_comments:TJSONArray;
nickname:string;
videoType:string;
i,j,sub_comment_count_i:integer;
video:TVideoInfo;
note_id:string;
sub_comment_has_more,user_id,content,line:string;
comment_id,sub_comment_cursor,sub_comment_count:string;
begin
try
json := TJSONObject.ParseJSONValue(data) as TJSONObject;
if json = nil then exit;
j1:=json.GetValue('data') as TJSONObject;
Fhasmore:=j1.GetValue('has_more').Value;
if(Fhasmore='false')then exit;
Fcursor:=j1.GetValue('cursor').Value;
ja:=j1.GetValue('comments') as TJSONArray;
for I := 0 to ja.Size-1 do
begin
j2:=ja.Get(i) as TJSONObject;
content:=j2.GetValue('content').Value;
j3:=j2.GetValue('user_info') as TJSONObject;
user_id:=j3.GetValue('user_id').Value;
nickname:=j3.GetValue('nickname').Value;
comment_id:=j2.GetValue('id').Value;
fcomment.Add(comment_id);
line:=user_id+' '+nickname+' '+content;
fcomment.Add(line);
sub_comment_has_more:=j2.GetValue('sub_comment_has_more').Value;
if(sub_comment_has_more='true')then
begin
sub_comment_count:=j2.GetValue('sub_comment_count').Value;
sub_comment_count_i:=getSubCommentCount(sub_comment_count);
if(sub_comment_count_i>3)then
begin
sub_comment_cursor:=j2.GetValue('sub_comment_cursor').Value;
FmoreComment.Add(comment_id+' '+sub_comment_cursor);
end;
sub_comments:=j2.GetValue('sub_comments') as TJSONArray;
for j := 0 to sub_comments.size-1 do
begin
j3:=sub_comments.Get(j) as TJSONObject;
content:=j3.GetValue('content').Value;
j4:=j3.GetValue('user_info') as TJSONObject;
user_id:=j4.GetValue('user_id').Value;
nickname:=j4.GetValue('nickname').Value;
line:=' '+user_id+' '+nickname+' '+content;
fcomment.Add(line);
end;
end;
end;
finally
if(json<>nil)then json.Free;
end;
end;
//判断节点是否存在
function TComment.JsonExist(parent:TJSONObject;child:string):boolean;
var
i:integer;
keyname:string;
begin
result:=false;
if(parent=nil)then exit;
for i:=0 to parent.count-1 do
begin
keyname:=parent.Get(i).JsonString.toString;
keyname:=midstr(keyname,2,length(keyname)-2);
if(keyname=child)then
begin
result:=true;
exit;
end;
end;
end;
//发送https get 请求
function TComment.getRequestResult(apiurl:string;Cookie:string;xs,xt,refer:string):string;
var
client: TNetHTTPClient;
ss: TStringStream;
s,id:string;
AResponse:IHTTPResponse;
i:integer;
begin
result:='';
try
client := TNetHTTPClient.Create(nil);
SS := TStringStream.Create('',TEncoding.UTF8); //TEncoding.UTF8
ss.Clear;
with client do
begin
ConnectionTimeout := 30000; // 30秒
ResponseTimeout := 30000; // 30秒
AcceptCharSet := 'utf-8';
UserAgent := USER_AGENT; //1
client.AllowCookies:=true;
client.HandleRedirects:=true;
Accept:='application/json, text/plain, */*'; //'*/*'
client.ContentType:='application/json'; //2
client.AcceptLanguage:='zh-CN,zh;q=0.9';
//client.AcceptEncoding:='gzip, deflate, br';
client.CustomHeaders['Cookie'] := cookie;
client.CustomHeaders['Referer'] := refer;
client.CustomHeaders['X-s'] := xs;//签名
client.CustomHeaders['X-t'] :=xt; //签名时间戳
try
AResponse:=Get(apiurl, ss);
if(AResponse.StatusCode=200)then
result:=ss.DataString;
except
on E: Exception do
Log(e.Message);
end;
end;
finally
ss.Free;
client.Free;
end;
end;
//------------------------------------------属性方法-------------------------------------
class procedure TComment.SetForm(const hForm: HWND);
begin
Fform:=hForm;
end;
class procedure TComment.SetCookie(const cookie: string);
begin
Fcookie:=cookie;
end;
procedure TComment.SetSaveDir(dir:string);
begin
Fsavedir:=dir;
end;
end.