十六进制的时间格式到底是个什么鬼?

我在制作“网管小贾的文件搜索器”(文末下载)时,遇到了一些坑。

其中有一个天坑着实让我这没几根头发的准光头抓了好几天头皮。

这个天坑就是十六进制时间格式转换。

什么来头?有时间你可以先看看之前的文章。

《别慌,文件找不到时我们应该这样做(一)》

《别慌,文件找不到时我们应该这样做(二)》


没时间也不要紧,可以尽量挤出点时间接着看下面的内容。


Office 最近打开文档的访问时间在哪里

在之前的文章中我有提到过,Office 最近打开文档信息通常会保存在注册表中。

以 Excel 为例,通常会放在这儿:

HKEY_CURRENT_USER\Software\Microsoft\Office\16.0\Excel\File MRU
 


而注册表项 File MRU 中,则有数条以 Item + 数字序列 的形式的键值。

这些键值中就包含了我所要的全部文件访问记录信息,类似于如下:

[F00000000][T01D6E0414E7254A0][O00000000]*C:\Users\Admin\Desktop\abc.xlsx
 


很明显哈,星号之后正是文件路径及文件名,更重要的还有那个访问时间:T01D6E0414E7254A0

你怎么知道这是个时间?

说实话,一开始我猜的!

但令我有些沮丧的是,这个以字母 T 开头的时间字符串却并不好转换成人类可直观的通用格式。

这到底是个什么时间格式啊,它又如何转换呢?!


初探迷之时间格式

如果你去百度或是Google,绝对查到的都是关于如何清除访问记录的内容,几乎没有说到如何解析这段字符串信息,T字开头的时间格式是个啥更是无从谈起。

这是上天要考验我这大才大德之人啊!

我高昂头颅向天呐喊,敢不敢再狠一点儿?

好吧,没人理我,只好老老实实继续研究吧!

前前后后我花了好几天时间,也才粗略地大概地部分地就差那么一点儿弄懂了许多遥不可及高大上的知识概念。


首先,我盯着这个时间格式看,果然看了半天有效果, T01D6E0414E7254A0 这个时间格式看上去应该是个十六进制的数字,一共8位。

嘿!没错啊,那我可以把它转成十进制的数字啊,会不会就是个 Unix 时间戳呢!

想到这儿,我立马打开了在线时间转换页面,把数字放了上去点击转换。

然而可惜的是转换未能成功,提示这种格式不正确,好像把它转换成十进制也无济于事啊。

看样子,它并不是一个普通的 Unix 时间戳,所以普通的转换方式并不适用。


黎明前的黑暗

我知道,时间存储随着系统的不同存在数十种不同的格式,然而我对这些也就知道一些皮毛。

所以我决定换个思路,还是要从文件访问记录项本身来查找方法。

果然,我找了这样一段老外的文字。

As Harlan mentioned in his previous post, each Item # value in the File and Place MRU subkeys should contain data similar to “[F00000000][T01CF2DF40FEB4220][O00000000]*Z:FilesPresentation1.pptx”, with the value in the second set of brackets being a FILETIME structure that corresponds to the last time the file or directory was accessed from the application.


哦哦,原来它是一种 FILETIME 结构的时间格式啊!

明白了,但是问题又来了,这玩意怎么整呢,呵呵哒!

尝试找一找关于 FILETIME 相关的 API 函数,看看有没有转换代码参考吧。

接着我又在网上搜索关于 FILETIME 的 API 函数,满心寄希望于通过程序自己来做转换。

找到了一些代码。

Public Type FILETIME
       dwLowDateTime As Long
       dwHighDateTime As Long
End Type

Public Type SYSTEMTIME
       wYear As Integer
       wMonth As Integer
       wDayOfWeek As Integer
       wDay As Integer
       wHour As Integer
       wMinute As Integer
       wSecond As Integer
       wMilliseconds As Integer
End Type

Private Declare Function FileTimeToSystemTime Lib "kernel32" (lpFileTime As FILETIME, lpSystemTime As SYSTEMTIME) As Long
Private Declare Function SystemTimeToVariantTime Lib "OLEAUT32.DLL" (lpSystemTime As SYSTEMTIME, vtime As Date) As Long
 


可是,非常可惜啊,这根本就不能用啊!

因为即使把十六进制转换成十进制,再通过这些 API 函数转换,得出的结果根本就不对路啊!

其实这里犯了个错误,这些转换函数是针对类似于 Unix 时间戳的,而手上的这种格式它根本就不是 Unix 时间戳嘛!

