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

用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)

联系作者

在 JavaScript 逆向时,有时候反爬代码会对console进行更改,导致使用 console.log 输出时没有结果,如下面的例子

1
2
3
console.log = function log(t) {
}
console.log('ssss')

这时我们可以使用 Object.freeze 方法把 console 冻结了,不让它更改,这样就会有输出了,例子如下

1
2
3
4
Object.freeze(console)
console.log = function log(t) {
}
console.log('ssss')

在MDN文档里可以看到,Object.freeze() 静态方法可以使一个对象被冻结。冻结对象可以防止扩展,并使现有的属性不可写入和不可配置。被冻结的对象不能再被更改:不能添加新的属性,不能移除现有的属性,不能更改它们的可枚举性、可配置性、可写性或值,对象的原型也不能被重新指定。freeze() 返回与传入的对象相同的对象。

当然反爬也可以使用 Object.isFrozen() 方法查看 console 是否被冻结, 发现被冻结了,就知道大概率是爬虫了,因为正常用户谁会好端端的冻结 console 呢?

参考资料:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze

联系作者

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

目标站点: aHR0cHM6Ly9kcHB0LmJlaWppbmcuY2hpbmF0YXguZ292LmNuOjg0NDMvZGlnaXRhbC10YXgtYWNjb3VudA==, 目标参数为请求头中的 lzkqow23819 参数和 请求url 中 6eMrZlPH 参数。

很早之前就听过盾山加密,说是要逆向出算法很有挑战,补环境则相对简单些,于是一直没有尝试。今年抽空试了下补环境,以备不时之需。

之前因为在补环境框架跑的太慢了,于是好久没搞补环境框架了,现在都直接用Node 补了。补的过程中就会发现这个补环境确实相对容易,没有各种花里胡哨的检测,就缺啥补啥完事。唯一需要注意的一点是 typeof 检测,typeof 没法用代理捕获到,得看代码分析了才知道。这个反爬缺啥补啥后就能直接跑起来,没有一点坏心思。

我对反爬的一些理解有提到过,补环境是可以无视混淆的,也不管算法。算法加密再厉害,混淆的再花里胡哨,反爬代码对环境的检测强度不够,一样是无效的。

联系作者

道友遇到一个无壳APP,有个加密token不知道怎么生成的,jadx反编译之后也搜索不到参数,于是请教我怎么搞。猜测有可能是在JavaScript里,因为之前遇到过一个Weex开发的APP,也是怎么搜索都找不到加密参数,最后在JavaScript里找到了。

那次Weex开发的APP,找了老半天都不知道咋回事,后面是通过hook string还是hook url的办法,发现它会去下载一个JavaScript脚本,也就是 app-service.js,在这个脚本里才找到了加密参数。

于是道友就去找JavaScript代码,在反编译的apk里的assets目录里,找到了一个 uniapp 开发的app-service.js,在代码里找到了相关接口,但还是找不到相关的加密参数。于是道友又困惑了,只好来请教我。

此时我才知道这是使用 uniapp 开发的APP,于是去应用宝里下载了一个apk, 安装后发现它提示APP需要更新,点击更新后,APP又去下载了一些东西。拿着apk 里的 app-service.js 看了之后,接口确实都有,怎么会没有参数呢?想到刚才 APP 提示更新,莫非是会更新了uniapp? 于是去找APP临时目录,在/data/data目录下找到 APP对应的目录,在里面找到了一个app-service.js, 和apk里的app-service.js 大小不一样,搜索加密参数,果然在这里。剩下的就交给道友去解决了。

联系作者