Xóa hàng loạt tweet, không cần tiện ích mở rộng
Một script miễn phí để dán vào console của Chrome nhằm dọn dẹp các bài đăng X cũ của bạn: chạy mô phỏng trước, giữ nguyên tweet gần đây, xóa theo từng lô có giới hạn. Không tiện ích mở rộng, không dịch vụ bên thứ ba, không gì rời khỏi trình duyệt của bạn.
DRY_RUN = true): nó liệt kê những gì sẽ bị xóa mà không đụng đến bất cứ thứ gì. Hãy tải bản lưu trữ X của bạn trước, kiểm tra bản mô phỏng, và chỉ sau đó mới chuyển sang xóa thật. Việc tự động hóa giao diện không nằm trong điều khoản sử dụng của X: hãy làm từ từ và tự chịu rủi ro.
Các bước thực hiện
- Mở hồ sơ của bạn: truy cập
x.com/ten_nguoi_dung, tab Bài đăng, khi đã đăng nhập vào tài khoản của bạn. - Mở console: nhấn
F12(hoặcCmd+Option+Jtrên Mac), rồi bấm vào tab Console. - Thiết lập script: sao chép đoạn mã bên dưới, thay
ten_nguoi_dungbằng tên định danh X của bạn (không có @), điều chỉnhKEEP_RECENT(số tweet gần đây cần giữ lại) và, nếu cần, liệt kê trongKEEP_STATUS_IDSmã định danh của những tweet không bao giờ được xóa. - Mô phỏng: dán script vào console và xác nhận bằng phím Enter. Với
DRY_RUN = true(cài đặt mặc định), nó hiển thị danh sách những tweet sẽ bị xóa mà không xóa gì cả. - Xóa: nếu bản mô phỏng vừa ý bạn, hãy chuyển
DRY_RUNsangfalsevà chạy lại. Script xóa tối đa 40 tweet mỗi lần chạy, với các khoảng dừng từ 6 đến 14 giây. - Làm lại: chạy lại script bao nhiêu lần tùy cần để xử lý phần còn lại, theo từng lô.
Đoạn mã script
(() => {
// ============ CẤU HÌNH ============
const HANDLE = "ten_nguoi_dung"; // tên định danh X của bạn, không có @
const KEEP_RECENT = 15; // KHÔNG BAO GIỜ xóa 15 tweet gần đây nhất (ở trên cùng)
const KEEP_STATUS_IDS = [ // lớp bảo vệ THÊM (tùy chọn): mã định danh không bao giờ được xóa
"1234567890123456789",
];
let DRY_RUN = true; // true = mô phỏng (chỉ liệt kê). false = XÓA THẬT.
const MIN_DELAY_MS = 6000;
const MAX_DELAY_MS = 14000;
const MAX_DELETIONS = 40; // giới hạn mỗi lần chạy (chạy lại để tiếp tục)
// ================================
const me = HANDLE.toLowerCase();
const KEEP = new Set(KEEP_STATUS_IDS.map(String));
const protectedRecent = new Set();
const RX_DELETE = /(Xóa|delete|supprimer)/i;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const rand = (a, b) => Math.floor(a + Math.random() * (b - a));
const skipped = new Set();
if (!location.pathname.toLowerCase().startsWith("/" + me)) {
console.warn(`%c[DỪNG] Hãy vào https://x.com/${HANDLE} (tab Bài đăng) rồi chạy lại.`,
"color:#e0245e;font-weight:bold");
return;
}
const articles = () => [...document.querySelectorAll('article[data-testid="tweet"]')];
const idOf = (art) => {
const href = [...art.querySelectorAll('a[href*="/status/"]')]
.map((x) => x.getAttribute("href"))
.find((h) => new RegExp(`^/${HANDLE}/status/\\d+`, "i").test(h));
if (!href) return null;
const m = href.match(/status\/(\d+)/);
return m ? m[1] : null;
};
// Tải và bảo vệ KEEP_RECENT tweet gần đây nhất (những cái ở trên cùng).
async function protectRecent() {
const order = [], seen = new Set();
let noNew = 0;
window.scrollTo(0, 0);
await sleep(1200);
while (order.length < KEEP_RECENT && noNew < 6) {
let added = 0;
for (const art of articles()) {
const id = idOf(art);
if (id && !seen.has(id)) { seen.add(id); order.push(id); added++; }
}
noNew = added ? 0 : noNew + 1;
if (order.length >= KEEP_RECENT) break;
const cs = articles();
(cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
await sleep(1400);
}
for (const id of order.slice(0, KEEP_RECENT)) protectedRecent.add(id);
return order.slice(0, KEEP_RECENT);
}
const eligible = (art) => {
const id = idOf(art);
if (!id) return null;
if (protectedRecent.has(id)) return null; // một trong những cái gần đây nhất
if (KEEP.has(id)) return null; // mã định danh được bảo vệ thêm
if (skipped.has(id)) return null;
return id;
};
async function deleteOne(art, id) {
const caret = art.querySelector('[data-testid="caret"]');
if (!caret) { skipped.add(id); return false; }
caret.click();
await sleep(700);
const del = [...document.querySelectorAll('[role="menuitem"]')]
.find((i) => RX_DELETE.test((i.innerText || "").trim()));
if (!del) { document.body.click(); skipped.add(id); return false; }
del.click();
await sleep(700);
const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (!confirmBtn) { skipped.add(id); return false; }
confirmBtn.click();
await sleep(1500);
return true;
}
async function dryScan() {
const seen = new Set();
let noNew = 0;
console.log("%c[MÔ PHỎNG] Sẽ không xóa gì cả. Đang đếm...", "color:#1d9bf0;font-weight:bold");
while (noNew < 4) {
let added = 0;
for (const art of articles()) {
const id = eligible(art);
if (id && !seen.has(id)) { seen.add(id); added++; }
}
noNew = added ? 0 : noNew + 1;
const cs = articles();
(cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
await sleep(1200);
}
console.log(`%c[MÔ PHỎNG] ${seen.size} tweet SẼ bị xóa (những cái gần đây nhất được bảo vệ).`,
"color:#1d9bf0;font-weight:bold");
console.log([...seen].slice(0, 60));
}
async function run() {
let count = 0, guard = 0, noHit = 0, started = false;
while (count < MAX_DELETIONS && guard++ < 1500) {
const pair = articles().map((a) => [a, eligible(a)]).find(([, id]) => id);
if (!pair) {
if ((started && noHit >= 6) || noHit >= 40) break;
const cs = articles();
(cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
await sleep(1500);
noHit++;
continue;
}
started = true; noHit = 0;
const [node, id] = pair;
node.scrollIntoView({ block: "center" });
await sleep(600);
const ok = await deleteOne(node, id);
if (ok) count++;
console.log(`${ok ? "đã xóa" : "đã bỏ qua"} ${id} (tổng: ${count}/${MAX_DELETIONS})`);
await sleep(rand(MIN_DELAY_MS, MAX_DELAY_MS));
}
console.log(`%c[XONG] Đã xóa ${count} tweet lượt này. Chạy lại script nếu vẫn còn.`,
"color:#00ba7c;font-weight:bold");
}
(async () => {
console.log(`Đang tải và bảo vệ ${KEEP_RECENT} tweet gần đây nhất...`);
const recent = await protectRecent();
if (recent.length < KEEP_RECENT) {
console.warn(`%c[DỪNG] Chỉ tải được ${recent.length} tweet (X đã giới hạn tốc độ). Tải lại trang (F5) rồi thử lại.`,
"color:#e0245e;font-weight:bold");
return;
}
console.log("%cĐã bảo vệ (những cái gần đây nhất):", "color:#00ba7c;font-weight:bold", recent);
if (DRY_RUN) { await dryScan(); return; }
await run();
})();
})();
Script bảo vệ bạn như thế nào
Có bốn lớp phòng vệ được tích hợp sẵn. Trước tiên là chế độ mô phỏng mặc định: chừng nào DRY_RUN còn bằng true, sẽ không có gì bị xóa, bạn chỉ nhận được danh sách các tweet liên quan. Tiếp theo là bảo vệ tweet gần đây: script bắt đầu bằng việc ghi nhớ những tweet gần đây nhất của bạn (mặc định là 15) và sẽ không bao giờ đụng đến chúng. Rồi đến danh sách trắng: mọi mã định danh đặt trong KEEP_STATUS_IDS đều không thể bị xóa, tiện lợi cho các tweet đã ghim hoặc thông báo của bạn. Cuối cùng là nhịp độ thận trọng: các khoảng dừng ngẫu nhiên từ 6 đến 14 giây giữa mỗi lần xóa và tự động dừng sau 40 lần, để không kích hoạt các giới hạn của X.
Trước khi dọn dẹp lớn, hãy nhớ lưu trữ những gì quan trọng: công cụ trích xuất email và URL lấy toàn bộ liên kết từ một bản xuất tweet, công cụ đếm từ cho bạn biết bao năm đăng bài của bạn nặng chừng nào, và công cụ làm sạch văn bản chỉnh trang lại một đoạn sao chép từ bản lưu trữ. Còn cho những tweet tương lai của bạn, đã có chữ cách điệu Unicode và công cụ chuyển đổi chữ hoa chữ thường. Mọi công cụ trong bộ công cụ đều chạy ngay trong trình duyệt của bạn, như hướng dẫn này, đúng theo tinh thần chính sách bảo mật của chúng tôi.
Câu hỏi thường gặp
Việc xóa có thể hoàn tác được không?
Không. Một tweet đã xóa là mất vĩnh viễn, X không có thùng rác nào cả. Vì thế script được cung cấp ở chế độ mô phỏng (DRY_RUN = true): trước tiên hãy chạy nguyên như vậy để lấy danh sách những gì sẽ bị xóa, kiểm tra lại, rồi chỉ chuyển sang false khi bạn thật sự chắc chắn. Cũng nên tải bản lưu trữ X của bạn trước đó (Cài đặt, rồi Tài khoản của bạn, rồi Tải xuống bản lưu trữ dữ liệu của bạn).
Script có thể xóa tweet của người khác không?
Không. X chỉ hiển thị nút Xóa trên chính các bài đăng của bạn: script chỉ có thể tác động lên tài khoản mà bạn đang đăng nhập, trên trang hồ sơ của chính bạn. Nó không đụng đến phần trả lời của người khác hay bất kỳ nội dung nào của bên thứ ba.
Vì sao lại giới hạn 40 lần xóa và có độ trễ ngẫu nhiên?
Để nhẹ nhàng với nền tảng. Xóa quá nhanh hoặc quá ồ ạt có thể kích hoạt các giới hạn của X, thậm chí khiến tài khoản bị khóa tạm thời. Script chờ từ 6 đến 14 giây giữa mỗi lần xóa và dừng lại sau 40 lần: hãy chạy lại để tiếp tục theo từng lô.
X có cho phép làm việc này không?
Bạn xóa nội dung của chính mình, đó là quyền của bạn. Tuy nhiên, việc tự động hóa giao diện web không nằm trong điều khoản sử dụng của X: hãy dùng script này một cách chừng mực và tự chịu rủi ro. Giải pháp chính thức là xóa thủ công, hoặc các dịch vụ chuyên dụng hoạt động thông qua API.
Làm sao để bảo vệ một số tweet cụ thể?
Có hai lớp bảo vệ cộng dồn: KEEP_RECENT tự động giữ lại những tweet gần đây nhất của bạn (mặc định là 15), và KEEP_STATUS_IDS nhận vào một danh sách các mã định danh không bao giờ được xóa. Mã định danh của một tweet là con số lớn ở cuối địa chỉ của nó: x.com/ten_nguoi_dung/status/1234567890123456789.