无奈还是回最初的问题,这个奇怪的十六进制时间格式到底是个什么东东?

我再次陷入了深思......


峰回路转、初见光明

好吧,再来!

已经好几天没有更新公众号了,我慌得一比!

还好还好,我终于找到了另一个老外的一段文字。


看完这段文字后我整个人犹如醍醐灌顶、茅塞顿开!

啊,原来这玩意是加密的啊,好像有一个叫什么 Dcode 的数字时间检测工具可以识别它。

还犹豫啥,快快请上 Dcode

我很轻易地找到了 Dcode ,并将它下载了下来。

官网链接:https://www.digital-detective.net/dcode/

文件大小22.6M,嫌官网下载慢也可以下载我备份的一份:

https://www.90pan.com/b2235640 提取码:281w


这是一款免费的时间格式解析神器,太棒了,为大神点赞!

我火速尝试用它解析手里的奇怪时间格式,没想到居然成功了!


哈哈,我向成功迈出了坚实的第一步,曙光就在眼前,喜乐充满心田!

这里要插句话,其实在此之前,我还一度在纠缠十六进制内存存储大小尾端的问题,这就是另一话题了太复杂就不啰嗦了。

好了,目前为止暂时可以得出一个结论,那就是这个十六进制的时间格式,是一种大尾端存储的格式。

有的小伙伴不禁要问,那这种格式要转换成人类能看得懂的样子,其算法是怎样的呢?


的确,这难倒了我,刚才的喜乐心情也瞬间淡然。

眼前只知道是这种时间格式,如何转换肯定不是网上通常的算法,嗯,努把力再接着找找吧。

偶然间发现 VB.net 之类的语言好像自带有转换方法。

Dim date1 As Date = CDate("01/01/0001 00:00")
MessageBox.Show(date1.Ticks.ToString)
date3 = date1.AddTicks(&H1C89628FBFACC74)
MessageBox.Show(date3.ToLongDateString & " " & date3.ToLongTimeString)
 


这段代码对于我来说并无参考价值,更何况我用的是 VB6 ,看样子 VB6 是没有现成的代码可供使用了。

难道我寻求真理的脚步真就止步于此了吗?


总有一天光明会来到

无独有偶,在网上扒了半天,不知是否是上天对我的眷顾,还是我的真诚打动了上苍,居然被我找到了一段用 C++ 语言写的转换算法代码。

其中一位大佬写了这样一段话。

In unix systems date/time is often (but not always) recorded as a 32-bit integer indicating seconds past since Epoch (00:00:00, Jan 1, 1970). But for Windows 64-bit LE format, it means "x100 nanoseconds past since 00:00:00.000,000,0 Jan 1, 1601". Your hexadecimal number is a raw binary integer, so it can be easily interpreted by scanf. Here's a piece of stuff that I found from one of my oldest C programs. You can optimize it and add time zone support by yourself.


简单讲就是在 Windows 64 位大尾端格式下,这种时间格式是指距离 1601-01-01 00:00:00 的以100纳秒为单位倍数的长度。

纳尼纳尼?喂,拜托能不能把话说清楚点儿?


好吧,还是举例吧:

首先,我们拿到了一个奇怪的时间格式 T01D6E0414E7254A0

然后,拿起你心爱的计算器,将它转成十进制,没错,也就是 132539810147620000

再然后,将这个十进制数字乘以 100,OK,可以得到 13253981014762000000

好了,这个数字的单位就是纳秒,就是距离 1601-01-01 00:00:00 过去了这么多纳秒,秒懂?

不过嘛,这里有个小问题,可能你的计算器比较 LOW (当然我的也是),它容纳不了这么多的位数,计算时会出错,怎么办呢?


那我们就换个思路,将它直接计算成秒不就行了?

根据 1秒 = 1000000000纳秒(9个零) 这个公式,我们可以很清楚地知道, 100纳秒(2个零) 的倍数可以将它除以 10000000(7个零) ,那么就很容易地得到单位是秒的数字啦。

所以前面的那个时间,我们最终可以得到 13253981014.762 秒!

也就是这个时间是距离 1601-01-01 00:00:00 过去了 13253981014.762 秒,很显然,那个小数点后的数字是指比秒还要小的微秒。


接下来的事情就变得简单多了,我们只需要拿小数点前面的数字部分(也就是秒数),将它转换成年、月、日、时、分、秒即可。

然后需要注意,年要加上 1601 ,因为是从 1601 年开始算的嘛。

