600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > 使用 Groovy 合并 MSN 聊天记录

使用 Groovy 合并 MSN 聊天记录

时间:2024-01-27 06:49:47

相关推荐

使用 Groovy 合并 MSN 聊天记录

做挨踢的一般都有无数台电脑,一会儿在服务器上登录,一会儿又到工作站,结果就是散落一地的 MSN 聊天记录。偏偏这些记录还有防身的作用(有些精英就喜欢用 MSN 下达指令,还特别喜欢在事后矢口否认……),所以,在适当的时候进行备份总是不错的。

但是,MSN 并没有提供日志的导入、合并功能,我只得求助于第三方。

PS. 我想有人会推荐“有备”之类的工具。恩,也许它真的很好很强大,但是一来我不希望为了修指甲而配备一把瑞士军刀,二来我也不大信得过这类软件……理由不解释。

Google 之下,发现了一个 Java 版本的记录合并工具 MSNHistoryCombiner (jungleford)。下载之,细查源码。代码不是我喜欢的类型,有很多其实什么也不做的 Exception。(实际上真正烦的是一大堆的 swing 代码,我虽然是 swing 的爱好者,但是在不需要 GUI 的情况下,我还是希望能简单一点)

Combiner 当然可以完成大部分工作,但是这里有几个小小的问题:

无法自动化运行:每次都必须通过 GUI 来操作,而且要手动输入参数 最近的几个 MSN 版本是支持多点登录的,这种情况下合并记录会产生重复(这个问题我也解决不了,原因见后文) 视频邀请、文件传输、群聊时大家的登录登出记录不是很完善

所以我决定重新发明个轮子。(好吧,最重要的问题在于我运行上面这个程序时报错了……)

首先要考察的是 MSN 记录的格式。通过肉眼观察,我看到了一大堆的, 之类的标签,格式算是比较简单的。很显然,MS 出品的缘故我并不奢望能找到一个官方的记录格式说明。所以,唯一可行的是写一个脚本大致了解下有哪些常用的参数。计划如下:遍历我所有的 MSN 记录,列举所有的标签和参数名称,然后以树形结构打印出来:

1: class Node {

2:String name = '*** Root ***'

3:Node parent

4:Set attrs

5:Set children = []

6:int level

7:

8:def Node(node, Node parent = null, int level = 0) {

9: this.level = level

10: this.parent = parent

11: if(node) {

12: name = node.name()

13: attrs = node.attributes().keySet()

14: node.children().each { merge(new Node(it, this, level + 1)) }

15: }

16:}

17:

18:void merge(Node other) {

19: if(children.contains(other)) {

20: def child = children.find { it == other }

21: child.attrs += other.attrs

22: other.children.each { child.merge(it) }

23: } else children << other

24:}

25:

26:private getIndent() { ' ' * 2 * level }

27:

28:String toString() {

29: """$indent$name${ attrs ? attrs : '' }${ children ? '/n' + children.collect { it.toString() }.join("/n") : '' }"""

30:}

31:

32:boolean equals(obj) { obj && obj instanceof Node && obj.name == name }

33:int hashCode() { name.hashCode() }

34: }

35:

36: def folder = new File('/home/hiarcs/deep_crazy4057207345/History')

37: def node = new Node(null)

38: folder.eachFileMatch(~/.*/.xml/) { node.merge(new Node(new XmlSlurper().parse(it), node)) }

39: println node

很直接了当的三十九行代码。(很想知道用 Java 写的话需要几页)

运行脚本,得到以下结果

*** Root ***

Log[LastSessionID, FirstSessionID]

Leave[SessionID, Time, DateTime, Date]

User[FriendlyName]

Text[Style]

InvitationResponse[SessionID, Time, Date, DateTime]

Text[Style]

Application

File

From

User[FriendlyName]

Message[SessionID, Time, DateTime, Date]

Text[Style]

To

User[FriendlyName]

From

User[FriendlyName]

Invitation[SessionID, Time, Date, DateTime]

Text[Style]

Application

File

From

User[FriendlyName]

Join[SessionID, Time, DateTime, Date]

User[FriendlyName]

Text[Style]

简单分析的话,就是每个记录文件都是一个 Log,其中包含了五种不同的消息类型。

PS II. 前面这个脚本其实蛮有用的,可以用来在没有定义文件的情况下考察简单 xml 文件的结构。但是它也有明显的缺点:无法显示标签之间的是否是互斥、依存等关系,只能了解到它曾经出现过。但是对多数情况而言,这个缺点也不是太了不起……

话说虽然 xml 很讨厌,但是这种简明树状结构还是非常容易建模的:通常每一层都对应一个类就可以了

1: import groovy.xml.*

2: import java.text.*

3:

4: // History对应包含着多个MSN日志文件(xml)的目录。

