`
RednaxelaFX
  • 浏览: 3049191 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

合并MSN聊天记录的脚本(探索中)

    博客分类:
  • Ruby
阅读更多
上周回了一趟老家,没网上,只好做些不用上线的事。正好找了点时间来写合并MSN(Windows Live Messenger)的聊天记录的脚本。

MSN的聊天记录以XML文件的形式保存,默认保存在%My Documents%\My Received Files\username\History里。中文系统的话是默认保存在%我的文档%\我接受到的文件\username\历史记录里。保存路径可以在登录MSN后设置。
一般我在同一台机器上重装系统的话,会把聊天记录的目录转移到新装的系统上。但是我也经常要用不同的机器,偶尔也需要在不是我的机器上登录MSN。这么一来,聊天记录就散得到处都是了。每次整理数据的时候都想解决一下这个问题,不过每次都是懒了……

既然是XML,处理起来应该是非常方便的——不用自己从lexer和parser开始写解析用的程序。那么来看看一个MSN的聊天记录文件大概长什么样:
<?xml version="1.0"?>
<?xml-stylesheet type='text/xsl' href='MessageLog.xsl'?>
<Log FirstSessionID="1" LastSessionID="1"><Message Date="5/31/2009" Time="7:50:34 AM" DateTime="2009-05-30T23:50:34.699Z" SessionID="1"><From><User FriendlyName="三工"/></From><To><User FriendlyName="Ravenex"/></To><Text Style="color:windowtext; ">http://www.engadget.com/2009/05/30/sonys-psp-go-leaks-out-before-e3-is-obviously-a-go/</Text></Message></Log>

可以看到该文件引用了一个XSLT文件来做渲染。事实上在MSN里浏览聊天记录的时候显示的就是通过该XSLT转换过的XML记录。
这个XML文件的根节点是Log,属性包括FirstSessionID和LastSessionID两个。Log下面的子节点主要是一个或多个Message节点,其中的Session属性表示会话序号,前面提到根节点的两个属性对应文件中所有Session值的状况;也可能存在另外两种节点,下文会提到。
合并记录的关键就在于那些Session值,其它内容都不用修改,直接复制过来就行。脚本应该根据日期正确的把聊天记录按顺序排起来,并重新计算各个Message(和另外两种节点)中的Session值,然后更新Log的FirstSessionID/LastSessionID,最后写出文件。

用Ruby来处理XML文件,我还是习惯性选择用Nokogiri来做。这里用的版本是Nokogiri 1.2.3。通过Nokogiri.parse得到一个Document对象doc后,通过doc/'Log/Message'就可以得到根节点下的所有Message节点了。我以为根节点下只有Message节点,试着写了个实验用脚本,发现合并了之后结果居然比其中一个源还要小,肯定出问题了。

于是就另外写了个脚本来看聊天记录的XML文件里根节点下到底有哪些类型的节点:
extract_node_names.rb:
require 'rubygems'
require 'nokogiri'

fname = ARGV[0]
doc = Nokogiri.parse File.read(fname)
log = doc.root
children = log.children

names = children.inject({}) do |acc, e|
  n = e.node_name
  acc[n] ||= e
  acc
end

p names.keys
puts
names.keys.each do |k|
  puts "#{k}:"
  puts names[k].to_xml('gbk'), ''
end


对某个文件运行该脚本,结果是:
["Message", "Invitation", "InvitationResponse"]

Message:
<Message Date="7/14/2009" Time="9:23:10 PM" DateTime="2009-07-14T13:23:10.898Z"
SessionID="1">
  <From>
    <User FriendlyName="Hg"/>
  </From>
  <To>
    <User FriendlyName="RednaxelaFX"/>
  </To>
  <Text Style="font-family:Segoe UI; color:#000000; ">现在有验证机制了么</Text>
</Message>

Invitation:
<Invitation Date="7/14/2009" Time="9:43:34 PM" DateTime="2009-07-14T13:43:34.678
Z" SessionID="1">
  <From>
    <User FriendlyName="RednaxelaFX"/>
  </From>
  <File>C:\Documents and Settings\RednaxelaFX\Desktop\amazon090714.txt</File>
  <Text Style="color:#545454; ">RednaxelaFX sends C:\Documents and Settings\RednaxelaFX\Desktop\New File.txt</Text>
</Invitation>

InvitationResponse:
<InvitationResponse Date="7/14/2009" Time="9:43:49 PM" DateTime="2009-07-14T13:4
3:49.529Z" SessionID="1">
  <From>
    <User FriendlyName="Hg"/>
  </From>
  <File>C:\Documents and Settings\RednaxelaFX\Desktop\amazon090714.txt</File>
  <Text Style="color:#545454; ">Transfer of "New File.txt" is complete.</Text>
</InvitationResponse>

原来除了Message节点外还有Invitation和InvitationResponse两种节点,对应传输文件的信息。或许我还没碰到所有类型的节点……不过只要这些节点有Session和DateTime属性就能用同样的方式去处理,所以倒不用怎么担心。

OK,那就写个简单的脚本来解决这个合并记录的问题:
cat_msn_logs.rb:
require 'rubygems'
require 'nokogiri'
require 'fileutils'

# Convert a Nokogiri::XML::Document into
# an array of arrays, grouped by SessionID.
# The inner arrays begin with the DateTime of
# their first Element, used later for sorting.
def group_log_by_sid(xml_log)
  xml_log.root.children.inject({}) {|acc, node|
    sid = node['SessionID']
    (acc[sid] ||= [ DateTime.parse(node['DateTime']) ]) << node
    acc
  }.values
end

# Join two arrays of Element groups into one,
# sorted by DateTime, and removing redundant
# entries.
def join_groups(grp1, grp2)
  groups = (grp1 + grp2).
    sort_by    {|g| g[0] }.
    inject([]) {|acc, g|
      (acc.empty? || acc.last[0] != g[0]) ?
      acc << g : acc }.
    map        {|g| g.shift; g }
  
  # fix SessionIDs
  groups.each_with_index do |grp, idx|
    sid = (idx + 1).to_s
    grp.each {|n| n['SessionID'] = sid }
  end
end

# Save a Nokogiri::XML::Document to file,
# with no formatting, and UTF-8 encoding.
def save_log(xml_log, fpath)
  File.open(fpath, 'w') do |f|
    # need to pass in save_options to
    # strip excessive whitespace/formatting
    xml_log.write_to(
      f,       # io
      'utf-8', # encoding
      Nokogiri::XML::Node::SaveOptions::AS_XML)
  end
end

# Join the two designated MSN logs into one.
def join_logs(fname, src_dir1, src_dir2, dest_dir)
  log1, log2 = [ src_dir1, src_dir2 ].map do |d|
    File.open(File.join(d, fname), 'r') do |f|
      Nokogiri.parse f
    end
  end
  
  nodes = join_groups(*(
    [ log1, log2 ].map {|log| group_log_by_sid log }))
  
  root = log1.root
  root['FirstSessionID'] = '1'
  root['LastSessionID']  = nodes.length.to_s
  root.inner_html = ''
  nodes.flatten.each {|n| root.add_child n }
  
  save_log log1, File.join(dest_dir, fname)
end

# command-line arguments:
# src_dir1,
# src_dir2,
# dest_dir (optional, defaults to src_dir1)
#
# variables to keep track of:
# src_dir1 : String
# src_dir2 : String
# dest_dir : String
# fname    : String
if __FILE__ == $0
  src_dir1, src_dir2, dest_dir = ARGV.map {|p| File.expand_path p }
  dest_dir ||= src_dir1
  FileUtils.makedirs dest_dir
  
  # check if files with the same name exist in both dirs,
  src_entries1, src_entries2 = [ src_dir1, src_dir2 ].map do |d|
    Dir.entries(d).grep(/\.xml/i)
  end
  in_both_dirs = src_entries1 & src_entries2
  [
    [ src_dir1, src_entries1 ],
    [ src_dir2, src_entries2 ]
  ].each do |g|
    src_dir = g[0]
    entries = g[1]
    unless src_dir.downcase == dest_dir.downcase
      (entries - in_both_dirs).each do |f|
        FileUtils.copy_file(
          File.join(src_dir, f),  # src
          File.join(dest_dir, f), # dest
          true                    # preserve
        )
      end
    end
  end
  # disable GC due to memory leak issues in Nokogiri
  GC.start
  GC.disable
  # otherwise, join the logs
  in_both_dirs.each do |f|
    print "processing #{f}..."
    join_logs f, src_dir1, src_dir2, dest_dir
    puts 'ok'
  end
  #GC.enable
end

__END__

# the following code may be used later in refactoring

# not used...
#~ def read_xml_file(fpath)
  #~ Nokogiri.parse File.read(fpath)
#~ end

# not used...
#~ def log_session_range(xml_log)
  #~ root = xml_log.root
  #~ root['FirstSessionID'].to_i..root['LastSessionID'].to_i
#~ end

# not used...
#~ def node_sid(xml_node)
  #~ xml_node['SessionID'].to_i
#~ end

# not used...
#~ def set_node_sid(xml_node, sid)
  #~ xml_node['SessionID'] = sid.to_s
#~ end

# not used...
#~ def node_datetime(xml_node)
  #~ DateTime.parse xml_node['DateTime']
#~ end

edge cases not handled:
/ - Archive(\d{8})\.xml/

(__END__之后的是我用来提醒自己用的东西……请忽略)
我用的Nokogiri 1.2.3看来在包装libxml2的时候什么地方没弄好,我一开始试的时候只要多处理几个XML文件就会出现segfault。我觉得很纳闷,想了些办法后发现是GC过后就出问题。于是干脆暂时把GC禁用掉,那样segfault只会出现在所有XML文件都处理完了之后,也就不影响使用了。我一直怀疑我是不是有什么该调用的清理用方法没调用导致segfault,hmm

除了Nokogiri带来的问题外,在实际使用过程中还发现了一个问题:
我一直以为这些聊天记录的XML文件中Log的FirstSessionID总是1,而LastSessionID跟文件中出现的Session数一样。后来发现原来MSN会在文件超过2MB后开新的文件,并把原来的文件重命名为“original_filename_without_extension - ArchiveYYYYMMDD.xml”的形式。这样,新开的XML文件中FirstSessionID就不是从1开始了。很不幸我的记录里有一个同学的记录就超过了2M,害我手动处理了 = =

我觉得处理新开文件的情况挺麻烦的。首先我得把Archive文件和当前文件中的内容合并到一起,再跟另外一边的源合并到一起。然后要模仿MSN的做法,在写出的时候记录是否达到了2MB,达到则新开个文件继续写。但是怎么记录已写出的文件大小呢?难道我要在重新计算好各节点的Session值之后,添加回到根节点时对每个节点调用Nokogiri::XML::Element#to_s然后数byte数?好烦啊 T T

本来想再把这个问题考虑进去,顺便重构一下再发出来的。想想还是先发个出来收集些建议再修改比较有效率。求改进建议 <(_ _)>
分享到:
评论
6 楼 RednaxelaFX 2009-08-14  
升级到Nokogiri 1.3.3之后更糟糕了……在Windows上用Nokogiri难道就是个错误么 T T
换回Hpricot再试……
5 楼 RednaxelaFX 2009-08-14  
night_stalker 写道
这么说 msn 认识巨大的聊天记录喽? 那不用拆了 ……

不拆应该不影响MSN的正常使用,但是超过1M的记录直接用浏览器打开的时候就已经很卡了(例如说通过IE调用MSXML来打开,它会自动处理XSLT,所以能正常显示出来;FF之类的也行)。所以我还是想按照MSN的方式来拆分,不想留下合并的痕迹。
4 楼 night_stalker 2009-08-14  
RednaxelaFX 写道

先输出大文件再拆,但是“再拆”的时候还是得把文件读进来,解析XML,然后再来……还是回到起点了啊。关键是MSN在拆分聊天记录的时候是不会在Session的中间断开的,这是因为它是在写新记录的时候读以前的记录,发现那个文件超过2MB了就开新文件;而以前的记录肯定是以某个完整的Session结束的(一关闭聊天窗口就保存了一次,也就结束了一个Session)。我还是得想办法以Session为单位获取输出的文本的大小才行


这么说 msn 认识巨大的聊天记录喽? 那不用拆了 ……
3 楼 RednaxelaFX 2009-08-14  
night_stalker 写道
前天我写的扩展也碰到一个莫名其妙的 segfault,一 puts 就 segfault,不 puts 就很正常。最后才发现是 GC 的问题: 在扩展中创建了 ruby 对象,但是忘记 mark 了,puts 正好触发 GC,就挂了 …… 后来添加了一个 mark 就没事了。

我觉得问题应该不难 …… 先输出一个大文件,再写个脚本拆。

就等着你来救火啊~~
我得看看Nokogiri有没有更新过,试试新版本会不会还segfault。会的话看来得动手弄个patch了 = =

先输出大文件再拆,但是“再拆”的时候还是得把文件读进来,解析XML,然后再来……还是回到起点了啊。关键是MSN在拆分聊天记录的时候是不会在Session的中间断开的,这是因为它是在写新记录的时候读以前的记录,发现那个文件超过2MB了就开新文件;而以前的记录肯定是以某个完整的Session结束的(一关闭聊天窗口就保存了一次,也就结束了一个Session)。我还是得想办法以Session为单位获取输出的文本的大小才行
2 楼 night_stalker 2009-08-14  
前天我写的扩展也碰到一个莫名其妙的 segfault,一 puts 就 segfault,不 puts 就很正常。最后才发现是 GC 的问题: 在扩展中创建了 ruby 对象,但是忘记 mark 了,puts 正好触发 GC,就挂了 …… 后来添加了一个 mark 就没事了。

我觉得问题应该不难 …… 先输出一个大文件,再写个脚本拆。
1 楼 lwwin 2009-08-14  
2M 说明你们聊得很凶^^~

相关推荐

    批量合并GDB的python脚本

    arcmap-数据处理-批量合并GDB的python脚本

    SQL脚本文件合并工具

    通过“SQL脚本文件合并工具”,我们可以将分散的SQL脚本整合到一起,形成一个大的SQL脚本文件,这样在SQL*Plus中只需要运行一次,就能完成所有脚本的执行,避免了反复打开、执行单个文件的繁琐步骤。 合并过程可能...

    缓存视频文件合并脚本.rar

    然后,运行“001合并.bat”脚本,根据脚本的提示或者“合并缓存视频注意事项.txt”中的说明进行操作。在脚本执行完成后,将得到一个完整的MP4视频文件,可直接播放。 总的来说,这个压缩包提供了一个便捷的工具,...

    SQL脚本文件合并工具.exe

    利于将多个分散的sal脚本合并为一个sql文件。

    获取微信账号信息PC微信数据库读取解密脚本天记录查看工具聊天记录导出支持所有微信版本

    聊天记录查看工具是此脚本的另一个亮点。它允许用户查看微信聊天记录,包括文本、语音消息、图片等多元化的通信内容。这可能通过解析SQLite数据库中的消息记录表实现,将数据结构化后展示出来。值得注意的是,它还...

    SQL脚本文件合并工具.zip

    标题中的"SQL脚本文件合并工具.zip"表明这是一个压缩文件,里面包含了一个名为"SQL脚本文件合并工具.exe"的应用程序。这个.exe文件是Windows操作系统下的可执行文件,用户可以通过双击运行它来执行SQL脚本的合并任务...

    B站视频合并脚本.rar

    【压缩包子文件的文件名称列表】: "B站视频合并脚本"表明压缩包中只有一个文件,那就是合并脚本本身。这可能是一个文本文件,如.sh (bash脚本) 或.py (Python脚本),需要在终端或命令行环境中运行。运行前,用户需要...

    boot.bin和app.bin的自动合并脚本,附demo merge_test.rar

    本示例提供的`merge_test.rar`压缩包包含了一个自动合并`boot.bin`和`app.bin`的批处理脚本(`.bat`文件),这对于开发和调试过程中的固件打包非常有用。这个脚本不仅能够将两个二进制文件合并,还能够追加版本号,...

    音麦漂流瓶的autojs自动聊天脚本.zip

    脚本可能需要读取预设的聊天模板或随机生成消息,然后在聊天窗口中输入并发送。这需要理解应用的UI结构,定位到输入框和发送按钮,并利用AutoJS的文本操作API来完成。 **5. UI元素识别** AutoJS使用UIAutomator库来...

    linux下将qq聊天记录分开存储及倒序

    在这个场景中,我们主要关注的是如何使用shell脚本处理QQ聊天记录,将其按照不同的QQ号码分开存储,并实现倒序排列。这涉及到Linux shell脚本编程的一些核心概念和技术,下面将详细介绍。 首先,让我们了解什么是...

    cocosCreator打包web-mobile合并html脚本

    - 调整HTML中的引用,使其指向合并后的资源和脚本。 - 如果有Mobile特有的配置或代码,可以通过条件判断来决定在Web或Mobile环境下执行哪部分代码。 3. **优化和适配**: 合并后,为了确保在不同设备上的兼容性...

    文件合并脚本 python

    文件合并脚本 python 文件合并脚本 python

    openfire 聊天记录插件(单聊群聊)

    用户可能需要在MySQL数据库中运行这些脚本来准备聊天记录的存储环境。 总的来说,这个Openfire聊天记录插件是一个实用的工具,能够帮助用户保存和管理他们的聊天对话,无论是私人的一对一交流还是多人的群组讨论。...

    EXECL跨页合并脚本

    此VBA脚本主要用于Excel工作表中实现跨页数据的合并操作。在处理包含多页的数据时,经常会遇到需要将某些单元格(尤其是标题行)跨页合并的情况,以保持报表或表格的清晰性和可读性。该脚本通过遍历每一页的页眉...

    WINCC中使用C脚本获得操作记录的方法(原创).pdf

    "WINCC中使用C脚本获得操作记录的方法" WINCC是一种工业自动化软件,能够帮助用户实时监控和控制工业过程。然而,在某些情况下,用户需要记录操作员的操作,以便进行事故分析或性能优化。这时,WINCC提供了一些对象...

    可实现hex文件三种方式的合并1,简单复制合并2,删除最后一行hex文件后合并3同时实现前两种形式同时合并

    本教程将详细介绍如何通过三种不同的方法合并Hex文件,并利用批处理脚本(BAT)来自动化这个过程。 1. **简单复制合并**: 这种方法是最直观的,适用于不关心数据重叠的情况。你只需将一个Hex文件的内容追加到另一...

    用于在macOS上从微信提取聊天记录的脚本-Swift开发

    从macOS上的WeChat提取聊天记录的脚本macOS的WeChat Deciphers此工具包包含三个DTrace脚本,用于与macOS上的WeChat.app混淆。 eavesdropper.d实时记录对话。 这显示了所有要保存到数据库的内容。 dbcracker.d揭示了...

    BAT批处理脚本-将所在目录的BAT文件合并成一个BAT文件,通过 选择 运行其中之一.zip

    本压缩包中的资源,名为"将所在目录的BAT文件合并成一个BAT文件,通过选择运行其中之一.bat",正是这样一个批处理脚本,它的目的是将当前目录下的所有BAT文件整合到一个单一的批处理文件中,用户可以通过这个新生成...

    Linux 使用纯Shell脚本实现多终端聊天室功能例子

    7、实现查看相应的聊天室聊天记录功能 8、实现各终端实时同步聊天消息功能(就像QQ群、微信群) 9、实现多终端同时登录、加入聊天室功能,理论上终端数量没有上限 这是一个shell脚本编码学习项目,没有什么实际用途...

    excel 文件、工作表 合并脚本

    ### Excel 文件与工作表合并脚本知识点解析 #### 脚本概述 此脚本主要实现了Excel文件及其内部工作表的合并功能。整个脚本由三个部分组成:`SubMergeWorkbooks`(用于合并多个Excel文件)、`FunctionLastRow`(获取...

Global site tag (gtag.js) - Google Analytics