大佬也给出了参考代码,只不过是 C++ 的。

#include <stdio.h>
int mDays[12]={31,28,31,30,31,30,31,31,30,31,30,31};
const int daySec = 86400;
int yearDays(int y){
    return 365 + (y%400==0 || (y%4==0 && y%100!=0));
    // Pre-Caesar calendar doesn't work well, don't add it
}
void win_u64tod(unsigned long long utime){
    int y=1601, d=0, mo=0, h=0, m=0, s=0, ms=0;
    ms = utime % 10000000;
    utime /= 10000000;

    while (utime >= daySec*yearDays(y)){
        utime -= daySec*yearDays(y);
        y ++;
    }
    if (yearDays(y) == 366) mDays[1]=29;
    d = utime / daySec;
    utime %= daySec;
    while (d >= mDays[mo]){
        d -= mDays[mo];
        mo ++;
    }
    h = utime/3600;
    utime %= 3600;
    m = utime/60;
    s = utime%60;
    printf("%4u-%2u-%2u %2u:%02u:%02u.%07u\n", y, 1+mo, 1+d, h, m, s, ms);
}

int main(){
    union{
        char c[8];
        unsigned long long ull;
    } a;
    int i;
    for (i = 0; i < 8; i ++){
        scanf("%2x", a.c + i);
    }
    win_u64tod(a.ull);
}
 


我用的是 VB6 ,于是我摸着石头过河,勉强将它“翻译”成了 VB6 的语法。

' 判断平年还是闰年
Public Function yearDays(ByVal y As Integer) As Integer
    If (y Mod 400) = 0 Or (y Mod 4) = 0 And (y Mod 100) <> 0 Then
        yearDays = 366
    Else
        yearDays = 365
    End If
End Function

Public Function Filetime2Datetime(ByVal UTime As Currency, Optional Offset As Integer = 0) As String
    Dim mDays(12) As Integer
    mDays(0) = 31: mDays(1) = 28: mDays(2) = 31: mDays(3) = 30: mDays(4) = 31: mDays(5) = 30: mDays(6) = 31: mDays(7) = 31: mDays(8) = 30: mDays(9) = 31: mDays(10) = 30: mDays(11) = 31

    Dim daySec As Long
    daySec = 86400

    Dim y As Long, mo As Long, d As Long, h As Long, m As Long, s As Long, ms As Long
    y = 1601: mo = 0: d = 0: h = 0: m = 0: s = 0: ms = 0

    Do While UTime >= daySec * yearDays(y)
        UTime = UTime - daySec * yearDays(y)
        y = y + 1
    Loop

    If yearDays(y) = 366 Then mDays(1) = 29
    d = Int(UTime / daySec)
    UTime = UTime Mod daySec

    Do While d >= mDays(mo)
        d = d - mDays(mo)
        mo = mo + 1
    Loop
    mo = mo + 1
    d = d + 1

    h = Int(UTime / 3600) + Offset
    If h > 24 Then
        d = d + 1
        h = h - 24
    End If

    UTime = UTime Mod 3600
    m = Int(UTime / 60)
    s = UTime Mod 60

    Filetime2Datetime = Format(y & "-" & mo & "-" & d & " " & h & ":" & m & ":" & s, "yyyy-MM-dd hh:mm:ss")
End Function

' 调用,参数1:十进制数字,参数2:时区
Filetime2Datetime(13253981014, 8)
 


写在最后

最后测试下来没有任何问题,日期时间转换都非常正确!

至此,怪异时间格式的转换问题被我攻克了!

经此一战,我深深地体会到,要相信自己一定会成功!

原因很简单,因为你要相信自己与大神有几亿光年的距离,而大神们早已在前方开疆拓土,自己就是个捡捡垃圾做做分类的小菜鸟,不成功没道理嘛!

希望本文能给小伙伴们带来帮助和启示,欢迎在留言区讨论!


另附参考文章及程序下载

文章链接:《别慌,文件找不到时我们应该这样做(二)》

程序下载:网管小贾的文件搜索器安装包(2.86M)

​ 下载链接:https://pan.baidu.com/s/1VQWALJvIAj9HqdAW8UJsOQ

​ 提取码:


输入密码,继续阅读



扫码关注微信公众号,回复文章ID免费获取密码


<文章ID:000798>


WeChat@网管小贾 | www.sysadm.cc



提交评论

安全码
刷新

© 2020-present 网管小贾 | 微信公众号 @网管小贾
许可协议:CC-BY-NC 4.0 | 转载文章请注明作者出处及相关链接