Hôm nay trên Hacker News có một bài lên top với tiêu đề rất đơn giản: "If you love a website, back it up". Đọc xong mình nghĩ ngay đến mấy lần "mất trắng" của chính mình: một blog kỹ thuật tiếng Việt cực hay về Linux bị hết hạn domain, một tutorial về Nginx mình bookmark từ 2019 giờ trả về 404, và cả blog cá nhân của mình trên một nền tảng miễn phí đã đóng cửa. Bookmark không phải là backup. Link sẽ chết, domain sẽ hết hạn, nền tảng sẽ đổi chính sách. Bài này mình chia sẻ cách mình đang archive web content thực tế: từ một trang lẻ, đến cả site, đến một hệ thống tự động chạy định kỳ.
Chọn đúng công cụ cho đúng nhu cầu
Trước khi gõ lệnh, cần xác định bạn muốn lưu cái gì. Mình chia làm 3 nhóm:
-
Một trang lẻ (bài viết, tài liệu): dùng
monolithhoặc extension SingleFile để gói toàn bộ HTML, CSS, ảnh, font thành một file.htmlduy nhất. Mở offline vẫn y nguyên. -
Cả một site tĩnh (blog, docs): dùng
wget --mirror, kèm theo file WARC nếu muốn chuẩn lưu trữ. - Bộ sưu tập lâu dài, có tìm kiếm: self-host ArchiveBox, nó gọi nhiều công cụ cùng lúc (wget, SingleFile, Chromium để chụp PDF/screenshot, readability để tách text).
flowchart TD
A[Muốn backup gì?] --> B{Phạm vi}
B -->|1 trang| C[monolith / SingleFile]
B -->|Cả site tĩnh| D[wget --mirror + WARC]
B -->|Bộ sưu tập lâu dài| E[ArchiveBox self-host]
C --> F[File .html độc lập]
D --> G[Thư mục HTML + file .warc.gz]
E --> H[Web UI + tìm kiếm full-text]
Một lưu ý quan trọng: site render bằng JavaScript (React/Vue SPA) thì wget chỉ tải về cái khung HTML rỗng. Với loại này bạn cần công cụ dùng headless browser như SingleFile CLI hoặc ArchiveBox (có Chromium bên trong).
Mirror cả site với wget (và tại sao nên bật WARC)
wget có sẵn trên hầu hết Linux, trên macOS cài bằng brew install wget. Từ bản 1.14 trở đi wget đã hỗ trợ ghi WARC (Web ARChive, chuẩn ISO 28500 mà Internet Archive dùng). Đây là lệnh mình hay dùng:
#!/usr/bin/env bash
# mirror-site.sh - backup một site tĩnh, kèm file WARC
set -euo pipefail
URL="$1"
NAME=$(echo "$URL" | sed -E 's#https?://##; s#/.*##')
DATE=$(date +%Y%m%d)
OUT="$HOME/web-archive/$NAME/$DATE"
mkdir -p "$OUT" && cd "$OUT"
wget --mirror --convert-links --adjust-extension --page-requisites --no-parent --wait=1 --random-wait --limit-rate=500k --user-agent='Mozilla/5.0 (personal-archive)' --warc-file="$NAME-$DATE" --warc-cdx "$URL"
echo "Xong: $OUT"
du -sh "$OUT"
Giải thích nhanh mấy flag quan trọng:
-
--mirror: tương đương-r -N -l inf --no-remove-listing, tải đệ quy và chỉ tải lại file đã thay đổi. -
--convert-links: sửa link trong HTML thành link local, để mở offline click được. -
--page-requisites: tải luôn CSS, JS, ảnh mà trang cần. -
--no-parent: không leo lên thư mục cha. Backupexample.com/blog/thì không kéo cả site về. -
--wait=1 --random-wait --limit-rate: lịch sự với server. Đừng biến backup thành DDoS nhỏ vào site mà bạn yêu thích. -
--warc-file: ngoài thư mục HTML, sinh thêm file.warc.gzchứa nguyên HTTP request/response. Sau này có thể replay bằngpywbhoặc ReplayWeb.page, hoặc nộp cho các dự án lưu trữ.
Mẹo: kiểm tra robots.txt và điều khoản của site trước. Backup để đọc cá nhân thì thường ổn, nhưng đừng đem nội dung người khác đi đăng lại.
Archive từng trang theo sitemap bằng Python + monolith
Với blog có sitemap.xml (gần như mọi blog WordPress, Hugo, Ghost đều có), mình thích archive từng bài thành một file .html độc lập bằng monolith. Mỗi bài một file, dễ grep, dễ đồng bộ sang ổ khác.
#!/usr/bin/env python3
# archive_sitemap.py - Python 3.10+, cần: pip install requests
import hashlib
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
from pathlib import Path
import requests
NS = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}
OUT_DIR = Path.home() / "web-archive" / "pages"
def get_urls(sitemap_url: str) -> list[str]:
resp = requests.get(sitemap_url, timeout=30)
resp.raise_for_status()
root = ET.fromstring(resp.content)
# sitemap index -> đệ quy vào từng sitemap con
if root.tag.endswith("sitemapindex"):
urls = []
for loc in root.findall("sm:sitemap/sm:loc", NS):
urls.extend(get_urls(loc.text.strip()))
return urls
return [loc.text.strip() for loc in root.findall("sm:url/sm:loc", NS)]
def archive(url: str) -> None:
key = hashlib.sha1(url.encode()).hexdigest()[:12]
target = OUT_DIR / f"{key}.html"
if target.exists():
print(f"[skip] {url}")
return
# -j: bỏ JavaScript, -I: cô lập file khỏi network khi mở lại
result = subprocess.run(
["monolith", url, "-j", "-I", "-o", str(target)],
capture_output=True, text=True, timeout=120,
)
status = "ok" if result.returncode == 0 else "FAIL"
print(f"[{status}] {url} -> {target.name}")
with open(OUT_DIR / "index.tsv", "a") as f:
f.write(f"{key}\t{status}\t{url}\n")
if __name__ == "__main__":
OUT_DIR.mkdir(parents=True, exist_ok=True)
for u in get_urls(sys.argv[1]):
archive(u)
time.sleep(2) # lịch sự với server
Chạy: python3 archive_sitemap.py https://example.com/sitemap.xml. File index.tsv giúp map hash ngược về URL gốc. Vì có check target.exists(), chạy lại nhiều lần chỉ tải bài mới, rất hợp để đưa vào cron.
Flag -j của monolith bỏ JavaScript, giúp file nhẹ và an toàn hơn khi mở lại sau nhiều năm. Nếu trang cần JS để hiện nội dung thì bỏ flag này hoặc chuyển sang SingleFile CLI.
Tự động hóa: ArchiveBox + cron + backup 3-2-1
Khi số lượng link lên tới hàng trăm, mình chuyển sang ArchiveBox (dòng 0.7.x chạy ổn định qua Docker). Nó lưu mỗi URL bằng nhiều định dạng cùng lúc: HTML gốc, SingleFile, PDF, screenshot, WARC, text đã tách. Cũng có web UI để tìm kiếm.
mkdir -p ~/archivebox/data && cd ~/archivebox
curl -fsSL 'https://docker-compose.archivebox.io' > docker-compose.yml
docker compose run archivebox init --setup
docker compose up -d
# thêm link lẻ hoặc cả file danh sách
docker compose run archivebox add 'https://example.com/bai-viet-hay'
docker compose run archivebox add < ~/links.txt
# cron: mỗi Chủ nhật 3h sáng, mirror lại blog yêu thích
# 0 3 * * 0 /home/andy/bin/mirror-site.sh https://example.com/blog/ >> /var/log/mirror.log 2>&1
Nhưng archive nằm trên đúng một ổ cứng thì vẫn chưa phải backup. Mình áp dụng quy tắc 3-2-1: 3 bản, 2 loại thiết bị, 1 bản ở ngoài nhà.
flowchart LR
A[cron hàng tuần] --> B[wget / monolith / ArchiveBox]
B --> C[(Ổ local ~/web-archive)]
C -->|restic backup| D[(NAS trong nhà)]
C -->|restic backup| E[(Object storage S3-compatible)]
D --> F[restic check định kỳ]
E --> F
Mình dùng restic (0.16+) vì nó tự deduplicate và mã hóa. Archive web có rất nhiều file giống nhau giữa các lần chạy (CSS, font, logo), nên deduplicate giảm dung lượng rất đáng kể:
restic -r /mnt/nas/restic-web init
restic -r /mnt/nas/restic-web backup ~/web-archive --tag weekly
restic -r /mnt/nas/restic-web forget --keep-weekly 8 --keep-monthly 12 --prune
restic -r /mnt/nas/restic-web check
Đừng quên bước cuối: thỉnh thoảng thử mở lại archive. Backup chưa từng restore thì coi như chưa có backup. Mỗi quý mình chọn ngẫu nhiên vài file .html và một file .warc.gz, mở thử xem còn đọc được không.
Kết luận
Internet không có nút "lưu vĩnh viễn". Những tài liệu, blog tiếng Việt chất lượng mà bạn học được nhiều thứ từ đó có thể biến mất bất cứ lúc nào. Vài việc làm được ngay tối nay:
- Liệt kê 5-10 site bạn sợ mất nhất: blog kỹ thuật yêu thích, docs nội bộ, và cả blog của chính bạn.
-
Trang lẻ: cài
monolithhoặc extension SingleFile, lưu những bài quan trọng thành file.htmlđộc lập. -
Site tĩnh: dùng
wget --mirror ... --warc-file, nhớ đặt--waitvà--limit-rateđể không làm phiền server. - Blog có sitemap: dùng script Python ở trên, đưa vào cron chạy hàng tuần.
- Hơn trăm link: self-host ArchiveBox bằng Docker.
- Áp dụng 3-2-1 với restic, và định kỳ kiểm tra khả năng restore.
Và nếu bạn đang viết blog trên một nền tảng nào đó: hãy export nội dung của chính mình ra Markdown ngay hôm nay. Người cần backup đầu tiên chính là bạn.
Top comments (0)