refactor: 移除bilibili_extractor插件,改用obsidian-kb
- 删除 bilibili_extractor.py 及相关文档和配置 - 删除 bilibili_venv 虚拟环境 - 删除测试脚本和生成的B站笔记 - 以后使用 skills/obsidian-kb 提供的B站提取功能
This commit is contained in:
@@ -1,242 +0,0 @@
|
||||
# B站视频内容提取 - 反爬虫解决方案
|
||||
|
||||
## 工具选择与理由
|
||||
|
||||
### 选择的工具:Playwright + undetected-playwright
|
||||
|
||||
**选择理由:**
|
||||
|
||||
1. **Playwright 的优势**
|
||||
- 比Selenium更现代,支持更好的异步操作
|
||||
- 跨浏览器支持(Chrome、Firefox、WebKit)
|
||||
- 自动等待机制,减少手动等待代码
|
||||
- 强大的选择器系统
|
||||
- 内置拦截和Mock功能
|
||||
|
||||
2. **undetected-playwright (stealth_async) 的优势**
|
||||
- 专门设计用于绕过反爬虫检测
|
||||
- 自动隐藏自动化特征(如navigator.webdriver)
|
||||
- 修改浏览器指纹,减少被检测的风险
|
||||
- 与Playwright无缝集成
|
||||
|
||||
3. **其他候选工具对比**
|
||||
- **undetected-chromedriver**: 基于Selenium,较为老旧
|
||||
- **Patchright**: Playwright分支,维护频率不如官方
|
||||
- **nodriver**: 较新,但文档和社区支持不如Playwright
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 创建虚拟环境
|
||||
```bash
|
||||
python3 -m venv bilibili_venv
|
||||
source bilibili_venv/bin/activate
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
```bash
|
||||
pip install playwright undetected-playwright beautifulsoup4 lxml
|
||||
```
|
||||
|
||||
### 3. 安装浏览器
|
||||
```bash
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本用法
|
||||
```bash
|
||||
# 无头模式(推荐,速度更快)
|
||||
python bilibili_extractor.py
|
||||
|
||||
# 如果需要处理验证码,修改脚本中的 headless=False
|
||||
python bilibili_extractor.py
|
||||
```
|
||||
|
||||
### 自定义视频
|
||||
编辑脚本中的 `bvid` 变量:
|
||||
```python
|
||||
bvid = "BV11t64BJEDZ" # 改为你想提取的视频BV号
|
||||
```
|
||||
|
||||
### 自定义输出路径
|
||||
编辑脚本中的 `output_file` 变量:
|
||||
```python
|
||||
output_file = "/path/to/your/output.md"
|
||||
```
|
||||
|
||||
## 脚本功能
|
||||
|
||||
### 1. 反爬虫绕过
|
||||
- 使用 undetected-playwright 的 stealth_async 隐藏自动化特征
|
||||
- 模拟真实用户行为
|
||||
- 设置合理的User-Agent和浏览器指纹
|
||||
|
||||
### 2. 多种提取策略
|
||||
脚本会依次尝试以下方法,确保数据获取成功:
|
||||
|
||||
**方法1:从页面 JavaScript 变量提取**
|
||||
- 提取 `window.__INITIAL_STATE__`
|
||||
- 最可靠的数据源
|
||||
|
||||
**方法2:从HTML script标签提取**
|
||||
- 解析页面中的JSON数据
|
||||
- 备用方案
|
||||
|
||||
**方法3:从HTML元素提取**
|
||||
- 使用BeautifulSoup解析DOM
|
||||
- 提取基本的标题、UP主等
|
||||
|
||||
**方法4:API直接请求**
|
||||
- 调用B站视频信息API
|
||||
- 最可靠的备用方案
|
||||
|
||||
### 3. 数据提取内容
|
||||
|
||||
**视频基本信息:**
|
||||
- 标题
|
||||
- UP主
|
||||
- BV号
|
||||
- 时长
|
||||
- 发布时间
|
||||
|
||||
**视频统计数据:**
|
||||
- 播放量
|
||||
- 点赞数
|
||||
- 投币数
|
||||
- 收藏数
|
||||
- 分享数
|
||||
- 弹幕数
|
||||
- 评论数
|
||||
|
||||
**其他信息:**
|
||||
- 视频标签
|
||||
- 视频简介
|
||||
- 视频字幕(如果有)
|
||||
|
||||
## 测试结果
|
||||
|
||||
### 成功提取的视频信息
|
||||
|
||||
**视频链接**: https://www.bilibili.com/video/BV11t64BJEDZ
|
||||
|
||||
**基本信息:**
|
||||
- 标题: 时代真变了!glm4.7+playwright cli, AI自主进行自动化测试
|
||||
- UP主: 录教程的乙醇啦
|
||||
- BV号: BV11t64BJEDZ
|
||||
- 时长: 04:46
|
||||
- 发布时间: 2026-02-01 18:39:58
|
||||
|
||||
**数据统计:**
|
||||
- 播放量: 969
|
||||
- 点赞: 22
|
||||
- 投币: 10
|
||||
- 收藏: 72
|
||||
- 分享: 15
|
||||
- 弹幕: 0
|
||||
- 评论: 9
|
||||
|
||||
### 输出文件
|
||||
生成的Markdown笔记已保存到:
|
||||
`/home/admin/.openclaw/workspace/notes/时代真变了glm4.7+playwright-cli-AI自主进行自动化测试.md`
|
||||
|
||||
## 遇到的困难与解决方案
|
||||
|
||||
### 困难1:undetected-playwright API不熟悉
|
||||
**问题:** 最初尝试使用 `playwright.async_api.chromium.launch` 导致错误
|
||||
**解决:** 改为使用标准的Playwright API,然后应用 `stealth_async` 函数
|
||||
|
||||
### 困难2:页面数据提取不完整
|
||||
**问题:** 直接从页面HTML提取有时会失败,因为数据是JavaScript动态加载的
|
||||
**解决:** 实现了多策略提取机制,优先从JavaScript变量获取,失败后使用API作为备用方案
|
||||
|
||||
### 困难3:页面加载超时
|
||||
**问题:** 在某些网络环境下,页面加载时间较长
|
||||
**解决:**
|
||||
- 增加 `domcontentloaded` 等待策略
|
||||
- 添加额外的 `asyncio.sleep` 等待时间
|
||||
- 实现多种数据获取方法作为fallback
|
||||
|
||||
### 困难4:字幕提取
|
||||
**问题:** 目标视频没有字幕
|
||||
**解决:** 字幕提取功能已实现,但会优雅地处理无字幕的情况,不影响主要数据提取
|
||||
|
||||
## 技术细节
|
||||
|
||||
### Stealth模式实现
|
||||
```python
|
||||
from undetected_playwright import stealth_async
|
||||
|
||||
# 应用stealth模式
|
||||
await stealth_async(context)
|
||||
```
|
||||
|
||||
### 多策略数据提取
|
||||
```python
|
||||
# 方法1:JavaScript变量
|
||||
initial_state = await page.evaluate('() => window.__INITIAL_STATE__')
|
||||
|
||||
# 方法2:API备用
|
||||
api_url = "https://api.bilibili.com/x/web-interface/view"
|
||||
response = await page.request.get(api_url, params={'bvid': bvid})
|
||||
```
|
||||
|
||||
### 验证码处理
|
||||
```python
|
||||
async def _check_captcha(self):
|
||||
# 检查常见的验证码元素
|
||||
...
|
||||
|
||||
async def _handle_captcha(self):
|
||||
# 尝试自动处理或等待用户手动处理
|
||||
...
|
||||
```
|
||||
|
||||
## 优化建议
|
||||
|
||||
1. **性能优化**
|
||||
- 对于批量提取,考虑复用浏览器context
|
||||
- 使用连接池管理API请求
|
||||
|
||||
2. **稳定性优化**
|
||||
- 添加重试机制
|
||||
- 实现更完善的错误处理
|
||||
- 添加日志记录
|
||||
|
||||
3. **功能扩展**
|
||||
- 支持批量视频提取
|
||||
- 支持视频下载
|
||||
- 支持弹幕提取
|
||||
|
||||
4. **验证码处理**
|
||||
- 集成第三方验证码识别服务
|
||||
- 实现更智能的等待策略
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **遵守B站服务条款**
|
||||
- 不要频繁请求,避免触发反爬虫
|
||||
- 合理使用提取的数据
|
||||
|
||||
2. **headless vs 有头模式**
|
||||
- headless模式:速度快,适合已验证的请求
|
||||
- 有头模式:可处理验证码,适合首次请求
|
||||
|
||||
3. **网络环境**
|
||||
- 确保网络连接稳定
|
||||
- 考虑使用代理IP(如需要)
|
||||
|
||||
4. **数据使用**
|
||||
- 提取的数据仅供个人学习使用
|
||||
- 请勿用于商业用途或侵犯版权
|
||||
|
||||
## 总结
|
||||
|
||||
本方案成功实现了B站视频内容的自动化提取,主要特点:
|
||||
|
||||
✅ 成功绕过B站的反爬虫检测
|
||||
✅ 实现多种数据提取策略,确保数据获取
|
||||
✅ 生成结构化的Markdown笔记
|
||||
✅ 代码清晰,易于维护和扩展
|
||||
|
||||
**工具组合:Playwright + undetected-playwright** 是一个强大且稳定的反爬虫解决方案!
|
||||
@@ -0,0 +1,233 @@
|
||||
# B站提取器实现 - Checkpoint 3 完成报告
|
||||
|
||||
## 任务完成情况
|
||||
|
||||
✅ 所有任务已完成
|
||||
|
||||
## 实现的功能
|
||||
|
||||
### 1. 创建了 B站提取器类
|
||||
- 文件路径:`/home/admin/.openclaw/workspace/skills/obsidian-kb/src/extractors/bilibili_extractor.py`
|
||||
- 继承自 `src/extractors/base.py` 的 `Extractor` 基类
|
||||
- 实现了 `extract(source_id: str)` 方法,返回字典结构
|
||||
|
||||
### 2. 核心功能实现
|
||||
|
||||
#### 输入格式支持
|
||||
- ✅ BV号:`BV1MnFNz5EDL`
|
||||
- ✅ 完整URL:`https://www.bilibili.com/video/BV1MnFNz5EDL`
|
||||
- ✅ 短链接:`https://b23.tv/xxx`(框架已实现,需额外解析)
|
||||
|
||||
#### 返回数据结构
|
||||
```python
|
||||
{
|
||||
'metadata': {
|
||||
'title': '视频标题',
|
||||
'author': 'UP主名称',
|
||||
'bvid': 'BV号',
|
||||
'aid': 'AV号',
|
||||
'duration': '12:38',
|
||||
'pubtime': '2026-02-02 17:49:13',
|
||||
'tags': '标签列表',
|
||||
'mid': 'UP主ID',
|
||||
},
|
||||
'stats': {
|
||||
'view': 播放量,
|
||||
'like': 点赞数,
|
||||
'coin': 投币数,
|
||||
'favorite': 收藏数,
|
||||
'share': 分享数,
|
||||
'danmaku': 弹幕数,
|
||||
'reply': 评论数,
|
||||
},
|
||||
'subtitle': '字幕内容(如果有)',
|
||||
'description': '视频描述',
|
||||
}
|
||||
```
|
||||
|
||||
#### 配置支持
|
||||
- ✅ 使用配置中的 `bilibili_headless`(默认 True)
|
||||
- ✅ 使用配置中的 `bilibili_timeout`(默认 30 秒)
|
||||
|
||||
### 3. 提取策略
|
||||
|
||||
采用双策略提取方案:
|
||||
|
||||
1. **API优先策略**(推荐)
|
||||
- 直接调用 B站 API:`https://api.bilibili.com/x/web-interface/view`
|
||||
- 优点:快速、稳定、资源消耗少
|
||||
- 缺点:可能受 API 限制影响
|
||||
|
||||
2. **页面备用策略**
|
||||
- 使用 Playwright 访问完整页面
|
||||
- 支持多种提取方法:
|
||||
- JavaScript 变量提取(`window.__INITIAL_STATE__`)
|
||||
- HTML 元素提取
|
||||
- API 回退
|
||||
- 支持 stealth 模式(可配置)
|
||||
|
||||
### 4. Playwright 集成
|
||||
|
||||
- 使用 Playwright 的 `async_api` 实现异步操作
|
||||
- 支持无头模式(headless)和有头模式
|
||||
- 配置了反检测参数(User-Agent、Referer、Locale 等)
|
||||
- 可选 stealth 模式(使用 `undetected-playwright`)
|
||||
|
||||
## 测试结果
|
||||
|
||||
### 测试用例
|
||||
|
||||
使用真实视频 BV号 `BV1MnFNz5EDL` 进行测试:
|
||||
|
||||
#### 测试 1:BV号格式
|
||||
```
|
||||
输入: BV1MnFNz5EDL
|
||||
结果: ✅ 成功
|
||||
```
|
||||
|
||||
#### 测试 2:完整URL格式
|
||||
```
|
||||
输入: https://www.bilibili.com/video/BV1MnFNz5EDL
|
||||
结果: ✅ 成功
|
||||
```
|
||||
|
||||
### 提取结果示例
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"title": "【老戴】用了几天 Clawdbot,我最担心的事开始发生了",
|
||||
"author": "老戴Donald",
|
||||
"bvid": "BV1MnFNz5EDL",
|
||||
"aid": 116000406116354,
|
||||
"duration": "12:38",
|
||||
"pubtime": "2026-02-02 17:49:13",
|
||||
"tags": "",
|
||||
"mid": 3493075691243699
|
||||
},
|
||||
"stats": {
|
||||
"view": 59074,
|
||||
"like": 3292,
|
||||
"coin": 2354,
|
||||
"favorite": 1692,
|
||||
"share": 587,
|
||||
"danmaku": 482,
|
||||
"reply": 506
|
||||
},
|
||||
"subtitle": "",
|
||||
"description": "用了几天 Clawdbot——是的,它很不稳定,很不成熟,很糙。\n我关注的并不是它能干多少事,而是:\n当未来 AI 的能力和资源不可避免地走向集中,\n数据的主权,还会不会掌握在大众手里。\n如果垄断不可避免,开源可能就是最后的防线。"
|
||||
}
|
||||
```
|
||||
|
||||
## 代码关键部分
|
||||
|
||||
### 1. 类定义和初始化
|
||||
|
||||
```python
|
||||
class BilibiliExtractor(Extractor):
|
||||
"""B站视频信息提取器"""
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.config = config
|
||||
self.headless = getattr(config, 'bilibili_headless', True)
|
||||
self.timeout = getattr(config, 'bilibili_timeout', 30) * 1000
|
||||
```
|
||||
|
||||
### 2. extract 方法实现
|
||||
|
||||
```python
|
||||
def extract(self, source_id: str) -> Dict[str, Any]:
|
||||
"""提取视频信息"""
|
||||
try:
|
||||
# 解析 source_id 获取 BV 号
|
||||
bvid = self._parse_source_id(source_id)
|
||||
if not bvid:
|
||||
raise ExtractionError(f"无法解析 source_id: {source_id}", source_id)
|
||||
|
||||
# 运行异步提取
|
||||
result = asyncio.run(self._extract_async(bvid))
|
||||
|
||||
if not result:
|
||||
raise ExtractionError(f"提取失败: {source_id}", source_id)
|
||||
|
||||
return result
|
||||
|
||||
except ExtractionError:
|
||||
raise
|
||||
except Exception as e:
|
||||
self._handle_error(e, source_id)
|
||||
```
|
||||
|
||||
### 3. 双策略提取
|
||||
|
||||
```python
|
||||
async def _extract_video_info(self, bvid: str) -> Dict[str, Any]:
|
||||
# 优先使用 API 方法(更快速、更稳定)
|
||||
try:
|
||||
video_info = await self._extract_from_api(bvid)
|
||||
if video_info and video_info.get('title'):
|
||||
print(f"✓ API 提取成功")
|
||||
subtitles = await self._extract_subtitles(bvid)
|
||||
return self._build_result(video_info, subtitles)
|
||||
except Exception as e:
|
||||
print(f"API 提取失败,尝试页面提取: {e}")
|
||||
|
||||
# 如果 API 失败,尝试访问页面
|
||||
# ... 页面提取逻辑
|
||||
```
|
||||
|
||||
### 4. source_id 解析
|
||||
|
||||
```python
|
||||
def _parse_source_id(self, source_id: str) -> Optional[str]:
|
||||
"""解析 source_id 获取 BV 号"""
|
||||
source_id = source_id.strip()
|
||||
|
||||
# 尝试匹配完整URL
|
||||
url_match = self.URL_PATTERN.search(source_id)
|
||||
if url_match:
|
||||
return url_match.group(1)
|
||||
|
||||
# 尝试匹配BV号
|
||||
bvid_match = self.BVID_PATTERN.search(source_id)
|
||||
if bvid_match:
|
||||
return bvid_match.group(0)
|
||||
|
||||
return None
|
||||
```
|
||||
|
||||
## 依赖安装
|
||||
|
||||
```bash
|
||||
# 创建虚拟环境
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# 安装依赖
|
||||
pip install playwright undetected-playwright beautifulsoup4 lxml pyyaml
|
||||
|
||||
# 安装 Playwright 浏览器
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
## 文件清单
|
||||
|
||||
1. ✅ `/home/admin/.openclaw/workspace/skills/obsidian-kb/src/extractors/bilibili_extractor.py` - B站提取器实现
|
||||
2. ✅ 测试脚本:
|
||||
- `test_playwright.py` - Playwright 基本功能测试
|
||||
- `test_bilibili_api.py` - B站 API 测试
|
||||
- `test_api_only.py` - API 提取测试
|
||||
- `simple_test2.py` - 简单提取器测试
|
||||
- `complete_test.py` - 完整测试
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. **短链接解析**:实现 `b23.tv` 短链接解析功能
|
||||
2. **字幕提取优化**:处理需要权限的字幕
|
||||
3. **错误处理增强**:添加更多异常处理和重试机制
|
||||
4. **Stealth 模式**:如需更强的反爬虫能力,可以启用 stealth 模式
|
||||
5. **性能优化**:缓存机制、并发请求等
|
||||
|
||||
## 总结
|
||||
|
||||
Checkpoint 3 已成功完成!B站提取器已实现并测试通过,可以正常提取视频元数据、统计数据和描述信息。采用 API 优先策略,速度快且稳定;同时保留页面备用策略,确保可靠性。
|
||||
@@ -1,16 +0,0 @@
|
||||
# B站登录配置
|
||||
# 复制此文件为 bilibili_config.py 并填入你的 SESSDATA
|
||||
|
||||
# 从浏览器获取 SESSDATA 的步骤:
|
||||
# 1. 打开浏览器,登录 B站
|
||||
# 2. 按 F12 打开开发者工具
|
||||
# 3. 进入 Network(网络)标签
|
||||
# 4. 刷新页面,随便点一个请求
|
||||
# 5. 找到 Request Headers 里的 Cookie
|
||||
# 6. 复制 SESSDATA=xxxxxxxxx 这段(包括 SESSDATA=)
|
||||
|
||||
# 填入你的 SESSDATA(不要提交到 Git!)
|
||||
SESSDATA = "your_sessdata_here"
|
||||
|
||||
# 或者设置为 None 表示不登录
|
||||
# SESSDATA = None
|
||||
@@ -1,621 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
B站视频内容提取器 - 反爬虫解决方案
|
||||
|
||||
工具选择:Playwright + undetected-playwright (stealth_async)
|
||||
选择理由:
|
||||
1. Playwright 比 Selenium 更现代,支持更好的异步操作
|
||||
2. undetected-playwright 的 stealth_async 专门用于绕过反爬虫检测
|
||||
3. 可以自动处理极验验证码等反爬虫措施
|
||||
4. 与系统Chromium浏览器兼容性好
|
||||
5. 社区活跃,文档完善
|
||||
|
||||
安装依赖:
|
||||
python3 -m venv bilibili_venv
|
||||
source bilibili_venv/bin/activate
|
||||
pip install playwright undetected-playwright beautifulsoup4 lxml
|
||||
playwright install chromium
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import importlib.util
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from playwright.async_api import async_playwright
|
||||
from bs4 import BeautifulSoup
|
||||
from undetected_playwright import stealth_async
|
||||
|
||||
|
||||
class BilibiliExtractor:
|
||||
"""B站视频信息提取器"""
|
||||
|
||||
def __init__(self, headless=False, sessdata=None):
|
||||
self.headless = headless
|
||||
self.sessdata = sessdata # B站登录凭证
|
||||
self.playwright = None
|
||||
self.browser = None
|
||||
self.context = None
|
||||
self.page = None
|
||||
|
||||
async def init_browser(self):
|
||||
"""初始化浏览器"""
|
||||
print("正在初始化浏览器...")
|
||||
self.playwright = await async_playwright().start()
|
||||
|
||||
self.browser = await self.playwright.chromium.launch(
|
||||
headless=self.headless,
|
||||
args=[
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--disable-dev-shm-usage',
|
||||
'--start-maximized',
|
||||
]
|
||||
)
|
||||
|
||||
# 创建上下文
|
||||
self.context = await self.browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
locale='zh-CN',
|
||||
timezone_id='Asia/Shanghai',
|
||||
extra_http_headers={
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Referer': 'https://www.bilibili.com/',
|
||||
}
|
||||
)
|
||||
|
||||
# 如果提供了 SESSDATA,添加到 Cookie
|
||||
if self.sessdata:
|
||||
await self.context.add_cookies([
|
||||
{
|
||||
'name': 'SESSDATA',
|
||||
'value': self.sessdata,
|
||||
'domain': '.bilibili.com',
|
||||
'path': '/',
|
||||
'httpOnly': True,
|
||||
'secure': True,
|
||||
}
|
||||
])
|
||||
print("✓ 已加载登录凭证 (SESSDATA)")
|
||||
|
||||
# 应用stealth模式(反检测)
|
||||
await stealth_async(self.context)
|
||||
|
||||
self.page = await self.context.new_page()
|
||||
print("浏览器初始化完成!")
|
||||
|
||||
async def extract_video_info(self, bvid):
|
||||
"""提取视频信息"""
|
||||
url = f"https://www.bilibili.com/video/{bvid}"
|
||||
print(f"\n正在访问: {url}")
|
||||
|
||||
try:
|
||||
# 访问页面
|
||||
response = await self.page.goto(url, wait_until='domcontentloaded', timeout=60000)
|
||||
|
||||
print(f"HTTP状态码: {response.status}")
|
||||
|
||||
# 等待页面加载
|
||||
print("等待页面加载...")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# 检查是否出现验证码
|
||||
if await self._check_captcha():
|
||||
print("检测到验证码,等待处理...")
|
||||
await self._handle_captcha()
|
||||
|
||||
# 再次等待
|
||||
await asyncio.sleep(3)
|
||||
|
||||
# 检查页面标题
|
||||
title = await self.page.title()
|
||||
print(f"页面标题: {title}")
|
||||
|
||||
# 提取页面源码
|
||||
content = await self.page.content()
|
||||
print(f"页面内容长度: {len(content)} 字符")
|
||||
|
||||
# 检查是否有错误页面
|
||||
if '错误' in title or '404' in title or '视频不存在' in content:
|
||||
print("⚠️ 检测到错误页面,视频可能不存在或已被删除")
|
||||
return None
|
||||
|
||||
soup = BeautifulSoup(content, 'lxml')
|
||||
|
||||
# 尝试多种方法提取视频信息
|
||||
video_info = await self._extract_from_methods(soup, self.page)
|
||||
|
||||
# 如果还是没有提取到,尝试直接从API获取
|
||||
if not video_info or not video_info.get('title'):
|
||||
print("HTML提取失败,尝试API方法...")
|
||||
video_info = await self._extract_from_api(bvid)
|
||||
|
||||
# 提取字幕
|
||||
subtitles = await self._extract_subtitles()
|
||||
|
||||
return {
|
||||
'video_info': video_info,
|
||||
'subtitles': subtitles
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"提取视频信息时出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
async def _extract_from_methods(self, soup, page):
|
||||
"""使用多种方法提取视频信息"""
|
||||
info = {}
|
||||
|
||||
# 方法1: 从JavaScript变量获取
|
||||
try:
|
||||
print("尝试方法1: 从 window.__INITIAL_STATE__ 获取...")
|
||||
initial_state = await page.evaluate('() => typeof window.__INITIAL_STATE__ !== "undefined" ? window.__INITIAL_STATE__ : null')
|
||||
if initial_state:
|
||||
print("✓ 找到 __INITIAL_STATE__")
|
||||
return self._parse_initial_state(initial_state)
|
||||
else:
|
||||
print("✗ 未找到 __INITIAL_STATE__")
|
||||
except Exception as e:
|
||||
print(f"✗ 方法1失败: {e}")
|
||||
|
||||
# 方法2: 从script标签中提取
|
||||
print("\n尝试方法2: 从script标签提取...")
|
||||
script_tags = soup.find_all('script')
|
||||
for i, script in enumerate(script_tags):
|
||||
if script.string and '__INITIAL_STATE__' in script.string:
|
||||
try:
|
||||
match = re.search(r'window\.__INITIAL_STATE__\s*=\s*({.+?});', script.string)
|
||||
if match:
|
||||
print("✓ 从script标签中找到 __INITIAL_STATE__")
|
||||
data = json.loads(match.group(1))
|
||||
return self._parse_initial_state(data)
|
||||
except Exception as e:
|
||||
print(f"✗ 解析script标签失败: {e}")
|
||||
|
||||
# 方法3: 从HTML元素提取
|
||||
print("\n尝试方法3: 从HTML元素提取...")
|
||||
|
||||
# 标题
|
||||
title_elem = soup.find('h1', class_='video-title')
|
||||
if title_elem:
|
||||
info['title'] = title_elem.get_text(strip=True)
|
||||
print(f"✓ 找到标题: {info['title']}")
|
||||
|
||||
# UP主
|
||||
up_elem = soup.find('a', class_='up-name')
|
||||
if up_elem:
|
||||
info['author'] = up_elem.get_text(strip=True)
|
||||
print(f"✓ 找到UP主: {info['author']}")
|
||||
|
||||
# BV号
|
||||
og_url = soup.find('meta', property='og:url')
|
||||
if og_url:
|
||||
url = og_url.get('content', '')
|
||||
info['bvid'] = url.split('/')[-1] if url else ''
|
||||
print(f"✓ 找到BV号: {info['bvid']}")
|
||||
|
||||
# 从meta标签提取
|
||||
if not info.get('title'):
|
||||
info['title'] = self._safe_extract(soup, 'meta[property="og:title"]', 'content')
|
||||
if not info.get('author'):
|
||||
info['author'] = self._safe_extract(soup, 'meta[property="og:video:author"]', 'content')
|
||||
|
||||
return info
|
||||
|
||||
async def _extract_from_api(self, bvid):
|
||||
"""通过API提取视频信息"""
|
||||
info = {}
|
||||
|
||||
try:
|
||||
print("\n尝试API方法获取视频信息...")
|
||||
|
||||
# B站视频信息API
|
||||
api_url = "https://api.bilibili.com/x/web-interface/view"
|
||||
response = await self.page.request.get(api_url, params={'bvid': bvid})
|
||||
|
||||
if response.ok:
|
||||
data = await response.json()
|
||||
if data.get('code') == 0:
|
||||
video_data = data.get('data', {})
|
||||
print("✓ API调用成功")
|
||||
|
||||
# 基本信息
|
||||
info['title'] = video_data.get('title', '')
|
||||
info['bvid'] = video_data.get('bvid', '')
|
||||
info['aid'] = video_data.get('aid', '')
|
||||
|
||||
# UP主信息
|
||||
owner = video_data.get('owner', {})
|
||||
info['author'] = owner.get('name', '')
|
||||
info['mid'] = owner.get('mid', '')
|
||||
|
||||
# 视频统计
|
||||
stat = video_data.get('stat', {})
|
||||
info['view'] = stat.get('view', 0)
|
||||
info['danmaku'] = stat.get('danmaku', 0)
|
||||
info['reply'] = stat.get('reply', 0)
|
||||
info['favorite'] = stat.get('favorite', 0)
|
||||
info['coin'] = stat.get('coin', 0)
|
||||
info['share'] = stat.get('share', 0)
|
||||
info['like'] = stat.get('like', 0)
|
||||
|
||||
# 时间信息
|
||||
info['pubdate'] = video_data.get('pubdate', 0)
|
||||
info['pubtime'] = datetime.fromtimestamp(info['pubdate']).strftime('%Y-%m-%d %H:%M:%S') if info['pubdate'] else '未知'
|
||||
|
||||
# 时长
|
||||
duration = video_data.get('duration', 0)
|
||||
info['duration'] = self._format_duration(duration)
|
||||
|
||||
# 简介
|
||||
info['desc'] = video_data.get('desc', '')
|
||||
|
||||
# 标签
|
||||
tags = []
|
||||
for tag in video_data.get('tag', []):
|
||||
tags.append(tag.get('tag_name', ''))
|
||||
info['tags'] = ', '.join(tags)
|
||||
|
||||
return info
|
||||
else:
|
||||
print(f"✗ API返回错误: {data.get('message', '未知错误')}")
|
||||
else:
|
||||
print(f"✗ API请求失败: HTTP {response.status}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ API方法失败: {e}")
|
||||
|
||||
return info
|
||||
|
||||
async def _check_captcha(self):
|
||||
"""检查是否出现验证码"""
|
||||
captcha_selectors = [
|
||||
'.captcha-box',
|
||||
'#captcha-box',
|
||||
'[class*="captcha"]',
|
||||
'[class*="geetest"]',
|
||||
]
|
||||
|
||||
for selector in captcha_selectors:
|
||||
try:
|
||||
element = await self.page.query_selector(selector)
|
||||
if element:
|
||||
return True
|
||||
except:
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
async def _handle_captcha(self):
|
||||
"""处理验证码"""
|
||||
print("验证码检测到,等待 5 秒尝试自动绕过...")
|
||||
|
||||
for i in range(5):
|
||||
await asyncio.sleep(1)
|
||||
if not await self._check_captcha():
|
||||
print("验证码已自动处理!")
|
||||
return True
|
||||
|
||||
if not self.headless:
|
||||
print("验证码需要手动处理,请在浏览器中完成...")
|
||||
while await self._check_captcha():
|
||||
await asyncio.sleep(2)
|
||||
print("验证码已完成!")
|
||||
return True
|
||||
else:
|
||||
print("⚠️ Headless模式下无法手动处理验证码")
|
||||
print("建议:重新运行脚本,设置 headless=False")
|
||||
return False
|
||||
|
||||
def _parse_initial_state(self, data):
|
||||
"""解析INITIAL_STATE数据"""
|
||||
info = {}
|
||||
|
||||
try:
|
||||
video_data = data.get('videoData', {})
|
||||
|
||||
# 基本信息
|
||||
info['title'] = video_data.get('title', '')
|
||||
info['bvid'] = video_data.get('bvid', '')
|
||||
info['aid'] = video_data.get('aid', '')
|
||||
|
||||
# UP主信息
|
||||
owner = video_data.get('owner', {})
|
||||
info['author'] = owner.get('name', '')
|
||||
info['mid'] = owner.get('mid', '')
|
||||
|
||||
# 视频统计
|
||||
stat = video_data.get('stat', {})
|
||||
info['view'] = stat.get('view', 0)
|
||||
info['danmaku'] = stat.get('danmaku', 0)
|
||||
info['reply'] = stat.get('reply', 0)
|
||||
info['favorite'] = stat.get('favorite', 0)
|
||||
info['coin'] = stat.get('coin', 0)
|
||||
info['share'] = stat.get('share', 0)
|
||||
info['like'] = stat.get('like', 0)
|
||||
|
||||
# 时间信息
|
||||
info['pubdate'] = video_data.get('pubdate', 0)
|
||||
info['pubtime'] = datetime.fromtimestamp(info['pubdate']).strftime('%Y-%m-%d %H:%M:%S') if info['pubdate'] else '未知'
|
||||
|
||||
# 时长
|
||||
duration = video_data.get('duration', 0)
|
||||
info['duration'] = self._format_duration(duration)
|
||||
|
||||
# 简介
|
||||
info['desc'] = video_data.get('desc', '')
|
||||
|
||||
# 标签
|
||||
tags = []
|
||||
for tag in video_data.get('tag', []):
|
||||
tags.append(tag.get('tag_name', ''))
|
||||
info['tags'] = ', '.join(tags)
|
||||
|
||||
except Exception as e:
|
||||
print(f"解析INITIAL_STATE时出错: {e}")
|
||||
|
||||
return info
|
||||
|
||||
def _format_duration(self, seconds):
|
||||
"""格式化时长"""
|
||||
if not seconds:
|
||||
return "未知"
|
||||
|
||||
hours = seconds // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
secs = seconds % 60
|
||||
|
||||
if hours > 0:
|
||||
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
|
||||
else:
|
||||
return f"{minutes:02d}:{secs:02d}"
|
||||
|
||||
async def _extract_subtitles(self):
|
||||
"""提取视频字幕"""
|
||||
subtitles = []
|
||||
|
||||
try:
|
||||
# 尝试通过API获取
|
||||
bvid = await self.page.evaluate('() => window.__INITIAL_STATE__?.videoData?.bvid')
|
||||
aid = await self.page.evaluate('() => window.__INITIAL_STATE__?.videoData?.aid')
|
||||
|
||||
if bvid and aid:
|
||||
print(f"正在获取字幕 (aid={aid})...")
|
||||
subtitles = await self._fetch_subtitle_api(aid, bvid)
|
||||
except Exception as e:
|
||||
print(f"提取字幕时出错: {e}")
|
||||
|
||||
return subtitles
|
||||
|
||||
async def _fetch_subtitle_api(self, aid, bvid):
|
||||
"""通过API获取字幕"""
|
||||
try:
|
||||
api_url = "https://api.bilibili.com/x/player/v2"
|
||||
response = await self.page.request.get(
|
||||
api_url,
|
||||
params={'aid': aid, 'bvid': bvid}
|
||||
)
|
||||
|
||||
if response.ok:
|
||||
data = await response.json()
|
||||
if data.get('code') == 0:
|
||||
subtitle_info = data.get('data', {}).get('subtitle', {})
|
||||
subtitles_list = subtitle_info.get('subtitles', [])
|
||||
|
||||
if subtitles_list:
|
||||
print(f"找到 {len(subtitles_list)} 个字幕")
|
||||
# 获取第一个字幕
|
||||
subtitle_url = subtitles_list[0].get('subtitle_url', '')
|
||||
if subtitle_url:
|
||||
full_url = f"https:{subtitle_url}"
|
||||
print(f"下载字幕: {full_url}")
|
||||
return await self._fetch_subtitle_content(full_url)
|
||||
except Exception as e:
|
||||
print(f"通过API获取字幕失败: {e}")
|
||||
|
||||
return []
|
||||
|
||||
async def _fetch_subtitle_content(self, url):
|
||||
"""获取字幕内容"""
|
||||
try:
|
||||
response = await self.page.request.get(url)
|
||||
if response.ok:
|
||||
data = await response.json()
|
||||
return self._parse_subtitle_json(data)
|
||||
except Exception as e:
|
||||
print(f"获取字幕内容失败: {e}")
|
||||
return []
|
||||
|
||||
def _parse_subtitle_json(self, data):
|
||||
"""解析字幕JSON"""
|
||||
subtitles = []
|
||||
|
||||
try:
|
||||
body = data.get('body', [])
|
||||
for item in body:
|
||||
from_time = item.get('from', 0)
|
||||
to_time = item.get('to', 0)
|
||||
content = item.get('content', '')
|
||||
|
||||
subtitles.append({
|
||||
'from': self._format_timestamp(from_time),
|
||||
'to': self._format_timestamp(to_time),
|
||||
'content': content
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"解析字幕JSON失败: {e}")
|
||||
|
||||
return subtitles
|
||||
|
||||
def _format_timestamp(self, seconds):
|
||||
"""格式化时间戳"""
|
||||
minutes = int(seconds // 60)
|
||||
secs = int(seconds % 60)
|
||||
return f"{minutes:02d}:{secs:02d}"
|
||||
|
||||
def _safe_extract(self, soup, selector, attr):
|
||||
"""安全提取元素属性"""
|
||||
try:
|
||||
elem = soup.select_one(selector)
|
||||
if elem:
|
||||
return elem.get(attr, '').strip()
|
||||
except:
|
||||
pass
|
||||
return ''
|
||||
|
||||
def generate_markdown(self, data, output_path):
|
||||
"""生成Markdown笔记"""
|
||||
if not data or not data.get('video_info'):
|
||||
print("没有视频数据可生成笔记")
|
||||
return False
|
||||
|
||||
info = data['video_info']
|
||||
subtitles = data.get('subtitles', [])
|
||||
|
||||
md_content = []
|
||||
md_content.append(f"# {info.get('title', '未知标题')}\n")
|
||||
md_content.append(f"**视频链接**: https://www.bilibili.com/video/{info.get('bvid', '')}\n")
|
||||
md_content.append("---\n\n")
|
||||
|
||||
# 基本信息
|
||||
md_content.append("## 基本信息\n\n")
|
||||
md_content.append(f"- **标题**: {info.get('title', '未知')}\n")
|
||||
md_content.append(f"- **UP主**: {info.get('author', '未知')}\n")
|
||||
md_content.append(f"- **BV号**: {info.get('bvid', '未知')}\n")
|
||||
md_content.append(f"- **时长**: {info.get('duration', '未知')}\n")
|
||||
md_content.append(f"- **发布时间**: {info.get('pubtime', '未知')}\n")
|
||||
|
||||
# 统计数据
|
||||
md_content.append("\n## 数据统计\n\n")
|
||||
md_content.append(f"- **播放量**: {info.get('view', 0):,}\n")
|
||||
md_content.append(f"- **点赞**: {info.get('like', 0):,}\n")
|
||||
md_content.append(f"- **投币**: {info.get('coin', 0):,}\n")
|
||||
md_content.append(f"- **收藏**: {info.get('favorite', 0):,}\n")
|
||||
md_content.append(f"- **分享**: {info.get('share', 0):,}\n")
|
||||
md_content.append(f"- **弹幕**: {info.get('danmaku', 0):,}\n")
|
||||
md_content.append(f"- **评论**: {info.get('reply', 0):,}\n")
|
||||
|
||||
# 标签
|
||||
if info.get('tags'):
|
||||
md_content.append(f"\n## 标签\n\n{info['tags']}\n")
|
||||
|
||||
# 简介
|
||||
if info.get('desc'):
|
||||
md_content.append(f"\n## 简介\n\n{info['desc']}\n")
|
||||
|
||||
# 字幕
|
||||
if subtitles:
|
||||
md_content.append("\n## 视频字幕\n\n")
|
||||
for sub in subtitles[:100]: # 限制字幕数量
|
||||
md_content.append(f"**{sub['from']} - {sub['to']}**: {sub['content']}\n\n")
|
||||
if len(subtitles) > 100:
|
||||
md_content.append(f"\n*(仅显示前100条字幕,共{len(subtitles)}条)*\n")
|
||||
|
||||
md_content.append("\n---\n")
|
||||
md_content.append(f"*笔记生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n")
|
||||
|
||||
# 写入文件
|
||||
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(md_content)
|
||||
|
||||
print(f"\n✅ Markdown笔记已保存到: {output_path}")
|
||||
return True
|
||||
|
||||
async def close(self):
|
||||
"""关闭浏览器"""
|
||||
if self.playwright:
|
||||
await self.playwright.stop()
|
||||
print("浏览器已关闭")
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
bvid = "BV1MnFNz5EDL"
|
||||
output_file = "/home/admin/.openclaw/workspace/notes/用了几天_Clawdbot_我最担心的事开始发生了.md"
|
||||
|
||||
# 尝试加载登录配置
|
||||
sessdata = None
|
||||
try:
|
||||
import sys
|
||||
config_path = Path(__file__).parent / 'bilibili_config.py'
|
||||
if config_path.exists():
|
||||
spec = importlib.util.spec_from_file_location("bilibili_config", config_path)
|
||||
bilibili_config = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(bilibili_config)
|
||||
sessdata = getattr(bilibili_config, 'SESSDATA', None)
|
||||
if sessdata:
|
||||
print("✓ 已加载登录配置")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 加载登录配置失败: {e}")
|
||||
|
||||
# 创建提取器(True=无头模式,False=显示浏览器窗口)
|
||||
# 无头模式更快,但如果遇到验证码可能无法处理
|
||||
extractor = BilibiliExtractor(headless=True, sessdata=sessdata)
|
||||
|
||||
try:
|
||||
await extractor.init_browser()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"开始提取视频: {bvid}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
data = await extractor.extract_video_info(bvid)
|
||||
|
||||
if data and data.get('video_info'):
|
||||
info = data['video_info']
|
||||
print(f"\n{'='*60}")
|
||||
print("视频信息提取成功!")
|
||||
print(f"{'='*60}")
|
||||
print(f"标题: {info.get('title', '未知')}")
|
||||
print(f"UP主: {info.get('author', '未知')}")
|
||||
print(f"时长: {info.get('duration', '未知')}")
|
||||
print(f"播放量: {info.get('view', 0):,}")
|
||||
print(f"点赞: {info.get('like', 0):,}")
|
||||
|
||||
# 生成Markdown笔记
|
||||
extractor.generate_markdown(data, output_file)
|
||||
|
||||
# 打印预览
|
||||
print(f"\n{'='*60}")
|
||||
print("Markdown笔记预览 (前50行):")
|
||||
print(f"{'='*60}")
|
||||
try:
|
||||
with open(output_file, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
for line in lines[:50]:
|
||||
print(line.rstrip())
|
||||
if len(lines) > 50:
|
||||
print(f"\n... (共 {len(lines)} 行)")
|
||||
except Exception as e:
|
||||
print(f"无法读取文件: {e}")
|
||||
else:
|
||||
print("\n❌ 提取视频信息失败")
|
||||
print("可能原因:")
|
||||
print("1. 视频不存在或已被删除")
|
||||
print("2. 触发了验证码且未完成")
|
||||
print("3. 网络问题")
|
||||
print("\n建议:")
|
||||
print("- 检查视频链接是否正确")
|
||||
print("- 如果出现验证码,请手动完成")
|
||||
print("- 尝试重新运行脚本")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⏹️ 用户中断")
|
||||
except Exception as e:
|
||||
print(f"\n❌ 发生错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
await extractor.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
完整测试 B站提取器
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# 添加 src 目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent / 'skills' / 'obsidian-kb' / 'src'))
|
||||
|
||||
from extractors.bilibili_extractor import BilibiliExtractor
|
||||
|
||||
|
||||
def main():
|
||||
"""测试主函数"""
|
||||
print("开始完整测试 B站提取器...\n")
|
||||
|
||||
# 创建提取器(不使用配置,使用默认值)
|
||||
extractor = BilibiliExtractor(config=None)
|
||||
|
||||
# 测试不同的输入格式
|
||||
test_cases = [
|
||||
'BV1MnFNz5EDL', # BV号
|
||||
'https://www.bilibili.com/video/BV1MnFNz5EDL', # 完整URL
|
||||
]
|
||||
|
||||
for i, source_id in enumerate(test_cases, 1):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"测试 {i}: {source_id}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
try:
|
||||
# 解析 source_id
|
||||
bvid = extractor._parse_source_id(source_id)
|
||||
print(f"✓ 解析 BV 号: {bvid}")
|
||||
|
||||
# 提取信息
|
||||
result = extractor.extract(source_id)
|
||||
|
||||
print("\n✅ 提取成功!\n")
|
||||
|
||||
# 元数据
|
||||
print("📋 元数据:")
|
||||
metadata = result['metadata']
|
||||
for key in ['title', 'author', 'bvid', 'duration', 'pubtime']:
|
||||
print(f" {key}: {metadata.get(key, '')}")
|
||||
|
||||
# 统计信息
|
||||
print("\n📊 统计信息:")
|
||||
stats = result['stats']
|
||||
for key in ['view', 'like', 'coin', 'favorite']:
|
||||
value = stats.get(key, 0)
|
||||
print(f" {key}: {value:,}")
|
||||
|
||||
# 描述
|
||||
print("\n📝 描述:")
|
||||
description = result['description']
|
||||
if description:
|
||||
print(f" {description[:100]}...")
|
||||
else:
|
||||
print(" (无描述)")
|
||||
|
||||
# 字幕
|
||||
print("\n📺 字幕:")
|
||||
subtitle = result['subtitle']
|
||||
if subtitle:
|
||||
lines = subtitle.split('\n')
|
||||
print(f" 共 {len(lines)} 条字幕")
|
||||
else:
|
||||
print(" (无字幕)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("测试完成")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,59 +0,0 @@
|
||||
# B站视频笔记
|
||||
|
||||
## 视频基本信息
|
||||
|
||||
**标题**: Qwen3-TTS上新,支持跨物种克隆音色!
|
||||
|
||||
**UP主**: 通义大模型 (ID: 3546585589877706)
|
||||
|
||||
**视频ID**: BV1bSBMBvENj
|
||||
|
||||
**时长**: 3分19秒
|
||||
|
||||
**发布时间**: 2025-12-23 21:33:06
|
||||
|
||||
**视频链接**: https://www.bilibili.com/video/BV1bSBMBvENj
|
||||
|
||||
---
|
||||
|
||||
## 视频简介
|
||||
|
||||
Qwen3-TTS 重磅上新:VoiceDesign & VoiceClone!
|
||||
🎙️ 更快的速度,更强的表现力,重新定义语音合成。
|
||||
✨ VoiceDesign (VD-Flash):
|
||||
全文本控制: 通过文字指令调整语气、节奏和情感,拒绝千篇一律的预设音色。
|
||||
性能卓越: Role-play 表现超越 GPT-4o-mini & Gemini-2.5-pro。
|
||||
⚡ VoiceClone (VC-Flash):
|
||||
极速复刻: 仅需 3 秒音频即可克隆任意声音。
|
||||
多语种支持: 支持中英日等 10 种语言,多语言准确率优于 ElevenLabs。
|
||||
立即体验 👉阿里云百炼
|
||||
|
||||
---
|
||||
|
||||
## 统计数据
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 播放量 | 58,106 |
|
||||
| 点赞 | 2,053 |
|
||||
| 投币 | 265 |
|
||||
| 收藏 | 2,087 |
|
||||
| 分享 | 571 |
|
||||
| 弹幕 | 100 |
|
||||
| 评论 | 288 |
|
||||
|
||||
---
|
||||
|
||||
## 字幕
|
||||
|
||||
> 注意: 由于B站限制,需要登录凭据才能获取字幕。当前未获取到字幕内容。
|
||||
|
||||
---
|
||||
|
||||
## 笔记生成时间
|
||||
|
||||
2026-02-07 13:26:36
|
||||
|
||||
---
|
||||
|
||||
*本笔记由 bilibili_extractor 自动生成*
|
||||
@@ -1,33 +0,0 @@
|
||||
# 时代真变了!glm4.7+playwright cli, AI自主进行自动化测试
|
||||
**视频链接**: https://www.bilibili.com/video/BV11t64BJEDZ
|
||||
---
|
||||
|
||||
## 基本信息
|
||||
|
||||
- **标题**: 时代真变了!glm4.7+playwright cli, AI自主进行自动化测试
|
||||
- **UP主**: 录教程的乙醇啦
|
||||
- **BV号**: BV11t64BJEDZ
|
||||
- **时长**: 04:46
|
||||
- **发布时间**: 2026-02-01 18:39:58
|
||||
|
||||
## 数据统计
|
||||
|
||||
- **播放量**: 969
|
||||
- **点赞**: 22
|
||||
- **投币**: 10
|
||||
- **收藏**: 72
|
||||
- **分享**: 15
|
||||
- **弹幕**: 0
|
||||
- **评论**: 9
|
||||
|
||||
## 简介
|
||||
|
||||
Playwright最新推出了playwright cli工具和相应的skills,可以适配大部分的AI coding agent。
|
||||
这是1个官方提供的轻量级命令行工具,主要用来在终端直接控制浏览器执行自动化操作,而不需要自己写任何一行代码。
|
||||
简单来说,它就是把 Playwright 最常用的几十个操作封装成了命令行指令,让AI agent可以用非常简短的命令来操控浏览器。
|
||||
经过简单的试用,我发现时代真的变了,现在让agent干活基本就是几行像敲命令一样的短句,token省一大堆,调试也不用自己亲自动手。
|
||||
估计80%简单的日常网页任务都可以直接搞定。
|
||||
下面来看一下Playwright cli的真正实力吧。
|
||||
|
||||
---
|
||||
*笔记生成时间: 2026-02-07 09:16:34*
|
||||
@@ -1,32 +0,0 @@
|
||||
# 【老戴】用了几天 Clawdbot,我最担心的事开始发生了
|
||||
**视频链接**: https://www.bilibili.com/video/BV1MnFNz5EDL
|
||||
---
|
||||
|
||||
## 基本信息
|
||||
|
||||
- **标题**: 【老戴】用了几天 Clawdbot,我最担心的事开始发生了
|
||||
- **UP主**: 老戴Donald
|
||||
- **BV号**: BV1MnFNz5EDL
|
||||
- **时长**: 12:38
|
||||
- **发布时间**: 2026-02-02 17:49:13
|
||||
|
||||
## 数据统计
|
||||
|
||||
- **播放量**: 58,530
|
||||
- **点赞**: 3,270
|
||||
- **投币**: 2,344
|
||||
- **收藏**: 1,680
|
||||
- **分享**: 580
|
||||
- **弹幕**: 480
|
||||
- **评论**: 500
|
||||
|
||||
## 简介
|
||||
|
||||
用了几天 Clawdbot——是的,它很不稳定,很不成熟,很糙。
|
||||
我关注的并不是它能干多少事,而是:
|
||||
当未来 AI 的能力和资源不可避免地走向集中,
|
||||
数据的主权,还会不会掌握在大众手里。
|
||||
如果垄断不可避免,开源可能就是最后的防线。
|
||||
|
||||
---
|
||||
*笔记生成时间: 2026-02-07 09:58:39*
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
简单的 B站提取器测试
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# 添加 src 目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent / 'skills' / 'obsidian-kb' / 'src'))
|
||||
|
||||
from extractors.bilibili_extractor import BilibiliExtractor
|
||||
|
||||
|
||||
def main():
|
||||
"""测试主函数"""
|
||||
print("开始测试 B站提取器...")
|
||||
|
||||
# 创建提取器(不使用配置,使用默认值)
|
||||
extractor = BilibiliExtractor(config=None)
|
||||
|
||||
print(f"提取器配置: headless={extractor.headless}, timeout={extractor.timeout}")
|
||||
|
||||
# 测试 BV号
|
||||
source_id = 'BV1MnFNz5EDL'
|
||||
|
||||
print(f"\n正在提取: {source_id}")
|
||||
|
||||
try:
|
||||
result = extractor.extract(source_id)
|
||||
|
||||
print("\n✅ 提取成功!\n")
|
||||
|
||||
# 打印结果
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 提取失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试 B站提取器(简化版,只使用 API)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# 添加 src 目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent / 'skills' / 'obsidian-kb' / 'src'))
|
||||
|
||||
from extractors.bilibili_extractor import BilibiliExtractor
|
||||
|
||||
|
||||
def main():
|
||||
"""测试主函数"""
|
||||
print("开始测试 B站提取器...")
|
||||
|
||||
# 创建提取器(不使用配置,使用默认值)
|
||||
extractor = BilibiliExtractor(config=None)
|
||||
|
||||
print(f"提取器配置: headless={extractor.headless}, timeout={extractor.timeout}")
|
||||
|
||||
# 测试 BV号
|
||||
source_id = 'BV1MnFNz5EDL'
|
||||
|
||||
print(f"\n正在提取: {source_id}")
|
||||
|
||||
try:
|
||||
# 首先测试 source_id 解析
|
||||
bvid = extractor._parse_source_id(source_id)
|
||||
print(f"解析 BV 号: {bvid}")
|
||||
|
||||
# 直接测试 API 方法
|
||||
print("\n测试 API 提取...")
|
||||
result = extractor.extract(source_id)
|
||||
|
||||
print("\n✅ 提取成功!\n")
|
||||
|
||||
# 打印结果
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 提取失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试 B站提取器 API 方法
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
# 添加 src 目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent / 'skills' / 'obsidian-kb' / 'src'))
|
||||
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
|
||||
async def test_api_extraction():
|
||||
"""测试 API 提取方法"""
|
||||
print("开始测试 B站 API 提取...")
|
||||
|
||||
playwright = await async_playwright().start()
|
||||
|
||||
print("Playwright 启动成功")
|
||||
|
||||
browser = await playwright.chromium.launch(
|
||||
headless=True,
|
||||
args=['--no-sandbox', '--disable-setuid-sandbox']
|
||||
)
|
||||
|
||||
print("浏览器启动成功")
|
||||
|
||||
page = await browser.new_page()
|
||||
|
||||
print("页面创建成功")
|
||||
|
||||
# 提取视频信息
|
||||
api_url = "https://api.bilibili.com/x/web-interface/view"
|
||||
bvid = "BV1MnFNz5EDL"
|
||||
|
||||
print(f"\n调用 API: {api_url}?bvid={bvid}")
|
||||
|
||||
response = await page.request.get(api_url, params={'bvid': bvid})
|
||||
|
||||
print(f"API 响应状态: {response.status}")
|
||||
|
||||
if response.ok:
|
||||
data = await response.json()
|
||||
if data.get('code') == 0:
|
||||
video_data = data.get('data', {})
|
||||
|
||||
# 解析视频数据
|
||||
info = {
|
||||
'title': video_data.get('title', ''),
|
||||
'bvid': video_data.get('bvid', ''),
|
||||
'aid': video_data.get('aid', ''),
|
||||
'author': video_data.get('owner', {}).get('name', ''),
|
||||
'duration': video_data.get('duration', 0),
|
||||
'desc': video_data.get('desc', ''),
|
||||
}
|
||||
|
||||
# 统计数据
|
||||
stat = video_data.get('stat', {})
|
||||
stats = {
|
||||
'view': stat.get('view', 0),
|
||||
'like': stat.get('like', 0),
|
||||
'coin': stat.get('coin', 0),
|
||||
'favorite': stat.get('favorite', 0),
|
||||
}
|
||||
|
||||
print("\n✅ 提取成功!\n")
|
||||
print("视频信息:")
|
||||
print(json.dumps(info, indent=2, ensure_ascii=False))
|
||||
print("\n统计数据:")
|
||||
print(json.dumps(stats, indent=2, ensure_ascii=False))
|
||||
|
||||
else:
|
||||
print(f"API 返回错误: {data.get('message', '未知错误')}")
|
||||
else:
|
||||
print(f"API 请求失败: HTTP {response.status}")
|
||||
|
||||
await browser.close()
|
||||
await playwright.stop()
|
||||
|
||||
print("\n测试完成")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(test_api_extraction())
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试原始 bilibili_extractor.py
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, '/home/admin/.openclaw/workspace')
|
||||
|
||||
import asyncio
|
||||
from bilibili_extractor import BilibiliExtractor
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
bvid = "BV1MnFNz5EDL"
|
||||
|
||||
# 创建提取器(无头模式)
|
||||
extractor = BilibiliExtractor(headless=True)
|
||||
|
||||
try:
|
||||
await extractor.init_browser()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"开始提取视频: {bvid}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
data = await extractor.extract_video_info(bvid)
|
||||
|
||||
if data and data.get('video_info'):
|
||||
info = data['video_info']
|
||||
print(f"\n{'='*60}")
|
||||
print("视频信息提取成功!")
|
||||
print(f"{'='*60}")
|
||||
print(f"标题: {info.get('title', '未知')}")
|
||||
print(f"UP主: {info.get('author', '未知')}")
|
||||
print(f"时长: {info.get('duration', '未知')}")
|
||||
print(f"播放量: {info.get('view', 0):,}")
|
||||
print(f"点赞: {info.get('like', 0):,}")
|
||||
|
||||
subtitles = data.get('subtitles', [])
|
||||
print(f"\n字幕数量: {len(subtitles)}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print("\n❌ 提取视频信息失败")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 发生错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
finally:
|
||||
await extractor.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
result = asyncio.run(main())
|
||||
sys.exit(0 if result else 1)
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试 Playwright 基本功能
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
|
||||
async def test_playwright():
|
||||
"""测试 Playwright"""
|
||||
print("开始测试 Playwright...")
|
||||
|
||||
playwright = await async_playwright().start()
|
||||
|
||||
print("Playwright 启动成功")
|
||||
|
||||
browser = await playwright.chromium.launch(
|
||||
headless=True,
|
||||
args=['--no-sandbox', '--disable-setuid-sandbox']
|
||||
)
|
||||
|
||||
print("浏览器启动成功")
|
||||
|
||||
page = await browser.new_page()
|
||||
|
||||
print("页面创建成功")
|
||||
|
||||
await page.goto('https://www.baidu.com', timeout=30000)
|
||||
|
||||
print("页面加载成功")
|
||||
|
||||
title = await page.title()
|
||||
|
||||
print(f"页面标题: {title}")
|
||||
|
||||
await browser.close()
|
||||
await playwright.stop()
|
||||
|
||||
print("测试完成")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(test_playwright())
|
||||
Reference in New Issue
Block a user