איך ניתן לחלץ טסקט מתוך HTML
לאחרונה הייתי צריך לייצר תקצירים טקסטואליים מתוך תוכן HTMLי. לא מדובר פה בניתוח סמנטי של התוכן, אלא רק שליפת עד 160 תווים ראשונים. מסתבר שגם זה לא כ"כ פשוט.
כשניגשתי למשימה, בניגוד לרגיל, לא קבעתי לעצמי מראש מה בדיוק קטע הקוד שאני כותב אמור לעשות, במקום זה התחלתי ממה שהוא וודאי אמור לעשות, ואז שיפרתי עד שקיבלתי את התוצאה שרציתי. בגדול, מה שהפונקציה צריכה לעשות הוא לדלג על כל הקטעים בתוכן שנמצאים בין התו < לבין התו > שאלו הם התגים לדוגמה ב: '<span dir="rtl">abc</span>' יישאר רק 'abc'.
ככל שהתקדמתי בכתיבת הקוד והרצת בדיקות מולו, גיליתי שיש בעיות נוספות שעלי לפתור על מנת להגיע להמרה איכותית.
אחת מהן היא טיפול בHTML Entities. אם למשל מופיע בHTML הרצף < ארצה שהוא יומר ל'<'. כאן באה השאלה מתי לבצע את ההמרה? שכן אם אבצע אותה אחרי שהסרתי את התגיות אז בדוגמה הבאה: '&l<b>t;</b><abc>' אני אקבל '<<abc>'. ואם אבצע אותה לפני הסרת התגיות אז אקבל '<', כשהתוצאה הרצויה היא: '<<abc>'.
בעיה נוספת שהיה עלי לפתור היא Whitespace collapsing. בHTML, מספר תווי רווח עוקבים נחשבים לאחד, ככה שהפלט עבור '<b>hello </b> <b> world</b>' צריך להיות 'hello world' עם רווח בודד בין המילים. גם כאן חשוב לזכור שביצוע הסרת רווחים מיותרים אחרי המרת HTML Entities יגרום להסרת כל ה שלא אמורים להיות מוסרים בwhitespace collapsing.
כמו"כ, חשוב לזכור את נושא הביצועים. כמה פעמים הפונקציה צריכה לרוץ על כל התוכן כדי לבצע את כל הפעולות האלו? מסתבר שמספיק פעם אחת בלבד!
public static string ExtractTextFromHtml(string html, bool collapseWhitespaces, bool convertBrToNewLine)
{
StringBuilder result = new StringBuilder();
bool inTag = false;
bool inWhiteSpace = false;
int tagStartIdx = 0;
int contentStartIdx = 0;
char chr;
for (int i = 0; i < html.Length; i++)
{
chr = html[i];
if (chr == '<')
{
if (!inTag)
{
if (!inWhiteSpace)
{
result.Append(HttpUtility.HtmlDecode(html.Substring(contentStartIdx, i - contentStartIdx)));
}
tagStartIdx = i + 1;
inTag = true;
}
}
else if (chr == '>')
{
inTag = false;
contentStartIdx = i + 1;
if (convertBrToNewLine &&
string.Equals(
html.Substring(tagStartIdx, i - tagStartIdx - ((html[i - 1] == '/') ? 1 : 0)).Trim(),
"br",
StringComparison.InvariantCultureIgnoreCase)) // <br /> <br>
{
result.Append("\n");
inWhiteSpace = true;
}
}
else if (collapseWhitespaces && char.IsWhiteSpace(chr))
{
if (!inTag && !inWhiteSpace)
{
result.Append(HttpUtility.HtmlDecode(html.Substring(contentStartIdx, i - contentStartIdx)));
result.Append(" ");
inWhiteSpace = true;
contentStartIdx = i + 1;
}
}
else
{
if (!inTag && inWhiteSpace)
{
contentStartIdx = i;
inWhiteSpace = false;
}
}
}
result.Append(HttpUtility.HtmlDecode(html.Substring(contentStartIdx, html.Length - contentStartIdx)));
return result.ToString();
}
את הפונקציה בדקתי מול המחרוזת הזו:
var html = "'<b>hello </b> <b> world</b>\n <i>&nb</i>sp;< br ><span dir=\"ltr\">line</span>< br />&l<b>t;</b><abc>'";
var str = ExtractTextFromHtml(html, true, true);
הפונקציה שהדגמתי תיתן את התוצאה הכי קרובה לParser מלא של HTML לדוגמה Html Agility Pack אבל במהירות גבוהה בהרבה. חשוב לציין שהיא תעבוד באופן זהה בין על XHTML תיקני ובין על HTML 4.
דרך נוספת, אבל פחות יעילה הן מבחינת ביצועים והן מבחינת תאימות, תהיה שימוש בביטוי רגולרי להסרת התגיות.