This commit is contained in:
mzl
2026-01-12 15:49:09 +08:00
parent 7c933f4a82
commit 7c0b8c4c09
12 changed files with 383 additions and 175 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
# Web 服务配置 # Web 服务配置
HOST=0.0.0.0 HOST=0.0.0.0
PORT=18080 PORT=18085
# 浏览器配置 # 浏览器配置
HEADLESS=false HEADLESS=false
+1 -1
View File
@@ -3,7 +3,7 @@
# Web 服务配置 # Web 服务配置
HOST=0.0.0.0 HOST=0.0.0.0
PORT=18080 PORT=18085
# 浏览器配置 # 浏览器配置
HEADLESS=false HEADLESS=false
+2 -2
View File
@@ -50,11 +50,11 @@ COPY . .
RUN mkdir -p /app/data /app/data/screenshots RUN mkdir -p /app/data /app/data/screenshots
# 暴露端口 # 暴露端口
EXPOSE 18080 EXPOSE 18085
# 设置环境变量 # 设置环境变量
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV PORT=18080 ENV PORT=18085
ENV HEADLESS=false ENV HEADLESS=false
ENV SCREENSHOT_ON_ERROR=true ENV SCREENSHOT_ON_ERROR=true
+7 -7
View File
@@ -63,7 +63,7 @@ playwright install chromium
python main.py python main.py
``` ```
服务将在 `http://localhost:18080` 启动。 服务将在 `http://localhost:18085` 启动。
### 2. 添加用户 ID ### 2. 添加用户 ID
@@ -100,7 +100,7 @@ python main.py
```env ```env
HOST=0.0.0.0 HOST=0.0.0.0
PORT=18080 PORT=18085
HEADLESS=false HEADLESS=false
SCREENSHOT_ON_ERROR=true SCREENSHOT_ON_ERROR=true
MAX_THREADS=5 MAX_THREADS=5
@@ -180,7 +180,7 @@ docker build -t xiaohongshu-scraper .
```bash ```bash
# 本地开发模式(可以看到浏览器窗口) # 本地开发模式(可以看到浏览器窗口)
docker run -d \ docker run -d \
-p 18080:18080 \ -p 18085:18085 \
-v $(pwd)/data:/app/data \ -v $(pwd)/data:/app/data \
-e HEADLESS=false \ -e HEADLESS=false \
--name xiaohongshu-scraper \ --name xiaohongshu-scraper \
@@ -188,7 +188,7 @@ docker run -d \
# 云服务器模式(无头模式) # 云服务器模式(无头模式)
docker run -d \ docker run -d \
-p 18080:18080 \ -p 18085:18085 \
-v $(pwd)/data:/app/data \ -v $(pwd)/data:/app/data \
-e HEADLESS=true \ -e HEADLESS=true \
--name xiaohongshu-scraper \ --name xiaohongshu-scraper \
@@ -219,7 +219,7 @@ docker exec -it xiaohongshu-scraper bash
| 变量 | 默认值 | 说明 | | 变量 | 默认值 | 说明 |
|------|--------|------| |------|--------|------|
| HOST | 0.0.0.0 | 监听地址 | | HOST | 0.0.0.0 | 监听地址 |
| PORT | 18080 | 监听端口(范围 10000-50000)| | PORT | 18085 | 监听端口(范围 10000-50000)|
| HEADLESS | false | 是否无头模式(云服务器设为 true)| | HEADLESS | false | 是否无头模式(云服务器设为 true)|
| SCREENSHOT_ON_ERROR | true | 错误时截图 | | SCREENSHOT_ON_ERROR | true | 错误时截图 |
| MAX_THREADS | 5 | 最大线程数 | | MAX_THREADS | 5 | 最大线程数 |
@@ -266,10 +266,10 @@ environment:
```bash ```bash
# 检查 Git 状态 # 检查 Git 状态
curl http://your-server:18080/api/git/status curl http://your-server:18085/api/git/status
# 拉取最新代码 # 拉取最新代码
curl -X POST http://your-server:18080/api/git/pull curl -X POST http://your-server:18085/api/git/pull
``` ```
#### 4. 重启容器 #### 4. 重启容器
+1 -1
View File
@@ -82,7 +82,7 @@ MAX_LOGS = 10000 # 最大日志条数
# ========== Web 配置 ========== # ========== Web 配置 ==========
WEB_HOST = os.getenv("HOST", "0.0.0.0") WEB_HOST = os.getenv("HOST", "0.0.0.0")
WEB_PORT = int(os.getenv("PORT", "18080")) # 默认端口 18080,范围 10000-50000 WEB_PORT = int(os.getenv("PORT", "18085")) # 默认端口 18085,范围 10000-50000
SCREENSHOT_ON_ERROR = os.getenv("SCREENSHOT_ON_ERROR", "true").lower() == "true" SCREENSHOT_ON_ERROR = os.getenv("SCREENSHOT_ON_ERROR", "true").lower() == "true"
# ========== Git 更新配置 ========== # ========== Git 更新配置 ==========
+11
View File
@@ -15,6 +15,7 @@ from fastapi.templating import Jinja2Templates
from fastapi.requests import Request from fastapi.requests import Request
from pydantic import BaseModel from pydantic import BaseModel
from typing import List as ListType from typing import List as ListType
from fastapi.middleware.cors import CORSMiddleware
from app import config from app import config
from app.log_manager import get_log_manager from app.log_manager import get_log_manager
@@ -61,6 +62,16 @@ log_manager = get_log_manager()
# ========== FastAPI 应用 ========== # ========== FastAPI 应用 ==========
app = FastAPI(title="小红书数据采集器", version="1.0.0") app = FastAPI(title="小红书数据采集器", version="1.0.0")
# 添加 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 生产环境应该指定具体域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"]
)
# 挂载静态文件和模板 # 挂载静态文件和模板
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
+2 -2
View File
@@ -5,13 +5,13 @@ services:
build: . build: .
container_name: xiaohongshu-scraper container_name: xiaohongshu-scraper
ports: ports:
- "18085:18080" # 外部端口18081映射到容器内部18080 - "18085:18085" # 外部端口18085映射到容器内部18085
volumes: volumes:
# 持久化数据目录(采集结果、日志、登录状态) # 持久化数据目录(采集结果、日志、登录状态)
- ./data:/app/data - ./data:/app/data
environment: environment:
- HOST=0.0.0.0 - HOST=0.0.0.0
- PORT=18080 - PORT=18085
- HEADLESS=false - HEADLESS=false
- SCREENSHOT_ON_ERROR=true - SCREENSHOT_ON_ERROR=true
- MAX_THREADS=5 - MAX_THREADS=5
+1 -1
View File
@@ -56,7 +56,7 @@ else
fi fi
echo "启动 Web 服务..." echo "启动 Web 服务..."
echo "端口: ${PORT:-18080}" echo "端口: ${PORT:-18085}"
echo "无头模式: ${HEADLESS:-false}" echo "无头模式: ${HEADLESS:-false}"
echo "================================" echo "================================"
+3 -1
View File
@@ -698,7 +698,9 @@ async function openLoginQrModal() {
elements.qrLoginStatus.textContent = '正在模拟登录...'; elements.qrLoginStatus.textContent = '正在模拟登录...';
try { try {
const response = await fetch('/api/login/qrcode'); const response = await fetch('/api/login/qrcode', {
method: 'POST'
});
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
+2 -2
View File
@@ -201,8 +201,8 @@
</div> </div>
</div> </div>
<div class="login-actions"> <div class="login-actions">
<button class="btn btn-primary" id="loginBtn"> <button class="btn btn-primary" id="loginBtn" title="模拟登录,不是真实扫码">
<span class="btn-icon">🔐</span> 登录小红书 <span class="btn-icon">🔐</span> 模拟登录
</button> </button>
<button class="btn btn-danger" id="logoutBtn"> <button class="btn btn-danger" id="logoutBtn">
<span class="btn-icon">🚪</span> 退出登录 <span class="btn-icon">🚪</span> 退出登录
-157
View File
@@ -1,157 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.btn {
background: #10b981;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 10px;
}
.btn:hover {
background: #059669;
}
.qr-container {
margin: 20px 0;
text-align: center;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
}
.status.success {
background: #d1fae5;
color: #065f46;
}
.status.error {
background: #fee2e2;
color: #991b1b;
}
</style>
</head>
<body>
<div class="container">
<h2>小红书登录测试</h2>
<button class="btn" onclick="generateQR()">生成二维码</button>
<button class="btn" onclick="checkStatus()">检查状态</button>
<button class="btn" onclick="logout()">退出登录</button>
<div id="status" class="status" style="display: none;"></div>
<div id="qrContainer" class="qr-container" style="display: none;">
<p>请使用小红书App扫描二维码登录</p>
<div id="qrcode"></div>
<div id="countdown"></div>
</div>
</div>
<script>
let countdownInterval = null;
async function generateQR() {
try {
const response = await fetch('/api/login/qrcode');
const data = await response.json();
if (response.ok) {
showQR(data.qr_url);
startCountdown();
logStatus('二维码生成成功', 'success');
} else {
logStatus('生成失败: ' + data.detail, 'error');
}
} catch (error) {
logStatus('网络错误: ' + error.message, 'error');
}
}
function showQR(qrUrl) {
const container = document.getElementById('qrcode');
container.innerHTML = `
<div style="background: white; padding: 20px; border-radius: 8px; display: inline-block;">
<img src="https://quickchart.io/qr?size=200x200&text=${encodeURIComponent(qrUrl)}" alt="登录二维码" style="max-width: 200px;">
</div>
`;
document.getElementById('qrContainer').style.display = 'block';
}
function startCountdown() {
let seconds = 120;
const countdownEl = document.getElementById('countdown');
countdownInterval = setInterval(() => {
countdownEl.textContent = `二维码将在 ${seconds} 秒后过期`;
seconds--;
if (seconds < 0) {
clearInterval(countdownInterval);
countdownEl.textContent = '二维码已过期';
document.getElementById('qrContainer').style.display = 'none';
}
}, 1000);
}
async function checkStatus() {
try {
const response = await fetch('/api/login/status');
const data = await response.json();
if (data.logged_in) {
logStatus(`已登录 - 登录时间: ${new Date(data.login_time).toLocaleString()}`, 'success');
} else {
logStatus('未登录', 'error');
}
} catch (error) {
logStatus('检查失败: ' + error.message, 'error');
}
}
async function logout() {
try {
const response = await fetch('/api/logout', { method: 'POST' });
const data = await response.json();
if (response.ok) {
logStatus('已退出登录', 'success');
document.getElementById('qrContainer').style.display = 'none';
clearInterval(countdownInterval);
} else {
logStatus('退出失败: ' + data.detail, 'error');
}
} catch (error) {
logStatus('退出失败: ' + error.message, 'error');
}
}
function logStatus(message, type) {
const statusEl = document.getElementById('status');
statusEl.textContent = message;
statusEl.className = `status ${type}`;
statusEl.style.display = 'block';
}
// 页面加载时检查登录状态
window.onload = checkStatus;
</script>
</body>
</html>
+352
View File
@@ -0,0 +1,352 @@
# 小红书数据采集器使用指南
## 系统概述
这是一个基于 FastAPI + Playwright 的小红书数据采集器,具有 Web 可视化监控界面,支持多线程采集、实时进度展示和持久化日志记录。
## 系统特点
- 🌐 **Web 可视化界面**:实时监控采集进度和状态
- 🔐 **模拟登录系统**:模拟登录成功状态,本地保存,24小时自动过期
- 👥 **账号管理**:支持添加、删除用户账号
- 📊 **实时日志**:分类显示操作日志,支持过滤和搜索
- 🔄 **多线程采集**:支持单线程/多线程模式切换
- 🐳 **Docker 部署**:容器化部署,支持自动更新
- 📁 **数据持久化**:所有数据本地存储,重启不丢失
## 快速开始
### 1. Docker 部署(推荐)
```bash
# 克隆或下载项目
cd /path/to/demo
# 构建并启动容器(使用18085端口)
docker-compose up -d --build
# 查看容器状态
docker-compose ps
# 查看日志
docker-compose logs -f
```
### 2. 本地运行
```bash
# 安装依赖
pip install -r requirements.txt
# 安装浏览器
playwright install chromium
# 创建数据目录
mkdir -p data data/screenshots
# 运行程序
python main.py
```
### 3. 访问系统
打开浏览器访问:`http://localhost:18085`
## 系统界面说明
### 1. 账号管理区域
![账号管理](./images/account-management.png)
- **添加账号**:点击添加账号按钮,输入小红书用户 ID
- **刷新账号**:重新加载账号列表和状态
- **登录状态**:
- 🔴 红色圆点:未登录
- 🟢 绿色圆点:已登录
- 显示剩余登录时间(小时:分钟)
### 2. 控制面板
![控制面板](./images/control-panel.png)
- **启动采集**:开始数据采集任务
- **停止**:停止正在进行的采集任务
- **清空日志**:清空所有历史日志
- **导出数据**:下载采集到的数据文件
- **模式选择**:
- 单线程:逐个处理账号
- 多线程:同时处理多个账号(可配置线程数)
- **线程数**:多线程模式下的并发线程数(1-10)
### 3. 进度监控
![进度监控](./images/progress.png)
- **整体进度**:显示采集进度条和百分比
- **活跃线程**:多线程模式下显示正在运行的线程数
- **用户列表**:显示每个用户的采集状态
- ✅ 已完成:显示采集的笔记数量
- 🔄 进行中:显示当前处理的笔记数量
- ⏳ 待处理:尚未开始处理的账号
- ❌ 失败:采集失败的账号
### 4. 日志面板
![日志面板](./images/logs.png)
- **日志级别过滤**:可选择查看 ALL/INFO/SUCCESS/WARNING/ERROR/DEBUG 级别的日志
- **搜索功能**:搜索特定关键词的日志
- **自动滚动**:开启后自动滚动到最新日志
- **时间戳**:显示每条日志的具体时间
- **线程标识**:多线程模式下显示日志来源线程
### 5. 错误截图
![错误截图](./images/screenshots.png)
如果采集过程中遇到错误(如风控检测),系统会自动截图并保存:
- 截图保存在:`data/screenshots/` 目录
- 在界面中显示错误截图缩略图
- 可点击查看大图或下载所有截图
## 操作流程
### 1. 首次使用
```bash
# 1. 启动系统
docker-compose up -d --build
# 2. 访问 Web 界面
# 打开 http://localhost:18080
# 3. 添加要采集的用户 ID
# 点击"添加账号"按钮,输入小红书用户 ID
# 格式:长字符串(如 5ef74296000000000100003a)
# 可从浏览器地址栏获取:https://www.xiaohongshu.com/user/profile/用户ID
# 4. 模拟登录
# 点击"模拟登录"按钮,系统会模拟登录成功并记录登录状态
# 注意:这是模拟登录,不是真实的扫码登录
# 登录状态有效期:24小时
```
### 2. 配置采集参数
1. 选择采集模式:
- **单线程**:稳定但速度慢,适合少量账号
- **多线程**:速度快,适合大量账号(推荐 3-5 个线程)
2. 设置线程数(多线程模式):
- 建议:3-5 个线程
- 过多线程可能触发风控
### 3. 开始采集
1. 确保已添加至少一个用户账号
2. 确保已登录(显示绿色状态)
3. 点击"启动采集"
4. 系统会:
- 检查登录状态
- 读取用户列表
- 开始采集并实时显示进度
### 4. 监控采集
采集过程中可以:
- 查看实时进度和状态
- 监控日志输出
- 观察各线程工作状态
- 随时停止采集(点击"停止")
### 5. 数据导出
采集完成后可以:
- 查看每个用户的数据文件:`data/{用户ID}.json`
- 下载所有日志:点击"导出数据"
- 下载错误截图(如有)
## 用户 ID 获取方法
### 方法1:浏览器地址栏
1. 打开小红书网页版
2. 访问目标用户主页:https://www.xiaohongshu.com/user/profile/用户名
3. 从浏览器地址栏复制用户 ID:
```
https://www.xiaohongshu.com/user/profile/5ef74296000000000100003a
```
复制 `5ef74296000000000100003a` 部分
### 方法2:开发者工具
1. 在用户页面按 F12 打开开发者工具
2. 切换到 Network(网络)标签
3. 刷新页面
4. 查找包含 "user" 或 "profile" 的请求
5. 从请求 URL 中提取用户 ID
## 数据文件说明
### 输入文件
```
data/users.txt # 用户 ID 列表,每行一个
```
### 输出文件
```
data/
├── auth_state.json # 登录状态缓存
├── logs.json # 操作日志历史
├── users.txt # 用户列表
├── 5ef74296000000000100003a.json # 用户采集数据
├── 5b6c1f9e0000000001000388.json # 用户采集数据
└── screenshots/ # 错误截图目录
├── error_xxx1.png
└── error_xxx2.png
```
### 数据格式示例
```json
// data/{用户ID}.json
{
"user_id": "5ef74296000000000100003a",
"last_updated": "2025-01-12T10:30:00",
"notes": [
{
"title": "笔记标题",
"publish_time": "2025-01-10 14:30",
"location": "上海",
"comment_count": 128,
"like_count": 5234,
"collect_count": 892,
"note_url": "https://www.xiaohongshu.com/explore/xxx"
}
]
}
```
## 常见问题
### Q1: 为什么提示"未登录"?
A: 登录状态 24 小时后会自动过期,请:
1. 点击"模拟登录"重新登录
2. 检查 `data/auth_state.json` 文件是否存在
### Q2: 采集速度很慢怎么办?
A: 可以尝试:
1. 使用多线程模式(建议 3-5 线程)
2. 检查网络连接
3. 避免在高峰期采集
### Q3: 如何避免被封禁?
A: 系统已内置防护措施:
1. 拟人化延迟(随机 2-60 秒)
2. 平滑滚动加载
3. 多线程错开启动
4. 风控检测自动截图
5. 建议不要设置过多线程
### Q4: 数据采集不全怎么办?
A: 可能原因:
1. 网络不稳定导致加载失败
2. 笔记数量过多,滚动加载不完整
3. 小红书页面结构变化
### Q5: Docker 容器无法启动?
A: 检查:
1. Docker 是否运行
2. 端口 18080 是否被占用
3. Docker Compose 配置是否正确
## 配置说明
### 环境变量
```env
# Web 服务器配置
PORT=18080
HOST=0.0.0.0
# 浏览器配置
HEADLESS=false # 是否无头模式
SCREENSHOT_ON_ERROR=true # 错误时截图
# 多线程配置
MAX_CONCURRENT_THREADS=5 # 最大并发线程数
THREAD_MIN_DELAY=2 # 线程间最小间隔(秒)
```
### 配置文件位置
```python
# app/config.py
DATA_DIR = "data" # 数据目录
SCREENSHOT_DIR = "data/screenshots" # 截图目录
MAX_CONCURRENT_THREADS = 5 # 最大并发线程数
```
## 更新和维护
### 1. 自动更新(Docker)
系统支持 Git 自动更新:
1. 代码提交到远程仓库
2. 系统自动拉取最新代码
3. 检测到 Python 代码变化时建议重启容器
### 2. 手动更新
```bash
# 拉取最新代码
git pull origin master
# 重新构建容器
docker-compose build
# 重启服务
docker-compose up -d
```
### 3. 数据备份
重要数据请定期备份:
```bash
# 备份数据目录
tar -czf backup-$(date +%Y%m%d).tar.gz data/
# 备份特定用户数据
cp data/*.json backup-$(date +%Y%m%d)/
```
## 技术支持
如果遇到问题:
1. 查看 Web 界面的日志面板
2. 检查 Docker 容器日志:`docker-compose logs`
3. 查看 `data/logs.json` 文件
4. 确保网络连接正常
## 版本更新日志
### v1.0.0 (2025-01-12)
- ✅ 实现基础采集功能
- ✅ Web 可视化界面
- ✅ 多线程支持
- ✅ 实时日志系统
- ✅ 账号管理功能
- ✅ 模拟登录系统
- ✅ Docker 部署支持
- ✅ 数据导出功能
---
**注意**:本工具仅供学习和研究使用,请遵守小红书的使用条款和相关法律法规。