`
RednaxelaFX
  • 浏览: 3052960 次
  • 性别: 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文件。

    wx 聊天记录备份查看工具Windows

    PyWxDump是PC,wx查看聊天记录、备份导出聊天记录为html(包含语音图片)的工具。 PyWxDump功能介绍(1)获取wx昵称、wx账号、wx手机号wx邮箱、 (2)获取wx的微信昵称、wx账号、wx手机号、wx邮箱、wx原始ID(wxid_****...

    批量合并MDB的python脚本

    批量合并MDB的python脚本

    openfire聊天记录插件(含有数据库脚本)

    在企业环境中,保存和管理聊天记录是重要的需求,以便于审计、合规性和信息追溯。"openfire聊天记录插件"就是为了解决这个问题而设计的。 这个插件的核心功能是存储和检索Openfire服务器上的聊天记录。它允许管理员...

    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库来...

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

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

    cocosCreator打包web-mobile合并html脚本

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

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

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

    文件合并脚本 python

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

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

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

    EXECL跨页合并脚本

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

    Max超级合并分离脚本

    对EditablePoly EditableMesh EditablePatch EditableSpline 物体的自动识别,并进行各种方式的分离和合并。

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

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

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

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

Global site tag (gtag.js) - Google Analytics