新闻搜索抓包与字段解析 新闻搜索请求如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 { "app" : "com.netease.newsreader.activity" , "duration" : "782ms" , "headers" : { "Host" : "gw.m.163.com" , "user-agent" : "NewsApp/116.1 Android/10 (google/Pixel XL)" , "x-nr-trace-id" : "1766026470029_264453819_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM" , "x-nr-ise" : "0" , "x-xr-original-host" : "gw.m.163.com" , "user-c" : "5pCc57Si" , "user-rc" : "UjgzLZ+E4Lemnj+sMro9qwqQ3xlDp4PUECu18073DbE1QczpsTWV4NoxlU/51iL/OFnZkytSJ8NVUttJRaK4C+5QNhz9TLXVpP2VcqembRcentYAtzPoQ3SIVcMWWlm3PBbI53PX4Gd3tY1EgH3Np+rL+kbjQ1nSgTZVI2y8+C4=" , "user-d" : "R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl" , "user-vd" : "YajLV6eMBiMvt5IKy6j+WFR1Ef3qFXuwpUC+xyv+n+Ax1J2VeRbbazQfmJ5LP9/v" , "user-appid" : "TItcOwjV9bndQ91C5VadYg==" , "user-sid" : "iw0t412hC9bMZcnwa47jlPCtJAAGDbcAXKj7U7awjJM=" , "user-lc" : "67NqtW9W02z/qXjaEOOHag==" , "user-n" : "VnE1Iqw3/SoXRqhFJu9cFg==" , "user-cn" : "8dabkxj70LEGQY+UurBODnjwStHTbMnr8pc6fFfTjog=" , "x-nr-ts" : "1766026470047" , "x-nr-sign" : "e042c3f8f8048430286cf510c479bb37" , "x-nr-net-lib" : "okhttp" , "accept-encoding" : "br,gzip" } , "method" : "GET" , "protocol" : "h2" , "remoteIp" : "36.99.227.75" , "remotePort" : 443 , "sessionId" : "b7f8b1c5-529f-4467-b7a9-fd7a124d8198" , "time" : "2025-12-18 10:54:30" , "url" : "https://gw.m.163.com/nc/api/v1/search/flow/comp?deviceId=R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl%2FHSMGl&version=116.1&channel=5pCc57Si&canal=QQ_news_yunying4&qId=&tabname=&ts=1766026469&lat=&lon=&sign=QsadZvZNwAvCpeHCxMyeEt64vy1W9GW9JiAd9WEuyvl48ErR02zJ6%2FKXOnxX046I&open=&openpath=&dtype=0&start=MA%3D%3D&limit=20&q=54Gr566t" }
响应如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 { "code" : 0 , "message" : "成功" , "data" : { "boxes" : [ 百度百科介绍以及用户推荐... ] , "tabList" : [ { "name" : "zonghe" , "label" : "综合" } , { "name" : "zhannei" , "label" : "站内" } , { "name" : "shipin" , "label" : "视频" } , { "name" : "yonghu" , "label" : "用户" } ] , "curtabname" : "zonghe" , "search_url" : "https://wp.m.163.com/163/html/frontend/newsapp-search-v1/index.html" , "tab_vertical" : "111" , "doc" : { "result" : [ 搜索结果, 根据score属性排名... ] , "total" : 586 , "pos" : -1 , "triggerTrustedSource" : false , "has_more" : 1 , "program" : "bjrec_search2v0e" , "nextCursorMark" : "sqtBC1oj+fjgXaqgR9V+1rYLCzSyGq5S2mDZlM/MM7Bjvfuh1fwiCgPeC4SIA5oj" , "qId" : "2539576465804824" } , "tab_vertical_show" : "1111" , "isGray" : false , "hideFeedback" : true } }
请求体字段解析 请求头字段
字段
值
注释
Host
gw.m.163.com
固定值
user-agent
NewsApp/116.1 Android/10 (google/Pixel XL)
用户设备信息
x-nr-trace-id
1766026470029_264453819_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM
毫秒级时间戳_8位随机数_设备标识
x-nr-ise
0
固定值
x-xr-original-host
gw.m.163.com
固定值
user-c
5pCc57Si
8位随机值,与用户行为相关
user-rc
UjgzLZ+E4Lemnj+sMro9qwqQ3xlDp4PUECu18073DbE1QczpsTWV4NoxlU/51iL/OFn ZkytSJ8NVUttJRaK4C+5QNhz9TLXVpP2VcqembRcentYAtzPoQ3SIVcMWWlm3PBbI53 PX4Gd3tY1EgH3Np+rL+kbjQ1nSgTZVI2y8+C4=
固定值,解码后是json
user-d
R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl
固定值,由设备标识加密得到
user-vd
YajLV6eMBiMvt5IKy6j+WFR1Ef3qFXuwpUC+xyv+n+Ax1J2VeRbbazQfmJ5LP9/v
固定值
user-appid
TItcOwjV9bndQ91C5VadYg==
固定值,由appid加密得到
user-sid
iw0t412hC9bMZcnwa47jlPCtJAAGDbcAXKj7U7awjJM=
同意会话中是固定值
user-lc
67NqtW9W02z/qXjaEOOHag==
固定值,由行政区代码加密得到
user-n
VnE1Iqw3/SoXRqhFJu9cFg==
由手机网络类型加密得到
user-cn
8dabkxj70LEGQY+UurBODnjwStHTbMnr8pc6fFfTjog=
固定值,由QQ_news_yunying4加密得到
x-nr-ts
1766026470047
毫秒级时间戳
x-nr-sign
e042c3f8f8048430286cf510c479bb37
当前请求的签名,由”设备标识+秒级时间戳“加密得到
x-nr-net-lib
okhttp
使用的网络库(固定值)
accept-encoding
br,gzip
固定值
sessionId
b7f8b1c5-529f-4467-b7a9-fd7a124d8198
会话id,随机值
url参数字段
字段
值
注释
deviceId
R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl
与user-d字段的值相同
version
116.1
app版本
channel
5pCc57Si
与user-c字段相同
canal
QQ_news_yunying4
固定值
qId
第一次为空,之后为响应中的qId字段值进行base64加密
tabname
第一次为空,之后为对应分类页面(zonghe, zhannei, shipin, yonghu)
ts
1766026469
秒级时间戳
lat
纬度
lon
经度
sign
QsadZvZNwAvCpeHCxMyeEt64vy1W9GW9JiAd9WEuyvl48ErR02zJ6/KXOnxX046I
请求签名
open
openpath
dtype
0
固定值
start
MA==
base64加密,第一次值为0,之后为响应中nextCursorMark的值
limit
20
最多获取的新闻条数
q
54Gr566t
搜索内容(base64加密)
参数生成逆向 **注意:很多参数都只生成一次,然后从缓冲中获取,因此很多参数的生成算法仅触发一次后不在触发,后续hook并不能捕获到信息。 **
请求头参数 x-nr-trace-id/X-NR-Trace-Id jadx搜索该字段(区别大小写)
hook 该函数后发现并没有触发!
我们可以换另一种方式定位,根据该参数值的格式,可以全局搜索 System.currentTimeMillis() + “_“,再根据是三个字段和两个”_“组合的,获取可疑函数如下
hook 这两个函数后发现都触发了(GalaxyRequest.c触发多次,util.getTraceId触发一次)
1 2 3 4 5 6 7 8 9 10 11 12 GalaxyRequest.c is called: str =82335006 GalaxyRequest.c result =1766036199783_82335006_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM GalaxyRequest.c is called: str =197139747 GalaxyRequest.c result =1766036200700_197139747_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM GalaxyRequest.c is called: str =251744591 GalaxyRequest.c result =1766036202227_251744591_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM util.getTraceId is called: requestIdentity =157192101 util.getTraceId result =1766036202526_157192101_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM GalaxyRequest.c is called: str =112736705 GalaxyRequest.c result =1766036202745_112736705_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM GalaxyRequest.c is called: str =79891345 GalaxyRequest.c result =1766036203343_79891345_OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM
同时hook和抓包,发现没有找到对应的值!?
Galaxy.N()最终调用Tools.B方法
这里是获取SharedPreferences 文件中的galaxy_pre_key_device_id的值,顾名思义就是标识设备的一串字符。
然而值得注意的是util.getTraceId最后一部分与GalaxyRequest.c最后一部分是相同的,且与我们的目标值一致,该方法如下
通过AdConfig.getDevice_id()获取
可以确认我们的目标值是device_id,具体生成方式见DeviceInfo.getDevicesId()
通过SharedPreferences存储和获取”GALAXY_PRE_KEY_DEVICE_ID”键对应的值(进入PrefHelper可知),其他大部分逻辑是生成device_id。SharedPreferences 文件的名称如下
1 public static final String PREFERENCES_PATH = "ntesepaddata" ;
具体文件路径为/data/data/com.netease.newsreader.activity/shared_prefs/ntesepaddata。
为了定该参数的生成位置,选择hook System.currentTimeMillis方法,同时进行抓包,通过请求包中改参数的毫秒级时间戳,最终打印堆栈如下
1 2 3 4 5 6 7 8 9 10 11 System.currentTimeMillis : 1766039948246 java.lang .Throwable at java.lang .System .currentTimeMillis (Native Method) at com.netease .newsreader .common .utils .sys .SystemUtilsWithCache .d0 (SystemUtilsWithCache.java :5 ) at com.netease .newsreader .common .net .BaseHttpClient$BuiltInInterceptor .intercept (BaseHttpClient.java :10 ) at okhttp3.internal .http .RealInterceptorChain .c (RealInterceptorChain.kt :12 ) at okhttp3.internal .connection .RealCall .getResponseWithInterceptorChain$okhttp (RealCall.kt :16 ) at okhttp3.internal .connection .RealCall$AsyncCall .run (RealCall.kt :6 ) at java.util .concurrent .ThreadPoolExecutor .runWorker (ThreadPoolExecutor.java :1167 ) at java.util .concurrent .ThreadPoolExecutor$Worker .run (ThreadPoolExecutor.java :641 ) at java.lang .Thread .run (Thread.java :919 )
com.netease.newsreader.common.utils.sys.SystemUtilsWithCache.d0函数如下
com.netease.newsreader.common.net.BaseHttpClient$BuiltInInterceptor.intercept函数如下
可以确认的是,x-nr-trace-id和X-NR-Trace-Id对应的值都是相同的。中间值是对象的哈希值,也就是说参数x-nr-trace-id的中间字段是随机值,后者是device_id,对我们来说是固定值,前者是毫秒级时间戳。
user-c/User-C jadx搜索user-c,定位如下
对于第一个标注处,右键查找字段f53186i的用例,定位如下
c方法如下
首先尝试获取现有的值,如果不存在,则通过URLEncoder.encode(StringUtil.e(str, "UTF-8"), "UTF-8");获取,参数str就是CommonGalaxy.o()函数的返回值。
再来看第二个标注处
同样也是通过URLEncoder.encode(StringUtil.e(o2, "UTF-8"), "UTF-8");获取,参数o2就是CommonGalaxy.o()函数的返回值。
查看StringUtil.e()方法
是一个Base64Api,尝试将结果通过base64解码,得到值”搜索“。为了二次确认,hook 该函数,然而,实际逻辑并没有触发该函数!!!
那么选择hook CommonGalaxy.o()函数,手动搜索新闻,打印的全是”搜索“二字:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 CommonGalaxy.o result=搜索 CommonGalaxy.o is called java.lang .Throwable at com.netease .newsreader .common .galaxy .CommonGalaxy .o (Native Method) at com.netease .newsreader .newarch .base .NewarchHttpUtils .e (NewarchHttpUtils.java :13 ) at com.netease .newsreader .base .net .client .NewsHttpClient$UserDataInterceptor .intercept (NewsHttpClient.java :3 ) at okhttp3.internal .http .RealInterceptorChain .c (RealInterceptorChain.kt :12 ) at com.netease .newsreader .common .net .sentry .SentryInterceptor .intercept (SentryInterceptor.java :9 ) at okhttp3.internal .http .RealInterceptorChain .c (RealInterceptorChain.kt :12 ) at com.netease .newsreader .common .net .interceptor .HostOptimizeInterceptor .intercept (HostOptimizeInterceptor.java :6 ) at okhttp3.internal .http .RealInterceptorChain .c (RealInterceptorChain.kt :12 ) at com.netease .newsreader .common .net .BaseHttpClient$BuiltInInterceptor .intercept (BaseHttpClient.java :16 ) at okhttp3.internal .http .RealInterceptorChain .c (RealInterceptorChain.kt :12 ) at okhttp3.internal .connection .RealCall .getResponseWithInterceptorChain$okhttp (RealCall.kt :16 ) at okhttp3.internal .connection .RealCall$AsyncCall .run (RealCall.kt :6 ) at java.util .concurrent .ThreadPoolExecutor .runWorker (ThreadPoolExecutor.java :1167 ) at java.util .concurrent .ThreadPoolExecutor$Worker .run (ThreadPoolExecutor.java :641 ) at java.lang .Thread .run (Thread.java :919 ) ...同上...
通过上面的堆栈函数可以知道,是执行了NewarchHttpUtils.e方法的,里面的c方法确实有StringUtil.e方法,但是c方法中调用StringUtil.e方法的前提是该值不存在!所以这就解释的通了。
另外我将https://gw.m.163.com/nc/api/v1/search/hot-word中的user-c参数也进行了base64解码
结果是”头条“二字。
综上,已经可以肯定了user-c参数的生成是根据“访问不同资源/页面的关键字”而生成的,并且只是简单的将明文进行了base64加密。
user-n/User-N 定位同user-c一样,两种方法都可行,同样也在相同位置处。通过字段查找用例定位如下
1 a (hashMap, f53185h, g ());
a方法区别于b方法如下
a方法会对值进行加密,而b方法则没有。
g方法最终调用NetUtil.i(),该方法如下
该方法主要是获取网络状态,其中networkInfo.getSubtypeName()获取的是移动网络子类型名称,常见值如下
网络
getSubtype()
getSubtypeName()
2G
NETWORK_TYPE_GPRS
"GPRS"
2G
EDGE
"EDGE"
3G
UMTS
"UMTS"
3G
HSDPA
"HSDPA"
3G
HSPA
"HSPA"
4G
LTE
"LTE"
4G+
LTE_CA
"LTE_CA"
5G
NR
"NR"
再来看看加密方法Encrypt.getEncryptedParams方法
在getEncryptedParams方法中尝试获取现有值,获取不到则调用getEncryptedParamsInner方法生成,在该方法中,主要是将callEncrypt(Core.context(), str, i2)的返回值进行base64加密。具体来看看该方法
最终调用native方法,加载的库是librandom.so。IDA加载该库后,搜索Java_com_netease_nr_biz_pc_sync_Encrypt_encrypt即可定位到对应方法。重命名第一个参数(JNIEnv指针)和第二个参数(静态方法对应jclass或实例方法对应jobject对象)。
首先执行getRandomKey方法获取RandomKey,该方法如下
主要通过i2的值选择不同的随机字符串作为密钥。根据i2=0,可以确认随机字符串为
1 neteasenewsboard (hex:6 E 65 74 65 61 73 65 6 E 65 77 73 62 6 F 61 72 64 )
然后执行doEn方法,该方法如下
加密逻辑主要是通过反射获取Java层的加密库进行AES/ECB/PKCS7Padding加密。解密如下
user-d/User-D 明文通过SystemUtilsWithCache.s()方法获取
返回值是device_id,就是x-nr-trace-id的最后一部分。
加密方法同user-n一致,解密得到的就是device_id。
user-rc/User-Rc
user-xxx系列
字段
初始值
还原后的值
备注
user-d
R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl
OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM
device_id,进一步base64解码得到9b0d317068740eca__google_Pixel XL
user-vd
YajLV6eMBiMvt5IKy6j+WFR1Ef3qFXuwpUC+xyv+n+Ax1J2VeRbbazQfmJ5LP9/v
MTc2NjAyNjA0OTQ3M18xOTI0NjE1NTVfaUkybkN3bng%3D
进一步base64解码得到1766026049473_192461555_iI2nCwnx,可能与App安装、启动等相关
user-appid
TItcOwjV9bndQ91C5VadYg==
2x1kfBk63z
user-sid
iw0t412hC9bMZcnwa47jlPCtJAAGDbcAXKj7U7awjJM=
dbhggi1766026422530(6个字符+毫秒级时间戳)
sessionID加密
user-lc
67NqtW9W02z/qXjaEOOHag==
110000
行政区划代码
user-n
VnE1Iqw3/SoXRqhFJu9cFg==
WIFI
手机网络类型
user-cn
8dabkxj70LEGQY+UurBODnjwStHTbMnr8pc6fFfTjog=
QQ_news_yunying4
channel_id
x-nr-sign jadx全局搜索该字符串,唯一定位到声明处,右键查找用例
显然第二处才是可能赋值的地方。
l方法应该是个键值添加的地方,x-nr-sign值通过a(query, currentTimeMillis)生成,
query则是通过HttpUrl.query()方法获取,该方法如下
f83693g是List<String>类型,根据函数名可以猜测是将url参数转字符串,通过hook 该方法也可以确认。
来看a方法
进一步调用StringUtils.n方法,参数为:query参数 + "gNlVGcSKf5" + 毫秒级时间戳。
该部分主要是通过g方法(getBytes()方法)将参数转成字节数组,进行md5加密,然后md5结果作为参数调用a方法。
f52113a和f52114b都是16进制字符数组,区别在于前者是小写后者是大写,根据参数z2=false可知选择小写。这部分说是加密,其实就是将md5小写化或者大写化(根据z2决定)。
Frida hook query和 a方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function main ( ){ Java .perform ( function ( ) { let HttpUrl = Java .use ("okhttp3.HttpUrl" ); HttpUrl ["query" ].implementation = function ( ) { console .log (`HttpUrl.query is called` ); var f83693g = this ["_g" ].value ; if (f83693g !== null ) { console .log (`HttpUrl._g: ${Java.use("java.util.ArrayList" ).$new(f83693g).toString()} ` ); } let result = this ["query" ](); console .log (`HttpUrl.query result: ${result} ` ); return result; }; let RequestSignInterceptor = Java .use ("com.netease.newsreader.common.net.interceptor.RequestSignInterceptor" ); RequestSignInterceptor ["a" ].implementation = function (str, j2 ) { console .log (`RequestSignInterceptor.a is called: str: ${str} , j2: ${j2} ` ); let result = this ["a" ](str, j2); console .log (`RequestSignInterceptor.a result: ${result} ` ); return result; }; } ); }
再结合抓包可以确认x-nr-sign参数生成部分如下
1 2 3 4 5 HttpUrl.query is called HttpUrl._g: [deviceId, R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl, version, 116.1 , channel, 5 pCc57Si, canal, QQ_news_yunying4, qId, OTEzMzAzNTc3MTMwNTE5, tabname, , ts, 1766121716 , lat, , lon, , sign, 4 w5LCmZUAXCNfX2GsDH7Cf2tDyi2gMCymYF+Vi33mN548ErR02zJ6/KXOnxX046I, open, , openpath, , dtype, 0 , start, MA==, limit, 20 , q, 57u d5a+56 Zu25bqm] HttpUrl.query result: deviceId=R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl&version =116.1 &channel =5 pCc57Si&canal =QQ_news_yunying4& qId=OTEzMzAzNTc3MTMwNTE5&tabname =&ts =1766121716 &lat =&lon =&sign =4 w5LCmZUAXCNfX2GsDH7Cf2tDyi2gMCymYF+Vi33mN548ErR02zJ6/KXOnxX046I&open =&openpath =&dtype =0 &start =MA==&limit =20 &q =57u d5a+56 Zu25bqm RequestSignInterceptor.a is called: str: deviceId=R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl&version =116.1 &channel =5 pCc57Si&canal =QQ_news_yunying4& qId=OTEzMzAzNTc3MTMwNTE5&tabname =&ts =1766121716 &lat =&lon =&sign =4 w5LCmZUAXCNfX2GsDH7Cf2tDyi2gMCymYF+Vi33mN548ErR02zJ6/KXOnxX046I&open =&openpath =&dtype =0 &start =MA==&limit =20 &q =57u d5a+56 Zu25bqm, j2: 1766121716635 RequestSignInterceptor.a result: 3e53 cc88982ae5d775adbcc07599ec8d
最终可以确认x-nr-sign参数生成过程为:**md5(URL参数字符串 + “gNlVGcSKf5” + 毫秒级时间戳)**。
URL参数 sign jadx全局搜索”flow/comp"
并判断是第二处,代码如下
搜索f43446c用例,定位代码如下
这里是URL组装,里面的UserReward.f42460J即为目标值sign,与deviceId值生成相比,不同的地方是获取device_id+秒级时间戳,调用StringUtils.n()进行md5加密
最后都是AES/ECB/PKCS7Padding加密再base64加密。
user-sid jadx搜索不到,但通过抓包可以发现
1 2 3 4 5 6 // 在搜索结果展示页面往下滑,触发更多请求 wKtraxGUri0o2WvFr1f1trY48uFm1UfUst8IJ9NXdFg= wKtraxGUri0o2WvFr1f1trY48uFm1UfUst8IJ9NXdFg=// 与上述不同会话的请求 iw0t412hC9bMZcnwa47jlPCtJAAGDbcAXKj7U7awjJM=3 smlZlQvTuyjzD6RUzrQBnn+ItIQn+cpmZB7ehGoIZg=
结合名字可以确认是会话级别参数,在同一会话中保持固定值。这些值通过同user-n的解密过程得到
1 2 3 aagiga1766122722452 dbhggi1766026422530 fdbgda1766133429518
可以确定明文是由”6个随机小写字母+毫秒级时间戳”组成,然后先后通过AES/ECB/PKCS7Padding加密和base64加密得到user-sid。
qId/start 同样还是在搜索结果展示页面往下滑,触发更多请求。
可以发现响应包中存在如下字段
1 2 "nextCursorMark" : "EUp6sZc6vOpW8qCe5ak2sfVkg0ziiOg29UyOGG9+AfnN2x7NHdzUOXnTxju3ZcVD" , "qId" : "7051912003775192"
结合之后的请求URL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 https: deviceId=R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl%2 FHSMGl version=116.1 channel=5 pCc57Si canal=QQ_news_yunying4 qId=NzA1MTkxMjAwMzc3NTE5Mg%3 D%3 D tabname=zonghe ts=1766122748 lat= lon= sign=eQOXxTy7Dk8ANH5HvzniLBiX%2 Fvh070Nb2LYh8lZn3H148ErR02zJ6%2 FKXOnxX046I open= openpath= dtype=0 start=RVVwNnNaYzZ2T3BXOHFDZTVhazJzZlZrZzB6aWlPZzI5VXlPR0c5K0Fmbk4yeDdOSGR6VU9YblR4anUzWmNWRA%3 D%3 D limit=20 q=54 Gr566t
将其中qId进行base64解码就是上一次响应包中的qId的值。
将start进行base64解码就是上一次响应包中的nextCursorMark的值。
请求脚本 循环请求脚本如下,可持续搜索同一关键字的相关新闻。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 import hashlibimport jsonimport randomimport timeimport requestsfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import padfrom base64 import b64encode, b64decodefrom urllib.parse import urlencodefrom string import ascii_lowercasedef AESEncrypt (plain: str , key: str , mode=AES.MODE_ECB ) -> bytes : plain_bytes = plain.encode("UTF-8" ) key_bytes = key.encode("UTF-8" ) ase = AES.new(key_bytes, mode) padded_data = pad(plain_bytes, AES.block_size) cipher = ase.encrypt(padded_data) return cipherdef base64Encode (plain ) -> str : if type (plain) != bytes : plain = plain.encode() return b64encode(plain).decode()def constructReqParams (qId, tabName, sign, start, queryWord ): reqParams = { "deviceId" : "R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl" , "version" : 116.1 , "channel" : "5pCc57Si" , "canal" : "QQ_news_yunying4" , "qId" : qId, "tabname" : tabName, "ts" : int (time.time()), "lat" : "" , "lon" : "" , "sign" : sign, "open" : "" , "openpath" : "" , "dtype" : 0 , "start" : start, "limit" : 20 , "q" : queryWord } return reqParamsdef constructReqHeaders (xNrTraceId, xNrSign, userSid ): reqHeaders = { "Host" : "gw.m.163.com" , "user-agent" : "NewsApp/116.1 Android/10 (google/Pixel XL)" , "x-nr-trace-id" : xNrTraceId, "x-nr-ise" : "0" , "x-xr-original-host" : "gw.m.163.com" , "user-c" : "5pCc57Si" , "user-rc" : "UjgzLZ+E4Lemnj+sMro9qwqQ3xlDp4PUECu18073DbE1QczpsTWV4NoxlU/51iL/OFnZkytSJ8NVUttJRaK4C+5QNhz9TLXVpP2VcqembRcentYAtzPoQ3SIVcMWWlm3PBbI53PX4Gd3tY1EgH3Np+rL+kbjQ1nSgTZVI2y8+C4\u003d" , "user-d" : "R5UTRhOqUODMPloPL2CjBANkzPltnVQlhjKXCEeQMqkMVHVe2Gn4XG4lUl/HSMGl" , "user-vd" : "YajLV6eMBiMvt5IKy6j+WFR1Ef3qFXuwpUC+xyv+n+Ax1J2VeRbbazQfmJ5LP9/v" , "user-appid" : "TItcOwjV9bndQ91C5VadYg\u003d\u003d" , "user-sid" : userSid, "user-lc" : "67NqtW9W02z/qXjaEOOHag\u003d\u003d" , "user-n" : "VnE1Iqw3/SoXRqhFJu9cFg\u003d\u003d" , "user-cn" : "8dabkxj70LEGQY+UurBODnjwStHTbMnr8pc6fFfTjog\u003d" , "x-nr-ts" : str (int (time.time() * 1000 )), "x-nr-sign" : xNrSign, "x-nr-net-lib" : "okhttp" , "accept-encoding" : "br,gzip" } return reqHeadersdef hashCode (): max = 0x400000 return random.randint(0 , max )def generateXNrTraceId (): return str (int (time.time() * 1000 )) + "_" + str (hashCode()) + "_" + "OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM" def generateXNrSign (url ): plain = url + "gNlVGcSKf5" + str (int (time.time() * 1000 )) XNrSign = hashlib.md5(plain.encode("UTF-8" )).hexdigest() return XNrSigndef generateSign (): plain = "OWIwZDMxNzA2ODc0MGVjYV9fZ29vZ2xlX1BpeGVsIFhM" + str (int (time.time())) md5Result = hashlib.md5(plain.encode("UTF-8" )).hexdigest() sign = base64Encode(AESEncrypt(md5Result, "neteasenewsboard" )) return signdef generateUserSid (): plain = "" .join(random.choice(ascii_lowercase) for _ in range (6 )) + str (int (time.time() * 1000 )) userSid = base64Encode(AESEncrypt(plain,"neteasenewsboard" )) return userSiddef constructRequest (queryWord, tabeName, nextCursorMark, qId, userSid ): url = "https://gw.m.163.com/nc/api/v1/search/flow/comp" reqParams = constructReqParams(base64Encode(qId), tabeName, generateSign(), base64Encode(nextCursorMark), base64Encode(queryWord)) reqHeaders = constructReqHeaders(generateXNrTraceId(), generateXNrSign(urlencode(reqParams)), userSid) req = requests.Request(method="GET" , url=url, headers=reqHeaders, params=reqParams) return reqdef search (keyword ): proxies = { "http" : "127.0.0.1:7890" , "https" : "127.0.0.1:7890" } session = requests.Session() userSid = generateUserSid() tabeName = "" nextCursorMark = "0" qId = "" print (f"[*] 正在进行关键词: {keyword} 的相关新闻搜索..." ) while 1 : time.sleep(round (random.uniform(3 , 5 ), 2 )) req = constructRequest(keyword, tabeName, nextCursorMark, qId, userSid) prepared = session.prepare_request(req) response = session.send(prepared, proxies=proxies) if response.status_code == 200 : rspResult = response.json() print (f"[*] 搜索结果如下:\n{rspResult} " ) data = rspResult["data" ] tabeName = data["curtabname" ] news = data["doc" ]["result" ] hasMore = data["doc" ]["has_more" ] nextCursorMark = data["doc" ]["nextCursorMark" ] qId = data["doc" ]["qId" ] if hasMore != 1 : print (f"[*] 关键词: {keyword} 相关新闻已搜索完毕!请尝试更换其他关键词进行新闻搜索" ) break else : raise Exception("网络请响应异常" )if __name__ == "__main__" : search("火箭" )