mirror of
https://git.victorphan.net/basketballcantho/study4.git
synced 2026-08-05 06:13:11 +07:00
Add source code, scraper extension, and result files
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# Hướng dẫn sử dụng Study4 Scraper
|
||||
|
||||
Đây là tiện ích mở rộng (Extension) dành cho trình duyệt Chrome/Edge, giúp bạn trích xuất tự động danh sách từ vựng từ trang web Study4 và xuất ra định dạng JSON, CSV hoặc chuẩn bị sẵn toàn bộ dữ liệu (Bao gồm file hình ảnh và âm thanh) để nhập vào phần mềm flashcard **Anki**.
|
||||
|
||||
## 1. Cài đặt Extension vào trình duyệt
|
||||
|
||||
1. Mở trình duyệt Chrome hoặc Edge.
|
||||
2. Truy cập vào trang quản lý tiện ích mở rộng:
|
||||
- Trên Chrome: gõ `chrome://extensions/` vào thanh địa chỉ.
|
||||
- Trên Edge: gõ `edge://extensions/` vào thanh địa chỉ.
|
||||
3. Bật **Chế độ nhà phát triển** (Developer mode) ở góc phải màn hình (hoặc thanh menu bên trái đối với Edge).
|
||||
4. Nhấn nút **Tải tiện ích đã giải nén** (Load unpacked).
|
||||
5. Chọn thư mục `study4_scraper_extension` chứa các file mã nguồn của extension này.
|
||||
6. Biểu tượng của Study4 Scraper sẽ xuất hiện trên thanh công cụ của trình duyệt. Bạn nên "Ghim" (Pin) biểu tượng này ra ngoài để dễ bấm.
|
||||
|
||||
## 2. Cách sử dụng cơ bản
|
||||
|
||||
1. Truy cập vào trang bài học chứa danh sách flashcard trên Study4 (ví dụ: một khóa học chứa từ vựng IELTS).
|
||||
2. Click vào biểu tượng của **Study4 Scraper** trên thanh công cụ.
|
||||
3. Sẽ có 3 lựa chọn xuất dữ liệu:
|
||||
- **Tải về CSV (Cơ bản)**: Chỉ tải file CSV chứa văn bản (từ, nghĩa, loại từ, v.v.).
|
||||
- **Tải về JSON**: Tải dữ liệu dạng JSON dành cho nhà phát triển.
|
||||
- **Tải về file ZIP cho Anki**: Tính năng cao cấp, tải đầy đủ file CSV, hình ảnh và âm thanh. Xem chi tiết ở phần 3.
|
||||
|
||||
## 3. Cách trích xuất và nhập (Import) vào Anki (Khuyên dùng)
|
||||
|
||||
Tính năng **"Tải về file ZIP cho Anki"** là tính năng mạnh mẽ nhất, giúp bạn mang toàn bộ nội dung học từ Study4 sang Anki để ôn tập offline.
|
||||
|
||||
### Bước 1: Trích xuất dữ liệu
|
||||
1. Mở trang Flashcard trên Study4.
|
||||
2. Click vào biểu tượng Extension, chọn nút màu cam: **Tải về file ZIP cho Anki (Kèm hình ảnh & âm thanh)**.
|
||||
3. Extension sẽ bắt đầu quét dữ liệu, tự động tải hình ảnh và âm thanh về. **Vui lòng chờ** (có thể mất vài chục giây) cho đến khi trình duyệt tự động tải xuống một file có tên `study4_anki_deck.zip`.
|
||||
|
||||
### Bước 2: Cài đặt Media vào Anki
|
||||
1. Giải nén file `study4_anki_deck.zip` vừa tải về. Bên trong thư mục bạn sẽ thấy:
|
||||
- 1 file `study4_anki.csv`
|
||||
- Rất nhiều file `.jpg`, `.png`, và `.mp3` (Đây là các file Media).
|
||||
2. Mở thư mục chứa dữ liệu Media của Anki trên máy tính bạn:
|
||||
- Trên Windows: Nhấn tổ hợp phím `Windows + R`, gõ `%APPDATA%\Anki2` và nhấn Enter. Mở tiếp thư mục mang tên Profile Anki của bạn (thường là `User 1`), sau đó mở thư mục `collection.media`.
|
||||
- Copy **toàn bộ** các file `.jpg`, `.png` và `.mp3` từ thư mục vừa giải nén thả vào thư mục `collection.media` này.
|
||||
|
||||
### Bước 3: Import file CSV vào Anki
|
||||
1. Mở phần mềm Anki.
|
||||
2. Chọn menu **File > Import...** (Hoặc nhấn nút Import File ở dưới cùng).
|
||||
3. Chọn file `study4_anki.csv` từ thư mục vừa giải nén.
|
||||
4. Một bảng cấu hình Import sẽ hiện ra. Hãy thiết lập như sau:
|
||||
- **Type (Loại thẻ)**: Chọn "Basic" (hoặc loại thẻ Flashcard mà bạn hay dùng).
|
||||
- **Deck (Bộ thẻ)**: Chọn bộ thẻ bạn muốn lưu từ vựng Study4.
|
||||
- **Field mapping (Ghép cột)**: Đảm bảo các cột (Field) tương ứng với Front/Back của thẻ. Trong file CSV có các cột thứ tự sau:
|
||||
1. Word
|
||||
2. Type
|
||||
3. Pronunciation
|
||||
4. Definition
|
||||
5. Examples
|
||||
6. Image
|
||||
7. UKAudio
|
||||
8. USAudio
|
||||
- **ĐẶC BIỆT QUAN TRỌNG**: Đánh dấu tích vào ô **"Allow HTML in fields"** (Cho phép dùng thẻ HTML). Nếu không chọn ô này, hình ảnh và âm thanh sẽ không hiển thị được.
|
||||
5. Nhấn **Import**.
|
||||
|
||||
Bây giờ bạn có thể mở bộ thẻ Anki lên và bắt đầu học với đầy đủ hình ảnh minh họa và âm thanh phát âm chuẩn. Chúc bạn học tốt!
|
||||
@@ -0,0 +1,118 @@
|
||||
(function() {
|
||||
function extractData(doc) {
|
||||
let items = [];
|
||||
const blocks = doc.querySelectorAll('.termlist-item.contentblock');
|
||||
|
||||
blocks.forEach(block => {
|
||||
let word = "";
|
||||
let type = "";
|
||||
let pronunciation = "";
|
||||
|
||||
// Extract from h2
|
||||
const h2 = block.querySelector('h2.h3');
|
||||
if (h2) {
|
||||
// Word is typically the first text node
|
||||
word = Array.from(h2.childNodes)
|
||||
.filter(node => node.nodeType === Node.TEXT_NODE)
|
||||
.map(node => node.textContent.trim())
|
||||
.join(' ')
|
||||
.trim();
|
||||
|
||||
const spans = h2.querySelectorAll('span');
|
||||
if (spans.length >= 2) {
|
||||
type = spans[0].textContent.trim();
|
||||
pronunciation = spans[1].textContent.trim();
|
||||
} else if (spans.length === 1) {
|
||||
type = spans[0].textContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Definition
|
||||
let definition = "";
|
||||
const defDiv = block.querySelector('.prewrap.mb-2');
|
||||
if (defDiv) {
|
||||
definition = defDiv.textContent.trim();
|
||||
}
|
||||
|
||||
// Examples
|
||||
let examples = [];
|
||||
const exListItems = block.querySelectorAll('.termlist-item-examples li');
|
||||
exListItems.forEach(li => {
|
||||
// Ignore the audio button text (usually inside span elements)
|
||||
const text = Array.from(li.childNodes)
|
||||
.filter(node => node.nodeType === Node.TEXT_NODE)
|
||||
.map(node => node.textContent.trim())
|
||||
.join(' ')
|
||||
.trim();
|
||||
if (text) examples.push(text);
|
||||
});
|
||||
|
||||
// Image
|
||||
let imageUrl = "";
|
||||
const img = block.querySelector('.termlist-item-images img');
|
||||
if (img) {
|
||||
imageUrl = img.getAttribute('data-src') || img.src;
|
||||
if (imageUrl && imageUrl.startsWith('/')) {
|
||||
imageUrl = window.location.origin + imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Audio (UK and US)
|
||||
let ukAudio = "";
|
||||
let usAudio = "";
|
||||
|
||||
// The structure is usually <audio><source src="...">
|
||||
const audioSpans = block.querySelectorAll('.jq-audio-player audio');
|
||||
audioSpans.forEach(audio => {
|
||||
const source = audio.querySelector('source');
|
||||
if (source) {
|
||||
let src = source.getAttribute('src');
|
||||
if (src && src.startsWith('/')) {
|
||||
src = window.location.origin + src;
|
||||
}
|
||||
if (src && src.includes('lang=en-GB')) {
|
||||
ukAudio = src;
|
||||
} else if (src && src.includes('lang=en-US')) {
|
||||
usAudio = src;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (word) {
|
||||
items.push({
|
||||
word: word,
|
||||
type: type,
|
||||
pronunciation: pronunciation,
|
||||
definition: definition,
|
||||
examples: examples,
|
||||
imageUrl: imageUrl,
|
||||
audio: {
|
||||
uk: ukAudio,
|
||||
us: usAudio
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
// Try to extract from the main document first
|
||||
let items = extractData(document);
|
||||
|
||||
// If none found, see if there's an iframe (like on the lesson page)
|
||||
if (items.length === 0) {
|
||||
try {
|
||||
const iframe = document.querySelector('iframe.learncourse-iframe');
|
||||
if (iframe && iframe.contentDocument) {
|
||||
items = extractData(iframe.contentDocument);
|
||||
}
|
||||
} catch (e) {
|
||||
// Might fail due to cross-origin if running locally without properly configured URLs,
|
||||
// but on study4.com it should work.
|
||||
console.error("Cannot access iframe content:", e);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
})();
|
||||
+13
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Study4 Vocabulary Scraper",
|
||||
"version": "1.0",
|
||||
"description": "Scrape vocabulary from Study4 flashcards pages.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"scripting"
|
||||
],
|
||||
"host_permissions": [
|
||||
"*://*.study4.com/*"
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "Study4 Scraper"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Study4 Scraper</title>
|
||||
<style>
|
||||
body { width: 250px; font-family: Arial, sans-serif; padding: 15px; margin: 0; background-color: #f8f9fa; }
|
||||
h3 { margin-top: 0; color: #333; text-align: center; }
|
||||
p { font-size: 13px; color: #555; text-align: center; margin-bottom: 15px; }
|
||||
button { width: 100%; padding: 10px; margin-bottom: 10px; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 4px; font-weight: bold; transition: background-color 0.2s; }
|
||||
button:hover { background-color: #0056b3; }
|
||||
.btn-success { background-color: #28a745; }
|
||||
.btn-success:hover { background-color: #218838; }
|
||||
#status { margin-top: 10px; font-size: 13px; color: #333; text-align: center; font-weight: bold; min-height: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Study4 Scraper</h3>
|
||||
<p>Nhấn nút bên dưới để tải danh sách từ vựng từ trang web hiện tại.</p>
|
||||
<button id="scrape-csv" class="btn-success">Tải về CSV (Cơ bản)</button>
|
||||
<button id="scrape-anki" style="background-color: #ff9800;">Tải về file ZIP cho Anki (Kèm hình ảnh & âm thanh)</button>
|
||||
<button id="scrape-json">Tải về JSON</button>
|
||||
<div id="status"></div>
|
||||
<script src="jszip.min.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,200 @@
|
||||
function downloadData(data, filename, type) {
|
||||
const blob = new Blob([data], { type: type });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function escapeCSV(cell) {
|
||||
let cellStr = cell ? String(cell) : "";
|
||||
if (cellStr.includes(",") || cellStr.includes("\n") || cellStr.includes('"')) {
|
||||
cellStr = '"' + cellStr.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return cellStr;
|
||||
}
|
||||
|
||||
function convertToCSV(items) {
|
||||
if (items.length === 0) return "";
|
||||
const headers = ["Word", "Type", "Pronunciation", "Definition", "Examples", "ImageURL", "UKAudioURL", "USAudioURL"];
|
||||
let csv = headers.join(",") + "\n";
|
||||
|
||||
items.forEach(item => {
|
||||
let row = [
|
||||
item.word,
|
||||
item.type,
|
||||
item.pronunciation,
|
||||
item.definition,
|
||||
item.examples.join(" | "),
|
||||
item.imageUrl,
|
||||
item.audio.uk,
|
||||
item.audio.us
|
||||
].map(escapeCSV);
|
||||
csv += row.join(",") + "\n";
|
||||
});
|
||||
return csv;
|
||||
}
|
||||
|
||||
function convertToAnkiCSV(items) {
|
||||
if (items.length === 0) return "";
|
||||
let csv = "";
|
||||
|
||||
items.forEach(item => {
|
||||
let imgTag = "";
|
||||
if (item.imageUrl) {
|
||||
let filename = item.imageUrl.split('/').pop().split('?')[0];
|
||||
// ensure filename doesn't have weird chars
|
||||
filename = decodeURIComponent(filename);
|
||||
imgTag = `<img src="${filename}">`;
|
||||
}
|
||||
|
||||
let ukTag = "";
|
||||
if (item.audio.uk) {
|
||||
let params = new URLSearchParams(item.audio.uk.split('?')[1]);
|
||||
let q = params.get('q') ? params.get('q').replace(/ /g, '_') : 'uk';
|
||||
ukTag = `[sound:${q}_uk.mp3]`;
|
||||
}
|
||||
|
||||
let usTag = "";
|
||||
if (item.audio.us) {
|
||||
let params = new URLSearchParams(item.audio.us.split('?')[1]);
|
||||
let q = params.get('q') ? params.get('q').replace(/ /g, '_') : 'us';
|
||||
usTag = `[sound:${q}_us.mp3]`;
|
||||
}
|
||||
|
||||
let row = [
|
||||
item.word,
|
||||
item.type,
|
||||
item.pronunciation,
|
||||
item.definition,
|
||||
item.examples.join("<br>"),
|
||||
imgTag,
|
||||
ukTag,
|
||||
usTag
|
||||
].map(escapeCSV);
|
||||
csv += row.join(",") + "\n";
|
||||
});
|
||||
return csv;
|
||||
}
|
||||
|
||||
document.getElementById('scrape-json').addEventListener('click', () => triggerScrape('json'));
|
||||
document.getElementById('scrape-csv').addEventListener('click', () => triggerScrape('csv'));
|
||||
if (document.getElementById('scrape-anki')) {
|
||||
document.getElementById('scrape-anki').addEventListener('click', () => triggerScrape('anki'));
|
||||
}
|
||||
|
||||
async function fetchAsBlob(url) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return await response.blob();
|
||||
}
|
||||
|
||||
async function triggerScrape(format) {
|
||||
const statusDiv = document.getElementById('status');
|
||||
statusDiv.textContent = "Đang xử lý...";
|
||||
|
||||
try {
|
||||
let [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
files: ['content.js']
|
||||
});
|
||||
|
||||
if (results && results[0] && results[0].result) {
|
||||
const data = results[0].result;
|
||||
|
||||
if (data.error) {
|
||||
statusDiv.textContent = "Lỗi: " + data.error;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
statusDiv.textContent = "Không tìm thấy từ vựng nào trên trang này.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === 'json') {
|
||||
statusDiv.textContent = `Đã tìm thấy ${data.length} từ. Đang tải xuống JSON...`;
|
||||
const jsonStr = JSON.stringify(data, null, 2);
|
||||
downloadData(jsonStr, 'study4_vocab.json', 'application/json');
|
||||
setTimeout(() => { statusDiv.textContent = "Hoàn tất!"; }, 2000);
|
||||
} else if (format === 'csv') {
|
||||
statusDiv.textContent = `Đã tìm thấy ${data.length} từ. Đang tải xuống CSV...`;
|
||||
const csvStr = convertToCSV(data);
|
||||
downloadData("\uFEFF" + csvStr, 'study4_vocab.csv', 'text/csv;charset=utf-8');
|
||||
setTimeout(() => { statusDiv.textContent = "Hoàn tất!"; }, 2000);
|
||||
} else if (format === 'anki') {
|
||||
statusDiv.textContent = `Đã tìm thấy ${data.length} từ. Đang chuẩn bị Media (có thể mất ít phút)...`;
|
||||
|
||||
const zip = new JSZip();
|
||||
|
||||
// 1. Add CSV
|
||||
const csvStr = convertToAnkiCSV(data);
|
||||
zip.file("study4_anki.csv", "\uFEFF" + csvStr);
|
||||
|
||||
let count = 0;
|
||||
let total = data.length * 3; // roughly 3 media per word (img, uk, us)
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const item = data[i];
|
||||
statusDiv.textContent = `Đang tải hình/âm thanh ${i+1}/${data.length}...`;
|
||||
|
||||
if (item.imageUrl) {
|
||||
try {
|
||||
let filename = decodeURIComponent(item.imageUrl.split('/').pop().split('?')[0]);
|
||||
const blob = await fetchAsBlob(item.imageUrl);
|
||||
zip.file(filename, blob);
|
||||
} catch (e) {
|
||||
console.error("Failed to load image", item.imageUrl, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.audio.uk) {
|
||||
try {
|
||||
let params = new URLSearchParams(item.audio.uk.split('?')[1]);
|
||||
let q = params.get('q') ? params.get('q').replace(/ /g, '_') : 'uk';
|
||||
const blob = await fetchAsBlob(item.audio.uk);
|
||||
zip.file(`${q}_uk.mp3`, blob);
|
||||
} catch (e) {
|
||||
console.error("Failed to load UK audio", item.audio.uk, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.audio.us) {
|
||||
try {
|
||||
let params = new URLSearchParams(item.audio.us.split('?')[1]);
|
||||
let q = params.get('q') ? params.get('q').replace(/ /g, '_') : 'us';
|
||||
const blob = await fetchAsBlob(item.audio.us);
|
||||
zip.file(`${q}_us.mp3`, blob);
|
||||
} catch (e) {
|
||||
console.error("Failed to load US audio", item.audio.us, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statusDiv.textContent = `Đang nén file ZIP...`;
|
||||
const content = await zip.generateAsync({type:"blob"});
|
||||
const url = URL.createObjectURL(content);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "study4_anki_deck.zip";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
statusDiv.textContent = "Hoàn tất! Hãy giải nén file ZIP, copy hình và âm thanh vào thư mục Anki collection.media, sau đó import file study4_anki.csv";
|
||||
}
|
||||
} else {
|
||||
statusDiv.textContent = "Không thể lấy dữ liệu. Hãy đảm bảo bạn đang ở trang chứa Flashcard Study4.";
|
||||
}
|
||||
} catch (e) {
|
||||
statusDiv.textContent = "Lỗi: " + e.message;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user