Html為樹結構->Json為數組結構
應用場景
H5或瀏覽器展示Html代碼沒有問題,但是讓原生APP或ReactNative直接展示Html可能會有很多不便
實現方法
可以通過正則表達式捕獲、替換,但是文本類型復雜的話,正則表達式的復雜度就會直線上升,所以這里考慮采用更靈活的實現方式
1 將Html字符串文本解析為樹結構對象HtmlNode
使用正則表達式或第三方框架確定並定位各節點首位未知
方法一 正則表達式匹配
用正則定位 <p></p>
標簽位置,及所有屬性,生成臨時樹結構HtmlTag,對象包含標簽起始位置,屬性和正文內容,供下一步加工使用
自己實現這種正則匹配需要很強的正則功底,正則不熟悉的同學請放棄,避免無謂浪費時間,最后實現出來不能用的代碼
方法二 第三方Html轉對象
目前C#、JAVA都有開源的Html轉樹對象代碼倉庫可以使用或參考,本人用CSQuery
優點,成熟的tokenize,完美匹配所有html標簽書寫不規范的情況,一句代碼實現CQ dom = CQ.Create(html);
,可以用nuget安裝
缺點,暫時沒有發現,如果出現解析錯誤的問題,可能后面的就沒法用了
遞歸遍歷所有元素或位置,轉換為HtmlNode樹,提取有用信息,過濾不需要的信息
先序遍歷dom樹結構,生成HtmlNode
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方法負責封裝將標簽轉換為什么類型的標簽(換行、默認文本、圖片等)
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對象和本層的對象,返回給上一層
每一次遞歸的邏輯:
1、如果有子節點,則對子節點進行后序遍歷生成list返回
2、如果無子節點則將本節點變為list對象返回
3、如果既存在子節點,本節點也存在內容,那將檢查本節點是否可以和子節點list合並,可以合並的話進行合並;不可以合並的加到List后部
4、對於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");