2
jox 2014-11-26 20:59:08 +08:00 1
。。。。。。。。
你使用了document attribute,我不是这样实现的,所以不清楚如果使用document attribute的话要怎么实现图片的点击事件。我不建议你这么做,这样真的很慢,不仅要把字符串转化成NSData,而且还有parse和render html的开销,我不清楚内部是怎么做的,我非常怀疑这样做的话实际上会调用web kit,我刚开始接触这个问题的时候也尝试过使用document attribute,慢的要死。 正确的做法是使用text kit或者core text,建议使用text kit,把图片作为富文本的附件和文本绑定,设定附件的尺寸,然后用text kit的layout manager算出附件的rect,然后再渲染图片,你如果使用textview渲染文本的话,iOS7之后textview有个delegate方法可以捕捉到图片和超链接的点击事件: textView:shouldInteractWithTextAttachment:inRange: 你还可以创建个gesture recognizer和textview绑定,然后在action里使用layout manager得到图片的信息,如果你不使用textview渲染文本的话,可以在touchesBegan:withEvent:那四个方法里响应tap事件,上面提到的layout manager获取图片信息的方法是这个: boundingRectForGlyphRange:inTextContainer: 如果你打算渲染gif的话,不要配置NSTextAttachment的image属性,只配置bounds属性,然后或者直接画或者使用UIImageView渲染,如果全是静态图片的话,直接配置image属性就行。 至于图片的缓存和缩略图方面你要自己实现,text kit就是用来对文本进行排版和渲染的,core text也可以,但是text kit用起来简单很多,text kit是在core text的基础上做的封装,记得如果一段文本里图片特别多的话你要注意图片的尺寸不要太高,不然会发生文字不显示的问题。。。 |
3
jox 2014-11-26 21:02:50 +08:00
忘了说了,如果使用text kit的话,你要自己parse html文本,将其转化成attributed string,你可能得写个简单的parser,这个挺简单的,借助别人写的一些工具就行,这个http://www.raywenderlich.com/14172/how-to-parse-html-on-ios 里面的那个hpple就不错,用的是libxml,速度非常快,因为会将html转化成dom树,用起来也很方便,递归地遍历dom树就行了
|
4
q84629462 OP @jox textkit就是NSAtrributedString和NSTextAttachment啊,跟我的用法有区别吗?
|
5
jox 2014-11-26 21:15:32 +08:00
当然有区别了,你是直接把html丢给text view然后就不管了,至于text view到底用的什么方法渲染html你是完全不清楚的,我说的是你自己使用text kit手动控制排版。
|
6
jox 2014-11-26 21:19:28 +08:00
text kit不是“NSAtrributedString和NSTextAttachment”,text kit是一整套的文字排版方案,NSAtrributedString是text kit能够处理的数据类型,NSTextAttachment是text kit接受的数据类型的其中一种属性。你能说出这种话说明你根本不理解text kit,再去看看文档和相关的资料吧,你只有理解了text kit的排版过程你才能解决你遇到的这个问题,不难的,看看文档就行了
|
7
PrideChung 2014-11-26 21:21:18 +08:00
我能想到的两种方案
1.用UIWebView+contentEditable属性代替UITextView 2.用NSURLProtocol拦截网络请求(不肯定能成功) |
8
jox 2014-11-26 21:26:38 +08:00
@PrideChung uiwebview很占内存,uiwebview不是用来渲染文本的,而且使用uiwebview的话不能控制排版,只有全部加载完之后才能知道准确的尺寸,如果一段html里有多个img tag的话,这些图片的请求是没法控制的,而且还得使用javascript,蛋疼的要死。
直接parse html得到图片的url之后自己控制网络请求就很简单了,uiwebview是用来显示网页的,不是用来处理使用html作为数据结构传输的数据的。 |
9
gonghao 2014-11-26 21:30:37 +08:00
我倒觉得可以用 NSAttributedString 但是不要用 HTML 就行,图片不是太多不便于管理的画,自己创建一个 attachment 加到 NSAttributedString 里面去就行,图片管理可以自行控制,点击图片的问题,可以用 touchBegin 获取点击坐标,然后 NSLayoutManager 去找到坐标对应的 character 再判断是否是 NSTextAttachment 就成
|
10
PrideChung 2014-11-26 21:30:56 +08:00
@jox 那先试试NSURLProtocol吧,我不太清楚你的需求,UIWebView你可以当成最后的手段。
|
11
jox 2014-11-26 21:37:05 +08:00
@PrideChung 我不是来问问题的,我是被lz召唤过来的,webview的use case是用来渲染某个url的,webview的frame是固定的,相当于在应用里嵌入了个浏览器,类似html里的iframe,不需要考虑排版
但是lz要使用缩略图来渲染图片,这就涉及到了排版,只能使用text kit或者core text来实现 |
12
PrideChung 2014-11-26 21:56:43 +08:00
@jox 那么看来NSURLProtocol是最好的候选了吧,只要拦截到图片的请求然后替换成缩略图的URL就好了,就是不知道这样的图片请求能不能被拦截到。
|
13
jox 2014-11-26 22:25:22 +08:00
@PrideChung 我不知道,我也刚接触iOS开发没多久,我没用过你说的这个NSURLProtocol,需要进行network call的时候我使用的是iOS7出来的NSURLSession,用起来挺简单的,按照你说的这种拦截网络请求怎么感觉那么别扭呢?程序得到一个url,程序想要得到这个url指向的资源,直接向这个url发送网络请求不就好了吗,替换url干啥啊。。。。
即使能够拦截网络请求,你也不能保证你从网上得到的一段html文本里面的某个img tag里的src指向的图片资源一定有对应的缩略图版本,即使有你也不一定知道对应的缩略图版本的url是什么 我觉得正确的做法就是先parse html,然后使用得到的src属性里的url试图从网上下载这个url指向的资源,如果下载成功,那么就使用image I/O框架提供的API将图片裁剪成合适的缩略图尺寸得到图片的数据,然后再考虑是否要cache,然后再想办法渲染图片。如果下载失败,那就不渲染,这个逻辑不是更简单吗? |
14
PrideChung 2014-11-26 22:45:35 +08:00
@jox 我一开始理解错了LZ的意思了,我以为他是想把完整图片的URL替换成缩略图的URL。不过NSURLProtocol还是可行,NSURLProtocol的强大在于不仅可以替换请求的NSURLRequest,甚至还能用自己组织的数据来响应拦截到的网络请求。
具体这么做: 通过NSURLProtocol拦截掉UITextView发出的图片加载请求,自己另外新建一个网络请求下载图片,完成缓存和裁剪缩略图的操作,然后再把缩略图当做请求成功的数据响应给之前被拦截的请求。 调用TextKit和Core Text排版是挺麻烦的事情,可免则免。 |
15
q84629462 OP 其实我也是刚刚接触iOS,所以自定义排版对我来说比较难。。。
缩略图其实也是网络图片,是专门用来给移动设备图文混排里展示的,用户不一定会点击看大图,所以把html夹带的图片转为缩略图显示就比较重要了。 我其实想先用正则把HTML里的img src替换为缩略图src 但这个想法恐怕行不通,我刚刚测试了一下, NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithData:[html dataUsingEncoding:NSUnicodeStringEncoding] options:options documentAttributes:nil error:&error]; 执行这句之后,html字符串中的图片就已经开始下载了。 我是这么测试出来的: 上门那句代码之后紧跟: [string enumerateAttribute:NSAttachmentAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { if (value) { NSTextAttachment *ment = value; ment.image = [UIImage imageNamed:@"noavatar"]; } }]; 然后再[uitextview setAttributedText:string]; 此时显示的noavatar这个图片已经不是noavatar原先的尺寸了,也就是说iOS已经在后台读取到了图片,获得了图片真实尺寸,而且还是同步读取图片。 恐怕还真要自己解析html了。。。 |
16
q84629462 OP @PrideChung 其实你的理解是对的,我就是想把原图转为缩略图src
|
17
jox 2014-11-26 22:53:11 +08:00
@PrideChung 。。。。。。。。。。我觉得操作网络请求更麻烦,core text是不太好用(其实也还行),但是text kit很简单啊,如果使用textview的话只需要parse一下html就可以了,不使用textview直接用text kit渲染也挺简单的啊,一共也没几行代码,怎么会麻烦呢?
|
18
q84629462 OP @PrideChung 不过接管图片下载过程是指找出是哪个类哪个方法实现了图片下载,想集成这个类这个方法自己图片实现下载并缓存到disk而已
我其实有个更大胆的想法,想用UITextView实现类似网页中的图片延迟读取,屏幕显示到哪个图片再去读取,但似乎太异想天开。。。 |
19
jox 2014-11-26 23:00:35 +08:00
@q84629462 文字的排版的确是很艰深的一个问题领域,不过你要处理的这个问题非常非常的简单,就用text kit就能解决,另外,请不要使用正则表达式来parse html,html是context free的语言,你不能使用正则表达式来处理html。
网上已经有写好的代码可以很简单地parse html,我说的那个hpple就很好用,请千万不要使用正则表达式来处理HTML,不信你可以试试,我保证你会后悔的 |
20
jox 2014-11-26 23:07:11 +08:00
@q84629462 这不是异想天开,这是lazy loading,你如果要渲染大量的html文本的话,lazy loading不是“异想天开”,而是你必须要实现的,要么借助uitableview,要么你自己实现,否则内存占用会成为很严重的一个问题,iOS下几乎所有的view都使用CALayer来渲染内容,Core Animation会cache生成的render数据,这些数据非常占内存,光渲染一大段文字就需要将近20M的内存,如果你不能确定将要渲染多少数据的话,图片必须经过裁剪再使用,而且也必须使用lazy loading的策略,否则内存占用轻轻松松就会破百。
|
21
PrideChung 2014-11-26 23:12:36 +08:00
@jox LZ的目的只是想干涉图片加载的URL,这种情况下我认为没必要从Parse HTML到文字排版整个过程重做一次,而且这么干的开销也不低。
|
22
q84629462 OP @jox 分割html再解析,关键在哪里截断html字符串,会有截断html标签的可能,这也是个难题。。。
lazy loading的话,怎么获取在UITextView屏幕中显示的图片呢? |
23
jox 2014-11-26 23:23:39 +08:00
@PrideChung lz似乎是想写v2ex的客户端?这样看来v2ex的API返回的数据也是html咯?
v2ex的帖子内容似乎只支持图片这一种html tag,那parse的话就很简单了,借助已有的工具根本不用自己做什么,拼凑一下就能用了,如果只是简单的图文混排的话就更容易了,使用text kit的话连定制都用不到,直接用就行了,我觉得完全没难度啊。 不过也可能是因为我不了解你说的这个urlprotocol的缘故,总之我的话就是parse html,然后使用text kit来排版和渲染html,html里如果有img标签的话就直接下载,然后裁剪和渲染,这样子。 |
24
jox 2014-11-26 23:29:37 +08:00
@q84629462 lazy loading手动实现还是很困难的,你要借助UIScrollView,获取当前SCrollVIew的bounds的值,假设你只使用一个text view来渲染文本,那么你就要判断text view的哪部分在当前scroll view 的bounds里,然后只渲染这部分的内容,我猜你是要写v2ex的iOS客户端是吗?如果是的话,你可以不用自己实现,使用uitableview或者collection view就行,那个自带lazy loading的机制。
虽然parse html借助工具很简单,不过你大概得了解一下词法分析和语法分析的概念,使用工具parse html得到的是一个树状的数据结构,人们叫这种数据结构为DOM(Document Object Model),你需要遍历这个数据结构然后将其转化成你需要的另外一种数据结构,在你遇到的这个问题里,你需要将DOM转化成NSAttributedString,然后再使用text kit进行排版和渲染 |
25
jox 2014-11-27 00:38:32 +08:00
“分割html再解析,关键在哪里截断html字符串,会有截断html标签的可能,这也是个难题。。。”
我又看了一下你的回复,上面我引用的这个,额,这不是难题。。。。现在的难题是你需要了解一些常识性的东西。。。 假设你得到了这样的一个字符串: dude check this <img src="/images/huge_rack"> out!!! 你可以认为这个字符串是一段html,经过parser处理之后你得到的数据结构可能是这样的: (tree, ( (text, "dude check this "), (img, nil, ((src, "/images/huge_rack"))), (text, " out!!!") )) 这个数据结构有三个子节点,第一个节点的类型是text,值是字符串"dude check this " 第二个节点的类型是img,值为空,包含一个类型为src的属性,属性的值为字符串"/images/huge_rack" 第三个节点的类型又是text,以此类推,如果你需要处理的数据都是这种结构的话,一个循环就够了,否则你就得采用深度优先的方式递归地遍历这个数据结构。 你遍历这个数据结构,并根据每个节点的类型执行相应的动作将其转化成NSAttributedString,然后交给text kit来负责排版,你负责下载图片(如果有的话)。text kit排版完之后在某个时间点会根据用户的操作会把排好版的字形渲染到屏幕上,这期间或者之后如果你从网上下载到了图片,你就要问text kit哪些地方它帮你为附件留了空,你把下载并裁剪好的图片插入或者绘制到这些空里。 额,我只能帮你到这里了,按照我说的方法的话就是这么做。 |
26
q84629462 OP @jox 回复太频繁被禁言1800秒,囧。。。
回顾一下当时提到截断时脑中想到的例子:<div>很多childnode或者很多文字</div> 这种单一node但是里面又有很长的内容,截断必然会把div拆开,这就涉及到截断后要闭合div的情况 当然这只是极端情况,我想问题会比较极端。。。 之前提到正则,我不是说用正则解析html,而是说把img src用正则替换成缩略图的src,当然这是下策 从15楼我的测试情况来看,用本地图片作为占位符也无法生效。虽然这是最快且工作量最少的解决办法,先占位,自己实现图片下载再填充到占位符。 倒是可以考虑用正则替换掉html里的img标签(同时匹配到src里的图片网址),明天试试这个方法。 最麻烦的自定义解析html并排版只能作为最后手段了。。。 |
27
q84629462 OP 也就是说把html代码里的img标签记录位置并删除,变成只有文字和文字样式的nsattributedstring,再处理图片
有关15楼的测试@jox @PrideChung @gonghao 还有更靠谱的测试方法吗,我始终觉得苹果不会这么坑,在nsattributedstring生成后,还没插入到uitextview显示前就已经开始读取nsattributedstring里的图片 |
28
jox 2014-11-27 07:24:53 +08:00
--
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithData:[html dataUsingEncoding:NSUnicodeStringEncoding] options:options documentAttributes:nil error:&error]; [string enumerateAttribute:NSAttachmentAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { if (value) { NSTextAttachment *ment = value; ment.image = [UIImage imageNamed:@"noavatar"]; } }]; -- 这是15L的代码,如果这里的变量html指向的数据是html字符串,那么哪来的NSTextAttachment属性?你怎么知道那个enumerate block里的value的类型一定是NSTextAttachment呢?你用UIImage的imageNamed这个方法,当然会读取图片了,但是你根本不知道ment这个指针指向的对象的类型是什么,所以你不能保证ment一定有个image属性,事实上在这里你什么都不知道,完全就是在胡搞 你要这样构造包含附件的富文本 -- NSMutableAttributedString *richText = [[NSMutableAttributedString alloc] initWithString:@"rich text with image attachment: "]; NSTextAttachment *attachment = [[NSTextAttachment alloc] init]; attachment.image = image; // suppose image exists attachment.bounds = CGRectZero; // configure size of attachment [richText appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]]; -- 然后你可以直接把这个richText丢给text view,让text view使用默认的配置来渲染这个包含附件的富文本,你也可以使用text kit或者core text来渲染。text kit会自动帮你进行排版,你只需要创建text kit的几个核心对象并把它们组合起来,然后使用richText作为text storage,剩下的都交给text kit来做。我建议你再好好看看我回复你的内容,如果你发现你并没有真正看明白我在说什么,说明你需要看文档了。 你认为的最麻烦的办法在我看来是最简单的办法,iOS平台图文混排大家都是这么做的,你检索一下iOS图文混排就知道了,当然,你也可以使用web view来渲染html,你可以使用一个html wrapper,然后使用css来控制排版,这样的话就是web kit在帮你做parse,typesetting,loading image, render这四个步骤的工作了,可是你确定要在你的应用里使用一个浏览器来渲染一小段html文本吗?如果你需要在多个地方显示html文本,比如在一个table view里,你怎么确定web view的高度是多少呢? 我看了你的另一个问有没有提供缩略图服务的帖子,我真是无语了。你知道你在要求什么吗?你在要求别人帮你从另一个地方下载图片,然后再帮你生成一个缩略图版本的图片传给你,然后猜猜what's the best part?for free!!如果是你你会接受这种要求吗?假设真有人傻到愿意提供这种服务,并且牛逼到能够很好地提供这种服务,你得到的这个缩略图需要多长时间你考虑过没?本来是Server->Client->resize的一个过程被你搞成了Server->Server->resize->Client,常识啊兄弟,你真的是一名程序员吗?你真的清楚自己在干什么吗? |
29
q84629462 OP @jox @jox
15楼我没有附上nsstring *html的内容,但放在本帖里讨论必然是包含<img>标签的,所以创建NSAttributedString后是必然包含NSTextAttachment的。 [NSAttributedString enumerateAttribute:NSAttachmentAttributeName] 这方法的作用是循环NSAttributedString里的NSTextAttachment对象,也就是说那个block里的value就是NSTextAttachment对象 还有可以指定循环其它对象,例如 NSLinkAttributeName(NSAttributedString里的<a>标签) 详情请看NSAttributedString.h的定义 15楼的测试我可能没有描述清楚,本意是测试NSAttributedString+UIView这种显示富文本组合,是在何时加载NSAttributedString里面包含的网络图片(即<img src="http://a.com/a.jpg">) 这个测试有两种结果: 1、如果是UITextView负责加载图片,那我就可以通过[NSAttributedString enumerateAttribute:NSAttachmentAttributeName]这个方法替换掉网络图片,改为显示APP自带的图片资源作为占位符。 2、创建NSAttributedString后就加载图片,那第一个结果的替换NSTextAttachment为占位符就 行不通了 15楼的测试我自己得出的是第二个结果,依据是在15楼的代码执行顺序下,在UITextView里显示的尺寸不是noavatar这个图片的尺寸,而是被替换掉的图片尺寸,这代表NSAttributedString已经 [同步] 读取过图片了,已经获得了这些图片的尺寸后,才去执行[NSAttributedString enumerateAttribute:NSAttachmentAttributeName]这一步。所以被替换成的noavatar已经被拉扯变形了。 ============= 至于另外一贴咨询有没有缩略图服务,难道要手机APP读取原图后再缩放?太耗流量太耗能,有什么好无语的?! 而且图片并不在我服务器上,而是第三方网站的图片,所以有生成第三方缩略图这个需求不是很正常吗?我只是不写产生这个需求的原因而直接到v2ex问问有没有网站提供这种服务而已,这又有什么好无语的呢。 我很清楚在干啥啊,就是想给手机省流量和节能而已,而且又可以提高APP的响应速度。 |
30
q84629462 OP @jox 我只是觉得自定义解析html和排版对我来说比较难掌握才把你的提议放在最终手段,而不是反感你的意见
由于昨晚被禁言了,所以被禁言时提交的内容没有发表出来,这里说一下: 我做的是某个论坛的APP,一边做一边学,所以帖子里的图片并不在我的服务器上 我的Android APP开发也是由此入门的并且APP已经做好了,因为只是学习阶段的产物,所以不会在v2ex放出来。 |
31
jox 2014-11-27 17:25:48 +08:00
@q84629462 网络图片下载下来之后都要自己裁剪的,会有哪个第三方的网站给你提供免费的裁剪服务啊?除非你请求的API返回的图片都是放在他们家的服务器上,并且有明确的接口提供缩略图,否则你在只有一个url的情况下只能自己做裁剪,难不成你还要parse每个图片的url看看这个url指向的网站是否提供缩略图服务?这不是搞笑呢吗?人家凭什么提供API帮你裁剪图片啊?你又不是拿着图片去别人的网站在线操作,你是想直接发送一个图片的url和指定你希望得到的尺寸,你就想让别人帮你裁剪图片再发给你?做梦呢?这些我觉得是常识,你怎么会希望有人提供这种服务呢?
如果你不使用text kit来进行排版的话,直接把html丢给text view或者web view,这些图片什么时候被加载你是没办法控制的,也许你可以像上面那个哥们说的,使用url protocol对每个发送的请求进行检查,但是你都说了是论坛的app,那么这个论坛的api返回的html里的图片就有可能来自世界上任意一台电脑,你的程序拿到的只是一个url而已,你甚至不知道这个url指向的是否是有效的资源,那么你除了尝试从这个url下载资源以外你还能做什么?你只能在图片成功下载下来之后再进行裁剪,再作为数据返回给之前发送请求的对象。绕了一圈之后既不会节省流量,也不会使你的应用响应速度变快。而且还要面对因为使用text view和web view渲染图片带来的性能问题,text view渲染html的速度非常慢,论坛返回的数据是一个列表,即使你使用table view也会至少有两三个web view存在于内存中,如果连续有几个帖子的内容很短,可能会有五六个web view存在于内存中,如果某个帖子的内容字数稍微多一点的话,再配合图片,你的应用的内存占用会很高,而且渲染的速度也不会很理想,很有可能会导致滑动时候的卡顿,这些你都要解决。 在我看来这样做很麻烦 |
32
ld0891 2014-11-28 10:27:07 +08:00
@jox 有的时候Regex还是很方便,比如我曾经做过的一个Discuz客户端,里面的帖子列表就是用正则Parse的,因为格式固定。
并且Regex是原生支持,比hpple用XPath解析快,试过一样的功能4s用Regex比hpple流畅。 貌似stackoverflow上有个很搞笑的问答就是说用Regex解析HTML的弊病,但也要看具体场合,一行Regex就能迅速解决的事情就没必要用hpple一个tag一个tag地找,特别是Discuz这种tag混乱必须要用到XPath高级语法才能正确Parse的网页。 |
33
jox 2014-11-28 10:57:08 +08:00
@ld0891 你的意思是tag混乱的网页也要使用regex来匹配吗?有些discuz的tag是嵌套的,并且block element嵌套在inline element这样的情况也有,这样的场合使用regex会很麻烦,有些语法regex根本没法匹配,regex不是用来匹配html这样的语言的,除非你确定你要匹配的html使用的tag种类非常简单并且没有嵌套且一定是语法准确的html,大多数情况下你都需要使用功能完善的parser来parse html。
NSRegularExpression是如何实现的我不清楚,hpple只是一个轻量级的libxml的wrapper,libxml是用C写的,libxml解析xml和html的速度非常快,我只用regex来匹配一般的字符串,所以不清楚regex如果用来解析它能够解析的html速度会快多少,使用hpple我觉得性能完全不是问题。 |
34
ld0891 2014-11-28 15:51:58 +08:00 via iPhone
@jox tag复杂只是原因之一,比如我提到的内容规则也适合Regex。
tag复杂是因为用XPath写起来太麻烦,往往要牵扯到多个child,ancestor之类的条件判断,写出来不论调试还是维护都很难。 用Regex不需要考虑tag的问题,因为Regex不是以tag做定位的。内容规则是有利条件罢了,并且大部分需要爬取的内容都是以列表呈现的,所以规则的情况比较多。tag复杂简单也决定不了用什么,综合考虑比较好。 |
35
jox 2014-11-28 16:54:26 +08:00
@ld0891 我没明白你的意思,对于html这样的语言,regex的适用范围是很有限的,而且我认为试图使用regex来匹配结构复杂的html是非常容易出错的,我不清楚你要parse的html是怎样的一种情况,regex只能匹配结构简单的html,以discuz代码为例,我用过的discuz可以处理这样的递归语法:
-- discuz_content: text discuz_code text: "plain string" discuz_code: [discuz_tag]discuz_content[/discuz_tag] discuz_tag: quote discuz_tag: font ... discuz_tag list ... discuz_tag: img -- 经过discuz的处理,可以出现任意递归深度的html,这种情况你是没办法使用regex来处理的,regex只能处理保证不会出现递归结构的html。另外你一直在强调xpath的问题,这点也让我感到很困惑。如果需要将html整个转化成NSAttributedString,我一般只需要用到少数几个很简单的xpath,用"//body"得到了dom树之后遍历一下就是了,如果碰到一些tag会使用固定的结构,也都是一些很简单的xpath,我似乎还从来没遇到过需要从一长串html中寻找某个特殊的tag以获得特定的数据的情况,我能想到的似乎只有爬取所有的超链接,或者爬取某个页面的某个特殊tag的内容,这些都很简单啊,html/css怎么定位xpath就怎么定位呗,一般这样的tag都有id属性,很好找啊。 |
36
q84629462 OP @jox hpple怎么获得<a>的href属性呢?
<a>的TFHppleElement tagName是text,attributes是空的 |
37
ld0891 2014-11-28 22:46:04 +08:00
@jox 不太清楚你所说的html递归是什么意思。。。我一般的应用是爬图片链接和文本内容。
举个例子吧,下面这行是我曾写过的一个XPath,用来爬文本Discuz发帖内容的。 //div[@class='pcb']//td[@class='t_f']//text()[not(ancestor::blockquote)][not(ancestor::ignore_js_op)] 就是因为text()的父系节点全部都是重名,并且所有的属性和值也都是重复,没有办法简单的指定@XXX='abc'来Parse。 特别是ignore_js_op中记得是Discuz的图片缩略功能,因为引入了js又是一大堆乱七八糟的tag重名。。。 只能用XPath比较复杂的语法,Axes轴的概念, http://www.w3school.com.cn/xpath/xpath_axes.asp 。 这样调试就很麻烦,因为光看一个节点没有用,全HTML的父系都得兼顾。。。 |
39
q84629462 OP 回顾了http://www.raywenderlich.com/14172/how-to-parse-html-on-ios
知道了[element objectForKey:@"href"]去href 原来是我的html文本里的a是这样的 <a href="javascript:;" dataitem="name_付之一笑" >@付之一笑</a> 看来是tfhpple无法识别这种类型的link啊 |
40
q84629462 OP @ld0891 某个论坛已经将帖子数据json化了,所以我不是用hpple来匹配网页中的数据,而是需要显示json里的帖子内容文本到uitableview里,一层回复就是一个tableviewcell
|
41
q84629462 OP |
42
jox 2014-11-28 23:17:51 +08:00
@q84629462 hpple就是把所有的html节点都解码成一个个的objective c对象,原来是什么样的,转化完就是什么样的,hpple本身不会识别任何类型的html tag,它做的工作只是解码,或者叫parse,真正做翻译或者识别的是开发者自己的工作。
就好像解析json一样,你得到一个json字符串,丢给json的parser让它解码,具体这个json有什么东西parser是不关心的,它只关心你丢给它的输入是否是合法的json字符串,如果不合法就会报错,合法就返回给你一个json的数据结构。hpple也是一样的道理,你得到一个html字符串,你丢给hpple,hpple返回html的dom给你,你得自己遍历这个dom才能得到你想要的数据。你想让hpple来识别就好像在要求json的parser识别某个特殊的json key一样,一般情况下是不可能的。 hpple算是用起来很简单的了,你看看tfhppleelement.h里面都有什么方法,一般就用children得到子节点列表,objectforkey得到某个特殊的html属性,tagname是tag的名字。 |
43
jox 2014-11-28 23:19:54 +08:00
@q84629462 不是吧,应该是这样的结构:
{ tagName:a, { href:javascript, dataitem:name_付之一笑 }, children:{ tagName:text, content:@付之一笑 } } 你再好好看看 |
44
jox 2014-11-28 23:26:47 +08:00
hpple返回的数据结构里,好像只有text和br是没有子节点的,img有没有子节点忘记了,你可以看看,就这三个比较特殊,其他的全部都有子节点,
我拿你的例子试了一下,这是代码: -- >>>> html = @"<a href=\"javascript:;\" dataitem=\"name_付之一笑\" >@付之一笑</a>"; >>>> TFHpple *parser = [TFHpple hppleWithHTMLData:[html dataUsingEncoding:NSUTF8StringEncoding]]; >>>> TFHppleElement *body = [[parser searchWithXPathQuery:@"//body"] firstObject]; -- 这是截图: |
45
jox 2014-11-28 23:37:19 +08:00
@ld0891 我的意思是regex不能匹配递归定义的语法,而html的语法就是递归定义的。当你需要将使用html标记的数据转化为一般的字符串或者像浏览器那样显示的话,regex没办法解决,必须使用专门的parser,举个例子,你要写一个论坛的客户端,该论坛的API返回的数据是html,有的时候会遇到这种结构:
<div>hello <div>@ld0891</div></div> 有的时候是这种: <div>hello <div>@ld0891</div> how are you doing <div><b><i>today?</i></b></div></div> 甚至是更复杂的结构,你不可能使用regex来匹配这样的字符串,但是如果这个论坛返回的html只有两种结构,一种是只包含text的: hello @ld0891 一种是带img标签的: hi @ld0891 have an upvote: <img src="upvote.gif"> 这样的情况你用regex就无所谓了,但是这种情况并不多见 |
46
q84629462 OP 首先感谢@jox,已经按照@jox的方法自行用TFHpple组建成NSAttributedString,包含的网络图片标签(<img src="http://巴拉巴拉巴拉">)也用AFNetWorking自行获取图片并用TMDIsk缓存起来了
但是由于是要放去UITableViewCell里用的,所以又有一个新问题:高度计算不正确 简单举例: UITextView *textView; [textView setDelegate:self]; NSTextAttachment *attachment = 长宽都是60的NSTextAttachment图片; NSAttributedString *string = [NSAttributedString attributedStringWithAttachment:attachment]; NSLog(@"string.size:%@", NSStringFromCGSize(string.size)); //string.size:{60, 60} CGSize size = CGSizeMake(textView.contentSize.width, CGFLOAT_MAX); CGRect rect = [string boundingRectWithSize:size options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading | NSStringDrawingUsesDeviceMetrics) context:nil]; NSLog(@"string rect:%@", NSStringFromCGRect(rect)); //string rect:{{0, 0}, {60, 60}} [textView setAttributedText:string]; 然后就是把当前的UITableViewCell的高度设为60了,结果是图片显示不全,症状是UITableViewCell的高度不足以把图片显示全 就不说多行的NSAttributedString文字,一样的症状。 @PrideChung @gonghao @jox @ld0891 各种求。 |
47
jox 2014-12-05 05:26:52 +08:00 via iPhone
用tableview的话你需要提前计算每个cell的高度并缓存计算结果,方法很简单,数据到位之后触发的callback里用一组textkit对象进行排版,然后使用layoutmanager获取排版之后的文字frame,高度就有了,因为排版是lazy方式触发的,你需要强制让排版触发,使用layoutmanager的ensure。。开头的几个方法之一,就可以强制要求layoutmanager排版,记住textkit只是个排版引擎,所以这里渲染不会发生,要搞清楚这点,这里只有排版的开销,真正需要渲染的时候依然要进行排版,但是因为你已经计算好了渲染文字需要的尺寸,到时候直接修改textview的frame就行。
睡觉热醒了,手机不方便贴代码,你要不明白回头我拿电脑的时候可以贴示例 |
48
jox 2014-12-05 05:32:15 +08:00 via iPhone
另外你不需要自己做缓存,如果你使用nsurlsession的话会自动缓存图片数据,用image io的话会自动缓存解码过的图片数据到内存,你可以用nscache缓存解码过的图片对象,不必担心内存,nscache在内存不够的时候会自动释放内存,这样你只不过需要重新解码,不会重新下载图片,因为nsurlsession会缓存图片到硬盘上,只要应用不关,系统是不会删除你的缓存数据的
|
50
jox 2014-12-05 16:59:41 +08:00
static NSTextStorage *textStorage;
static NSTextContainer *textContainer; static NSLayoutManager *layoutManager;; static CGFloat contentWidth; static dispatch_once_t oncePredicate; UIViewController * __weak weakSelf = self; dispatch_once(&oncePredicate, ^{ contentWidth = CGRectGetWidth(weakSelf.tableView.frame); textStorage = [[NSTextStorage alloc] init]; layoutManager = [[NSLayoutManager alloc] init]; [textStorage addLayoutManager:layoutManager]; textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(contentWidth, FLT_MAX)]; textContainer.lineFragmentPadding = 0; [layoutManager addTextContainer:textContainer]; contentOrigin = CGPointZero; }); [textStorage setAttributedString:@"the content"]; [layoutManager ensureLayoutForTextContainer:textContainer]; CGRect frame = [layoutManager usedRectForTextContainer:textContainer]; frame.size.width = contentWidth; // frame should be the frame of the target text view, and you can calculate the height of the cell based on this |
51
jox 2014-12-05 17:03:14 +08:00
上面
[textStorage setAttributedString:] 这个应该是你得到的NSAttributedString,不是NSString,这是基于我写的程序里的代码改的,我改的时候忘记了。 |
52
q84629462 OP @jox
找到三种计算富文本字符串高度的方法,三种方法计算出来的高度是一样的 但与shouldInteractWithTextAttachment(UITextViewDelegate点击富文本里的图片后的回调)输出的UITextView内容高度始终不同,本文最后附上了NSLog记录 ps:如果富文本字符串只包含图片,例如图片是高50的,那UITextView.contentSize.height就是66,图片是高60的,那UITextView.contentSize.height就是76,必然相差了16,求解 再ps:下文三个width参数我明明赋值了320,但NSLog出来只有318.62,求解 - (void)Calculating_Text_Height_1_Width:(CGFloat)width WithString:(NSAttributedString *)string { NSTextStorage *textStorage = [[NSTextStorage alloc] init]; NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; [textStorage addLayoutManager:layoutManager]; NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(width, FLT_MAX)]; [textContainer setLineFragmentPadding:0.0]; [layoutManager addTextContainer:textContainer]; [textStorage setAttributedString:string]; [layoutManager ensureLayoutForTextContainer:textContainer]; CGRect frame = [layoutManager usedRectForTextContainer:textContainer]; NSLog(@"1:%@", NSStringFromCGRect(frame)); /* http://www.v2ex.com/t/149498 */ } - (void)Calculating_Text_Height_2_Width:(CGFloat)width WithString:(NSAttributedString *)string { CGRect frame = [string boundingRectWithSize:CGSizeMake(width, FLT_MAX) options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) context:nil]; NSLog(@"2:%@", NSStringFromCGRect(frame)); /* http://blog.csdn.net/iunion/article/details/12185077 */ } - (void)Calculating_Text_Height_3_Width:(CGFloat)width WithString:(NSAttributedString *)string { NSTextStorage *textStorage = [[NSTextStorage alloc] init]; NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; [textStorage addLayoutManager:layoutManager]; NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(width, FLT_MAX)]; [textContainer setLineFragmentPadding:0.0]; [layoutManager addTextContainer:textContainer]; [textStorage setAttributedString:string]; [layoutManager glyphRangeForTextContainer:textContainer]; CGRect frame = [layoutManager usedRectForTextContainer:textContainer]; NSLog(@"3:%@", NSStringFromCGRect(frame)); /* https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextLayout/Tasks/StringHeight.html http://www.cocoachina.com/b/?p=160 */ } - (BOOL)textView:(UITextView *)textView shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment inRange:(NSRange)characterRange { NSLog(@"img:%@", NSStringFromCGRect(textAttachment.bounds)); NSLog(@"total:%@", NSStringFromCGSize(textView.contentSize)); return true; } ======== 2014-12-05 22:04:05.640 test[3673:112783] View Controller Bounds:{{0, 0}, {320, 568}} 2014-12-05 22:04:05.641 test[3673:112783] UITextView Controller Bounds:{{0, 0}, {600, 600}} 2014-12-05 22:04:05.671 test[3673:112783] 1:{{0, 0}, {318.62, 637.36000000000001}} 2014-12-05 22:04:05.672 test[3673:112783] 2:{{0, 0}, {318.62, 637.36000000000001}} 2014-12-05 22:04:05.673 test[3673:112783] 3:{{0, 0}, {318.62, 637.36000000000001}} 2014-12-05 22:04:08.612 test[3673:112783] img:{{0, 0}, {50, 50}} 2014-12-05 22:04:08.612 test[3673:112783] total:{320, 664} =========== |
53
jox 2014-12-05 22:46:25 +08:00
。。。。。。
你忘记设置text view的textContainerInset了,这个默认上下各是8points,所以会有16的差距,你要把这个弄成UIEdgeZero,这样算出来的就对了,你算出来后把这个inset考虑进去。至于你说的有一两点point的差距是正常的,不可能每一行都放下同样数量的glyph,这个不用理会,你只需要按照你需要的尺寸设定宽度就行了,layout manger和text container配合排版的时候会考虑到你设定的宽度,最终排版完高度不就有了么?话说你算宽度干啥?你贴了太多东西了,我有点累了,没仔细看 |
54
jox 2014-12-05 22:54:53 +08:00
还有你折腾text view的contentSize干啥,这个属性应该是不需要理会的,我用text kit的时候不用text view来渲染文字,为了追求性能,我是渲染完文字之后直接输出位图的,用text view的话不是很简单么,你设定text container的宽度,排版,然后query高度,这样你就得到了text view渲染一定数量文字需要的尺寸,如果后面还要往里面贴图,就用boundingRect。。。的那个方法找到图片的位置把图片贴进去或者画进去,如果是静态图片的画也可以直接赋给attachment的image,这样layout manager会自动帮你渲染图片。
|
56
jox 2014-12-05 23:08:46 +08:00 1
gif跟位图无关,你需要读取gif的所有帧数(使用 Image IO),然后使用uiimage的那个生成动画的方法创建个uiimage,然后直接用uiimageview就能渲染gif了,这个是最简单的,也可以使用更复杂的办法,使用flipboard的开源的那段代码,输出位图也很简单,直接使用text kit排版然后使用off-screen render技术渲染,然后得到bitmap图片,然后赋给layer的contents属性,如果你看不懂我在说什么,那么你需要看Core Animation的文档,以及Drawing and Printing的文档,以及Core Graphics的文档,大概看看了解一下iOS的应用是怎样把数据转化成屏幕的像素的,可以使用哪些技术来draw(core graphics,open GL),calayer是怎么回事,Core Animation是怎样工作的,哪些工作由CPU完成,哪些工作由GPU完成,lots of documentations to read, my friend
|
57
q84629462 OP @jox
截图里,左边是iOS模拟器,右边是论坛帖子网页(UITextView的textContainerInset的16已经计上了) 由于计算出来的高度不够,最后一段被吃了显示不出来。 这情况我也不知道该怎么做高度补偿。。。 |
58
jox 2014-12-06 16:46:27 +08:00
我用text kit算高度有的时候也会遇到高度稍微差一点导致文字显示不全的时候,但是没有你差得这么多啊,这个具体的原因我没有深入研究,不过我的解决办法是找到个合适的magic number做为padding,一般就是10points到20points之间,把textcontainerInset设成0,然后在layout manager算出来的高度基础上再加上padding,这样的画text view最下面的空白高度会随着数据发生一点点浮动,大概也就5points以内,不过基本不影响整体的效果,我想也不会有人在意这个的吧,你可以试试。
|
60
q84629462 OP @jox 大神我又来问问题了,如何知道一个nssattributedstring里的nstextattachment的x,y坐标?我想用UIimage显示GIf图片
只能先把NSAttributedString显示到UITextView后才能知道NSAttributedString里面的nsTextAttachment的显示坐标吧? |
61
jox 2014-12-21 15:20:43 +08:00
我觉得你还是没有整明白text kit到底该怎么用,text kit是一个排版引擎,不是专门用来给text view渲染文字的。你问的这个问题很简单,text kit排版完成之后就可以得到版式相关的数据了,跟排版后的文字是否已经被渲染过没有关系。
你好好看看文档啊,不能指望网上的某个陌生人(v2ex上一个id为jox的家伙)每次在你遇到问题的时候都蹦出来回答你的问题吧?如果我半年才上一次或者对v2ex厌倦了不再上线了怎么办?你的这个问题看看文档很容易就能解决,首先把text kit的核心对象创建好并彼此连接,然后使用ensure....系列的方法强制layout manager进行排版,layout manager在这个过程中会生成字形,然后进行排版,排版完成之后使用boundingRect。。。。那个方法就可以得到某个字形的位置和尺寸。 好好看看文档,顺便了解一下文字的排版过程,相信对你理解text kit的用法会有帮助。不要再@我了,有问题直接去stackoverflow上去问,那个网站我每天都会上线,碰到我了解的问题我会回答的,为了在选择答案的时候的客观性,也别问我的stackoverflow的id了,v2ex这个网站不适合用来提问与回答技术类的问题。 |
62
OctWu 2014-12-23 22:58:15 +08:00
@jox cool,在做富文本输出的时候搜到了这篇文章,帮助很大。虽然可能v2ex不适用做提问,我还是很好奇,因为我在parse html然后用text kit进行了排版后。遇到了两个问题
1.我们的图片大小不统一,这样我就需要来给这个根据NSTextAttachment的生成的attributedString加一个居中的段落属性,但是相对应的是这个图片就必须要单独一个段落了,是否有更好的方法呢。 2.因为我现在的应用的html内容有时候会出现大量的图片,内存还是比较吃紧的。不知道你有没有遇到这样的问题 |
63
jox 2014-12-23 23:09:48 +08:00
@OctWu
1.图片我的处理方法是创建nstextattachment的时候配置bounds属性,给一个非常大的width,比屏幕最大宽度还大,这样text kit就会把绑定图片的那个特殊字符单独放一行,排版完成之后使用layout manager找到图片的位置,然后再设置图片的frame来进行手动排版。之所以要这么做是因为parse html之后得到的只是一个url,在图片下载下来之前不知道图片到底有多大,索性就所有图片使用统一的缩略图尺寸,如果用户想要看一个图片的细节,点击图片之后会使用整个屏幕来单独显示图片,这时候就可以使用图片的原始尺寸。 2. 使用NSCache缓存解压缩过的图片,内存不够的时候NSCache会释放图片对象,只保留图片的url,我使用nsurlsession,这个会做缓存,图片数据会保存在硬盘上,用到的时候如果NSCache里找不到解压缩过的图片,那就重新下载图片,因为有缓存,所以不会从源再次下载,只需要从硬盘读取图片数据然后重新解压缩就可以了,实际效果挺不错的,Image I/O的性能非常出众,图片是很占内存的,必须使用缩略图,只有在需要的时候才会单独显示一张大的图片 |
64
OctWu 2014-12-24 08:33:33 +08:00
@jox
关于第一点我的理解,你是通过NSTextAttachment在布局的时候,通过bounds的设定占了一段空白位置。然后再去粘贴UIImageView?如果是这样,因为我们的图片比例非常的奇怪(1 :1, 1: 7等等)所以设置一个比较通用的bounds还是微难的。可能是我没有找到,有没有很好的方法单独来重新绘制某个NSTextAttachment?现在我都是通过storege replaceAttributedString来重绘 |
65
jox 2014-12-24 09:31:37 +08:00 via iPhone 1
@OctWu 我只用text kit帮我留空白 至于图片是用imageview 还是自己画是无所谓的 我不用text kit画图片 因为那样不好控制 我只用text kit画文字和排版 你如果事先知道图片的尺寸 排版就很容易了
|
66
OctWu 2014-12-25 10:28:40 +08:00 via iPhone
唔,尺寸不可控。不过都是独立一行.剧中。
|
68
jox 2014-12-26 00:52:02 +08:00
@OctWu sizeThatFits?我没用到这个。黑魔法指的是?图片的高度不能弄得太高,实际上图片的尺寸要尽可能的小,因为图片都是独占一行,如果图片的高度比较高,那么整个需要渲染的像素数量就会被拉高很多,内存,CPU,GPU的压力都会升高,你算一下就知道了,iPhone 6的分辨率 750 x 1334,假如一次性需要渲染两屏幕,总共需要渲染的像素数就是750 x 1334 x 2 = 2001000,每个像素需要4bytes,要想渲染这么多像素就需要2001000 x 4 = 8004000 bytes = 7.6MB,有的时候我的应用需要渲染超过10000 points的图片,一下就要用到将近50MB的内存,多个线程同时画的图片都这么大的话内存直接就爆掉了,就拿我们现在在的这个帖子来说,如果使用iOS 7新出的text style,一般字体的高度是17 points左右,上面那几个稍微长点的帖子很容易就能占据两三屏幕,再夹杂一些图片,就更长了。我所有的图片都是100 x 100 points,iPhone 6 Plus以外的设备就是200 x 200 pixels,iPhone 6 Plus是300*0.84 x 300*0.84 pixels,不管原尺寸多大,都是等比例缩小的,看着也能大概看出来图片内容是啥,用户点击图片能看到缩放到屏幕尺寸的图片,还可以双击缩放某一个区域,两个手指头pinch进行缩放,旋转之类的。很多需要图文混排的应用几乎都是这么做的,腾讯的微信,百度的百度贴吧,都只显示固定尺寸的缩略图,你可以参考一下。
|
69
OctWu 2014-12-26 09:23:18 +08:00 via iPhone
@jox 多线程同时画图片?textView本身是scrollView的子类。屏幕外的东西是不用绘制的。你说的内存的问题。是不是你却并没有根据分辨率重新切割图片而是直接交给imagview。这样渲染的时候CG会消耗非常大。还有就是图片本身的大小也很关键。50如果是真机,应该去检查下内存使用在哪了。关于缩略图是这样,我们的图片是需要用户可以看清的。但是比例千奇百怪囧rz,所以没有使用这种方案。
|
70
OctWu 2014-12-26 09:29:59 +08:00 via iPhone
@jox 你有统计过用来占位的NSTextAttchment对内存的压力么。其实对于显示这点来说。完全可以设置抗拒范围。而不用占位。其实我再尝试使用tableview配合来显示,因为image已经固定了格式。还可以重用。当然textview也是OK的
|
71
jox 2014-12-26 09:31:10 +08:00 via iPhone
@OctWu 我没用textview 另外textview应该也是一次性把所有文字都画出来的 你在主线程丢给textview一大段文字看看主线程会不会阻塞就知道了 大图片就是要那么多内存 你用VM tracker就能看到了 xcode里只显示heap上的内存占用 那不是真正的内存占用 Xcode里显示占用内存不到10MB 实际上却可能占用将近100MB 图片占用的内存没有被计算进去
|
72
OctWu 2014-12-26 17:12:59 +08:00
@jox 你提到了textView是一次性绘制,我就尝试替换为了tableView(原因也是因为图片都是单独一行并且居中),真机测试结果消耗内存从30 ~ 55降低到了稳定的9 ~ 17。 很感谢你上面的回答解决了我很多的困惑。
|
73
jox 2014-12-26 17:26:42 +08:00
@OctWu 谢谢你感谢我 :)
你怎么看的内存占用?xcode里面Debug navigator里的内存占用不是真正的内存占用,你需要使用VM Tracker才能得到真正的内存占用。内存占用方面可以看看WWDC 2012里的关于内存的那个session,名字我忘记了,那里提到了如何优化应用的内存占用。 |
75
jox 2014-12-26 21:43:44 +08:00
@OctWu 可以给我发邮件,交流起来比较方便,其实我也是刚开始iOS的开发没多久,也是新手,哈。
这个: http://weibo.com/u/5341809257 这是我用来做图床的新浪微博,我刚随便发了个微博,你可以去给我发私信,我可以把我的邮箱地址告诉你,v2ex网站不能发私信,我不想把我的邮箱发到公网上 |
77
WildCat 2015-09-19 22:16:23 +08:00
楼主还在么?这个帖子的问题最后解决得如何了?
|
78
1027963459 2015-10-05 08:50:42 +08:00
我想做的时类似记事本那种 用 textView 做的 在输入的事后在光标处插入图片 我用的是 NSTextAttachment 插入的 但是之后保存到本地时 在当前文本上 找不到图片 也没法点击图片触发事件 图片应该只占了一个字符的空间 插入图片后 整体字符串长度值增加了 1 各位大神有木有解决办法或者好方法啊
|