5: class History {

6:final folder, logs = []

7:

8:def History(folder) {

9: this.folder = folder

10: folder.eachFileMatch(~/.*/.xml/) { logs << new Log(it) }

11:}

12:

13:def merge(other) {

14: other.logs.each { log ->

15: def bak = logs.find { it.account == log.account }

16: if(bak) { bak.merge(log) } else logs << log

17: }

18:}

19:

20:def saveTo(folder) {

21: if(!folder.exists()) folder.mkdir()

22: assert folder.isDirectory()

23: logs.each { it.saveTo(folder) }

24:}

25:def save() { saveTo(folder) }

26: }

27:

28: class Util {

29:static builder = {

30: def builder = new StreamingMarkupBuilder()

31: builder.encoding = 'UTF-8'

32: builder

33:}

34:

35:static export(binding) {

36: def builder = builder()

37: builder.bind(binding)

38:}

39: }

40:

41: // Log对应单个的xml文件

42: class Log {

43:final account // 文件名(hash过的MSN帐号)

44:def sessions

45:def Log(file) {

46: account = file.name - '.xml'

47: sessions = groupSessions(new XmlSlurper().parse(file))

48:}

49:

50:String export() {

51: def log = {

52: mkp.xmlDeclaration()

53: mkp.pi('xml-stylesheet': "type='text/xsl' href='MessageLog.xsl'")

54: Log(FirstSessionID: 1, LastSessionID: sessions.size()) {

55: sessions.sort().eachWithIndex { session, index ->

56: unescaped << session.export(index)

57: }

58: }

59: }

60: Util.export(log)

61:}

62:

63:def merge(log) {

64: assert log.account == account

65: log.sessions.each { if(!sessions.contains(it)) sessions << it }

66:}

67:

68:def saveTo(folder) {

69: new File(folder, "${account}.xml").write(export())

70:}

71:

72://这里把文件中解析到不同的session。Session仅仅影响到日志的显示格式(背景颜色),不分也没有关系

73:private groupSessions(node) {

74: def list = []

75: node.children().each { list << it }

76: list = list.groupBy { it.@SessionID.text() }.values()

77: //这里仅仅处理5种不同的实体类型,可能存在其它的类型

78: list.collect { nodes ->

79: new Session(sections: nodes.collect {

80: switch(it.name()) {

81: case ['Leave', 'Join']:

82:new Participation(it)

83:break

84: case ['InvitationResponse', 'Invitation']:

85:new Invitation(it)

86:break

87: case 'Message':

88:new Message(it)

89:break

90: default:

91:throw new IllegalStateException("Unexpected name: ${ it.name() }")

92: }

93: })

94: }

95:}

96: }

97:

98: //一组对话(也就是开着聊天窗口不断聊,只要不关闭窗口或断线就算一个会话

99: class Session implements Comparable {

100:def sections

101:

102:def export(index) { sections.collect { it.export(index) }.join() }

103:

104:// Session的日期时间即第一个消息的时间,仅供比较排序用

105:def getDate() { sections ? sections[0].date : null }

106:

107:int compareTo(other) { date?.compareTo(other?.date) }

108:

109:// 这里的判断比较阳春。只有在两个Session的结构完全相同的情况下才返回true。但是在最近几个版本的MSN里都有

110:// 多点登录的功能。如果一台机器始终处于登录状态,另一台机器则是断断续续的登录,则同一段会话在两台机器上会

111:// 被记录为不同的Session。通过时间比对是不现实的,因为MSN记录的是本机时间,所以误差会影响判断。只能结合

112:// 时间和内容猜测两组Session是否对应着同一段会话,但计算成本会很高,就备份MSN日志这样的应用来说犯不着。

113:boolean equals(obj) { obj && obj instanceof Session && obj.sections == sections }

114:int hashCode() { sections.hashCode() }

115: }

116:

117: //对应每一条具体的消息片段

118: abstract class Section {

119:def node, date, text, name

120:def init(node) {

121: this.node = node

122: name = node.name()

123: date = new LogDate(node.@DateTime.text())

124: text = new Text(node.Text.text(), node.Text.@Style.text())

125:}

126:

127:def prefix = ''

128:def suffix = ''

129:

130:def export(index) {

131: def section = {

132: "$name"(Date: date.date, Time: date.time, DateTime: date.datetime, SessionID: index + 1) {

133: unescaped << prefix

134: Text(Style: text.style) { out << text.content }

135: unescaped << suffix

136: }

137: }

138: Util.export(section)

139:}

140:

141:boolean equals(obj) { obj && obj instanceof Section && obj.name == name && obj.text == text }

142:int hashCode() { text.hashCode() }

143: }

144:

145: // 用于表述日志中的日期格式

146: // 本来这个类没有那么复杂,但是看到其中的DateTime格式后,我强烈怀疑MSN的代码有问题,应该是格式字符串给弄错了

147: // 长日期格式的最后应该是时区,但是日志中却是代表时区格式的'Z'字符,应该是MS的程序员多写了一对单引号吧。

148: // 最初我是拿DateTime的字符串直接截取成Date和Time,结果发现时区偏移了8小时……于是一冲动就玩了把日期解析。

149: // 其实简单的做法应该是直接从xml里读取全部的三个参数,那样完全可以少写7行代码的:(

150: // 安慰自己下,这样的话至少可以检测出日志里错误的日期格式啊

