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("&nbsp;", " ");
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");

Java源代码参考
C#源代码参考

C# 实现Html转JSON的更多相关文章

  1. 使用TSQL查询和更新 JSON 数据

    JSON是一个非常流行的,用于数据交换的文本数据(textual data)格式,主要用于Web和移动应用程序中.JSON 使用“键/值对”(Key:Value pair)存储数据,能够表示嵌套键值对 ...

  2. 【疯狂造轮子-iOS】JSON转Model系列之二

    [疯狂造轮子-iOS]JSON转Model系列之二 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇<[疯狂造轮子-iOS]JSON转Model系列之一> ...

  3. 【疯狂造轮子-iOS】JSON转Model系列之一

    [疯狂造轮子-iOS]JSON转Model系列之一 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 之前一直看别人的源码,虽然对自己提升比较大,但毕竟不是自己写的,很容易遗 ...

  4. Taurus.MVC 2.2 开源发布:WebAPI 功能增强(请求跨域及Json转换)

    背景: 1:有用户反馈了关于跨域请求的问题. 2:有用户反馈了参数获取的问题. 3:JsonHelper的增强. 在综合上面的条件下,有了2.2版本的更新,也因此写了此文. 开源地址: https:/ ...

  5. .NET Core系列 : 2 、project.json 这葫芦里卖的什么药

    .NET Core系列 : 1..NET Core 环境搭建和命令行CLI入门 介绍了.NET Core环境,本文介绍.NET Core中最重要的一个配置文件project.json的相关内容.我们可 ...

  6. 一个粗心的Bug,JSON格式不规范导致AJAX错误

    一.事件回放  今天工作时碰到了一个奇怪的问题,这个问题很早很早以前也碰到过,不过没想到过这么久了竟然又栽在这里. 当时正在联调一个项目,由于后端没有提供数据接口,于是我直接本地建立了一个 json ...

  7. JSON.parse()和JSON.stringify()

    1.parse 用于从一个字符串中解析出json 对象.例如 var str='{"name":"cpf","age":"23&q ...

  8. json与JavaScript对象互换

    1,json字符串转化为JavaScript对象: 方法:JSON.parse(string) eg:var account = '{"name":"jaytan&quo ...

  9. .NET平台开源项目速览(18)C#平台JSON实体类生成器JSON C# Class Generator

    去年,我在一篇文章用原始方法解析复杂字符串,json一定要用JsonMapper么?中介绍了简单的JSON解析的问题,那种方法在当时的环境是非常方便的,因为不需要生成实体类,结构很容易解析.但随着业务 ...

  10. WebApi接口 - 响应输出xml和json

    格式化数据这东西,主要看需要的运用场景,今天和大家分享的是webapi格式化数据,这里面的例子主要是输出json和xml的格式数据,测试用例很接近实际常用情况:希望大家喜欢,也希望各位多多扫码支持和点 ...

随机推荐

  1. Java 大数类BigInteger和BigDecimal的基本函数

    在Java中有两个类BigInteger和BigDecimal分别表示不可变的任意精度的整数和不可变的有符号的任意精度的十进制数(浮点数).主要用于高精度计算中.这两个类使得java中的大数,高精度运 ...

  2. 将电脑文件复制到vm虚拟机中,然后安装步骤

    [root@lixiaohu 桌面]# cp openssl-1.0.1f.tar.gz /usr/src     /usr/src  这是复制到的路径[root@lixiaohu 桌面]# cd / ...

  3. 使用setTimeout实现setInterval

    setInterval = () =>{ console.log(1) //使用递归 setTimeout(setInterval,1000); }; setInterval()

  4. Android初学:Gradle 'HelloWorld' project refresh failed

    Gradle 'HelloWorld' project refresh failed Error:Failed to open zip file.Gradle's dependency cache m ...

  5. v-for并判断当前元素是否选中:$set实现响应添加属性

    前言 一直纠结着使用v-for进行列表渲染时如何为当前的元素添加是否选中的标识. 1.v-for进行列表渲染 <div class="lists"> <ul> ...

  6. MVC编程实例----简易电子商务网站(一)

    一.总体概览.规划 本文将会创建一个基本的电子商务网站.由于电子商务网站的基本功能都是差不多的,此处省去了需求分析等工作,直接总结出结论.分为4个基本功能: 商品浏览 会员功能 购物车 订单结账 其中 ...

  7. NopCommerce用core重写ef

    最近看了NopCommerce源码,用core学习着写了一个项目,修改的地方记录下.项目地址 NopCommerce框架出来好久了.18年的第一季度 懒加载出来后也会全部移动到.net core.那么 ...

  8. dedecms 图集标签{dede:productimagelist} {dede:field name='imgurls'}&nbs

    1.{dede:productimagelist}{/dede:productimagelist} 2.{dede:field name='imgurls'}{/dede:field} 这两个图集标签 ...

  9. 怎样实现给DEDE的栏目增加栏目图片(2)

    2.3 打开dede/templets/catalog_edit.htm页面,查找 栏目名称:

  10. Google免费GPU使用教程

    今天突然看到一篇推文,里面讲解了如何薅资本主义羊毛,即如何免费使用Google免费提供的GPU使用权. 可以免费使用的方式就是通过Google Colab,全名Colaboratory.我们可以用它来 ...