Vấn đề: Mỗi ngày bạn đang lãng phí bao nhiêu thời gian?
Có một số việc mình phải làm gần như mỗi ngày khi làm việc với server: kiểm tra log cũ, dọn file tạm, backup database, rồi gửi báo cáo cho team. Mỗi việc chỉ mất 5–10 phút. Nhưng cộng lại trong một tuần, đó là gần một tiếng đồng hồ — cho những thao tác hoàn toàn có thể script hóa.
Vấn đề không chỉ là thời gian. Làm thủ công thì dễ quên, dễ bỏ bước — và đúng ngày server crash là ngày bạn quên chạy backup. Chính vì vậy mình dần viết script cho hết những việc lặp này.
Bài này đi thẳng vào các script mình đang chạy crontab hàng ngày — không phải ví dụ học thuật. Nếu đã đọc bài về APScheduler hay REST API trên blog, phần này cho thấy Python ứng dụng ở tầng hệ thống: file system, process, và push notification trực tiếp.
Khái niệm cốt lõi: Python làm việc với hệ thống
Để viết script tự động hóa, bạn cần nắm 4 module stdlib quan trọng:
- os / pathlib — Duyệt thư mục, kiểm tra file, lấy metadata
- shutil — Copy, move, xóa file và thư mục
- subprocess — Chạy lệnh shell từ Python (tar, mysqldump, rsync…)
- smtplib / requests — Gửi email hoặc gọi webhook thông báo
Python không thay thế bash cho mọi thứ, nhưng với tác vụ phức tạp hơn — lọc file theo nhiều điều kiện, parse JSON response, gọi HTTP API — code sẽ ngắn gọn và debug được hơn hẳn.
Thực hành: 3 script tự động hóa thực tế
Script 1 — Tự động dọn dẹp thư mục Downloads
Thư mục ~/Downloads là nơi mọi thứ đổ vào rồi không ai dọn. Script này phân loại file theo đuôi mở rộng và di chuyển vào thư mục tương ứng:
from pathlib import Path
import shutil
DOWNLOADS = Path.home() / "Downloads"
CATEGORIES = {
"images": [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"],
"documents": [".pdf", ".docx", ".xlsx", ".pptx", ".txt", ".md"],
"archives": [".zip", ".tar", ".gz", ".rar", ".7z"],
"scripts": [".py", ".sh", ".js", ".ts"],
"videos": [".mp4", ".mkv", ".mov", ".avi"],
}
def organize_downloads():
moved = 0
for file in DOWNLOADS.iterdir():
if not file.is_file():
continue
ext = file.suffix.lower()
for category, extensions in CATEGORIES.items():
if ext in extensions:
dest_dir = DOWNLOADS / category
dest_dir.mkdir(exist_ok=True)
dest = dest_dir / file.name
# Tránh ghi đè nếu file cùng tên đã tồn tại
if dest.exists():
dest = dest_dir / f"{file.stem}_{file.stat().st_mtime_ns}{ext}"
shutil.move(str(file), str(dest))
moved += 1
break
print(f"Đã di chuyển {moved} file.")
if __name__ == "__main__":
organize_downloads()
Lần đầu chạy trên máy mình, script gom gọn 340 file tích lũy từ mấy tháng trước. Thêm vào crontab là xong — sáng nào vào cũng thấy sạch.
Script 2 — Backup database MySQL tự động với rotation
Đây là script mình dùng thực tế trên VPS. Phần hay là nó tự xóa backup cũ hơn N ngày để không chiếm disk:
import subprocess
import os
from pathlib import Path
from datetime import datetime, timedelta
DB_HOST = "localhost"
DB_USER = "backup_user"
DB_PASS = os.environ["MYSQL_BACKUP_PASS"] # Đọc từ env, KHÔNG hardcode
DB_NAME = "myapp_db"
BACKUP_DIR = Path("/var/backups/mysql")
RETENTION_DAYS = 7
def backup_mysql():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = BACKUP_DIR / f"{DB_NAME}_{timestamp}.sql.gz"
cmd = [
"mysqldump",
f"--host={DB_HOST}",
f"--user={DB_USER}",
f"--password={DB_PASS}",
"--single-transaction",
"--quick",
DB_NAME,
]
with open(filename, "wb") as f:
dump = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
gzip = subprocess.Popen(["gzip"], stdin=dump.stdout, stdout=f)
dump.stdout.close()
gzip.communicate()
dump.wait() # Đợi mysqldump kết thúc để lấy returncode chính xác
if dump.returncode != 0:
raise RuntimeError(f"mysqldump thất bại: {dump.stderr.read().decode()}")
print(f"Backup xong: {filename} ({filename.stat().st_size / 1024:.1f} KB)")
return filename
def cleanup_old_backups():
cutoff = datetime.now() - timedelta(days=RETENTION_DAYS)
removed = 0
for f in BACKUP_DIR.glob(f"{DB_NAME}_*.sql.gz"):
mtime = datetime.fromtimestamp(f.stat().st_mtime)
if mtime < cutoff:
f.unlink()
removed += 1
if removed:
print(f"Đã xóa {removed} backup cũ hơn {RETENTION_DAYS} ngày.")
if __name__ == "__main__":
backup_mysql()
cleanup_old_backups()
Lưu ý quan trọng: password đọc từ environment variable MYSQL_BACKUP_PASS, không bao giờ hardcode trong script. Mình đã từng mắc lỗi này và sau đó commit cả password lên Git — một bài học đắt giá.
Script 3 — Monitor thư mục và gửi thông báo Telegram
Script này poll thư mục mỗi 30 giây. Khi phát hiện file mới, nó ping ngay qua Telegram Bot. Dùng tốt cho upload folder của khách, thư mục báo cáo đêm tự động xuất, hoặc bất kỳ chỗ nào bạn cần biết ngay khi có file mới đổ vào:
import time
import requests
import os
from pathlib import Path
WATCH_DIR = Path("/var/www/uploads")
TG_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
TG_CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
POLL_INTERVAL = 30 # giây
def send_telegram(message: str):
url = f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage"
requests.post(url, json={"chat_id": TG_CHAT_ID, "text": message}, timeout=10)
def monitor_directory():
print(f"Đang theo dõi: {WATCH_DIR}")
known_files = set(WATCH_DIR.iterdir())
while True:
time.sleep(POLL_INTERVAL)
current_files = set(WATCH_DIR.iterdir())
new_files = current_files - known_files
for f in new_files:
size_kb = f.stat().st_size / 1024
msg = f"📁 File mới: {f.name}\nKích thước: {size_kb:.1f} KB\nThư mục: {WATCH_DIR}"
send_telegram(msg)
print(msg)
known_files = current_files
if __name__ == "__main__":
monitor_directory()
Chạy tự động với crontab
Để các script này chạy định kỳ, thêm vào crontab:
# Mở crontab
crontab -e
# Dọn Downloads mỗi sáng 8h
0 8 * * * /usr/bin/python3 /home/user/scripts/organize_downloads.py >> /var/log/organize.log 2>&1
# Backup MySQL mỗi ngày lúc 2h sáng
0 2 * * * /usr/bin/python3 /home/user/scripts/backup_mysql.py >> /var/log/backup.log 2>&1
Redirect cả stdout và stderr vào log file (2>&1) để dễ debug khi có lỗi xảy ra lúc nửa đêm.
Bài học từ thực tế: Hiệu năng khi xử lý số lượng lớn
Hồi xử lý file access log ~500MB của nginx, script crash với MemoryError vì đọc cả file vào RAM một lần. Fix thực ra rất đơn giản: bỏ readlines(), để Python tự stream từng dòng. Đây là hai cách viết — kết quả như nhau, memory usage khác hẳn:
# Cách naive — OOM nếu file lớn
with open("big_file.csv") as f:
lines = f.readlines() # Load toàn bộ vào RAM!
for line in lines:
process(line)
# Cách đúng — stream từng dòng
with open("big_file.csv") as f:
for line in f: # Generator, chỉ load 1 dòng tại một thời điểm
process(line)
Sự khác biệt rất rõ: file 500MB chiếm 1–2GB RAM với cách đầu, trong khi cách thứ hai gần như không đổi dù file lớn đến đâu. Script xử lý log hàng đêm của mình giảm từ crash xuống còn dưới 50MB constant memory sau khi đổi cách này.
Kết luận
Ba script, ba vấn đề khác nhau — nhưng cùng một triết lý: viết một lần, chạy mãi mãi. Syntax và module không phải phần khó. Cái khó là xây thói quen nhận ra: “Việc này tuần sau mình sẽ lại làm — script nó đi thôi.”
Quy tắc mình theo: nếu bạn làm một việc theo cùng một cách hơn 3 lần, hãy viết script. Bỏ ra 30 phút hôm nay, script sẽ thu lại sau vài tuần — và quan trọng hơn, bạn không còn phải giữ “checklist thủ công” trong đầu nữa.