151: class LogDate implements Comparable {

152:private static final timezone = TimeZone.getTimeZone('Asia/Shanghai')

153:private static final dtf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")

154:private static final df = new SimpleDateFormat('yyyy-MM-dd')

155:private static final tf = new SimpleDateFormat('HH:mm:ss')

156:final String datetime, date, time

157:def LogDate(datetime) {

158: this.datetime = datetime

159: def d = dtf.parse(datetime)

160: d = new Date(d.time - timezone.getOffset(d.time))

161: date = df.format(d)

162: time = tf.format(d)

163:}

164:

165:int compareTo(other) { pareTo(other?.datetime) }

166: }

167:

168: class Text {

169:final content, style

170:def Text(content, style) {

171: this.content = content

172: this.style = style

173:}

174:boolean equals(obj) {

175: obj && obj instanceof Text && content == obj.content && style == obj.style

176:}

177:int hashCode() { content.hashCode() }

178: }

179:

180: class User {

181:final friendlyName

182:def User(friendlyName) {

183: this.friendlyName = friendlyName

184:}

185:def export() { Util.export({ User(FriendlyName: friendlyName) }) }

186: }

187:

188: class UserList {

189:final type, users = []

190:def UserList(node) {

191: type = node.name()

192: node.children().each {

193: if(it.name() != 'User') throw new IllegalStateException("Unexpected name: ${it.name}")

194: users << new User(it.@FriendlyName.text())

195: }

196:}

197:

198:def export() { """<$type>${users.collect{ it.export() }.join()}""" }

199: }

200:

201: // 群聊的时候用户加入和离开的信息

202: class Participation extends Section {

203:def user

204:def Participation(node) {

205: init(node)

206: user = new User(node.User.@FriendlyName.text())

207:}

208:

209:String getPrefix() { user.export() }

210: }

211:

212: class Message extends Section {

213:def from, to

214:def Message(node) {

215: init(node)

216: node.children().each { child ->

217: switch(child.name()) {

218: case 'Text': break

219: case 'To': to = new UserList(child); break

220: case 'From': from = new UserList(child); break

221: default: throw new IllegalStateException("Unexpected name: ${child.name}")

222: }

223: }

224:}

225:

226:String getSuffix() { "${to.export()}${from.export()}" }

227: }

228:

229: // 文件邀请、视频邀请的记录

230: class Invitation extends Section {

231:def from, contents = [:]

232:def Invitation(node) {

233: init(node)

234: node.children().each { child ->

235: switch(child.name()) {

236: case 'Text': break

237: case 'From': from = new UserList(child); break

238: default: contents.put(child.name(), child.text())

239: }

240: }

241:}

242:

243:String getSuffix() {

244: """${contents.collect { key, value -> Util.export({ "$key" { out << value } }) }.join()}${from.export()}"""

245:}

246: }

247:

248: //-----------------------------------------------------------------

249: // 待整合的文件目录(该目录下的文件不会发生变化)

250: path = '/home/hiarcs/deep_crazy4057207345/History'

251: // 整合目标(对应的文件将变大)

252: backup = new History(new File('/home/hiarcs/msn'))

253:

254: folder = new File(path)

255: history = new History(folder)

256:

257: backup.merge(history)

258: backup.save()

259: 'Done'

运行这个脚本即可完成目录级的合并。关键在于,通过和 Live Mesh 以及计划任务的配合,可以完全自动化的定期合并、同步所有机器上的聊天记录,方法如下:

将本机的默认聊天记录目录同步到 Live Mesh。(千万不要和其它的机器共享,否则会相互覆盖) 在 Live Mesh 上建立一个同步到 SkyDriver 的目录,和所有机器共享,用于存放合并后的记录。 在每台机器上均建立一个计划任务,其步骤包括 将本机默认目录的内容合并至共享目录 备份默认目录的文件(我习惯用 Winrar 的命令行软件来操作),然后用合并后的文件覆盖。注意第一步和第二步里从哪个目录合并到哪个目录其实不重要,只要保证合并完后两个目录均为合并后的内容即可 设置一个自动运行计划。(如果网速非常快非常稳定,那么自动运行时间不太重要,稍稍错开就可以。对于我这种超细小水管的,半个月合并一次,每台机器的合并时间错开几天就可以了)

PS III. 也许更好的办法是合并一次,然后在某台服务器上二十四小时登录,那么这台服务器上的记录以后就会是最全的。问题在于总有宕机的时候,而且用手机MSN登录的话服务器就会被踢下线来……

最后提下这个脚本的缺点:

格式太死:目前就支持那么五种消息类型,微软一更新就可能要修改代码 无法识别多点登录下的重复对话:如果不同登录点的网络状况都很好,那么没有问题,不会发生重复。但是如果有哪边一会儿上一会儿下的,那么不同的登录点的会话数量会不同。由于微软并没有在每个消息的记录上提供GUID,又在记录中使用了本机时间,所以,理论上我们只能“猜测”两段对话是否相同。反正重复的结果并不严重,所以我就把它忽略了。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。