Node服务内存泄漏问题排查一文中,我们介绍了排查内存泄漏的方法。其实最初我教道友的并不是这个方法,所以道友没有找到内存泄漏原因,后来道友把他的代码发我排查之后,给他找到了解决办法。

后来为了写Node服务内存泄漏问题排查一文,更深入的研究了下这个内存泄漏,才知道主要是看 Retained Size (指的是一个对象被垃圾回收后,能实际释放的内存量),从上往下找就行,简单易懂。所以还是得多写文章才行啊。

写完Node服务内存泄漏问题排查一文后,用新的方法,一下子就找到了是console内存泄漏了。困惑的是最右边Retained Size对不上,没搞明白。

至于解决的办法也很简单,加上 global.console = undefined 就行了,代码如下

1
2
3
4
5
6
7
app.post("/generateCookies", (req, res) => {
let url = req.body.url;
let html = req.body.html;
let cookies = sdk.generateCookies(url, html);
global.console = undefined
res.json({ code: 0,'data': {'cookies': cookies}})
}

问题是为啥 console 会引发内存泄漏?看了代码后,可以知道它重定义了 console 里的方法,像log, info这些方法。对于这个问题有两种解决办法,一种是把这些重定义代码删掉,如果重定义代码在控制流或者vmp里,就不好找到重定义代码并删除。

另一种是不让它重定义这些方法。而不让它重定义这些方法,就可以用到Object.freeze 在JavaScript逆向时的妙用一文中介绍的 Object.freeze 方法。把console给冻结了,不让它修改,也就不会有内存泄漏。最终代码如下

1
2
3
4
5
6
7
Object.freeze(console)
app.post("/generateCookies", (req, res) => {
let url = req.body.url;
let html = req.body.html;
let cookies = sdk.generateCookies(url, html);
res.json({ code: 0,'data': {'cookies': cookies}})
}

后来发现,这个禁用console 是JavaScript Obfuscator 里的逻辑,剥离业务代码后,将测试代码放在https://github.com/dengshilong/js_reverse/tree/main/disable_console 这里,有兴趣的话可以去测试下 console 引发的内存泄漏。

联系作者

在使用Express搭建Node服务时,有一个非常值得关注的问题,那就是内存泄漏。内存泄漏会导致服务越来越慢,直至服务崩溃。

最近有个道友反馈,他的服务会内存泄漏,我的服务和他的功能一样,那么也一样会内存泄漏。于是我在Express服务里再加一个接口,用于dump出服务的内存,主要就是使用v8模块的writeHeapSnapshot函数,代码如下。

1
2
3
4
5
6
7
8
9
10
const v8 = require("v8")
app.get("/dump", (req, res) => {
try {
const fileName = v8.writeHeapSnapshot();
console_log(`Heap snapshot written to: ${fileName}`);
res.status(200).send(`Heap snapshot written);
} catch (err) {
res.status(500).send("Internal Server Error");
}
});

接下来就是请求服务接口,等它内存泄漏之后,调用这个dump内存接口,生成如Heap.20250710.170608.4721.0.001.heapsnapshot 这种文件。之后打开Chrome 开发者调试工具,在Memory里, 导入内存镜像,开始分析内存泄漏原因。

我们可以看到Retained Size(指的是一个对象被垃圾回收后,能实际释放的内存量) 几乎都在global里, 于是重点排查这里。

点开global这里,我们能看到,Retained Size都集中在fetch这个变量,于是我们在代码的最后添加global.fetch = undefined 来释放内存,最终代码如下。之后重启服务,继续测试,内存泄漏问题不再出现, 收工。

1
2
3
4
5
6
7
8
9
10
11
12
13
function executeJs(code, cookies, initParam) {
// 拼接新的 JS 代码
const newHeadJs = headJs + ";;;\n" + "window.param=" + JSON.stringify(initParam) + ";;;;\n" + code;
// 执行拼接的 JS 代码
eval(newHeadJs);
// 构造返回结果
const result = {
cookies: utils.changeToCookies(window.document.cookie)
};
// 清除全局 fetch
global.fetch = undefined;
return result;
}

联系作者

声明: 本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码。抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请在公众号 【静夜随想】 联系作者立即删除!

在看了土木佬的 某数反爬方案讨论 后,也想搞一个浏览器渲染的方案。之前同事研究cef的时候,就想让他搞一个,但他没继续研究了,这次正好补上。

https://github.com/dengshilong/browser_server/blob/main/node_playwright_server/simple_server.js 代码的基础上,继续问AI,

Playwright的配置参数里,有解决自动化检测的参数吗?AI又哼哧哼哧给了一大堆建议,其中建议有以下一条,加上之后进行测试。

1
2
3
4
5
6
7
browser = playwright.chromium.launch(
args=[
"--disable-blink-features=AutomationControlled", # 关键参数,隐藏自动化控制标识
"--no-sandbox",
"--disable-setuid-sandbox"
]
)

会遇到获取cookies时还未生成的情况,于是又让AI加上等待cookies不为空之后再返回结果功能,之后就可以测试了。

先试了加速乐,搞定。再试了下vmp + wasm里生成的某乎__zse_ck,搞定。再试了下某数,也搞定。

浏览器用来补环境是真舒服了, 就是生成速度比纯js补环境慢一些。

联系作者

声明: 本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码。抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请在公众号 【静夜随想】 联系作者立即删除!

用Playwright和Flask写了个浏览器渲染服务后,测试下来会有内存泄漏的现象,同时也发现它最终是调用 Node 的 Playwright库来实现浏览器渲染。那么我还不如直接用Node 的 Playwright 库来写一个渲染服务,这样应该速度会更快,也可能不会有内存泄漏。

于是又让AI吭哧吭哧的写了一段代码,代码如下

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
const express = require('express');
const bodyParser = require('body-parser');
const {
chromium
} = require('playwright');
// 初始化 Express 应用
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(bodyParser.json({
limit: '100mb'
}));
app.use(bodyParser.urlencoded({
limit: '100mb',
extended: true
}));
// 保持一个持久的浏览器实例以提高性能
let browser;
// 初始化 playwright 浏览器
async function initializeBrowser() {
try {
browser = await chromium.launch({
headless: true,
args: [
'--no-sandbox',
]
});
console.log('playwright browser initialized successfully');
} catch (error) {
console.error('Failed to initialize playwright browser:', error);
throw error;
}
}
// 创建延迟函数 - 强制等待指定毫秒数
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 将cookies数组转换为字典(键值对)
function convertCookiesToDict(cookies) {
const cookieDict = {};
if (Array.isArray(cookies)) {
cookies.forEach(cookie => {
// 使用cookie的name作为键,value作为值
if (cookie.name && cookie.value !== undefined) {
cookieDict[cookie.name] = cookie.value;
}
});
}
return cookieDict;
}
app.post('/get-cookies', async (req, res) => {
const {
url,
html,
user_agent
} = req.body;
if (!url || !html) {
return res.status(400).json({
error: '请同时提供url和html参数'
});
}
let page;
try {
// 配置页面选项,包括可选的userAgent
const pageOptions = {};
if (user_agent) {
pageOptions.userAgent = user_agent;
console.log(`使用自定义User-Agent: ${user_agent}`);
}
// 使用配置选项创建新页面
page = await browser.newPage(pageOptions);
// 设置路由拦截 - 拦截所有请求
await page.route('**/*', async (route, request) => {
// 获取当前请求的URL
const requestUrl = request.url();
// 检查当前请求是否匹配目标URL
if (requestUrl == url) {
// 对目标URL返回自定义HTML
await route.fulfill({
status: 200,
content_type: 'text/html',
body: html
});
console.log(`已处理目标URL请求: ${requestUrl}`);
} else {
// 其他所有请求都直接终止
await route.abort('aborted');
console.log(`已终止非目标URL请求: ${requestUrl}`);
}
});
// 导航到目标URL,此时会被我们的路由拦截处理
await page.goto(url, {
timeout: 30000
});
// 获取并返回Cookie
let cookies = await page.context().cookies();
cookies = convertCookiesToDict(cookies);
res.json({
success: true,
cookies: cookies,
});
} catch (error) {
console.error('处理请求时出错:', error);
res.status(500).json({
success: false,
error: error.message
});
} finally {
if (page) {
await page.close();
}
}
});
// 启动服务器并初始化浏览器
async function startServer() {
try {
await initializeBrowser();
app.listen(PORT, () => {
console.log(`JavaScript execution service running on port ${PORT}`);
console.log(`API endpoints:`);
console.log(`- POST /get-cookies: Execute JavaScript code`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
// 处理进程退出,确保浏览器正确关闭
process.on('SIGINT', async () => {
console.log('Shutting down...');
if (browser) {
await browser.close();
}
process.exit(0);
});
// 启动服务
startServer();

代码都放在Github仓库https://github.com/dengshilong/browser_server里了,有兴趣的可以看看。

联系作者

声明: 本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码。抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请在公众号 【静夜随想】 联系作者立即删除!
最近对浏览器自动化热情越来越高,于是想用浏览器来取代之前的补环境方案。借助越来越强的AI能力,写写提示词,让它写了一个渲染服务。

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
import json
import time
from playwright.sync_api import sync_playwright
from flask import Flask, jsonify, request
app = Flask(__name__)
playwright = sync_playwright().start()
browser = playwright.chromium.launch(
headless=True,
args=[
"--no-sandbox",
])
def generate_cookies(url, html, user_agent=None):
default_user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'
if user_agent:
default_user_agent = user_agent
context = browser.new_context(
user_agent=default_user_agent,
)
page = context.new_page()
def handle_request(route, request):
print('hererer ', request.url, url)
if url in request.url:
# 返回自定义的 HTML 内容
route.fulfill(status=200, content_type='text/html', body=html)
else:
route.abort()
page.route("**/*", handler=handle_request)
page.goto(url)
# time.sleep(1)
cookies = context.cookies()
result = {}
for item in cookies:
result[item['name']] = item['value']
# 关闭页面
page.close()
return result
@app.route("/get-cookies", methods=["POST"])
def get_cookies():
data = request.json
url = data.get('url', '')
html = data.get('html', '')
user_agent = data.get('user_agent', '')
cookies = generate_cookies(url, html, user_agent=user_agent)
result = {}
result['cookies'] = cookies
return jsonify(result)
if __name__ == "__main__":
app.run(host="0.0.0.0", threaded=False, processes=1, port=3000, debug=False)

测试代码

1
2
3
4
5
6
7
8
9
10
import requests
headers = {
'Content-Type': 'application/json',
}
json_data = {
'url': 'https://www.baidu.com',
'html': '<html><head><title>Test Page</title></head><body><script>document.cookie = "test=123; path=/"; document.cookie = "user=testuser; path=/";</script></body></html>',
}
response = requests.post('http://localhost:3000/get-cookies', headers=headers, json=json_data)
print(response.status_code, response.text)

简简单单,有点意思。当然要运用到生产环境中,还需要解决很多问题,比如内存泄漏,性能等等。
代码都放在Github仓库https://github.com/dengshilong/browser_server里了,有兴趣的可以看看。

联系作者

前段时间,为了排查问题,在Hive表里各种查询,最后发现数据清洗真是太重要了,数据不给你清洗好,爬虫采集的再好也是白搭。

我们知道在数据仓库体系中一般有ODS、DWD、DWS 和 ADS 四层。数据清洗的时候,会先把爬虫采集的数据先存到ODS,之后经过处理到DWD,DWS,ADS,这其中每一步出问题都有可能导致数据丢失。一般情况,存入ODS不会出问题,因为这一步没有做什么特别操作,就把数据从HDFS文件写到ODS表中。

清洗容易挖坑的情况是表设计不好,比如ODS里会有来自很多个地方的数据,不给你加上数据来源字段,排查问题就很麻烦。比如不记录数据采集时间,也会造成排查问题麻烦。

更坑的是ODS到DWD,DWD到DWS或者DWS到ADS,因为清洗规则写的太复杂,把数据给清洗丢了,可能丢的不多,但长时间累积就很多了,正常应该保证一条都不丢。

还有一些很细的清洗情况。比如一个数值字段,之前都是正常阿拉伯计数,如1400000,后面突然改成科学计数法,如1.4E6,如果刚开始清洗时没考虑到这个情况,会导致这个字段清洗出错,此时得加一些告警才能发现问题。

还有一些情况是一条记录的发布时间字段,刚开始是有的,后面发布时间字段又变成空了,此时应该保留原来的发布时间。

数据清洗是个精细活,得慢慢来。

联系作者

有段时间要处理验证码,要训练验证码得先把验证码图片下载下来,但是下载图片有加密参数,加密接口还没有逆向,不知道得搞多久。于是就学习怎么使用Selenium, 对着文档写好了一个下载脚本,不用管任何加密参数,又快又稳,舒舒服服。

有个网站需要注册账号,逆向注册接口一直不知道错在哪里,后来用自动化DrissionPage写了个注册账号的脚本,跑的稳稳的。

之前遇到滑块,看加密参数,搞了好久搞不定,结果用自动化,搞一下轨迹就过了。

刚入行的时候,看不上自动化,因为速度慢,但在一些量小的场景下,自动化确实是又快又稳的方案。要知道技术只是手段,业务方可不会管你是怎么做爬虫的,只要你能满足他的需求就行。自动化,补环境,扣算法都只是技术手段,在满足需求的情况下,没有高低之分。

此时想起了爬虫行业金句,嘲讽金角,理解金角,成为金角,超越金角。

联系作者

从业以来第一次遇到站点无法使用快捷键打开开发者工具,猜测是监听了键盘事件,于是只好在浏览器设置里点击打开

发现打开开发者工具后,会跳转about:blank空白页面,于是把网页代码下载到本地,之后在代码里搜索about:blank, 发现如下可疑代码

把window.location.href=”about:blank”删除之后,用Charles等抓包工具里的Map Local(映射本地文件)进行文件替换。此时还是无法使用快捷键打开开发者工具,但依然通过浏览器设置打开开发者工具,好在页面不再跳转空白页。

此时console日志一直在打印,跳到日志输出的代码文件,大概看了下反调试逻辑,用了很多方案,窗口大小,时间等等。在代码文件里找到一个链接 https://theajack.github.io/disable-devtool/404.html,于是我们知道这个反调试是用的https://github.com/theajack/disable-devtool 这个库。

查看这个库的源码,我们可以知道它是通过监听keydown事件,屏蔽了打开开发者工具的快捷键。代码片段如下

1
2
3
4
5
6
7
8
9
10
11
target.addEventListener('keydown', (e) => {
e = e || target.event;
const keyCode = e.keyCode || e.which;
if (
keyCode === KEY.F12 || // 禁用f12
isOpenDevToolKey(e, keyCode) || // 禁用 ctrl + shift + i
isViewSourceCodeKey(e, keyCode) // 禁用 ctrl + u 和 ctrl + s 查看和保存源码
) {
return preventEvent(target, e);
}
}, true);

之后看了下初始化代码,找到绕过的办法。只要不让它执行initInterval,disableKeyAndMenu,initDetectors这几个函数即可,直接设置disableDevtool.isRunning = true即可。 网站代码经过混淆,但没有高度混淆,就webpack打包了下,在对应的代码里找到isRunning变量,把它设置为true即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (disableDevtool.isRunning) return r('already running');
initIS(); // ! 首先初始化env
initLogs(); // 然后初始化log
mergeConfig(opts);
// 被 token 绕过 或者
if (checkTk()) return r('token passed');
// 开启了保护seo 并且 是seobot
if ((config.seo && IS.seoBot)) return r('seobot');
disableDevtool.isRunning = true;
initInterval(disableDevtool);
disableKeyAndMenu(disableDevtool);
initDetectors();
return r();

最后发现disable-devtool这个库挺多star的, 作者是国内开发者,是个精力旺盛的开发,作者还给了一个测试网站

https://theajack.github.io/disable-devtool/,有兴趣的可以试试。

联系作者

声明: 本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码。抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请在公众号 【静夜随想】 联系作者立即删除!

之前就知道这个 https://www.52pojie.cn/forum.php?mod=viewthread&tid=2012413,但一直没去试。正好周末闲着没事,于是研究了下这个补环境方案。

这个方案没有加 window.ActiveXObject=undefined 这个代码,也就没有走IE那一套,而是直接按照 Chrome 硬补,form 表单校验,document.all 校验这些都给补了,这就很好。而它没有用vm2 创建沙盒环境,直接在 Node 上补,这就减少了创建沙盒环境的时间,在速度上也就有了保证。

虽然这个环境没有严格的 DOM 树操作,但它的确能跑通,而能跑通对于绝大多数爬虫来说就足够了。

将它进行了改造,改成Express服务,发现还是挺通用的,测试了几个普通站点能过,但校验严格的站点还是过不去。知足了,毕竟免费的,还想怎样。

联系作者

某数反爬方案调研听闻sdenv被反爬 里,我们都提到了sdenv, 它就是一个在 jsdom 上魔改的某数补环境方案。而在听闻sdenv被反爬里,也提到过它的很多环境设置还是有问题,那么如果要魔改sdenv又要如何操作呢?有道友问了这个问题,这里记录一下。

sdenv 执行的时候有一行代码很关键,那就是 browser(window, ‘chrome’) 这行,加上这行它会去加载一个写好的 chrome 环境代码,而这些代码就在browser/chrome目录里。所以我们要定制sdenv, 也可以在这里增加代码。

举个简单例子来说,sdenv 在执行window.document.toString()时返回的结果是[object Document]而不是 [object HTMLDocument],有什么办法修改它的这个toString吗?其实只要修改browser/chrome目录里的document.js文件就好, 给它加上如下两行代码,之后window.document.toString() 就会发生变化了。

1
2
3
4
window.document.toString = function toString() {
return '[object HTMLDocument]'
}
sdenv.tools.setFuncNative(window.document.toString)

联系作者