כולנו מכירים את זה. השעה 23:30, פריסה לייצור מתקדמת ברקע בלי תקלות, ואתם פותחים את Hacker News ״רק לחמש דקות״. שעתיים אחר כך אתם כבר שקועים במסע קריאה על ההיסטוריה של מכונת Lisp, עמידות בפני תקלות ביזנטיות או הקשר בין אמות המים הרומיות לארכיטקטורות מודרניות של שירותי־מיקרו.
כמפתחים, יש לנו תיאבון מיוחד ובלתי נדלה למידע נישתי, טכני מאוד ועשיר בהיסטוריה. ויקיפדיה היא לעיתים קרובות היעד לצלילות העומק האלה. אבל איך מפרידים בין העיקר לרעש? איך מגלים את ערכי ויקיפדיה המסוימים שבאמת מעוררים את הסקרנות המשותפת של קהילת הנדסת התוכנה?
פרויקט HN Wikipedia של OrangeCrumbs שותף בפרסום Show HN שכותרתו ״גלו ערכי ויקיפדיה פופולריים ב־Hacker News״. הוא משמש מקור השראה לכלי לגילוי חומרי קריאה. תהליך העיבוד שבהמשך הוא תכנון להמחשה, ולא תיאור מאומת של רישיון הפרויקט, הדירוג שלו או המימוש הפנימי שלו.
נחבר שליפה של פריטי HN, ניתוח כתובות ויקיפדיה, העשרה בנתוני צפיות יומיים ושמירה במטמון. הקטעים מדגימים את הרכיבים האלה; הם אינם שירות שלם בעל תפוקה גבוהה. עדיין צריך לממש הגבלת עבודה, ניסיונות חוזרים, פתרון כותרות קנוניות ומתזמן רענון.
הארכיטקטורה של תהליך לגילוי עניין טכנולוגי
קל יותר להבין תהליך גילוי מובנה מאשר גירוד חוזר של דף בית מרונדר. בתכנון יש שלושה שלבים, והעבודה התפעולית נעשית בנפרד מטעינת הדף בצד הלקוח:
- קליטה: שלפו פריטי HN ועברו באופן מבוקר על הקשרים בין כתבות לתגובות, עם הגבלת היקף העבודה.
- חילוץ וסינון: קראו כתובות של כתבות ונתחו HTML של תגובות לחילוץ ערכי href, ואז אמתו ונרמלו כתובות ויקיפדיה נתמכות.
- צבירה והעשרה: שלפו סדרות של צפיות, שמרו את משמעות הפרויקט, התאריך וסוג התעבורה שלהן ושמרו אותן במטמון לניתוח.
כך נראית באופן עקרוני זרימת הנתונים בתהליך קל המשקל הזה:
[ HN Firebase API ] ---> [ Bounded Ingestion Worker ]
|
v
[ HTML href extraction / URL parser ]
|
v
[ Wikimedia Pageviews API ]
|
v
[ Redis Cache / SQLite DB ] ---> [ Frontend UI ]
דוגמאות JavaScript שלהלן משתמשות בגרסת Node.js נתמכת עם fetch מובנה, ניתוח URL ותמיכה בזמן קצוב. בדיקות לא מקוונות כיסו מקרי ניתוח, תאריכים וסכמה נבחרים, ולא אינטגרציה חיה עם API או ביצועים.
שלב 1: כריית נתונים מה־API של Hacker News
HN חושף מידע ציבורי דרך API מבוסס Firebase כמעט בזמן אמת. פריטים בודדים ורשימות מובילות זמינים ב־HTTP, וספריות Firebase יכולות לעקוב אחר עדכונים. פונקציות העזר הפשוטות שלהלן מביאות תמונות מצב; הן אינן מממשות קליטה בזרימה או מנוע חיפוש מורכב.
התחילו בשליפת מזהי הרשימה המובילה ופריטים בודדים. הרשימה יכולה לכלול עד 500 פריטים, ובהם משרות; לא מדובר בהכרח ב־500 כתבות. הפונקציות האלה עדיין אינן מסננות קישורי ויקיפדיה או עוברות על תגובות.
// Use a supported Node.js release with built-in fetch; not a complete ingestion worker.
const HN_BASE_URL = 'https://hacker-news.firebaseio.com/v0';
async function requestJson(url, options = {}) {
const response = await fetch(url, {
...options,
signal: AbortSignal.timeout(10000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
async function getTopStoryIds() {
const ids = await requestJson(`${HN_BASE_URL}/topstories.json`);
if (!Array.isArray(ids)) throw new Error('Expected an ID array');
return ids.filter(id => Number.isSafeInteger(id) && id > 0);
}
async function getItemDetails(id) {
if (!Number.isSafeInteger(id) || id <= 0) throw new Error('Invalid item ID');
return requestJson(`${HN_BASE_URL}/item/${id}.json`);
}
לפריטי כתבה עשוי להיות שדה url; תגובות עשויות להכיל HTML בשדה text. עברו באופן מבוקר על kids, טפלו בפריטים חסרים, שנמחקו או שסומנו כלא פעילים, והגבילו עומק, היקף עבודה ומקביליות. חלצו מאפייני href בעזרת מנתח HTML לפני העברת כתובות בודדות לפונקציה הבאה. שילוב מנגנון המעבר ומנתח ה־HTML אינו כלול כאן.
שלב 2: חילוץ מדויק של מזהי ערכים בוויקיפדיה
כותרת מזהה דף בתוך ויקי מסוים, ולא באופן גלובלי בכל מהדורות השפה. כתובות עשויות להשתמש במארח רגיל או נייד, וכותרות יכולות להכיל Unicode, סוגריים או לוכסנים. הפונקציה הבאה תומכת רק בכתובות /wiki/ וברשימת פרויקטים מפורשת באנגלית, בגרמנית ובעברית; היא אינה מנתחת טקסט שרירותי או כל צורת כתובת.
השתמשו במנתח URL לאימות המארח והנתיב, ואז פענחו את הכותרת בזהירות. התוצאה היא זוג מועמד של פרויקט וכותרת; יש לפתור הפניות וכותרות קנוניות דרך ה־API המתאים לפני איחוד רשומות. קידוד אחוזים פגום מחזיר null במקום להפיל את תהליך הקליטה.
// Deliberately limited project allowlist; expand only with verified projects.
const SUPPORTED_PROJECTS = new Set([
'en.wikipedia.org', 'de.wikipedia.org', 'he.wikipedia.org'
]);
// Input must be one extracted URL, not raw HTML or arbitrary prose.
function parseWikipediaUrl(rawUrl) {
if (typeof rawUrl !== 'string') return null;
let url;
try { url = new URL(rawUrl); } catch { return null; }
if (!['https:', 'http:'].includes(url.protocol)) return null;
if (url.username || url.password || url.port) return null;
const match = /^([a-z][a-z0-9-]*)\.(?:m\.)?wikipedia\.org$/.exec(url.hostname);
if (!match || !url.pathname.startsWith('/wiki/')) return null;
const project = `${match[1]}.wikipedia.org`;
if (!SUPPORTED_PROJECTS.has(project)) return null;
let slug;
try { slug = decodeURIComponent(url.pathname.slice(6)).replace(/ /g, '_'); }
catch { return null; }
if (!slug || /[\u0000-\u001f\u007f]/.test(slug)) return null;
return { lang: match[1], project, slug };
}
const example = parseWikipediaUrl(
'https://en.wikipedia.org/wiki/C_(programming_language)#History'
);
// Expected: { lang: 'en', project: 'en.wikipedia.org',
// slug: 'C_(programming_language)' }
שלב 3: העשרת הנתונים באמצעות Wikimedia Pageviews API
ה־Pageviews API הציבורי של ויקימדיה יכול להעשיר הפניה לערך בנתוני תעבורה מצרפיים. הוא אינו אומר אילו מבקרים הגיעו מ־HN או אם אזכור גרם לזינוק. השתמשו ב־User-Agent תיאורי עם פרטי קשר תקפים של מפעיל הלקוח ופעלו לפי מדיניות הגישה לשירות.
הפונקציה הבאה מבקשת 30 ימי UTC שהסתיימו, עד אתמול, מסננת תעבורה אוטומטית שזוהתה באמצעות user ומקודדת את הכותרת כולה כרכיב יחיד בנתיב URI. היא בודקת שלמות במקום לפרש נתון חסר כאפס. נתונים עדכניים עדיין עשויים להתעכב. הפונקציות הקודמות והקוד הזה צריכים להימצא באותו מודול, או להשתמש בייבוא מפורש. לא בוצעה בקשה חיה לצורך התיקון הזה.
// Uses SUPPORTED_PROJECTS and requestJson from the preceding examples.
function pageviewRequest(project, slug, now = new Date()) {
if (!SUPPORTED_PROJECTS.has(project)) throw new Error('Unsupported project');
if (typeof slug !== 'string' || !slug) throw new Error('Missing title');
const end = new Date(Date.UTC(
now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 1
));
const start = new Date(end);
start.setUTCDate(start.getUTCDate() - 29);
const format = date => date.toISOString().slice(0, 10).replace(/-/g, '');
const dates = [];
for (const date = new Date(start); date <= end; date.setUTCDate(date.getUTCDate() + 1)) {
dates.push(format(date));
}
const title = encodeURIComponent(slug.replace(/ /g, '_'));
const url = 'https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/' +
`${project}/all-access/user/${title}/daily/${format(start)}/${format(end)}`;
return { url, dates };
}
async function getArticlePageviews(project, slug, userAgent, now = new Date(), fetchJson = requestJson) {
if (typeof userAgent !== 'string' || !userAgent.trim()) {
throw new Error('Supply your own descriptive User-Agent and contact information');
}
const { url, dates } = pageviewRequest(project, slug, now);
const data = await fetchJson(url, { headers: { 'User-Agent': userAgent } });
if (!Array.isArray(data.items)) throw new Error('Missing pageview data');
const rows = new Map();
for (const day of data.items) {
if (typeof day.timestamp !== 'string' || !/^\d{10}$/.test(day.timestamp) ||
!Number.isSafeInteger(day.views) || day.views < 0) {
throw new Error('Invalid pageview row');
}
const date = day.timestamp.slice(0, 8);
if (rows.has(date)) throw new Error('Duplicate pageview date');
rows.set(date, day.views);
}
if (rows.size !== 30 || dates.some(date => !rows.has(date))) {
throw new Error('Incomplete date range: keep missing data distinct from zero');
}
return dates.map(date => ({ date, views: rows.get(date) }));
}
ספקו User-Agent תיאורי משלכם ופרטי קשר: אתר רלוונטי, דף משתמש בוויקי או כתובת דוא״ל. אל תשתמשו בזהותו של כותב דוגמה. זיהוי חסר או כללי מדי עלול להוביל לחסימה. לקוחות בדפדפן עשויים להזדקק לחלופה המתועדת Api-User-Agent; הדוגמה הזאת משתמשת ב־Node.js בצד השרת.
תכנון שכבת מטמון בעלת ביצועים גבוהים
שליפת כל פריט מוביל והתגובות שלו בכל טעינת דף יוצרת ריבוי בקשות מיותר. תיעוד ה־API של HN מציין כרגע שאין מגבלת קצב; לוויקימדיה יש מדיניות גישה נפרדת. שמרו תוצאות במטמון, רעננו בנפרד מהצגת הדף, הגבילו עבודה וטפלו בהגבלת קצב או בכשלים זמניים באמצעות ניסיונות חוזרים מוגבלים. קטעי הקוד עדיין אינם מממשים את המתזמן או את מדיניות הניסיונות הזאת.
Redis או SQLite יכולים לתמוך בתכנון הזה. WAL של SQLite מאפשר לקוראים ולכותב להתקדם במקביל, אך רק לכותב יחיד בכל רגע; הוא מוסיף checkpoints וקובצי WAL וזיכרון משותף, ודורש גישה מאותו מארח ולא דרך מערכת קבצים משותפת ברשת. הוא אינו מבטיח קריאות מהירות יותר או אפס תקורה. בחרו ומדדו לפי עומס העבודה שלכם.
שמרו כל אזכור ערך נפרד לפי פריט HN ופרויקט, כולל זמן הפרסום ב־HN בנפרד מזמן הקליטה. מפתחו את מטמון הצפיות לפי פרויקט, כותרת, שיטת גישה, סוג סוכן ויום; צברו חלונות תאריכים בזמן הקריאה. כך אפשר לשמור מספר ערכים בתגובה אחת, בלי למזג כותרות זהות משפות שונות. יצירת הסכמה לבדה אינה מפעילה WAL.
CREATE TABLE IF NOT EXISTS wikipedia_mentions (
id INTEGER PRIMARY KEY,
hn_item_id INTEGER NOT NULL,
project TEXT NOT NULL,
article_title TEXT NOT NULL,
hn_score INTEGER,
hn_published_at INTEGER NOT NULL,
ingested_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (hn_item_id, project, article_title)
);
CREATE TABLE IF NOT EXISTS pageview_cache (
project TEXT NOT NULL,
article_title TEXT NOT NULL,
access TEXT NOT NULL,
agent TEXT NOT NULL,
day TEXT NOT NULL,
views INTEGER NOT NULL CHECK (views >= 0),
fetched_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (project, article_title, access, agent, day)
);
מדד זינוק צריך להשוות את היום המלא של פרסום HN לקו בסיס קודם שנבחר במפורש ואינו כולל את היום הזה. לפרסום ישן, שלפו את החלון ההיסטורי המתאים ולא את 30 הימים האחרונים נכון להיום. מעבר מ־200 ל־15,000 צפיות ביום הוא דוגמה היפותטית, ולא מדידה שבוצעה כאן. הגדירו טיפול בקווי בסיס אפסיים או חסרים ובימים לא מלאים; מתאם בזמן אינו מוכיח הפניה מ־HN או השפעה סיבתית שלו.
למה כלים כאלה הופכים אותנו למפתחים טובים יותר
אפשר לתכנן פרויקט גילוי שיעדיף סקרנות, היסטוריה ועומק טכני על פני מעורבות קצרת טווח. זו בחירת תכנון עריכתית, ולא טענה שנמדדה על כל הרשתות החברתיות או תכונה אוטומטית של אלגוריתם המלצות.
הזנת קריאה כזאת עשויה להפנות אתכם לפרוטוקולי רשת היסטוריים, לביוגרפיות של חלוצי המחשוב או לאבטחת תשתיות פיזיות. אלה כיווני קריאה אפשריים, ולא ממצאים ממאגר נתונים שנותח במאמר הזה. המטרה היא לתמוך בלמידה איטית ומכוונת יותר.
סיכום: בנו מנוע גילוי משלכם
פרויקט HN Wikipedia מדגים רעיון מושך לגילוי תוכן. מימוש נפרד יכול לשלב ממשקי API ציבוריים, ניתוח שמבין כתובות URL ומטמונים עם מפתחות מדויקים. אמינות עדיין תלויה בחילוץ שנבדק, במשמעות נתונים ברורה ובמגבלות תפעוליות, ולא רק בכמות קטנה של קוד.
נסו להתאים את התכנון לרשימת קריאה שמעניינת אתכם. אם מוסיפים קהילות או שירותים אחרים, בדקו בנפרד את ממשקי ה־API, תנאי הגישה ומשמעות הנתונים שלהם, ואל תניחו שכללי HN וויקימדיה חלים גם עליהם.
מהו מסע הקריאה המעניין ביותר בוויקיפדיה שאליו הגעתם דרך Hacker News? ספרו לי בתגובות או שתפו את המחשבות שלכם ב־Twitter/X.
עד הפעם הבאה, תכנות מהנה!
מקורות ראשוניים לתיקונים
- ה־API הרשמי של Hacker News
- דיון Show HN המקורי
- אתר פרויקט OrangeCrumbs
- כללי כותרות ב־MediaWiki
- ממשק URL של Node.js
- fetch מובנה וביטול בקשות ב־Node.js
- מדיניות הגישה לנתוני ויקימדיה
- מדיניות User-Agent של ויקימדיה
- לקוח PageViewInfo של ויקימדיה
- משמעות צפיות ותעבורה אוטומטית
- אילוצי טבלאות ב־SQLite
- יתרונות ומגבלות של WAL ב־SQLite