HtmlAgilityPack – The best way to parse HTML
בקושי עובר שבוע בלי שמישהו שואל שאלה בפורומי ASP.NET על ניתוח HTML למטרה זו או אחרת. בעיקר, השאלות מנוסחות במונחים של 'מציאת ערכים "או דומה,
מה שגרם תגובות מהקהילה שממליצות שימוש ב REGEX, בטיפול HTML כמחרוזת של טקסט ללא מבנה או כללים.
למעשה, HTML הוא פורמט מסמך מובנה עם סט של כללים מוגדרים בבירור, מה שאומר שזה יכול להיות מנותח בקלות בהינתן הכלי הנכון.
הכלי האהוב שלי עבור ניתוח HTML הוא HtmlAgilityPack.
HtmlAgilityPack בסביבה כבר זמן מה, והוא זמין באמצעות Nuget. ניתן להתקין אותו באמצעות הפקודה
install-package htmlagilitypack
HAP מקבלת HTML כמחרוזת, קובץ, Stream או אובייקט TextReader. קוד ה- HTML הוא נטען לתוך אובייקט HtmlDocument בשיטת טעינה עבור Stream, קבצים וכן TextReader,
ושיטת LoadHtml עבור HTML כמחרוזת. שתי השיטות הנפוצות ביותר הן אלה לטעון קובץ או מחרוזת:
var html = new HtmlDocument(); html.Load(@"C:\HtmlDocs\test.html"); // load a file html.LoadHtml(new WebClient().DownloadString("http://www.somedomain.com")); // load a string
לאחר טעינת ה HTML לצורך ניתוחו, ניתן לגשת אליו באמצעות אובייקט DocumentNodes של HtmlDocument שמחזירה את אלמנט השורש.
משם ניתן להשתמש ב LINQ או XPATH על מנת "לתשאל" את המסמך או ליתר דיוק לגשת לאוסף ה HtmlNode שמוחזר על ידי שימוש בפונקציה Descendants.
var html = new HtmlDocument(); html.LoadHtml(new WebClient().DownloadString("http://www.asp.net")); var root = html.DocumentNode; var nodes = root.Descendants(); var totalNodes = nodes.Count();
הקוד הנ"ל יחזיר את המספר הכולל של אובייקטי HtmlNode (או רכיבי HTML) נמצאו במסמך. ניתן לסנן אותם במספר דרכים. לדוגמה, ניתן להוסיף שם תגית לפונקציית Descendants
ובעצם לסנן את השאילתה לפי תגית ה HTML. בקטע הקוד הבא נוכל לראות כיצד נסנן ונחזיר את כל תגיות ה anchor (עוגן) ורשימות לא סדורות במסמך:
var html = new HtmlDocument(); html.LoadHtml(new WebClient().DownloadString("http://www.asp.net")); var root = html.DocumentNode; var anchors = root.Descendants("a"); var unorderedLists = root.Descendants("ul");
תוכל לחדד את החיפוש על-ידי ציון אלמנטים שיש להם ערך מאפיין מסוים. דוגמה זו מחפשת את כל האלמנטים עם class של "-common-link":
var html = new HtmlDocument(); html.LoadHtml(new WebClient().DownloadString("http://www.asp.net")); var root = html.DocumentNode; var commonPosts = root.Descendants().Where(n => n.GetAttributeValue("class", "").Equals("common-post"));
איתור מידע ספציפי במסמך
אחד השימושים של HAP הוא לאיתור חלקים ספציפיים של תוכן במסמך HMTL. הדוגמה הבאה תמחיש איך להשיג תוכן ספציפי.
הצעד הראשון הוא לבדוק את ה- HTML הרלוונטי. כללתי רק קטע קטן המכיל את התוכן שברצוני לאתר (שורה 10) והדגשתי אותו:
<div class="module-common"> <h2 class="common-header-underline transform-none"> Community Recognition <span class="recognition-new-rules"><a href="/t/2024428.aspx">New Rules</a></span> </h2> <div class="module-profile-recognition"> <h3>Mikesdotnetting</h3> <div class="post-rating All-Star"></div> <div class="clear"></div> <p>Has 164330 points and achieved the <strong>All-Star</strong> level</p> <a href="http://www.asp.net/community/recognition/hall-of-fame">Hall of Fame</a><span class="separator">|</span><a href="http://www.asp.net/community/recognition">About</a><span class="separator">|</span><a href="javascript:;" data-uitype="reputation-history" data-username="Mikesdotnetting">Details</a> <table> <thead> <tr><th>Location</th><th style="width:60%;">Activity</th><th style="width:10%;text-align:right">Points</th></tr> </thead> <tbody id="reputation-activities-container"> <tr> <td colspan="3" style="width:100%;height:65px;" class="busy"></td> </tr> </tbody> </table> </div> </div>
התוכן אותו אני רוצה למקד ממוקם באלמנט p ללא תיוג מיוחד, כגון id או class attribute.במסמך ישנם מספר אלמנטים מסוג P, לכן מיקוד כולם לא יהיה מועיל. האסטרטגיה הטובה ביותר היא למקד אלמנט בודד לזיהוי בקלות, ולאחר מכן לנווט משם. ישנם כמה מועמדים ברורים למדי: div עם class של "post-rating" ועוד אחד בעל class של "module-profile-recognition". אם הייתי יוצר כלי לנתח את אותו הדף באופן קבוע, הייתי נמנע בדרך כלל מלמקד רכיבים על ידי שימוש ב CLASS, למרות היתכנות כי יופיעו רק פעם אחת בדף (כפי שקורה לשני יעדים פוטנציאליים במקרה זה), בעתיד עלולים להיות יותר מאחד.
ואחרי שהבהרנו את הנושא, אציג קוד המשתמש באלמנט "module-profile-recognition":
var html = new HtmlDocument(); html.LoadHtml(new WebClient().DownloadString("http://forums.asp.net/members/Mikesdotnetting.aspx")); var root = html.DocumentNode; var p = root.Descendants() .Where(n => n.GetAttributeValue("class", "").Equals("module-profile-recognition")) .Single() .Descendants("p") .Single(); var content = p.InnerText;
כעת, אחרי שחילצנו את הטקסט מהאלמנט הרלוונתי (במקרה שלנו הפסקה), ניתן לחלץ את המספר עצמו עלי ידי שימוש ב Regex:
var points = Regex.Match(content, @"\d+").Value;
Leave a Reply