xhs-note-extractor 0.1.dev2__py3-none-any.whl → 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xhs_note_extractor/DEVICE_RETRY_GUIDE.md +98 -0
- xhs_note_extractor/agent_login.py +264 -0
- xhs_note_extractor/date_desc_utils.py +80 -0
- xhs_note_extractor/extractor.py +644 -108
- xhs_note_extractor/login_propmt.py +145 -0
- xhs_note_extractor/number_utils.py +44 -0
- xhs_note_extractor/sms_verification.py +307 -0
- xhs_note_extractor/test_device_retry.py +100 -0
- xhs_note_extractor/test_initialization_fix.py +46 -0
- {xhs_note_extractor-0.1.dev2.dist-info → xhs_note_extractor-0.1.4.dist-info}/METADATA +4 -1
- xhs_note_extractor-0.1.4.dist-info/RECORD +19 -0
- xhs_note_extractor-0.1.dev2.dist-info/RECORD +0 -11
- {xhs_note_extractor-0.1.dev2.dist-info → xhs_note_extractor-0.1.4.dist-info}/WHEEL +0 -0
- {xhs_note_extractor-0.1.dev2.dist-info → xhs_note_extractor-0.1.4.dist-info}/entry_points.txt +0 -0
- {xhs_note_extractor-0.1.dev2.dist-info → xhs_note_extractor-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {xhs_note_extractor-0.1.dev2.dist-info → xhs_note_extractor-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
phone_agent_protocol_v3_t1="""
|
|
2
|
+
自动化验证码登录协议 - 任务 1:触发验证码
|
|
3
|
+
你是一个强执行型 Agent,必须严格按照以下协议执行,不得有任何偏差。
|
|
4
|
+
|
|
5
|
+
【最高优先级:异常检测与处理】
|
|
6
|
+
- 在执行**每一个操作前**,必须首先检查是否出现异常情况
|
|
7
|
+
- 一旦发现异常,必须**立即终止所有操作**,直接返回指定JSON格式错误信息
|
|
8
|
+
- 异常处理优先于任何其他任务步骤,不得延迟或忽略
|
|
9
|
+
|
|
10
|
+
核心任务
|
|
11
|
+
目标:在小红书 App 中触发手机号 {0} 的验证码发送。
|
|
12
|
+
注意:本任务仅负责触发验证码,**只有看到纯数字验证码输入框(可直接输入数字的界面)出现即视为完成**。
|
|
13
|
+
【重要】:图片验证码(包括需要点击文字、旋转图片、滑块、拼图、拖拽等任何包含图片元素的验证机制)**不属于数字验证码输入框**,遇到此类情况应立即终止任务。
|
|
14
|
+
|
|
15
|
+
执行流程
|
|
16
|
+
|
|
17
|
+
=== 操作前强制检查 ===
|
|
18
|
+
在执行任何操作前,必须先检查当前界面是否存在以下异常:
|
|
19
|
+
1. 检查是否存在**图片验证码**(包括但不限于:需要点击文字的验证码、旋转图片的验证码、滑块验证码、拼图验证码、拖拽验证码等任何包含图片元素的验证机制)
|
|
20
|
+
2. 检查是否存在**需要人工介入**的提示或界面(包括但不限于:扫码验证、人脸识别、指纹验证等)
|
|
21
|
+
3. 检查是否存在**发送次数限制**的提示(包括但不限于:"已超过今天发送短信条数限"、"发送次数过多"、"请稍后再试"等)
|
|
22
|
+
|
|
23
|
+
【触发finish指令】若发现上述任何异常,立即终止任务并返回对应JSON格式错误信息。
|
|
24
|
+
【特别注意】:图片验证码不属于任务完成标准中的数字验证码输入框,遇到图片验证码必须立即终止任务。
|
|
25
|
+
|
|
26
|
+
=== 1. 登出清理 ===
|
|
27
|
+
1. 打开小红书 App
|
|
28
|
+
2. **检查异常**:操作前先执行【操作前强制检查】
|
|
29
|
+
3. 检查登录状态:
|
|
30
|
+
- 若已登录,执行:我 -> 设置 -> 向下滑到底部 -> 点击退出登录
|
|
31
|
+
- 若未登录,直接进入下一步
|
|
32
|
+
4. 输出:`{{"status":"info","message":"当前已处于未登录状态"}}`
|
|
33
|
+
|
|
34
|
+
=== 2. 触发验证码 ===
|
|
35
|
+
1. **检查异常**:操作前先执行【操作前强制检查】
|
|
36
|
+
2. 在登录页选择【手机号验证码登录】
|
|
37
|
+
3. **检查异常**:操作前先执行【操作前强制检查】
|
|
38
|
+
4. 输入手机号 {0},输入后必须确认手机号完整(忽略空格)
|
|
39
|
+
5. **检查异常**:操作前先执行【操作前强制检查】
|
|
40
|
+
6. 点击【获取验证码】按钮
|
|
41
|
+
7. **立即检查异常**:点击后立即执行【操作前强制检查】
|
|
42
|
+
|
|
43
|
+
=== 3. 异常处理(触发后立即执行) ===
|
|
44
|
+
【必须严格执行,不得有任何延迟】
|
|
45
|
+
|
|
46
|
+
- **情况1:出现任何形式的图片验证码**
|
|
47
|
+
→ 【触发finish指令】立即终止任务并返回:`{{"status":"error","message":"账号出现异常","reason":"检测到图片验证码","code":"CAPTCHA_DETECTED"}}`
|
|
48
|
+
|
|
49
|
+
- **情况2:出现需要人工介入的情况**
|
|
50
|
+
→ 【触发finish指令】立即终止任务并返回:`{{"status":"error","message":"账号出现异常","reason":"需要人工介入验证","code":"HUMAN_VERIFICATION_REQUIRED"}}`
|
|
51
|
+
|
|
52
|
+
- **情况3:出现发送次数限制提示**
|
|
53
|
+
→ 【触发finish指令】立即终止任务并返回:`{{"status":"error","message":"今日短信发送次数已达上限","reason":"验证码发送次数超过限制","code":"SMS_LIMIT_EXCEEDED"}}`
|
|
54
|
+
|
|
55
|
+
=== 4. 任务完成检查 ===
|
|
56
|
+
- 【强制检查】:在判断任务是否完成前,必须先执行【操作前强制检查】
|
|
57
|
+
- 只有当界面显示**纯数字验证码输入框**(可直接输入数字的界面,不包含任何图片元素)且无任何异常时,任务才算完成
|
|
58
|
+
- 【重要】:若界面显示的是图片验证码(包括需要点击文字、旋转图片、滑块、拼图、拖拽等任何包含图片元素的验证机制),即使显示了文字验证输入框,也**不视为任务完成**,应立即终止任务
|
|
59
|
+
- 【触发finish指令】若满足完成条件,立即停止所有操作,返回:`{{"status":"success","message":"任务已完成","reason":"成功触发验证码发送","code":"CAPTCHA_SENT"}}`
|
|
60
|
+
|
|
61
|
+
=== 禁止的行为 ===
|
|
62
|
+
1. 禁止查看任何协议的具体内容(包括但不限于隐私协议、用户协议等),但可以直接同意或确认协议
|
|
63
|
+
2. 禁止点击【账号与安全】等与账号安全相关的操作
|
|
64
|
+
3. 禁止等待验证码完全生成并显示
|
|
65
|
+
4. 禁止点击任何复选框
|
|
66
|
+
5. 禁止执行任何人工行为或请求人工介入
|
|
67
|
+
6. 禁止在发现异常后继续执行任何操作
|
|
68
|
+
|
|
69
|
+
=== 完成标准 ===
|
|
70
|
+
- **成功标准**:必须同时满足以下条件
|
|
71
|
+
1. 界面显示**纯数字验证码输入框**(可直接输入数字的界面,不包含任何图片元素)
|
|
72
|
+
2. 没有检测到任何异常情况(图片验证码、人工介入提示、发送次数限制等)
|
|
73
|
+
3. 成功返回标准化JSON格式成功信息
|
|
74
|
+
|
|
75
|
+
- **失败标准**:满足以下任一条件
|
|
76
|
+
1. 检测到任何异常情况(图片验证码、人工介入提示、发送次数限制等)
|
|
77
|
+
2. 界面显示的是图片验证码(包括需要点击文字、旋转图片、滑块、拼图、拖拽等任何包含图片元素的验证机制)
|
|
78
|
+
3. 没有看到纯数字验证码输入框
|
|
79
|
+
4. 返回标准化JSON格式错误信息
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
phone_agent_protocol_v3_t3="""
|
|
84
|
+
自动化验证码登录协议 - 任务 3:完成登录
|
|
85
|
+
你是一个强执行型 Agent,必须严格按照以下协议执行,不得有任何偏差。
|
|
86
|
+
|
|
87
|
+
【最高优先级:异常检测与处理】
|
|
88
|
+
- 在执行**每一个操作前**,必须首先检查是否出现异常情况
|
|
89
|
+
- 一旦发现异常,必须**立即终止所有操作**,直接返回指定JSON格式错误信息
|
|
90
|
+
- 异常处理优先于任何其他任务步骤,不得延迟或忽略
|
|
91
|
+
|
|
92
|
+
核心任务
|
|
93
|
+
目标:使用验证码 {1} 完成手机号 {0} 的登录。
|
|
94
|
+
注意:本任务仅处理纯数字验证码登录流程,**遇到图片验证码(包括需要点击文字、旋转图片、滑块、拼图、拖拽等任何包含图片元素的验证机制)应立即终止任务**。
|
|
95
|
+
|
|
96
|
+
执行流程
|
|
97
|
+
|
|
98
|
+
=== 操作前强制检查 ===
|
|
99
|
+
在执行任何操作前,必须先检查当前界面是否存在以下异常:
|
|
100
|
+
1. 检查是否存在**图片验证码**(包括但不限于:需要点击文字的验证码、旋转图片的验证码、滑块验证码、拼图验证码、拖拽验证码等任何包含图片元素的验证机制)
|
|
101
|
+
2. 检查是否存在**验证码错误/失效**的提示(包括但不限于:"验证码错误"、"验证码已失效"、"验证码过期"、"验证码不正确"等)
|
|
102
|
+
3. 检查是否存在**需要人工介入**的提示或界面(包括但不限于:扫码验证、人脸识别、指纹验证等)
|
|
103
|
+
|
|
104
|
+
【触发finish指令】若发现上述任何异常,立即终止任务并返回对应JSON格式错误信息。
|
|
105
|
+
【特别注意】:本任务仅处理纯数字验证码登录流程,图片验证码不属于正常登录流程,遇到此类情况应立即终止任务。
|
|
106
|
+
|
|
107
|
+
=== 1. 回到小红书 ===
|
|
108
|
+
1. 打开小红书 App(应处于等待输入验证码界面)
|
|
109
|
+
2. **检查异常**:操作前先执行【操作前强制检查】
|
|
110
|
+
|
|
111
|
+
=== 2. 输入与登录 ===
|
|
112
|
+
1. **检查异常**:操作前先执行【操作前强制检查】
|
|
113
|
+
2. 点击验证码输入框
|
|
114
|
+
3. **检查异常**:操作前先执行【操作前强制检查】
|
|
115
|
+
4. 输入验证码 {1}
|
|
116
|
+
5. **检查异常**:操作前先执行【操作前强制检查】
|
|
117
|
+
6. 点击登录按钮
|
|
118
|
+
7. **立即检查异常**:点击后立即执行【操作前强制检查】
|
|
119
|
+
|
|
120
|
+
=== 3. 异常处理(触发后立即执行) ===
|
|
121
|
+
【必须严格执行,不得有任何延迟】
|
|
122
|
+
|
|
123
|
+
- **情况1:提示验证码错误/失效**(包括但不限于:验证码错误、验证码已失效、验证码过期、验证码不正确等)
|
|
124
|
+
→ 【触发finish指令】立即终止任务并返回:`{{"status":"error","message":"验证码失效,请重新执行全流程","reason":"验证码验证失败","code":"VERIFICATION_CODE_FAILED"}}`
|
|
125
|
+
|
|
126
|
+
- **情况2:出现任何形式的图片验证码**(包括但不限于:需要点击特定文字的验证码、旋转图片的验证码、滑块验证码、拼图验证码、拖拽验证码等)
|
|
127
|
+
→ 【触发finish指令】立即终止任务并返回:`{{"status":"error","message":"验证码失效,请重新执行全流程","reason":"检测到图片验证码","code":"CAPTCHA_DETECTED"}}`
|
|
128
|
+
|
|
129
|
+
- **情况3:出现需要人工介入的情况**(包括但不限于:扫码验证、人脸识别、指纹验证等)
|
|
130
|
+
→ 【触发finish指令】立即终止任务并返回:`{{"status":"error","message":"验证码失效,请重新执行全流程","reason":"需要人工介入验证","code":"HUMAN_VERIFICATION_REQUIRED"}}`
|
|
131
|
+
|
|
132
|
+
=== 4. 完成标准检查 ===
|
|
133
|
+
1. 检查是否成功进入小红书首页
|
|
134
|
+
2. 若成功进入首页,立即停止所有操作【触发finish指令】,返回:`{{"status":"success","message":"登录成功","reason":"成功完成小红书账号登录","code":"LOGIN_SUCCESS"}}`
|
|
135
|
+
3. 若未进入首页,检查是否存在异常,若有异常则返回对应错误信息的JSON格式
|
|
136
|
+
|
|
137
|
+
=== 禁止的行为 ===
|
|
138
|
+
1. 禁止查看任何协议的具体内容(包括但不限于隐私协议、用户协议等),但可以直接同意或确认协议
|
|
139
|
+
2. 禁止在发现异常后继续执行任何操作
|
|
140
|
+
3. 禁止执行任何人工行为或请求人工介入
|
|
141
|
+
4. 禁止修改或伪造验证码
|
|
142
|
+
|
|
143
|
+
完成标准
|
|
144
|
+
成功进入小红书首页并返回标准化的JSON格式成功信息:`{{"status":"success","message":"登录成功","reason":"成功完成小红书账号登录","code":"LOGIN_SUCCESS"}}`
|
|
145
|
+
"""
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
数字格式转换工具
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
def parse_count_to_int(count_str: str) -> int:
|
|
6
|
+
"""
|
|
7
|
+
将小红书的互动数字符串转换为整数
|
|
8
|
+
|
|
9
|
+
支持格式:
|
|
10
|
+
- "3.1万" -> 31000
|
|
11
|
+
- "3.1w" / "3.1W" -> 31000
|
|
12
|
+
- "1234" -> 1234
|
|
13
|
+
- "12.5万" -> 125000
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
count_str: 数字字符串
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
int: 转换后的整数
|
|
20
|
+
"""
|
|
21
|
+
if not count_str or count_str == "0":
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
count_str = count_str.strip()
|
|
25
|
+
|
|
26
|
+
# 处理 "万" 或 "w/W"
|
|
27
|
+
if '万' in count_str:
|
|
28
|
+
num_part = count_str.replace('万', '').strip()
|
|
29
|
+
try:
|
|
30
|
+
return int(float(num_part) * 10000)
|
|
31
|
+
except ValueError:
|
|
32
|
+
return 0
|
|
33
|
+
elif 'w' in count_str.lower():
|
|
34
|
+
num_part = count_str.lower().replace('w', '').strip()
|
|
35
|
+
try:
|
|
36
|
+
return int(float(num_part) * 10000)
|
|
37
|
+
except ValueError:
|
|
38
|
+
return 0
|
|
39
|
+
else:
|
|
40
|
+
# 普通数字
|
|
41
|
+
try:
|
|
42
|
+
return int(float(count_str))
|
|
43
|
+
except ValueError:
|
|
44
|
+
return 0
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from playwright.async_api import async_playwright
|
|
6
|
+
|
|
7
|
+
# 从环境变量获取配置,允许用户自定义
|
|
8
|
+
SMS_SYSTEM_URL = os.getenv('SMS_SYSTEM_URL', 'https://smssystem.dicbs.com/admin/smsrecord')
|
|
9
|
+
|
|
10
|
+
async def parse_sms_table(page, phone_number, send_time):
|
|
11
|
+
"""
|
|
12
|
+
解析短信记录表格,提取符合条件的验证码
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
page: Playwright页面对象
|
|
16
|
+
phone_number: 目标手机号
|
|
17
|
+
send_time: 发送时间,只返回在此时间之后的验证码
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
str: 符合条件的6位数字验证码,如果没有找到则返回None
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
# 查找表格元素 - 使用更通用的选择器
|
|
24
|
+
table_selectors = [
|
|
25
|
+
'tbody', # 尝试tbody元素
|
|
26
|
+
'#dataList tbody', # 尝试特定ID下的tbody
|
|
27
|
+
'table tbody', # 尝试任何表格下的tbody
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
tbody = None
|
|
31
|
+
for selector in table_selectors:
|
|
32
|
+
try:
|
|
33
|
+
tbody = await page.wait_for_selector(selector, timeout=3000)
|
|
34
|
+
print(f'使用选择器 {selector} 找到表格')
|
|
35
|
+
break
|
|
36
|
+
except:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
if not tbody:
|
|
40
|
+
print('未找到表格元素,使用替代方案提取验证码')
|
|
41
|
+
return await extract_codes_from_text(page, send_time)
|
|
42
|
+
|
|
43
|
+
# 获取所有行
|
|
44
|
+
rows = await tbody.query_selector_all('tr')
|
|
45
|
+
print(f'找到表格行数量: {len(rows)}')
|
|
46
|
+
|
|
47
|
+
# 只处理第一条数据行(最新的记录)
|
|
48
|
+
if len(rows) > 0:
|
|
49
|
+
row = rows[0]
|
|
50
|
+
try:
|
|
51
|
+
# 获取行内所有单元格
|
|
52
|
+
cells = await row.query_selector_all('td')
|
|
53
|
+
|
|
54
|
+
if len(cells) < 5: # 确保有足够的单元格(根据截图,表格有5列)
|
|
55
|
+
print(f'单元格数量不足(实际: {len(cells)}),跳过该行')
|
|
56
|
+
return await extract_codes_from_text(page, send_time)
|
|
57
|
+
|
|
58
|
+
# 根据截图调整列索引:
|
|
59
|
+
# 第0列:ID
|
|
60
|
+
# 第1列:短信内容
|
|
61
|
+
# 第2列:发送号码
|
|
62
|
+
# 第3列:接收号码
|
|
63
|
+
# 第4列:创建时间
|
|
64
|
+
|
|
65
|
+
# 获取手机号(第4列)
|
|
66
|
+
phone_cell = cells[3]
|
|
67
|
+
phone_text = await phone_cell.inner_text()
|
|
68
|
+
print(f'检查手机号: {phone_text}')
|
|
69
|
+
|
|
70
|
+
# 检查是否是目标手机号
|
|
71
|
+
if phone_number not in phone_text:
|
|
72
|
+
print('手机号不匹配,尝试从页面文本提取')
|
|
73
|
+
return await extract_codes_from_text(page, send_time)
|
|
74
|
+
|
|
75
|
+
# 获取短信内容(第2列)
|
|
76
|
+
content_cell = cells[1]
|
|
77
|
+
content_text = await content_cell.inner_text()
|
|
78
|
+
print(f'短信内容: {content_text}')
|
|
79
|
+
|
|
80
|
+
# 查找6位数字验证码
|
|
81
|
+
codes = re.findall(r'\d{6}', content_text)
|
|
82
|
+
if not codes:
|
|
83
|
+
print('未找到6位验证码,尝试从页面文本提取')
|
|
84
|
+
return await extract_codes_from_text(page, send_time)
|
|
85
|
+
|
|
86
|
+
# 获取发送时间(第5列)
|
|
87
|
+
time_cell = cells[4]
|
|
88
|
+
time_text = await time_cell.inner_text()
|
|
89
|
+
print(f'发送时间: {time_text}')
|
|
90
|
+
|
|
91
|
+
# 解析时间格式
|
|
92
|
+
try:
|
|
93
|
+
sms_time = datetime.strptime(time_text, '%Y-%m-%d %H:%M:%S')
|
|
94
|
+
except ValueError:
|
|
95
|
+
print(f'无法解析时间格式: {time_text}')
|
|
96
|
+
return await extract_codes_from_text(page, send_time)
|
|
97
|
+
|
|
98
|
+
# 检查时间是否在发送时间之后
|
|
99
|
+
if sms_time > send_time:
|
|
100
|
+
print(f'找到符合条件的验证码: {codes[0]} (时间: {time_text})')
|
|
101
|
+
return codes[0]
|
|
102
|
+
else:
|
|
103
|
+
print(f'验证码时间 {time_text} 早于发送时间 {send_time.strftime("%Y-%m-%d %H:%M:%S")}')
|
|
104
|
+
return await extract_codes_from_text(page, send_time)
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f'解析表格行失败: {e}')
|
|
108
|
+
return await extract_codes_from_text(page, send_time)
|
|
109
|
+
|
|
110
|
+
print('表格中未找到符合条件的验证码,尝试从页面文本提取')
|
|
111
|
+
# 如果表格中没有找到,尝试从整个页面文本提取
|
|
112
|
+
return await extract_codes_from_text(page, send_time)
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f'解析表格失败: {e}')
|
|
116
|
+
# 失败时回退到文本提取
|
|
117
|
+
return await extract_codes_from_text(page, send_time)
|
|
118
|
+
|
|
119
|
+
async def extract_codes_from_text(page, send_time):
|
|
120
|
+
"""
|
|
121
|
+
从页面文本中提取验证码的备用方案
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
page: Playwright页面对象
|
|
125
|
+
send_time: 发送时间
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
str: 符合条件的验证码,如果没有找到则返回None
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
# 提取所有文本内容
|
|
132
|
+
all_text = await page.inner_text('body')
|
|
133
|
+
print(f'页面文本内容长度: {len(all_text)} 字符')
|
|
134
|
+
|
|
135
|
+
# 查找所有6位数字验证码
|
|
136
|
+
all_codes = re.findall(r'\d{6}', all_text)
|
|
137
|
+
print(f'找到验证码数量: {len(all_codes)} 个')
|
|
138
|
+
|
|
139
|
+
if all_codes:
|
|
140
|
+
# 返回最新的验证码(页面上的最后一个)
|
|
141
|
+
verification_code = all_codes[-1]
|
|
142
|
+
print(f'获取到验证码:[{verification_code}]')
|
|
143
|
+
return verification_code
|
|
144
|
+
else:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
print(f'从文本提取验证码失败: {e}')
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
async def get_verification_code(phone_number, send_time=None, max_retries=5, retry_interval=5):
|
|
152
|
+
"""
|
|
153
|
+
获取指定手机号的最新验证码
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
phone_number (str): 目标手机号
|
|
157
|
+
send_time (datetime, optional): 发送时间,只返回在此时间之后的验证码
|
|
158
|
+
默认使用当前时间
|
|
159
|
+
max_retries (int, optional): 最大重试次数,默认5次
|
|
160
|
+
retry_interval (int, optional): 重试间隔时间(秒),默认5秒
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
str: 6位数字验证码
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
Exception: 如果经过多次尝试后仍未找到验证码
|
|
167
|
+
"""
|
|
168
|
+
# 如果未指定发送时间,使用当前时间
|
|
169
|
+
if send_time is None:
|
|
170
|
+
send_time = datetime.now()
|
|
171
|
+
print(f'未指定发送时间,默认使用: {send_time.strftime("%Y-%m-%d %H:%M:%S")}')
|
|
172
|
+
|
|
173
|
+
for retry in range(max_retries):
|
|
174
|
+
try:
|
|
175
|
+
print(f'\n=== 第 {retry + 1}/{max_retries} 次尝试 ===')
|
|
176
|
+
# 启动Playwright
|
|
177
|
+
async with async_playwright() as p:
|
|
178
|
+
# 启动浏览器
|
|
179
|
+
browser = await p.chromium.launch(
|
|
180
|
+
headless=False,
|
|
181
|
+
args=['--no-sandbox', '--disable-setuid-sandbox'],
|
|
182
|
+
timeout=60000
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# 创建页面
|
|
186
|
+
page = await browser.new_page()
|
|
187
|
+
|
|
188
|
+
# 访问登录页面
|
|
189
|
+
await page.goto('https://smssystem.dicbs.com/admin/user/login',
|
|
190
|
+
wait_until='domcontentloaded', timeout=60000)
|
|
191
|
+
|
|
192
|
+
print(f'当前页面: {page.url}')
|
|
193
|
+
|
|
194
|
+
# 检查是否需要登录
|
|
195
|
+
if 'login' in page.url:
|
|
196
|
+
print('检测到未登录状态,开始登录...')
|
|
197
|
+
|
|
198
|
+
# 输入账号密码
|
|
199
|
+
await page.fill('input[name="username"]', '京东xhs')
|
|
200
|
+
await page.fill('input[name="password"]', '123456')
|
|
201
|
+
await page.fill('input[name="captcha"]', '1234')
|
|
202
|
+
|
|
203
|
+
# 点击登录按钮
|
|
204
|
+
await page.click('button')
|
|
205
|
+
await page.wait_for_load_state('domcontentloaded', timeout=30000)
|
|
206
|
+
|
|
207
|
+
# 检查是否登录成功
|
|
208
|
+
if 'login' not in page.url:
|
|
209
|
+
print('登录成功!')
|
|
210
|
+
else:
|
|
211
|
+
print('登录失败,可能是验证码错误,但继续尝试...')
|
|
212
|
+
|
|
213
|
+
# 确保在目标页面
|
|
214
|
+
if SMS_SYSTEM_URL not in page.url:
|
|
215
|
+
await page.goto(SMS_SYSTEM_URL, wait_until='domcontentloaded', timeout=60000)
|
|
216
|
+
|
|
217
|
+
# 尝试搜索手机号
|
|
218
|
+
print(f'尝试搜索手机号:{phone_number}...')
|
|
219
|
+
try:
|
|
220
|
+
# 查找搜索输入框
|
|
221
|
+
search_box = await page.wait_for_selector('input[name*="receiver"], input[id*="receiver"]', timeout=10000)
|
|
222
|
+
if search_box:
|
|
223
|
+
await search_box.fill(phone_number)
|
|
224
|
+
await page.click('#memberSearch')
|
|
225
|
+
await page.wait_for_load_state('domcontentloaded', timeout=15000)
|
|
226
|
+
print('搜索完成')
|
|
227
|
+
else:
|
|
228
|
+
print('未找到搜索框,直接提取表格内容')
|
|
229
|
+
except Exception as e:
|
|
230
|
+
print(f'搜索失败: {e}')
|
|
231
|
+
|
|
232
|
+
# 解析表格内容
|
|
233
|
+
print('解析表格内容...')
|
|
234
|
+
verification_code = await parse_sms_table(page, phone_number, send_time)
|
|
235
|
+
|
|
236
|
+
if verification_code:
|
|
237
|
+
await browser.close()
|
|
238
|
+
return verification_code
|
|
239
|
+
else:
|
|
240
|
+
print('未找到符合条件的验证码')
|
|
241
|
+
await browser.close()
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
print(f'获取验证码失败: {type(e).__name__}: {str(e)}')
|
|
245
|
+
|
|
246
|
+
# 如果不是最后一次尝试,等待一段时间后重试
|
|
247
|
+
if retry < max_retries - 1:
|
|
248
|
+
print(f'等待 {retry_interval} 秒后重试...')
|
|
249
|
+
await asyncio.sleep(retry_interval)
|
|
250
|
+
|
|
251
|
+
raise Exception(f'经过 {max_retries} 次尝试后仍未找到符合条件的验证码')
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_verification_code_sync(phone_number, send_time=None, max_retries=5, retry_interval=5):
|
|
255
|
+
"""
|
|
256
|
+
同步版本的获取验证码函数
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
phone_number (str): 目标手机号
|
|
260
|
+
send_time (datetime, optional): 发送时间,只返回在此时间之后的验证码
|
|
261
|
+
默认使用当前时间
|
|
262
|
+
max_retries (int, optional): 最大重试次数,默认5次
|
|
263
|
+
retry_interval (int, optional): 重试间隔时间(秒),默认5秒
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
str: 6位数字验证码
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
Exception: 如果经过多次尝试后仍未找到验证码
|
|
270
|
+
"""
|
|
271
|
+
return asyncio.run(get_verification_code(
|
|
272
|
+
phone_number=phone_number,
|
|
273
|
+
send_time=send_time,
|
|
274
|
+
max_retries=max_retries,
|
|
275
|
+
retry_interval=retry_interval
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
if __name__ == '__main__':
|
|
280
|
+
"""
|
|
281
|
+
示例用法
|
|
282
|
+
"""
|
|
283
|
+
import sys
|
|
284
|
+
|
|
285
|
+
if len(sys.argv) < 2 or len(sys.argv) > 3:
|
|
286
|
+
print('用法: python sms_verification.py <手机号> [<发送时间>]')
|
|
287
|
+
print('发送时间格式: YYYY-MM-DD HH:MM:SS')
|
|
288
|
+
sys.exit(1)
|
|
289
|
+
|
|
290
|
+
phone_number = sys.argv[1]
|
|
291
|
+
|
|
292
|
+
if len(sys.argv) == 3:
|
|
293
|
+
try:
|
|
294
|
+
send_time = datetime.strptime(sys.argv[2], '%Y-%m-%d %H:%M:%S')
|
|
295
|
+
except ValueError:
|
|
296
|
+
print('发送时间格式错误,请使用YYYY-MM-DD HH:MM:SS格式')
|
|
297
|
+
sys.exit(1)
|
|
298
|
+
else:
|
|
299
|
+
send_time = None
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
# 使用同步版本调用
|
|
303
|
+
code = get_verification_code_sync(phone_number, send_time)
|
|
304
|
+
print(f'\n最终结果:获取到验证码:[{code}]')
|
|
305
|
+
except Exception as e:
|
|
306
|
+
print(f'\n执行失败: {str(e)}')
|
|
307
|
+
sys.exit(1)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
设备重试机制测试脚本
|
|
5
|
+
|
|
6
|
+
该脚本用于测试小红书笔记提取器的设备重试功能,
|
|
7
|
+
当某个设备需要登录时,会自动尝试其他设备。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
13
|
+
|
|
14
|
+
from extractor import XHSNoteExtractor, extract_note_from_url
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
# 配置日志
|
|
18
|
+
logging.basicConfig(
|
|
19
|
+
level=logging.INFO,
|
|
20
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
21
|
+
)
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
def test_device_retry():
|
|
25
|
+
"""测试设备重试机制"""
|
|
26
|
+
print("=" * 60)
|
|
27
|
+
print("小红书笔记提取器 - 设备重试机制测试")
|
|
28
|
+
print("=" * 60)
|
|
29
|
+
|
|
30
|
+
# 测试URL(请替换为实际的小红书笔记URL)
|
|
31
|
+
test_url = "https://www.xiaohongshu.com/explore/65a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
# 方式1:使用便捷函数
|
|
35
|
+
print("\n1. 使用便捷函数测试...")
|
|
36
|
+
result = extract_note_from_url(test_url, enable_time_logging=True)
|
|
37
|
+
|
|
38
|
+
if result:
|
|
39
|
+
print(f"✓ 成功提取笔记数据")
|
|
40
|
+
print(f" 作者: {result.get('author_name', '未知')}")
|
|
41
|
+
print(f" 点赞数: {result.get('likes', '0')}")
|
|
42
|
+
print(f" 收藏数: {result.get('collects', '0')}")
|
|
43
|
+
print(f" 评论数: {result.get('comments', '0')}")
|
|
44
|
+
print(f" 图片数: {len(result.get('image_urls', []))}")
|
|
45
|
+
print(f" 正文预览: {result.get('content', '')[:100]}...")
|
|
46
|
+
else:
|
|
47
|
+
print("✗ 提取失败,所有设备都需要登录或不可用")
|
|
48
|
+
|
|
49
|
+
# 方式2:使用类实例
|
|
50
|
+
print("\n2. 使用类实例测试...")
|
|
51
|
+
extractor = XHSNoteExtractor(enable_time_logging=True)
|
|
52
|
+
|
|
53
|
+
# 显示可用设备
|
|
54
|
+
print(f"可用设备: {extractor.available_devices}")
|
|
55
|
+
|
|
56
|
+
result = extractor.extract_note_data(url=test_url)
|
|
57
|
+
|
|
58
|
+
if result:
|
|
59
|
+
print(f"✓ 成功提取笔记数据")
|
|
60
|
+
print(f" 作者: {result.get('author_name', '未知')}")
|
|
61
|
+
print(f" 点赞数: {result.get('likes', '0')}")
|
|
62
|
+
print(f" 收藏数: {result.get('collects', '0')}")
|
|
63
|
+
print(f" 评论数: {result.get('comments', '0')}")
|
|
64
|
+
print(f" 图片数: {len(result.get('image_urls', []))}")
|
|
65
|
+
else:
|
|
66
|
+
print("✗ 提取失败,所有设备都需要登录或不可用")
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(f"✗ 测试过程中发生错误: {e}")
|
|
70
|
+
logger.exception("测试异常")
|
|
71
|
+
|
|
72
|
+
def test_device_switching():
|
|
73
|
+
"""测试设备切换功能"""
|
|
74
|
+
print("\n" + "=" * 60)
|
|
75
|
+
print("设备切换功能测试")
|
|
76
|
+
print("=" * 60)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
extractor = XHSNoteExtractor(enable_time_logging=True)
|
|
80
|
+
|
|
81
|
+
print(f"初始设备: {extractor.device.serial if extractor.device else '无'}")
|
|
82
|
+
print(f"可用设备列表: {extractor.available_devices}")
|
|
83
|
+
|
|
84
|
+
if len(extractor.available_devices) > 1:
|
|
85
|
+
print("\n测试设备切换...")
|
|
86
|
+
success = extractor.switch_to_next_device()
|
|
87
|
+
if success:
|
|
88
|
+
print(f"✓ 成功切换到设备: {extractor.device.serial}")
|
|
89
|
+
else:
|
|
90
|
+
print("✗ 设备切换失败")
|
|
91
|
+
else:
|
|
92
|
+
print("只有一个或没有可用设备,无法测试切换功能")
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"✗ 测试过程中发生错误: {e}")
|
|
96
|
+
logger.exception("测试异常")
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
test_device_retry()
|
|
100
|
+
test_device_switching()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
测试设备重试机制的初始化修复
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
9
|
+
|
|
10
|
+
from extractor import XHSNoteExtractor
|
|
11
|
+
|
|
12
|
+
def test_initialization():
|
|
13
|
+
"""测试初始化是否正常"""
|
|
14
|
+
print("开始测试初始化...")
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
# 测试初始化
|
|
18
|
+
extractor = XHSNoteExtractor()
|
|
19
|
+
print("✓ 初始化成功")
|
|
20
|
+
|
|
21
|
+
# 测试属性是否存在adb connect 11.138.141.113:7121
|
|
22
|
+
print(f"可用设备: {extractor.available_devices}")
|
|
23
|
+
print(f"当前设备索引: {extractor.current_device_index}")
|
|
24
|
+
print(f"设备对象: {extractor.device}")
|
|
25
|
+
|
|
26
|
+
# 测试连接设备
|
|
27
|
+
if extractor.connect_device():
|
|
28
|
+
print("✓ 设备连接成功")
|
|
29
|
+
else:
|
|
30
|
+
print("⚠ 设备连接失败(可能无设备连接)")
|
|
31
|
+
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
except Exception as e:
|
|
35
|
+
print(f"✗ 测试失败: {e}")
|
|
36
|
+
import traceback
|
|
37
|
+
traceback.print_exc()
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
success = test_initialization()
|
|
42
|
+
if success:
|
|
43
|
+
print("\n✓ 所有测试通过")
|
|
44
|
+
else:
|
|
45
|
+
print("\n✗ 测试失败")
|
|
46
|
+
sys.exit(1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xhs-note-extractor
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: A Python package for extracting Xiaohongshu (Little Red Book) note data from URLs
|
|
5
5
|
Author-email: JoyCode Agent <agent@joycode.com>
|
|
6
6
|
License: MIT
|
|
@@ -27,6 +27,9 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Dist: uiautomator2>=2.16.17
|
|
29
29
|
Requires-Dist: requests>=2.25.0
|
|
30
|
+
Requires-Dist: playwright>=1.48.0
|
|
31
|
+
Requires-Dist: typing-extensions>=4.13.2
|
|
32
|
+
Requires-Dist: pydantic>=2.10.6
|
|
30
33
|
Provides-Extra: dev
|
|
31
34
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
32
35
|
Requires-Dist: pytest-cov>=2.0; extra == "dev"
|