Python Tools với AI
Xây dựng CLI tools, file automation, web scrapers và API clients chuyên nghiệp — tất cả với sức mạnh của AI và Python.
🎯 Mục tiêu học tập
- Tạo CLI tool chuyên nghiệp với Python và Click library
- Xây dựng file organizer tự động bằng pathlib + AI
- Viết web scraper với requests và BeautifulSoup
- Tích hợp REST API bên ngoài vào tool Python
- Quản lý virtual environment và dependencies đúng chuẩn
Bài 4.1 — Python Fundamentals trong VS Code
pip install package, pip freeze > requirements.txt để lưu dependencies, pip install -r requirements.txt để restore.@click.command(), @click.option(), @click.argument().requests: đơn giản, sync. httpx: hỗ trợ async, type hints tốt hơn, tương lai của requests.requests để scrape web. Selector: soup.find(), soup.find_all(), soup.select('css.selector').Virtual Environment — Không thể bỏ qua
Virtual environment (venv) cô lập dependencies của mỗi project — như Docker nhưng nhẹ hơn nhiều. Luôn luôn dùng venv cho mọi Python project.
# Tạo project mới
mkdir my-python-tool
cd my-python-tool
# Tạo virtual environment
python -m venv venv
# Kích hoạt venv
# Windows:
venv\Scripts\activate
# Mac/Linux:
source venv/bin/activate
# Dấu hiệu venv đang bật: (venv) xuất hiện ở đầu dòng terminal
# (venv) C:\my-python-tool>
# Cài package
pip install requests click rich
# Lưu dependencies
pip freeze > requirements.txt
# Tắt venv khi xong
deactivate
Khi bạn tạo venv trong folder project, VS Code sẽ hỏi "Use this virtual environment?" — nhấn Yes. Hoặc nhấn vào Python interpreter ở status bar để chọn manually.
Cấu trúc project Python chuẩn
my-tool/
├── venv/ # Virtual environment (KHÔNG commit lên git)
├── src/
│ ├── __init__.py
│ └── main.py # Entry point
├── tests/
│ └── test_main.py
├── .gitignore # Thêm: venv/, __pycache__/, *.pyc
├── requirements.txt # Dependencies
└── README.md
Bài 4.2 — CLI Tool đầu tiên với Click
So Sánh Thư Viện CLI Python
| Thư viện | Cú pháp | Độ phức tạp | Khi nào dùng |
|---|---|---|---|
| argparse | Verbose, nhiều boilerplate | ⭐⭐ | Script đơn giản, không cần cài thêm |
| Click | Decorator-based, gọn | ⭐⭐⭐ | CLI tool chuyên nghiệp — khuyên dùng |
| Typer | Type hints tự động, minimal code | ⭐⭐ | Dự án dùng TypeScript-style, FastAPI ecosystem |
| HTTP Libraries | |||
| requests | Simple, sync only | ⭐ | Script nhanh, không cần async |
| httpx | requests-compatible + async | ⭐⭐ | App cần async, type hints, HTTP/2 |
| aiohttp | Full async, cao cấp hơn | ⭐⭐⭐ | High-performance async scraping |
Click là thư viện Python phổ biến nhất để xây dựng CLI (Command Line Interface) tool. Với AI hỗ trợ, chúng ta có thể tạo CLI tool chuyên nghiệp rất nhanh.
Cài đặt và Hello World
pip install click rich
import click
from rich.console import Console
from rich.table import Table
console = Console()
@click.group()
@click.version_option("1.0.0")
def cli():
"""🚀 My First CLI Tool — built with Python + AI"""
pass
@cli.command()
@click.option("--name", "-n", prompt="Nhập tên của bạn", help="Tên người dùng")
@click.option("--count", "-c", default=1, help="Số lần chào")
def greet(name: str, count: int):
"""Chào mừng người dùng."""
for i in range(count):
console.print(f"[bold green]👋 Xin chào, {name}![/bold green]")
@cli.command()
@click.argument("numbers", nargs=-1, type=float)
def calculate(numbers):
"""Tính tổng một dãy số."""
if not numbers:
console.print("[red]Cần ít nhất 1 số[/red]")
return
total = sum(numbers)
console.print(f"[cyan]Tổng: {total}[/cyan]")
console.print(f"[cyan]Trung bình: {total / len(numbers):.2f}[/cyan]")
if __name__ == "__main__":
cli()
# Chạy
python src/main.py --help
python src/main.py greet --name "Minh" --count 3
python src/main.py calculate 10 20 30.5
Dự án Mini: File Renamer Tool
Cùng dùng AI để xây dựng tool đổi tên file hàng loạt. Đây là prompt gửi cho Copilot:
Tôi đang dùng Python với Click và Rich.
Viết CLI command "rename" nhận:
--pattern: glob pattern để chọn files (vd: "*.jpg")
--prefix: tiền tố thêm vào tên file (optional)
--suffix: hậu tố thêm vào trước extension (optional)
--dry-run: chỉ preview không thực sự đổi tên
--folder: folder để thao tác (default: thư mục hiện tại)
Dùng pathlib.Path, hiển thị preview table bằng Rich trước khi rename,
hỏi xác nhận "Bạn có chắc? [y/N]" trước khi thực thi.
Có error handling đầy đủ.
@cli.command()
@click.option("--pattern", "-p", default="*", help="Glob pattern (vd: *.jpg)")
@click.option("--prefix", default="", help="Tiền tố thêm vào")
@click.option("--suffix", default="", help="Hậu tố thêm vào (trước extension)")
@click.option("--folder", "-f", default=".", type=click.Path(exists=True))
@click.option("--dry-run", is_flag=True, help="Chỉ preview, không thực thi")
def rename(pattern, prefix, suffix, folder, dry_run):
"""Đổi tên file hàng loạt."""
from pathlib import Path
folder_path = Path(folder)
files = list(folder_path.glob(pattern))
if not files:
console.print(f"[yellow]Không tìm thấy file nào với pattern '{pattern}'[/yellow]")
return
# Hiển thị preview table
table = Table(title="Preview - File sẽ được đổi tên")
table.add_column("Tên cũ", style="red")
table.add_column("Tên mới", style="green")
renames = []
for f in files:
stem = f.stem # Tên không có extension
ext = f.suffix # Extension (.jpg, .txt, ...)
new_name = f"{prefix}{stem}{suffix}{ext}"
new_path = f.parent / new_name
renames.append((f, new_path))
table.add_row(f.name, new_name)
console.print(table)
if dry_run:
console.print("[yellow]🔍 Dry-run mode: không có thay đổi nào được thực thi.[/yellow]")
return
# Xác nhận
if not click.confirm(f"\nĐổi tên {len(renames)} file?"):
console.print("Đã hủy.")
return
# Thực thi
success = 0
for old_path, new_path in renames:
try:
old_path.rename(new_path)
success += 1
except Exception as e:
console.print(f"[red]Lỗi khi đổi tên {old_path.name}: {e}[/red]")
console.print(f"[green]✅ Đã đổi tên thành công {success}/{len(renames)} file[/green]")
Bài 4.3 — File Automation Tool
Dự án: File Organizer tự động
Tool này tự động sắp xếp files trong một folder theo loại file (ảnh, video, tài liệu, code...).
"""
File Organizer Tool — tự động phân loại file theo extension
Được tạo với Python + GitHub Copilot
"""
from pathlib import Path
from datetime import datetime
import shutil
import click
from rich.console import Console
from rich.progress import track
console = Console()
# Mapping extension → folder
FILE_CATEGORIES = {
"Ảnh": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp"],
"Video": [".mp4", ".avi", ".mov", ".mkv", ".wmv", ".flv"],
"Âm thanh": [".mp3", ".wav", ".flac", ".aac", ".ogg"],
"Tài liệu": [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"],
"Code": [".py", ".js", ".ts", ".html", ".css", ".json", ".yaml", ".yml"],
"Nén": [".zip", ".rar", ".7z", ".tar", ".gz"],
"Khác": [], # Fallback
}
def get_category(extension: str) -> str:
"""Xác định category của file dựa theo extension."""
ext_lower = extension.lower()
for category, extensions in FILE_CATEGORIES.items():
if ext_lower in extensions:
return category
return "Khác"
@click.command()
@click.argument("source_folder", type=click.Path(exists=True))
@click.option("--dest", "-d", default=None, help="Thư mục đích (mặc định: tạo subfolder trong source)")
@click.option("--dry-run", is_flag=True, help="Preview không thực thi")
@click.option("--by-date", is_flag=True, help="Tổ chức theo năm/tháng")
def organize(source_folder, dest, dry_run, by_date):
"""Tự động tổ chức files trong SOURCE_FOLDER theo loại file."""
source = Path(source_folder)
destination = Path(dest) if dest else source / "_organized"
# Collect all files (không include subdirectory files)
files = [f for f in source.iterdir() if f.is_file()]
if not files:
console.print("[yellow]Không có file nào trong thư mục này.[/yellow]")
return
console.print(f"[cyan]Tìm thấy {len(files)} files trong '{source}'[/cyan]")
moved = 0
for file in track(files, description="Đang phân loại..."):
category = get_category(file.suffix)
if by_date:
# Tổ chức theo năm/tháng
modified = datetime.fromtimestamp(file.stat().st_mtime)
target_dir = destination / category / str(modified.year) / f"{modified.month:02d}"
else:
target_dir = destination / category
target_path = target_dir / file.name
if not dry_run:
target_dir.mkdir(parents=True, exist_ok=True)
# Nếu file trùng tên, thêm số
counter = 1
while target_path.exists():
stem = file.stem
target_path = target_dir / f"{stem}_{counter}{file.suffix}"
counter += 1
shutil.move(str(file), str(target_path))
else:
console.print(f" [dim]{file.name}[/dim] → [green]{category}/{file.name}[/green]")
moved += 1
if dry_run:
console.print(f"\n[yellow]Dry-run: {moved} files sẽ được di chuyển[/yellow]")
else:
console.print(f"\n[green]✅ Đã tổ chức {moved} files vào '{destination}'[/green]")
if __name__ == "__main__":
organize()
# Preview trước khi thực thi
python organizer.py ~/Downloads --dry-run
# Tổ chức vào thư mục mặc định
python organizer.py ~/Downloads
# Tổ chức theo ngày tháng, chỉ định thư mục đích
python organizer.py ~/Downloads --dest ~/Organized --by-date
Bài 4.4 — Web Scraping Tool
Trước khi scrape, hãy kiểm tra robots.txt của website (VD: example.com/robots.txt). Không scrape website không cho phép. Thêm delay giữa các request. Không làm quá tải server.
pip install requests beautifulsoup4 lxml
"""
News Headline Scraper — scrape tiêu đề tin tức
"""
import time
import json
import requests
from bs4 import BeautifulSoup
from datetime import datetime
from pathlib import Path
import click
from rich.console import Console
from rich.table import Table
console = Console()
# Headers để tránh bị block
HEADERS = {
"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"
)
}
def scrape_vnexpress_headlines(limit: int = 20) -> list[dict]:
"""Scrape headlines từ VnExpress (cho mục đích học tập)."""
url = "https://vnexpress.net"
try:
response = requests.get(url, headers=HEADERS, timeout=10)
response.raise_for_status()
response.encoding = "utf-8"
except requests.RequestException as e:
console.print(f"[red]Lỗi kết nối: {e}[/red]")
return []
soup = BeautifulSoup(response.text, "lxml")
articles = []
# Tìm tất cả link bài viết
for a_tag in soup.select("h3.title-news a, h2.title-news a")[:limit]:
title = a_tag.get_text(strip=True)
link = a_tag.get("href", "")
if title and link:
articles.append({
"title": title,
"url": link,
"scraped_at": datetime.now().isoformat()
})
return articles
@click.command()
@click.option("--limit", "-l", default=10, help="Số bài viết lấy về")
@click.option("--output", "-o", default=None, help="Lưu ra file JSON")
@click.option("--format", "fmt", type=click.Choice(["table", "json"]), default="table")
def scrape(limit, output, fmt):
"""Scrape tin tức mới nhất từ VnExpress."""
console.print(f"[cyan]🔍 Đang scrape {limit} bài viết...[/cyan]")
articles = scrape_vnexpress_headlines(limit)
if not articles:
console.print("[red]Không lấy được bài viết nào.[/red]")
return
if fmt == "table":
table = Table(title=f"Tin tức VnExpress — {datetime.now().strftime('%d/%m/%Y %H:%M')}")
table.add_column("#", style="dim", width=4)
table.add_column("Tiêu đề", style="white", max_width=70)
table.add_column("URL", style="blue dim", max_width=40)
for i, article in enumerate(articles, 1):
table.add_row(str(i), article["title"], article["url"][:40] + "...")
console.print(table)
else:
console.print_json(json.dumps(articles, ensure_ascii=False, indent=2))
if output:
output_path = Path(output)
output_path.write_text(
json.dumps(articles, ensure_ascii=False, indent=2),
encoding="utf-8"
)
console.print(f"[green]✅ Đã lưu {len(articles)} bài vào '{output}'[/green]")
if __name__ == "__main__":
scrape()
Bài 4.5 — API Integration Tool
Dự án thực tế: Xây dựng Weather CLI Tool — lấy thông tin thời tiết từ OpenWeatherMap API.
pip install requests python-dotenv rich click
OPENWEATHER_API_KEY=your_api_key_here
"""
Weather CLI Tool — xem thời tiết từ command line
API: OpenWeatherMap (free tier: 60 calls/phút)
"""
import os
import requests
from rich.console import Console
from rich.panel import Panel
from rich.columns import Columns
from rich.text import Text
import click
from dotenv import load_dotenv
load_dotenv() # Load .env file
console = Console()
WEATHER_API_BASE = "https://api.openweathermap.org/data/2.5"
WEATHER_ICONS = {
"Clear": "☀️",
"Clouds": "☁️",
"Rain": "🌧️",
"Drizzle": "🌦️",
"Thunderstorm": "⛈️",
"Snow": "❄️",
"Mist": "🌫️",
"Fog": "🌫️",
}
def get_weather(city: str, api_key: str) -> dict:
"""Lấy thông tin thời tiết hiện tại."""
params = {
"q": city,
"appid": api_key,
"units": "metric",
"lang": "vi",
}
response = requests.get(f"{WEATHER_API_BASE}/weather", params=params, timeout=10)
if response.status_code == 404:
raise ValueError(f"Không tìm thấy thành phố: {city}")
if response.status_code == 401:
raise ValueError("API key không hợp lệ")
response.raise_for_status()
return response.json()
def format_weather_display(data: dict) -> None:
"""Hiển thị thời tiết theo format đẹp."""
city = data["name"]
country = data["sys"]["country"]
temp = data["main"]["temp"]
feels_like = data["main"]["feels_like"]
humidity = data["main"]["humidity"]
description = data["weather"][0]["description"].capitalize()
main_weather= data["weather"][0]["main"]
wind_speed = data["wind"]["speed"]
visibility = data.get("visibility", 0) / 1000 # Convert to km
icon = WEATHER_ICONS.get(main_weather, "🌡️")
content = Text()
content.append(f"\n {icon} {description}\n\n", style="bold")
content.append(f" 🌡️ Nhiệt độ: ", style="dim")
content.append(f"{temp:.1f}°C", style="bold yellow")
content.append(f" (cảm giác {feels_like:.1f}°C)\n")
content.append(f" 💧 Độ ẩm: ", style="dim")
content.append(f"{humidity}%\n", style="cyan")
content.append(f" 💨 Gió: ", style="dim")
content.append(f"{wind_speed} m/s\n", style="green")
content.append(f" 👁️ Tầm nhìn: ", style="dim")
content.append(f"{visibility:.1f} km\n", style="white")
console.print(Panel(
content,
title=f"[bold]{city}, {country}[/bold]",
border_style="blue"
))
@click.command()
@click.argument("city")
@click.option("--api-key", envvar="OPENWEATHER_API_KEY", help="OpenWeatherMap API Key")
def weather(city, api_key):
"""Xem thời tiết hiện tại của một CITY."""
if not api_key:
console.print("[red]❌ Cần OPENWEATHER_API_KEY. Set trong .env hoặc --api-key[/red]")
return
try:
data = get_weather(city, api_key)
format_weather_display(data)
except ValueError as e:
console.print(f"[red]❌ {e}[/red]")
except requests.RequestException as e:
console.print(f"[red]❌ Lỗi kết nối: {e}[/red]")
if __name__ == "__main__":
weather()
python weather.py "Hanoi"
python weather.py "Ho Chi Minh City"
python weather.py "London"
Bài 4.6 — Dự Án Thực Hành: Xây Dựng File Organizer CLI Hoàn Chỉnh
Trong bài này chúng ta sẽ xây dựng hoàn toàn từ đầu một công cụ CLI dọn dẹp thư mục — tự động phân loại file theo đuôi mở rộng, hỗ trợ preview trước khi di chuyển, có undo, và output màu sắc đẹp. Đây là dự án thực tế giống hệt các tool bạn sẽ làm cho công việc.
Tool organizer với 4 lệnh: sort (phân loại), preview (xem trước), undo (hoàn tác), stats (thống kê). Chạy được từ terminal bất kỳ đâu.
Bước 1 — Tạo Cấu Trúc Thư Mục Dự Án
Mở terminal (trong VS Code nhấn Ctrl+`) và chạy lần lượt từng lệnh:
# Tạo thư mục dự án
mkdir file-organizer
cd file-organizer
# Tạo virtual environment (bắt buộc — cô lập dependencies)
python -m venv venv
# Kích hoạt virtual environment
# Windows:
venv\Scripts\activate
# macOS/Linux:
source venv/bin/activate
# Kiểm tra đã kích hoạt (terminal sẽ hiển thị "(venv)" ở đầu dòng)
python --version
# Cài các thư viện cần thiết
pip install click rich
# Lưu danh sách thư viện vào file requirements.txt
pip freeze > requirements.txt
# Kiểm tra nội dung requirements.txt
cat requirements.txt
# Sẽ thấy: click==8.x.x và rich==13.x.x
# Tạo các file cần thiết (Windows dùng ni thay touch)
# Windows:
ni organizer.py, config.py, README.md
# macOS/Linux:
touch organizer.py config.py README.md
Cấu trúc thư mục sau khi tạo xong:
file-organizer/
├── venv/ ← Virtual environment (KHÔNG commit lên Git)
├── organizer.py ← File chính chứa toàn bộ logic
├── config.py ← Cấu hình các loại file
├── requirements.txt ← Danh sách dependencies
└── README.md ← Tài liệu hướng dẫn
Bước 2 — Viết File config.py
Mở config.py và dán nội dung sau (hoặc nhờ Copilot generate từ comment):
# config.py — Cấu hình phân loại file theo đuôi mở rộng
# Prompt cho Copilot: "Tạo dict phân loại file extensions thành các nhóm"
FILE_CATEGORIES = {
"Hình ảnh": {
"extensions": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg", ".ico", ".tiff"],
"folder": "Images",
"color": "bright_cyan"
},
"Video": {
"extensions": [".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".m4v"],
"folder": "Videos",
"color": "bright_magenta"
},
"Âm thanh": {
"extensions": [".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a", ".wma"],
"folder": "Audio",
"color": "bright_yellow"
},
"Tài liệu": {
"extensions": [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".odt", ".ods"],
"folder": "Documents",
"color": "bright_green"
},
"Code": {
"extensions": [".py", ".js", ".ts", ".html", ".css", ".json", ".xml", ".yaml", ".yml", ".sh", ".bash", ".java", ".cpp", ".c", ".h"],
"folder": "Code",
"color": "bright_blue"
},
"Lưu trữ": {
"extensions": [".zip", ".rar", ".7z", ".tar", ".gz", ".bz2"],
"folder": "Archives",
"color": "yellow"
},
"File khác": {
"extensions": [], # Default cho các file không khớp
"folder": "Others",
"color": "white"
}
}
# File lưu lịch sử để hỗ trợ undo
HISTORY_FILE = ".organizer_history.json"
Bước 3 — Viết File organizer.py (Logic Chính)
Đây là file quan trọng nhất. Copy từng phần và để Copilot hỗ trợ hoàn thiện:
#!/usr/bin/env python3
"""
File Organizer CLI — Tự động phân loại file trong thư mục
Tác giả: ThanhDoIT | Phiên bản: 1.0.0
"""
import click
import json
import shutil
from pathlib import Path
from datetime import datetime
from rich.console import Console
from rich.table import Table
from rich.progress import track
from rich.panel import Panel
from rich import print as rprint
from config import FILE_CATEGORIES, HISTORY_FILE
console = Console()
def get_category(file_ext: str) -> dict:
"""Tìm danh mục cho một đuôi file."""
ext_lower = file_ext.lower()
for category_name, info in FILE_CATEGORIES.items():
if ext_lower in info["extensions"]:
return category_name, info
return "File khác", FILE_CATEGORIES["File khác"]
def save_history(moves: list, source_dir: str):
"""Lưu lịch sử di chuyển file để hỗ trợ undo."""
history_path = Path(source_dir) / HISTORY_FILE
history = {
"timestamp": datetime.now().isoformat(),
"source_dir": source_dir,
"moves": moves
}
with open(history_path, "w", encoding="utf-8") as f:
json.dump(history, f, ensure_ascii=False, indent=2)
def load_history(source_dir: str) -> dict | None:
"""Đọc file lịch sử."""
history_path = Path(source_dir) / HISTORY_FILE
if not history_path.exists():
return None
with open(history_path, "r", encoding="utf-8") as f:
return json.load(f)
@click.group()
@click.version_option(version="1.0.0", prog_name="organizer")
def cli():
"""🗂 File Organizer — Tự động dọn dẹp và phân loại file"""
pass
@cli.command()
@click.argument("directory", default=".", type=click.Path(exists=True))
def preview(directory):
"""Xem trước những gì sẽ bị di chuyển (không thực sự di chuyển)."""
source = Path(directory).resolve()
files = [f for f in source.iterdir() if f.is_file() and not f.name.startswith(".")]
if not files:
console.print("[yellow]⚠ Không tìm thấy file nào trong thư mục.[/yellow]")
return
table = Table(title=f"Preview — {source}", show_lines=True)
table.add_column("File", style="cyan")
table.add_column("Loại", style="green")
table.add_column("Sẽ di chuyển đến", style="yellow")
table.add_column("Kích thước", justify="right")
for file in sorted(files):
cat_name, cat_info = get_category(file.suffix)
size_kb = file.stat().st_size / 1024
size_str = f"{size_kb:.1f} KB" if size_kb < 1024 else f"{size_kb/1024:.1f} MB"
table.add_row(file.name, cat_name, cat_info["folder"], size_str)
console.print(table)
console.print(f"\n[dim]Tổng: {len(files)} file sẽ được phân loại[/dim]")
@cli.command()
@click.argument("directory", default=".", type=click.Path(exists=True))
@click.option("--yes", "-y", is_flag=True, help="Xác nhận không hỏi lại")
def sort(directory, yes):
"""Phân loại và di chuyển file vào các thư mục con."""
source = Path(directory).resolve()
files = [f for f in source.iterdir() if f.is_file() and not f.name.startswith(".")]
if not files:
console.print("[yellow]⚠ Không tìm thấy file nào.[/yellow]")
return
console.print(Panel(f"[bold]Sẽ phân loại {len(files)} file trong:[/bold]\n{source}", title="File Organizer"))
if not yes:
click.confirm("Tiếp tục?", abort=True)
moves = []
errors = []
for file in track(files, description="Đang phân loại..."):
cat_name, cat_info = get_category(file.suffix)
dest_folder = source / cat_info["folder"]
dest_folder.mkdir(exist_ok=True)
dest_file = dest_folder / file.name
# Tránh ghi đè: thêm timestamp nếu file đã tồn tại
if dest_file.exists():
stem = file.stem
suffix = file.suffix
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
dest_file = dest_folder / f"{stem}_{timestamp}{suffix}"
try:
shutil.move(str(file), str(dest_file))
moves.append({"from": str(file), "to": str(dest_file)})
except Exception as e:
errors.append({"file": file.name, "error": str(e)})
# Lưu lịch sử
if moves:
save_history(moves, str(source))
# Báo cáo kết quả
console.print(f"\n[bold green]✅ Hoàn thành![/bold green]")
console.print(f" Di chuyển thành công: [green]{len(moves)}[/green] file")
if errors:
console.print(f" Lỗi: [red]{len(errors)}[/red] file")
for err in errors:
console.print(f" [red]✗[/red] {err['file']}: {err['error']}")
@cli.command()
@click.argument("directory", default=".", type=click.Path(exists=True))
def undo(directory):
"""Hoàn tác lần phân loại gần nhất."""
source = Path(directory).resolve()
history = load_history(str(source))
if not history:
console.print("[yellow]⚠ Không tìm thấy lịch sử để hoàn tác.[/yellow]")
return
moves = history["moves"]
timestamp = history["timestamp"]
console.print(f"[dim]Lịch sử tìm thấy: {timestamp}[/dim]")
click.confirm(f"Hoàn tác {len(moves)} di chuyển?", abort=True)
restored = 0
for move in track(reversed(moves), description="Đang hoàn tác..."):
src = Path(move["to"])
dst = Path(move["from"])
if src.exists():
shutil.move(str(src), str(dst))
restored += 1
# Xóa file lịch sử sau khi undo
(source / HISTORY_FILE).unlink(missing_ok=True)
console.print(f"[bold green]✅ Đã hoàn tác {restored} file.[/bold green]")
@cli.command()
@click.argument("directory", default=".", type=click.Path(exists=True))
def stats(directory):
"""Hiển thị thống kê các file trong thư mục."""
source = Path(directory).resolve()
all_files = list(source.rglob("*"))
files_only = [f for f in all_files if f.is_file() and not f.name.startswith(".")]
if not files_only:
console.print("[yellow]⚠ Thư mục trống.[/yellow]")
return
# Đếm theo danh mục
cat_count = {}
total_size = 0
for file in files_only:
cat_name, _ = get_category(file.suffix)
cat_count[cat_name] = cat_count.get(cat_name, 0) + 1
total_size += file.stat().st_size
table = Table(title=f"Thống kê — {source.name}", show_lines=True)
table.add_column("Danh mục", style="cyan")
table.add_column("Số file", justify="right", style="green")
table.add_column("Phần trăm", justify="right", style="yellow")
for cat, count in sorted(cat_count.items(), key=lambda x: x[1], reverse=True):
pct = (count / len(files_only)) * 100
table.add_row(cat, str(count), f"{pct:.1f}%")
console.print(table)
size_mb = total_size / (1024 * 1024)
console.print(f"\nTổng: [bold]{len(files_only)}[/bold] file | Dung lượng: [bold]{size_mb:.2f} MB[/bold]")
if __name__ == "__main__":
cli()
Bước 4 — Chạy Tool Và Test
# Xem trợ giúp
python organizer.py --help
# Xem trợ giúp của từng lệnh
python organizer.py sort --help
python organizer.py preview --help
# Preview thư mục Downloads (chưa di chuyển gì)
python organizer.py preview ~/Downloads
# Preview thư mục hiện tại
python organizer.py preview .
# Phân loại thư mục Downloads (sẽ hỏi xác nhận)
python organizer.py sort ~/Downloads
# Phân loại không hỏi xác nhận
python organizer.py sort ~/Downloads --yes
# Xem thống kê sau khi phân loại
python organizer.py stats ~/Downloads
# Hoàn tác nếu không vừa ý
python organizer.py undo ~/Downloads
Bước 5 — Cài Tool Toàn Hệ Thống (Dùng Được Ở Bất Kỳ Đâu)
# Tạo file setup.py để đóng gói tool
# (Prompt Copilot: "Viết setup.py cho CLI tool organizer dùng click")
# Nội dung setup.py:
# from setuptools import setup
# setup(
# name="file-organizer",
# version="1.0.0",
# py_modules=["organizer", "config"],
# install_requires=["click", "rich"],
# entry_points={"console_scripts": ["organizer=organizer:cli"]},
# )
# Cài tool vào hệ thống (vẫn trong venv)
pip install --editable .
# Bây giờ có thể dùng lệnh ngắn gọn từ bất kỳ thư mục nào:
organizer --help
organizer preview ~/Desktop
organizer sort ~/Desktop --yes
Bạn vừa xây dựng một CLI tool Python thực sự, với đầy đủ: phân loại file, preview an toàn, undo, thống kê, output màu sắc, error handling, và hỗ trợ cài globally. Đây là cấp độ tool mà nhiều developer dùng hàng ngày.
Mở Rộng Tool — Thử Thách Nâng Cao
- Thêm option
--dry-run(alias cho preview) - Thêm lệnh
watchđể tự động phân loại khi có file mới (dùng thư việnwatchdog) - Thêm config file (
.organizer.yaml) để người dùng tùy chỉnh rules - Thêm lệnh
cleanđể xóa file trùng lặp (dùng hashlib để so sánh MD5) - Thêm progress bar khi xử lý thư mục có nhiều nghìn file
- Mỗi script Python nên có if __name__ == "__main__": — giúp code reusable và testable. AI thường quên điều này, hãy luôn thêm vào.
- Dùng pathlib.Path thay vì os.path — code đọc dễ hơn, cross-platform hơn. Khi AI dùng os.path, nhắc nó chuyển sang pathlib.
- Luôn lưu API keys trong .env và dùng python-dotenv. KHÔNG BAO GIỜ hardcode key vào code, dù chỉ để test tạm thời.
- Rich library (pip install rich) giúp terminal output đẹp hơn rất nhiều — tables, progress bars, colors. AI biết cách dùng Rich rất tốt.
Dự án Chapter 4: "My First Python Tool"
Chọn 1 trong 3 dự án để làm:
- Option A: Password Generator CLI — tạo mật khẩu an toàn với độ dài và ký tự tùy chọn
- Option B: PDF to Text Converter — tool đọc PDF và extract text (dùng
pypdf2) - Option C: Reminder Tool — tool đặt nhắc nhở lưu vào file JSON, list và xóa
Yêu cầu chung:
- Sử dụng Click cho CLI, Rich cho output đẹp
- Có
requirements.txtvàREADME.md - Error handling đầy đủ (try/except)
- Dùng AI để generate code, sau đó review và hiểu từng phần
🗒 Tóm Tắt Chương 4
- Virtual environment (venv) — LUÔN dùng, cô lập dependencies mỗi project
- Click + Rich — combo tốt nhất để xây dựng CLI tool Python chuyên nghiệp
- File automation với pathlib — Path object mạnh hơn os.path rất nhiều
- Web scraping với requests + BeautifulSoup — tôn trọng robots.txt và rate limiting
- API integration — luôn lưu API key trong .env, không hardcode, không commit
- AI workflow: viết comment rõ ràng → AI generate → review → test → iterate
Bài 4.7 — Selenium Web Automation với AI
Selenium là thư viện Python cho phép bạn điều khiển trình duyệt Chrome/Firefox bằng code — như thể có người thật ngồi dùng máy tính nhưng tất cả đều tự động. Đây là công cụ cực kỳ mạnh để xây dựng automation tools có giá trị thương mại cao.
🎯 Bạn sẽ học được
- Cài đặt Selenium + Chrome tự động với webdriver-manager
- Tìm elements bằng CSS, XPath, ID, Class
- Click, gõ text, submit form, navigate trang
- Chờ đúng cách với WebDriverWait (không time.sleep)
- Screenshot, xử lý alerts, multiple tabs
- Headless mode — chạy không cần mở browser
- 3 dự án thực hành có thể bán ngay
💡 Khi nào dùng Selenium?
- Website dùng JavaScript nhiều (không scrape được bằng requests)
- Cần login trước khi lấy dữ liệu
- Tự động hoá quy trình lặp đi lặp lại
- Tự động điền form hàng loạt
- Test web UI tự động
- Price monitoring, job alerts, data collection
Trước khi scrape/automate bất kỳ website nào: Đọc file robots.txt (thêm /robots.txt vào URL gốc) và Terms of Service. Chỉ tự động hoá những gì bạn được phép làm. Không tải quá nhiều request gây ảnh hưởng server.
Cài Đặt Môi Trường Trong VS Code
# Mở VS Code Terminal (Ctrl+`)
mkdir selenium-automation && cd selenium-automation
python -m venv venv
# Kích hoạt venv
# Windows:
venv\Scripts\activate
# Mac/Linux:
source venv/bin/activate
# Bạn sẽ thấy (venv) ở đầu dòng terminal
pip install selenium webdriver-manager python-dotenv
# Tạo requirements.txt
pip freeze > requirements.txt
Nhấn Ctrl+Shift+P → gõ Python: Select Interpreter → chọn interpreter trong thư mục venv của project. VS Code sẽ tự dùng đúng Python có selenium.
"""
driver_setup.py — Chrome WebDriver factory
Dùng webdriver-manager để tự động cài ChromeDriver đúng version
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
def get_driver(headless: bool = False, window_size: tuple = (1920, 1080)) -> webdriver.Chrome:
"""
Tạo Chrome WebDriver đã cấu hình sẵn.
headless=True: chạy ẩn, không hiện cửa sổ Chrome
"""
options = Options()
if headless:
options.add_argument('--headless=new')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--disable-blink-features=AutomationControlled')
options.add_experimental_option('excludeSwitches', ['enable-automation'])
options.add_experimental_option('useAutomationExtension', False)
options.add_argument(f'--window-size={window_size[0]},{window_size[1]}')
options.add_argument(
'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'
)
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=options)
driver.implicitly_wait(10)
driver.execute_script(
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
)
return driver
Hiểu Locators — Tìm Elements Trên Trang
Trước khi click hay gõ text, bạn cần tìm được element đó. Mở Chrome DevTools (F12) → click vào element → chuột phải → Inspect để xem HTML và tìm selector phù hợp.
id="username" — dùng ngay, nhanh và chính xác nhất.class, #id, [attr=val], parent > childname attribute. Nhanh, đơn giản.find_elements (số nhiều) để lấy danh sách. Tránh nếu class hay thay đổi.<a> theo text. Dùng PARTIAL_LINK_TEXT nếu chỉ biết một phần.Mẹo tìm XPath nhanh: Trong Chrome DevTools → Elements tab → chuột phải vào element → Copy → Copy XPath. Nhưng XPath copy tự động thường rất dài và dễ vỡ. Hãy đơn giản hoá: nhờ Copilot Chat: "Simplify this XPath: [paste XPath] — make it more robust"
Wait Strategies — Chờ Đúng Cách (Quan Trọng!)
Nguyên nhân số 1 khiến Selenium script fail: không chờ đủ lâu cho element load. Tuyệt đối không dùng time.sleep(3) — chậm và không ổn định.
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
wait = WebDriverWait(driver, timeout=15)
# Chờ element visible
element = wait.until(EC.visibility_of_element_located((By.ID, "result")))
# Chờ element clickable (phổ biến nhất!)
btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#submit-btn")))
btn.click()
# Chờ text xuất hiện trong element
wait.until(EC.text_to_be_present_in_element((By.ID, "status"), "Thành công"))
# Chờ URL thay đổi (sau khi submit form)
wait.until(EC.url_contains("/dashboard"))
# Chờ element biến mất (loading spinner)
wait.until(EC.invisibility_of_element_located((By.CLASS_NAME, "loading")))
Actions Thường Dùng — Tham Khảo Nhanh
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import Select
# Navigation
driver.get("https://example.com")
driver.back(); driver.forward(); driver.refresh()
print(driver.title, driver.current_url)
# Find & interact
el = driver.find_element(By.ID, "email")
els = driver.find_elements(By.CLASS_NAME, "item") # Tìm nhiều
el.click()
el.send_keys("hello@email.com")
el.clear()
el.send_keys(Keys.RETURN) # Nhấn Enter
el.send_keys(Keys.CONTROL, 'a') # Ctrl+A
# Get info
print(el.text) # Text hiển thị
print(el.get_attribute("href")) # Lấy attribute
print(el.get_attribute("value")) # Giá trị input
print(el.is_displayed(), el.is_enabled())
# Dropdown
select_el = Select(driver.find_element(By.ID, "country"))
select_el.select_by_visible_text("Việt Nam")
select_el.select_by_value("VN")
# Scroll
driver.execute_script("window.scrollTo(0, document.body.scrollHeight)")
driver.execute_script("arguments[0].scrollIntoView(true);", el)
# Screenshot
driver.save_screenshot("screenshot.png")
el.screenshot("element.png")
# Hover
ActionChains(driver).move_to_element(el).perform()
# Multiple tabs
driver.execute_script("window.open('https://google.com')")
handles = driver.window_handles
driver.switch_to.window(handles[1])
driver.close()
driver.switch_to.window(handles[0])
# Alerts
alert = driver.switch_to.alert
print(alert.text); alert.accept()
# File upload
upload = driver.find_element(By.CSS_SELECTOR, "input[type='file']")
upload.send_keys(os.path.abspath("myfile.pdf"))
# Cookies
driver.add_cookie({"name": "session", "value": "abc123"})
cookies = driver.get_cookies()
driver.delete_all_cookies()
Tôi cần viết Selenium script để tự động: [Mô tả workflow cần tự động - ví dụ:] 1. Mở trang https://example.com/login 2. Điền email và password 3. Click nút "Đăng nhập" 4. Chờ redirect đến /dashboard 5. Tìm tất cả rows trong bảng .data-table 6. Lấy text từ cột 1 và 3 của mỗi row 7. Lưu vào CSV file Python 3.11, Selenium 4, webdriver-manager, VS Code Không dùng time.sleep — dùng WebDriverWait Xử lý lỗi: nếu login fail → screenshot + log error + exit Viết code hoàn chỉnh, có comments giải thích từng bước.
Error Handling Chuyên Nghiệp
"""
robust_automation.py — Template với error handling đầy đủ
"""
import logging
import os
from datetime import datetime
from selenium.common.exceptions import (
TimeoutException, NoSuchElementException,
StaleElementReferenceException, WebDriverException
)
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler('automation.log', encoding='utf-8'),
logging.StreamHandler()
]
)
log = logging.getLogger(__name__)
def safe_click(driver, locator, timeout=15, retries=3):
"""Click an element với retry tự động."""
wait = WebDriverWait(driver, timeout)
for attempt in range(retries):
try:
element = wait.until(EC.element_to_be_clickable(locator))
element.click()
log.info(f"Clicked {locator}")
return True
except StaleElementReferenceException:
log.warning(f"Stale element, retry {attempt + 1}/{retries}")
except TimeoutException:
screenshot_on_error(driver, f"timeout_click_{attempt}")
return False
return False
def screenshot_on_error(driver, name="error"):
"""Tự động chụp screenshot khi có lỗi."""
os.makedirs("screenshots", exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
path = f"screenshots/{name}_{timestamp}.png"
driver.save_screenshot(path)
log.info(f"Screenshot saved: {path}")
def run_automation():
driver = None
try:
driver = get_driver(headless=False)
log.info("Automation started")
driver.get("https://target-website.com")
safe_click(driver, (By.CSS_SELECTOR, "button[type='submit']"))
WebDriverWait(driver, 15).until(EC.url_contains("/dashboard"))
log.info("Login successful!")
# --- Logic chính của bạn ---
log.info("Automation completed")
except WebDriverException as e:
log.error(f"WebDriver error: {e}")
if driver: screenshot_on_error(driver, "webdriver_error")
raise
finally:
if driver:
driver.quit()
log.info("Browser closed")
if __name__ == "__main__":
run_automation()
Headless Mode — Chạy Trên Server: Khi deploy lên server (không có màn hình), dùng headless=True. Trên Linux cần cài Chrome: apt-get install -y google-chrome-stable. Thêm --disable-gpu và --disable-software-rasterizer vào options.
Bài 4.8 — Dự Án: 3 Automation Tools Có Thể Bán Ngay
Đây là 3 dự án Selenium thực tế, mỗi cái giải quyết một vấn đề thực sự và có thể kiếm tiền. Build từng cái theo hướng dẫn để nắm vững automation.
Nhiều website chỉ hiển thị data sau khi login — requests thông thường không làm được. Selenium tự động hoá hoàn toàn luồng này.
"""
auto_login_fetcher.py — Tự động login và lấy dữ liệu
"""
import csv, os
from dotenv import load_dotenv
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from driver_setup import get_driver
load_dotenv()
CONFIG = {
"url": "https://target-website.com/login",
"email_field": "#email",
"pass_field": "#password",
"login_btn": "button[type='submit']",
"success_url_contains": "/dashboard",
"data_table": ".data-table tbody tr",
}
def login(driver, email, password):
wait = WebDriverWait(driver, 15)
driver.get(CONFIG["url"])
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, CONFIG["email_field"]))).send_keys(email)
driver.find_element(By.CSS_SELECTOR, CONFIG["pass_field"]).send_keys(password)
driver.find_element(By.CSS_SELECTOR, CONFIG["login_btn"]).click()
wait.until(EC.url_contains(CONFIG["success_url_contains"]))
print("✅ Login thành công!")
return True
def fetch_data(driver):
wait = WebDriverWait(driver, 15)
wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, CONFIG["data_table"])))
rows = driver.find_elements(By.CSS_SELECTOR, CONFIG["data_table"])
results = []
for i, row in enumerate(rows):
cells = row.find_elements(By.TAG_NAME, "td")
if cells:
results.append({
"stt": i + 1,
"col_1": cells[0].text.strip() if len(cells) > 0 else "",
"col_2": cells[1].text.strip() if len(cells) > 1 else "",
"col_3": cells[2].text.strip() if len(cells) > 2 else "",
})
print(f"📊 Thu thập được {len(results)} records")
return results
def save_to_csv(data, filename="output/data.csv"):
os.makedirs("output", exist_ok=True)
with open(filename, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
print(f"💾 Đã lưu vào {filename}")
def main():
email = os.getenv("LOGIN_EMAIL")
password = os.getenv("LOGIN_PASSWORD")
driver = get_driver(headless=False)
try:
if login(driver, email, password):
save_to_csv(fetch_data(driver))
finally:
driver.quit()
if __name__ == "__main__":
main()
Tôi có website [URL] với form login. HTML của form login: [paste HTML] HTML của bảng dữ liệu sau login: [paste HTML] Hãy điều chỉnh CONFIG dictionary trong auto_login_fetcher.py để khớp với website này — cho tôi CSS selectors chính xác cho: - Email input, password input, login button - Data table rows và từng column cần lấy
Theo dõi giá hàng trăm sản phẩm tự động, lưu lịch sử, thông báo khi giá đạt ngưỡng. Đây là tool có thể bán $10-50/tháng.
"""
price_tracker.py — Theo dõi giá sản phẩm tự động
Chạy định kỳ: Task Scheduler (Windows) hoặc cron (Linux)
"""
import json, re, smtplib, os
from datetime import datetime
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from dotenv import load_dotenv
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from driver_setup import get_driver
load_dotenv()
PRODUCTS = [
{
"name": "iPhone 15 Pro 256GB",
"url": "https://tiki.vn/...",
"price_selector": ".product-price .price",
"target_price": 25_000_000, # Thông báo khi giá <= số này
},
# Thêm products...
]
HISTORY_FILE = "price_history.json"
def load_history():
if os.path.exists(HISTORY_FILE):
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def save_history(history):
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump(history, f, ensure_ascii=False, indent=2)
def extract_price(price_text):
"""'26.990.000₫' → 26990000.0"""
numbers = re.sub(r'[^\d]', '', price_text)
return float(numbers) if numbers else 0.0
def get_price(driver, product):
try:
driver.get(product["url"])
price_el = WebDriverWait(driver, 15).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, product["price_selector"]))
)
price = extract_price(price_el.text)
print(f" 💵 {product['name']}: {price:,.0f}₫")
return price
except Exception as e:
print(f" ❌ Không lấy được giá: {e}")
return None
def send_alert_email(product, current_price):
sender = os.getenv("EMAIL_SENDER")
password = os.getenv("EMAIL_PASSWORD")
receiver = os.getenv("EMAIL_RECEIVER")
if not all([sender, password, receiver]):
return
msg = MIMEMultipart("alternative")
msg["Subject"] = f"🔔 Giá {product['name']} đã giảm!"
msg["From"] = sender; msg["To"] = receiver
html = f"""
🎉 Giá mục tiêu đạt được!
{product['name']}
Giá hiện tại: {current_price:,.0f}₫
Mục tiêu: {product['target_price']:,.0f}₫
🛒 Mua ngay
"""
msg.attach(MIMEText(html, "html"))
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
server.login(sender, password)
server.sendmail(sender, receiver, msg.as_string())
print(" 📧 Đã gửi email thông báo!")
def track_prices():
print(f"\n{'='*50}\n🔍 Kiểm tra giá lúc {datetime.now().strftime('%d/%m/%Y %H:%M')}")
history = load_history()
driver = get_driver(headless=True)
try:
for product in PRODUCTS:
print(f"\n📦 {product['name']}")
current_price = get_price(driver, product)
if current_price is None:
continue
name = product["name"]
if name not in history:
history[name] = []
history[name].append({"time": datetime.now().isoformat(), "price": current_price})
history[name] = history[name][-90:]
if current_price <= product["target_price"]:
print(f" 🎯 ĐẠT MỤC TIÊU!")
send_alert_email(product, current_price)
finally:
driver.quit()
save_history(history)
print("✅ Hoàn thành!")
if __name__ == "__main__":
track_prices()
Nhiều doanh nghiệp cần điền cùng một form hàng trăm, hàng nghìn lần. Tool này tiết kiệm hàng chục giờ thủ công.
"""
auto_form_filler.py — Tự động điền form từ CSV
"""
import csv, time, os
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC
from driver_setup import get_driver
FORM_CONFIG = {
"url": "https://target-website.com/register",
# Mapping: tên cột CSV → CSS selector
"fields": {
"ho_ten": "#fullName",
"email": "#email",
"dien_thoai": "#phone",
"dia_chi": "#address",
"tinh_thanh": "#province",
},
"select_fields": ["tinh_thanh"], # Các field là dropdown
"submit_btn": "button[type='submit']",
"success_indicator": "Đăng ký thành công",
"delay_between_submissions": 2,
}
def read_csv_data(filepath):
with open(filepath, "r", encoding="utf-8-sig") as f:
return list(csv.DictReader(f))
def fill_form(driver, data):
wait = WebDriverWait(driver, 15)
try:
driver.get(FORM_CONFIG["url"])
for field_name, selector in FORM_CONFIG["fields"].items():
value = data.get(field_name, "").strip()
if not value:
continue
element = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))
if field_name in FORM_CONFIG["select_fields"]:
Select(element).select_by_visible_text(value)
else:
element.clear(); element.send_keys(value)
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, FORM_CONFIG["submit_btn"]))).click()
wait.until(EC.text_to_be_present_in_element((By.TAG_NAME, "body"), FORM_CONFIG["success_indicator"]))
return True
except Exception as e:
print(f" ❌ Lỗi: {e}")
driver.save_screenshot(f"screenshots/error_{data.get('email','unknown')}.png")
return False
def run_batch(csv_file):
os.makedirs("screenshots", exist_ok=True)
rows = read_csv_data(csv_file)
print(f"📋 Tổng cộng {len(rows)} records")
driver = get_driver(headless=False)
success = fail = 0
try:
for i, row in enumerate(rows, 1):
print(f"[{i}/{len(rows)}] {row.get('ho_ten', 'Record')}")
if fill_form(driver, row):
success += 1; print(" ✅ OK")
else:
fail += 1
if i < len(rows):
time.sleep(FORM_CONFIG["delay_between_submissions"])
finally:
driver.quit()
print(f"\n✅ Thành công: {success} | ❌ Thất bại: {fail}")
if __name__ == "__main__":
if not os.path.exists("data.csv"):
with open("data.csv", "w", encoding="utf-8-sig", newline="") as f:
f.write("ho_ten,email,dien_thoai,dia_chi,tinh_thanh\n")
f.write("Nguyễn Văn A,a@email.com,0912345678,123 Lê Lợi,Hà Nội\n")
print("📄 Tạo data.csv mẫu — điền dữ liệu thật rồi chạy lại")
else:
run_batch("data.csv")
🎯 Thử Thách — Build Price Tracker Hoàn Chỉnh:
- Chọn 1 website bán hàng bạn hay dùng (Tiki, Shopee, Lazada)
- Mở DevTools (F12) → Inspect giá → Copy CSS selector
- Copy code price_tracker.py, cập nhật CONFIG
- Chạy thử để xem có lấy được giá không
- Setup Task Scheduler (Windows) / cron (Linux) chạy mỗi 1 tiếng
- Bonus: Thêm Telegram bot notification thay vì email
Thêm Telegram bot notification vào price_tracker.py. Khi giá đạt target → gửi Telegram message thay vì email. - Dùng requests gọi Telegram Bot API trực tiếp - Message format đẹp với emoji - Có inline keyboard "Mua ngay" → link sản phẩm - Token từ .env: TELEGRAM_TOKEN, TELEGRAM_CHAT_ID Viết hàm send_telegram_alert() và tích hợp vào track_prices(). Giữ nguyên email notification — có cả 2 cách.
Sau bài 4.7 và 4.8 bạn đã có trong tay 3 automation tools thực sự có giá trị: Auto Login Fetcher (thu thập data sau login), Price Tracker (theo dõi giá + alert), và Form Auto-Filler (điền form hàng loạt). Mỗi tool có thể tùy chỉnh cho nhiều use case khác nhau và bán được ngay.