Html爲樹結構->Json爲數組結構html
應用場景
H5或瀏覽器展現Html代碼沒有問題,可是讓原生APP或ReactNative直接展現Html可能會有不少不便git
實現方法
能夠經過正則表達式捕獲、替換,可是文本類型複雜的話,正則表達式的複雜度就會直線上升,因此這裏考慮採用更靈活的實現方式github
1 將Html字符串文本解析爲樹結構對象HtmlNode
使用正則表達式或第三方框架肯定並定位各節點首位未知
方法一 正則表達式匹配
用正則定位 <p></p>
標籤位置,及全部屬性,生成臨時樹結構HtmlTag,對象包含標籤起始位置,屬性和正文內容,供下一步加工使用正則表達式
本身實現這種正則匹配須要很強的正則功底,正則不熟悉的同窗請放棄,避免無謂浪費時間,最後實現出來不能用的代碼數組
方法二 第三方Html轉對象
目前C#、JAVA都有開源的Html轉樹對象代碼倉庫可使用或參考,本人用CSQuery瀏覽器
優勢,成熟的tokenize,完美匹配全部html標籤書寫不規範的狀況,一句代碼實現CQ dom = CQ.Create(html);
,能夠用nuget安裝框架
缺點,暫時沒有發現,若是出現解析錯誤的問題,可能後面的就無法用了dom
遞歸遍歷全部元素或位置,轉換爲HtmlNode樹,提取有用信息,過濾不須要的信息
先序遍歷dom樹結構,生成HtmlNodemaven
private HtmlNode IterateIDOMElement(IDomObject element, HtmlNode parent) { HtmlNode current; if (parent == null) //root節點 { current = HtmlNode.CreateRoot(); } else { if (element.NodeName == "#text") { current = HtmlNode.CreatePlainTag(element.ToString()); } else { //這裏能夠增長解析代碼,將部分信息精簡提取出來 var attrs = new StringBuilder(); string link = null; if (element.HasAttributes) { element.Attributes.ToList().ForEach(x => attrs.AppendFormat("{0}='{1}' ", x.Key, x.Value)); var href = element.Attributes.FirstOrDefault(x => x.Key.ToLower() == "href"); link = href.Value; } current = HtmlNode.Create(attrs.ToString(), NameToTagType(element.NodeName)); current.link = link; } parent.AddChild(current); } //遍歷子樹 if (element.HasChildren) { for (var i = 0; i < element.ChildNodes.Count; i++) { IterateIDOMElement(element[i], current);//遞歸建立子節點 } } return current; }
NameToTagType方法負責封裝將標籤轉換爲何類型的標籤(換行、默認文本、圖片等)ui
public enum TagType { Breaking, Image, Default } private static TagType NameToTagType(string tagname) { if(string.IsNullOrEmpty(tagname)) return TagType.Default; switch (tagname.ToLower().Trim()) { case "p": case "br": { return TagType.Breaking; } case "img": { return TagType.Image; } default: return TagType.Default; } }
HtmlNode節點屬性說明
public class HtmlNode { private TagType type { get; set; }//節點類型 private string properties { get; set; }//節點屬性 private string content { get; set; }//節點正文內容(純文本) //全部子節點,若是本節點既有正文又有子節點,則應該把文本轉換成節點,不然就損失了前後順序 private readonly List<HtmlNode> ChildTags = new List<HtmlNode>(); private string _link;//保存連接 public stirng link{ private get { //... } set { _link = value; } } private string src//保存圖片URL { get { //... } } }
HtmlNode內置的建立幫助方法
public static HtmlNode Create(string properties, TagType type, string content = null){ return new HtmlNode() { type = type, properties = properties, content = content }; } public static HtmlNode CreateRoot(){ return Create(null, TagType.Default); } public static HtmlNode CreatePlainTag(string text){ return Create(null, TagType.Default, text); } public void AddChild(HtmlNode child){ if (child == null) return; if (child.type == TagType.Default && !string.IsNullOrEmpty(child.content)) child.content = child.content.Replace(" ", " "); ChildTags.Add(child); }
2 後序遍歷HtmlNode樹,翻譯各個節點,併合並生成List
第一步 完成後會獲得一個根節點,後續遍歷根節點,轉換爲List列表
List<ClientContentItem> returnList = mockHtmlTag.BackOrderTravel();//對根節點進行後序遍歷
第二步 後序遍歷,先訪問添加全部葉節點到數組,最後訪問添加本節點
BackOrderTravel方法
如圖,先把葉子節點轉換爲List對象,上層負責處理直接下一層的list對象和本層的對象,返回給上一層
每一次遞歸的邏輯:
一、若是有子節點,則對子節點進行後序遍歷生成list返回
二、若是無子節點則將本節點變爲list對象返回
三、若是既存在子節點,本節點也存在內容,那將檢查本節點是否能夠和子節點list合併,能夠合併的話進行合併;不能夠合併的加到List後部
四、對於Html標籤中的連接、樣式,上層連接或樣式會影響下層連接或樣式,可是若是下層有一樣的連接或樣式,則保留下層連接或樣式
List<ClientContentItem> reList = new List<ClientContentItem>(); //後續遍歷 if (ChildTags != null && ChildTags.Count > 0) foreach (var r in ChildTags) AddClientContentItems(ref reList, r.BackOrderTravel(),this.link); //訪問根節點 ClientContentItem item = TransCurrentNodeToClientContentItem(); //加入本節點 if (item != null) { if (reList.Count > 0) { var lastitem = reList.Last(); if (lastitem.JoinAbleWith(item)) { lastitem.JoinWith(item); } else { reList.Add(item); } } else { reList.Add(item); } } return reList;
AddClientContentItems方法:將子節點生成的List依序加入總體List
private void AddClientContentItems(ref List<ClientContentItem> reList, List<ClientContentItem> backOrderTravel,string olink) { if (backOrderTravel == null || backOrderTravel.Count == 0) return; //上層Link影響下層內容 if (!string.IsNullOrEmpty(olink)) { backOrderTravel.Where(r=>r.TextType == ClientTextType.Text).ToList().ForEach(x=> { x.TextType = ClientTextType.Href; x.Link = olink; }); backOrderTravel.Where(r=>r.TextType == ClientTextType.Photo).ToList().ForEach(x=>x.Link = olink); } if (reList == null || reList.Count == 0) { reList = backOrderTravel; return; } //將兩個list進行合併,若是有能夠合併的item元素,則進行合併 var lastitem = reList.Last(); foreach (ClientContentItem curItem in backOrderTravel) { if (lastitem.JoinAbleWith(curItem)) { lastitem.JoinWith(curItem); } else { lastitem = curItem; reList.Add(lastitem); } } }
TransCurrentNodeToClientContentItem方法:將本節點轉爲listItem
private ClientContentItem TransCurrentNodeToClientContentItem() { if (string.IsNullOrEmpty(content) && string.IsNullOrEmpty(link) && this.type == TagType.Default) return null; var currentItem = new ClientContentItem(); if (this.type == TagType.Image) { currentItem.Text = src;//TODO:解析onclick和src currentItem.TextType = ClientTextType.Photo; if (!string.IsNullOrEmpty(link)) currentItem.Link = link; } else if (this.type == TagType.Breaking) { if (!string.IsNullOrEmpty(content)) currentItem.Text = content; else currentItem.Text = string.Empty; currentItem.Text += "\r\n"; currentItem.TextType = ClientTextType.Text; if (!string.IsNullOrEmpty(link)) { currentItem.Link = link; currentItem.TextType = ClientTextType.Href; } } else if (this.type == TagType.Default && !string.IsNullOrEmpty(link)) { if (!string.IsNullOrEmpty(content)) currentItem.Text = content; currentItem.Link = link; currentItem.TextType = ClientTextType.Href; } else { if (!string.IsNullOrEmpty(content)) currentItem.Text = content; currentItem.TextType = ClientTextType.Text; } return currentItem; }
JoinAbleWith方法:檢查兩個ListItem是否能夠合併,好比連接相同、或者沒有連接的文本能夠合併爲一個元素
public bool JoinAbleWith(ClientContentItem curItem) { if (this.TextType == curItem.TextType) { switch (this.TextType) { case ClientTextType.Href: { return this.Link == curItem.Link; } case ClientTextType.Text: { return true; } } } return false; }
JoinWith方法:合併兩個ListItem
public void JoinWith(ClientContentItem curItem) { if (this.TextType == curItem.TextType) { switch (this.TextType) { case ClientTextType.Href: { if (this.Link == curItem.Link) { this.Text = (this.Text ?? String.Empty) + curItem.Text; } break; } case ClientTextType.Text: { this.Text = (this.Text ?? String.Empty) + curItem.Text; break; } } } }
3 最後加工
如今已經生成好List對象了,能夠對生成好的list對象進行再處理,完成某些兼容操做
例如,通常富文本維護的時候都會套一個P標籤,P標籤的尾部是換行符\r\n,這個時候能夠把最末尾的換行符都去掉,以避免影響展現
//List中的特殊處理 if (returnList != null && returnList.Count > 0) { //去掉最後多餘的換行符 while (returnList.Count > 0 && !string.IsNullOrEmpty(returnList.Last().Text) && returnList.Last().Text.EndsWith("\r\n")) { returnList.Last().Text = returnList.Last().Text.Substring(0, returnList.Last().Text.LastIndexOf("\r\n", StringComparison.Ordinal)); if (string.IsNullOrEmpty(returnList.Last().Text) && returnList.Last().TextType == ClientTextType.Text) { returnList.RemoveAt(returnList.Count - 1); } } for (int i = 0; i < returnList.Count - 1; i++) { if (returnList[i].TextType == ClientTextType.Photo && returnList[i + 1].TextType == ClientTextType.Href && !string.IsNullOrEmpty(returnList[i + 1].Link) && string.IsNullOrEmpty(returnList[i + 1].Text)) { returnList[i].Link = returnList[i + 1].Link; } } //過濾郵件和電話連接,產生新的Type foreach (var clientOntentItem in returnList.Where(r => r.TextType == ClientTextType.Href && !string.IsNullOrEmpty(r.Link))) { if (clientOntentItem.Link.ToLower().Contains("mailto:")) { clientOntentItem.Link = string.Empty; clientOntentItem.TextType = ClientTextType.Email; } if (clientOntentItem.Link.ToLower().Contains("telto:")) { clientOntentItem.Link = string.Empty; clientOntentItem.TextType = ClientTextType.Tel; } } }
JAVA 遷移
CsQuery 使用的HtmlParser是從Java遷移過來的 The Validator.nu HTML Parser
除此以外在maven上還有HTML Parser Jar和Jericho HTML Parser
Java在作樹解析的時候能夠參考引用
jsoup和CSQuery最相近:
String html = "您好,您能夠點擊 <a lizard-catch=\"off\" onclick=\"chatUrlJumpLib.Jump('http://123123123', 2)\" >這裏</a> ssssssss。"; Document doc = Jsoup.parse(html); Elements body = doc.select("body");