添加学习教程目录(learning/)及对应单元测试
- 新增 DrissionPage 基础教程(01-05) - 新增 Playwright 基础教程(01-05) - 新增网络基础教程(01-05) - 新增 test_learning_examples.py 单元测试 - 更新 .gitignore 忽略 learning/*/output/ 目录
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
练习 01: 用 DrissionPage 打开一个网页。
|
||||
|
||||
目标:
|
||||
1. 自己导入 `ChromiumPage`
|
||||
2. 创建浏览器页面对象
|
||||
3. 打开一个 URL
|
||||
4. 打印当前页面标题或 URL
|
||||
|
||||
建议:
|
||||
- 第一版先不要封装函数
|
||||
- 能打开页面就算过关
|
||||
- 如果你想停住窗口,可以在最后加 `input()`
|
||||
|
||||
运行:
|
||||
./.venv/bin/python learning/drissionpage_basics/01_open_page.py
|
||||
"""
|
||||
|
||||
DEFAULT_URL = "https://www.douyin.com/"
|
||||
|
||||
def main() -> None:
|
||||
from DrissionPage import ChromiumPage
|
||||
|
||||
page = ChromiumPage()
|
||||
page.get(DEFAULT_URL)
|
||||
print(f"[INFO] 已打开 URL: {page.url}")
|
||||
print(f"[INFO] 页面标题: {page.title}")
|
||||
input("按回车退出...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
练习 02: 附着到已经启动的 Chrome 调试端口。
|
||||
|
||||
前置:
|
||||
1. 先执行 `./.venv/bin/python login_douyin.py --browser-port 9223`
|
||||
2. 在打开的浏览器里完成登录
|
||||
|
||||
目标:
|
||||
1. 自己导入 `ChromiumPage` 和 `ChromiumOptions`
|
||||
2. 通过 `127.0.0.1:9223` 附着到已启动浏览器
|
||||
3. 打印当前页面 URL 或标题
|
||||
4. 手动确认它附着的是你刚才登录的那个浏览器
|
||||
|
||||
运行:
|
||||
./.venv/bin/python learning/drissionpage_basics/02_attach_browser.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from Douyin import ensure_browser_debug_port_ready
|
||||
|
||||
DEFAULT_BROWSER_PORT = 9223
|
||||
|
||||
|
||||
def build_browser_address(browser_port: int) -> str:
|
||||
return f"127.0.0.1:{browser_port}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
from DrissionPage import ChromiumOptions
|
||||
from DrissionPage import ChromiumPage
|
||||
|
||||
browser_address = build_browser_address(DEFAULT_BROWSER_PORT)
|
||||
ensure_browser_debug_port_ready(DEFAULT_BROWSER_PORT)
|
||||
|
||||
options = ChromiumOptions().set_address(browser_address).existing_only(True)
|
||||
page = ChromiumPage(options)
|
||||
print(f"[INFO] 已附着浏览器: {browser_address}")
|
||||
print(f"[INFO] 当前 URL: {page.url}")
|
||||
print(f"[INFO] 页面标题: {page.title}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
练习 03: 监听抖音作品接口。
|
||||
|
||||
前置:
|
||||
1. 先执行 `./.venv/bin/python login_douyin.py --browser-port 9223`
|
||||
2. 在浏览器里完成登录
|
||||
3. 打开某个抖音博主主页
|
||||
|
||||
目标:
|
||||
1. 附着到浏览器
|
||||
2. 调用 `page.listen.start(...)`
|
||||
3. 等待一个接口包
|
||||
4. 打印 `packet`、`packet.response`、`packet.response.body` 的类型或部分内容
|
||||
|
||||
建议:
|
||||
- 第一版不要急着做完整解析
|
||||
- 先看清楚监听回来的对象长什么样
|
||||
- 如果没监听到,手动滚动页面再试
|
||||
|
||||
运行:
|
||||
./.venv/bin/python learning/drissionpage_basics/03_listen_api.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from Douyin import DEFAULT_USER_URL
|
||||
from Douyin import build_browser_address
|
||||
from Douyin import ensure_browser_debug_port_ready
|
||||
|
||||
LISTEN_TARGET = "web/aweme/post/"
|
||||
DEFAULT_BROWSER_PORT = 9223
|
||||
|
||||
|
||||
def main() -> None:
|
||||
from DrissionPage import ChromiumOptions
|
||||
from DrissionPage import ChromiumPage
|
||||
|
||||
ensure_browser_debug_port_ready(DEFAULT_BROWSER_PORT)
|
||||
options = ChromiumOptions().set_address(build_browser_address(DEFAULT_BROWSER_PORT)).existing_only(True)
|
||||
page = ChromiumPage(options)
|
||||
|
||||
page.listen.start(LISTEN_TARGET)
|
||||
page.get(DEFAULT_USER_URL)
|
||||
print(f"[INFO] 当前 URL: {page.url}")
|
||||
print("[INFO] 已开始监听作品接口。如果当前页不是博主主页,请在浏览器里切到博主主页并轻微滚动。")
|
||||
|
||||
packet = page.listen.wait(timeout=10)
|
||||
if packet is None:
|
||||
print("[WARN] 10 秒内没有监听到接口包。请切到博主主页并滚动后重试。")
|
||||
return
|
||||
|
||||
response = packet.response
|
||||
body = getattr(response, "body", None)
|
||||
print(f"[INFO] packet 类型: {type(packet).__name__}")
|
||||
print(f"[INFO] response 类型: {type(response).__name__}")
|
||||
print(f"[INFO] response.body 类型: {type(body).__name__}")
|
||||
|
||||
if isinstance(body, dict):
|
||||
print(f"[INFO] body keys: {list(body.keys())[:10]}")
|
||||
aweme_list = body.get("aweme_list")
|
||||
if isinstance(aweme_list, list):
|
||||
print(f"[INFO] aweme_list 数量: {len(aweme_list)}")
|
||||
if aweme_list:
|
||||
print(f"[INFO] 第一条 aweme keys: {list(aweme_list[0].keys())[:10]}")
|
||||
else:
|
||||
raw_body = getattr(response, "raw_body", "")
|
||||
print(f"[INFO] raw_body 前 300 个字符: {str(raw_body)[:300]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
练习 04: 解析 aweme_list。
|
||||
|
||||
这一题不连接浏览器,只练 JSON 结构解析。
|
||||
|
||||
目标:
|
||||
1. 看懂 `aweme_list` 的层级
|
||||
2. 提取 title / video_id / video_url
|
||||
3. 返回一个列表,列表里每项都是字典
|
||||
|
||||
预期输出格式:
|
||||
[
|
||||
{
|
||||
"title": "...",
|
||||
"video_id": "...",
|
||||
"video_url": "..."
|
||||
}
|
||||
]
|
||||
|
||||
运行:
|
||||
./.venv/bin/python learning/drissionpage_basics/04_parse_aweme.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
SAMPLE_PAYLOAD: dict[str, Any] = {
|
||||
"aweme_list": [
|
||||
{
|
||||
"aweme_id": "7500000000000000001",
|
||||
"desc": "第一个示例视频",
|
||||
"video": {
|
||||
"play_addr": {
|
||||
"url_list": [
|
||||
"https://example.com/play/first",
|
||||
"https://v11-weba.douyinvod.com/example/first.mp4",
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"aweme_id": "7500000000000000002",
|
||||
"desc": "第二个示例视频",
|
||||
"video": {
|
||||
"play_addr": {
|
||||
"url_list": [
|
||||
"https://v26-web.douyinvod.com/example/second.mp4",
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def choose_video_url(url_list: list[str]) -> str:
|
||||
for url in url_list:
|
||||
if "douyinvod.com" in url:
|
||||
return url
|
||||
if url_list:
|
||||
return url_list[0]
|
||||
raise ValueError("url_list 为空,无法选择视频地址。")
|
||||
|
||||
|
||||
def parse_aweme_items(body: dict[str, Any]) -> list[dict[str, str]]:
|
||||
aweme_list = body.get("aweme_list")
|
||||
if not isinstance(aweme_list, list):
|
||||
raise ValueError("body 里缺少 aweme_list。")
|
||||
|
||||
items: list[dict[str, str]] = []
|
||||
for aweme in aweme_list:
|
||||
if not isinstance(aweme, dict):
|
||||
continue
|
||||
|
||||
video_id = str(aweme.get("aweme_id") or "").strip()
|
||||
if not video_id:
|
||||
continue
|
||||
|
||||
title = str(aweme.get("desc") or "").strip() or "untitled"
|
||||
video = aweme.get("video") or {}
|
||||
play_addr = video.get("play_addr") or {}
|
||||
url_list = play_addr.get("url_list") or []
|
||||
if not isinstance(url_list, list) or not url_list:
|
||||
continue
|
||||
|
||||
items.append(
|
||||
{
|
||||
"title": title,
|
||||
"video_id": video_id,
|
||||
"video_url": choose_video_url([str(url) for url in url_list]),
|
||||
}
|
||||
)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def main() -> None:
|
||||
items = parse_aweme_items(SAMPLE_PAYLOAD)
|
||||
print(items)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
练习 05: 用 requests 下载一个 mp4 到本地。
|
||||
|
||||
目标:
|
||||
1. 自己导入 requests
|
||||
2. 发起 GET 请求
|
||||
3. 把响应内容写入本地文件
|
||||
4. 手动确认文件确实存在
|
||||
|
||||
建议:
|
||||
- 先从你在 03 / 04 里拿到的真实 mp4 链接开始
|
||||
- 不要一开始就封装复杂函数
|
||||
- 第一版先写死 URL 和输出文件名
|
||||
|
||||
运行:
|
||||
./.venv/bin/python learning/drissionpage_basics/05_download_video.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from Douyin import (
|
||||
LISTEN_TARGET,
|
||||
build_headers,
|
||||
download_video,
|
||||
ensure_browser_debug_port_ready,
|
||||
extract_aweme_payload,
|
||||
parse_aweme_items,
|
||||
sanitize_filename,
|
||||
)
|
||||
|
||||
DEFAULT_BROWSER_PORT = 9223
|
||||
OUTPUT_DIR = Path("learning/drissionpage_basics/output")
|
||||
TIMEOUT_SECONDS = 20
|
||||
|
||||
|
||||
def build_output_path(title: str, video_id: str, output_dir: Path = OUTPUT_DIR) -> Path:
|
||||
safe_title = sanitize_filename(title, fallback="practice-video")
|
||||
return output_dir / f"{safe_title}-{video_id}.mp4"
|
||||
|
||||
|
||||
def attach_to_browser(browser_port: int = DEFAULT_BROWSER_PORT):
|
||||
from DrissionPage import ChromiumOptions
|
||||
from DrissionPage import ChromiumPage
|
||||
|
||||
ensure_browser_debug_port_ready(browser_port)
|
||||
options = ChromiumOptions().set_address(f"127.0.0.1:{browser_port}").existing_only(True)
|
||||
return ChromiumPage(options)
|
||||
|
||||
|
||||
def download_first_real_video(
|
||||
page,
|
||||
requests_module,
|
||||
output_dir: Path = OUTPUT_DIR,
|
||||
timeout: int = TIMEOUT_SECONDS,
|
||||
) -> Path:
|
||||
current_url = page.url
|
||||
page.listen.start(LISTEN_TARGET)
|
||||
page.get(current_url)
|
||||
packet = page.listen.wait(timeout=timeout)
|
||||
if not packet or not hasattr(packet, "response"):
|
||||
raise RuntimeError("当前页面没有监听到作品接口,请先切到博主主页并滚动页面后重试。")
|
||||
|
||||
payload = extract_aweme_payload(packet.response)
|
||||
items = parse_aweme_items(payload)
|
||||
if not items:
|
||||
raise RuntimeError("当前页面没有解析到可下载视频,请先确认页面已加载出作品。")
|
||||
|
||||
first_item = items[0]
|
||||
output_path = build_output_path(
|
||||
title=first_item["title"],
|
||||
video_id=first_item["video_id"],
|
||||
output_dir=output_dir,
|
||||
)
|
||||
headers = build_headers(page.url)
|
||||
download_video(
|
||||
requests_module=requests_module,
|
||||
headers=headers,
|
||||
video_url=first_item["video_url"],
|
||||
output_path=output_path,
|
||||
)
|
||||
return output_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import requests
|
||||
|
||||
page = attach_to_browser()
|
||||
print(f"[INFO] 当前页面: {page.title}")
|
||||
print(f"[INFO] 当前 URL: {page.url}")
|
||||
print("[INFO] 正在监听当前页面的作品接口,必要时请在浏览器中轻微滚动一下页面。")
|
||||
output_path = download_first_real_video(page=page, requests_module=requests)
|
||||
print(f"[OK] 已下载到: {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,121 @@
|
||||
# DrissionPage Basics
|
||||
|
||||
这里放的是你已经学过的第一阶段内容。
|
||||
|
||||
这一组练习围绕当前项目真实使用的 `DrissionPage` 工作流展开,重点是先把“打开页面、附着浏览器、监听接口、解析数据、下载文件”这条链路打通。
|
||||
|
||||
## 建议学习顺序
|
||||
|
||||
1. `01_open_page.py`
|
||||
2. `02_attach_browser.py`
|
||||
3. `03_listen_api.py`
|
||||
4. `04_parse_aweme.py`
|
||||
5. `05_download_video.py`
|
||||
|
||||
## 开始前先准备
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install requests DrissionPage
|
||||
```
|
||||
|
||||
如果你要练附着浏览器,先单独启动登录浏览器:
|
||||
|
||||
```bash
|
||||
./.venv/bin/python login_douyin.py --browser-port 9223
|
||||
```
|
||||
|
||||
## 练习原则
|
||||
|
||||
- 不要复制主脚本全部代码
|
||||
- 每次只补一个文件
|
||||
- 跑起来后先 `print()` 看对象和数据结构
|
||||
- 先写最小可运行版本,再做整理
|
||||
- 如果报错,优先看报错里提到的对象类型和字段名
|
||||
|
||||
## 每个文件你应该关注什么
|
||||
|
||||
### `01_open_page.py`
|
||||
|
||||
- 学会创建 `ChromiumPage`
|
||||
- 学会 `page.get(url)`
|
||||
- 学会确认页面是否真的打开
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
./.venv/bin/python learning/drissionpage_basics/01_open_page.py
|
||||
```
|
||||
|
||||
### `02_attach_browser.py`
|
||||
|
||||
- 学会通过 `127.0.0.1:9223` 附着到已启动 Chrome
|
||||
- 学会验证附着的是不是你刚才登录的浏览器
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
./.venv/bin/python learning/drissionpage_basics/02_attach_browser.py
|
||||
```
|
||||
|
||||
### `03_listen_api.py`
|
||||
|
||||
- 学会 `page.listen.start(...)`
|
||||
- 学会 `page.listen.wait(timeout=...)`
|
||||
- 学会观察返回包的 `response.body` 和 `response.raw_body`
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
./.venv/bin/python learning/drissionpage_basics/03_listen_api.py
|
||||
```
|
||||
|
||||
### `04_parse_aweme.py`
|
||||
|
||||
- 不连浏览器,只拿示例数据练解析
|
||||
- 学会从 `aweme_list` 提取标题、视频 id、视频 url
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
./.venv/bin/python learning/drissionpage_basics/04_parse_aweme.py
|
||||
```
|
||||
|
||||
### `05_download_video.py`
|
||||
|
||||
- 学会用 `requests` 下载一个 mp4
|
||||
- 学会写入本地文件
|
||||
- 学会验证文件是否真的下载成功
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
./.venv/bin/python learning/drissionpage_basics/05_download_video.py
|
||||
```
|
||||
|
||||
## 推荐你的手动练法
|
||||
|
||||
1. 先补 `01`,只做到“能打开页面”
|
||||
2. 再补 `02`,只做到“能附着到浏览器”
|
||||
3. 再补 `03`,只做到“能打印监听结果”
|
||||
4. 再补 `04`,只做到“能把字段提出来”
|
||||
5. 最后补 `05`,把一个视频链接下载到 `learning/drissionpage_basics/output/`
|
||||
|
||||
## 练完后再回看主脚本
|
||||
|
||||
等你把这 5 个文件都手写过一遍,再回头看这些文件会更清楚:
|
||||
|
||||
- [Douyin.py](/Users/wangshaoqing/Desktop/MiaoSi/Study/douyin-crawler-poc/Douyin.py)
|
||||
- [login_douyin.py](/Users/wangshaoqing/Desktop/MiaoSi/Study/douyin-crawler-poc/login_douyin.py)
|
||||
|
||||
重点看:
|
||||
|
||||
- `create_page`
|
||||
- `page.listen.start`
|
||||
- `page.listen.wait`
|
||||
- `extract_aweme_payload`
|
||||
- `parse_aweme_items`
|
||||
- `download_video`
|
||||
Reference in New Issue
Block a user