打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
在线CHM阅读器(2)——文件提取及关键文件解析

1、文件提取

在线CHM阅读器(1)一文中已提到,CHM其实就是一个结构化存储文件(Structured Storage),如果也阅读CHM文档,就必须将文件,图片等从CHM文件中提取出来,提取需要用到Structured Storage的StgOpenStorage函数以及IStorage和IStream接口,不过这些在.NET中都不能直接使用,需要先“包装”一下。如何使用IStorage和IStream可以参考这篇文章:

CHM Help File Extractor

不过这篇文章提供的源代码是用于反编译出CHM的所有文件的,开发在线CHM阅读器并不需要先反编译出所有的文件,只需要把浏览器当前请求的那个文件提取出来发送到客户端即可。提取文件的代码如下:

public class CHH{    /// <summary>    /// 提取CHM中的文件    /// </summary>    /// <param name="chm">chm文件的路径</param>    /// <param name="res">要提取的文件的全路径</param>    /// <returns></returns>    public static Stream Find(string chm, string res)    {        IStorage storage = ((ITStorage)new ITStorageClass()).StgOpenStorage(chm, IntPtr.Zero, 0x20, IntPtr.Zero, 0);        try        {            DateTime s = DateTime.Now;            IStream stream = Find(storage, res.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries), 0);            double e = (DateTime.Now - s).TotalMilliseconds;            if (stream == null) return null;            if (res.ToUpper() == "#SYSTEM")            {                //#System是二进制文件,javascript无法处理,因此处理成json格式的文本,数据格式为:                //{                //    HomePage:起始页,                //    Encoding:编码,                //    Title:标题                //}                using (Stream cs = new ComStream(stream))                {                    try                    {                        return (new ChmInfo(cs)).MakeStream();                    }                    finally                    {                        cs.Close();                    }                }            }            else            {                return new ComStream(stream);            }        }        finally        {            Marshal.ReleaseComObject(storage);        }    }    static IStream Find(IStorage storage, string[] res, int first)    {        if (first == res.Length - 1)        {            IStream stream = null;            try            {                //找到对应的文件                stream = storage.OpenStream(res[first], IntPtr.Zero, 0x20, 0);            }            catch            {            }            if (stream == null && res[first].ToUpper() == ".HHC")            {                //由于目录文件的文件名一般不确定,因此做特殊处理                System.Runtime.InteropServices.ComTypes.STATSTG stats;                IEnumSTATSTG enumStats;                int i = 0;                storage.EnumElements(0, IntPtr.Zero, 0, out enumStats);                try                {                    enumStats.Reset();                    //枚举所有文件查找                    while (enumStats.Next(1, out stats, out i) == 0)                    {                        if (System.IO.Path.GetExtension(stats.pwcsName).ToUpper() == ".HHC" && stats.type == 2)                        {                            stream = storage.OpenStream(stats.pwcsName, IntPtr.Zero, 0x20, 0);                            return stream;                        }                    }                }                finally                {                    Marshal.ReleaseComObject(enumStats);                }            }            return stream;        }        else        {            //在文件夹中递归查找目标文件            IStorage next = storage.OpenStorage(res[first], IntPtr.Zero, 0x20, IntPtr.Zero, 0);            try            {                return Find(next, res, first + 1);            }            finally            {                Marshal.ReleaseComObject(next);            }        }    }}

2、处理#SYSTEM文件

上文提取文件的代码已提到,#SYSTEM是一个二进制文件,而javascript是不能处理二进制文件的,因此,必须在后台处理#SYSTEM文件,转换成json格式的文本发送到客户端。#SYSTEM的格式如下图所示:

根据上图所示的规律,既可以处理#SYSTEM文件了,代码如下:

    class ChmInfo    {        const UInt16 ID_HHC = 0x0000;        //        const UInt16 ID_MAIN = 0x0002;        //起始页的ID        const UInt16 ID_Version = 0x0009;   //版本的ID        const UInt16 ID_TITLE = 0x0003;        //标题的ID        const UInt16 ID_PRJNAME = 0x0006;    //工程的ID        const UInt16 ID_LAN = 0x0004;        //区域的ID        public String MainPage = String.Empty, PrjName = String.Empty, Title = String.Empty, HHA_Version = String.Empty, HHC = String.Empty;        public Encoding Encoding = null;        Hashtable _session = new Hashtable();        private bool ReadSession(BinaryReader reader)        {            if (reader.BaseStream.Position >= reader.BaseStream.Length) return false;            UInt16 id = reader.ReadUInt16();            UInt16 count = reader.ReadUInt16();            if (count + reader.BaseStream.Position <= reader.BaseStream.Length)            {                if (count > 0)                {                    _session[id] = reader.ReadBytes(count);                }                return true;            }            else            {                return false;            }        }        public ChmInfo(Stream stream)        {            BinaryReader reader = new BinaryReader(stream);            //读取所有数据及其对应的ID并保存到一个Hashtable中            while (ReadSession(reader)) ;            try            {                if (_session.ContainsKey(ID_LAN))                {                    Byte[] data = _session[ID_LAN] as Byte[];                    CultureInfo info = new CultureInfo(data[1] * 0x100 + data[0]);                    Encoding = Encoding.GetEncoding(info.TextInfo.ANSICodePage);                }            }            catch            {            }            if (Encoding == null) Encoding = Encoding.GetEncoding("GB2312");            if (_session.ContainsKey(ID_MAIN))            {                Byte[] data = _session[ID_MAIN] as Byte[];                MainPage = Encoding.GetString(data, 0, data.Length - 1);            }            if (_session.ContainsKey(ID_TITLE))            {                Byte[] data = _session[ID_TITLE] as Byte[];                Title = Encoding.GetString(data, 0, data.Length - 1);            }            if (_session.ContainsKey(ID_PRJNAME))            {                Byte[] data = _session[ID_PRJNAME] as Byte[];                PrjName = Encoding.GetString(data, 0, data.Length - 1);            }            if (_session.ContainsKey(ID_Version))            {                Byte[] data = _session[ID_Version] as Byte[];                HHA_Version = Encoding.GetString(data, 0, data.Length - 1);            }            if (_session.ContainsKey(ID_HHC))            {                Byte[] data = _session[ID_HHC] as Byte[];                HHC = Encoding.GetString(data, 0, data.Length - 1);            }        }        public Stream MakeStream()        {            //生成JSON并保存到一个MemoryStream中            String json = String.Format(                "{{\"MainPage\":\"{0}\",\"Title\":\"{1}\",\"HHC\":\"{2}\",\"Encoding\":\"{3}\"}}",                TransferCharJavascript(MainPage),                TransferCharJavascript(Title),                TransferCharJavascript(HHC),                TransferCharJavascript(Encoding.HeaderName)            );            Byte[] buffer = Encoding.UTF8.GetBytes(json);            Stream stream = new MemoryStream(buffer.Length);            stream.Write(buffer, 0, buffer.Length);            stream.Seek(0, SeekOrigin.Begin);            return stream;        }        public static string TransferCharJavascript(string s)        {            StringBuilder ret = new StringBuilder();            foreach (char c in s)            {                switch (c)                {                case '\r':                case '\t':                case '\n':                case '\f':                case '\v':                case '\"':                case '\\':                case '\'':                case '<':                case '>':                case '\0':                    ret.AppendFormat("\\u{0:X4}", (int)c);                    break;                default:                    ret.Append(c);                    break;                }            }            return ret.ToString();        }    }

3、处理目录(*.hhc)文件

目录文件保存着一个CHM文件的目录结构,它是一个文本文件,为了减轻服务器的负担,将目录文件放到浏览器来处理。在在线CHM阅读器(1)一文中已提到,目录文件大概的规律是:每一个<LI><OBJECT>…<OBJECT>对应着目录树中的一个节点,<OBJECT>…<OBJECT>中的参数记录着该节点的属性(对应的页面,名称等)。如果这个节点有子节点的话,那么<LI>后面会紧跟着一个<UL></UL>,<UL>里面所有的节点都是其子节点。处理的代码如下:

function ChmHHC(buffer){    var position = 0;    var RegxTagName = /(<|<\/)([a-zA-Z]+)(\s[\S\s]*|)>/i;    var RegxAttrs = /([a-zA-Z1-9]+)\s*=\s*\x22([^\x22]+)\x22/ig;        //读取下一个标志(<标志名>)    function ReadTag()    {        var tag = {            Name: "",            Type: "",            Attrs: {}        };        var res = null;        while (res == null)        {            if (position >= buffer.length) return null;            while (position < buffer.length && buffer.charAt(position) != '<') position++;            if (position >= buffer.length) return null;            var s = position;            while (position < buffer.length && buffer.charAt(position) != '>') position++;            if (position >= buffer.length) return null;            var e = position;            position++;            var tag_str = buffer.substr(s, e - s + 1);            RegxTagName.lastIndex = 0;            res = RegxTagName.exec(tag_str);        }        tag.Name = res[2].toUpperCase();        tag.Type = res[1] == '<' ? "Begin": "End";        if (tag.Type == "Begin" && res.length > 3 && res[3] != "")        {            RegxAttrs.lastIndex = 0;            var atrr = null;            while ((attr = RegxAttrs.exec(res[3])) != null)            {                tag.Attrs[attr[1].toLowerCase()] = attr[2];            }        }        return tag;    }    var current = null;    function IsBeginTag(tag, name)    {        return tag.Type == "Begin" && tag.Name == name;    }    function IsEndTag(tag, name)    {        return tag.Type == "End" && tag.Name == name;    }    function RenderTag()    {        if (current != null && IsBeginTag(current, "LI"))        {            var node = {                NodeType: "LI",                SubNodes: []            };            current = ReadTag();            if (current != null && IsBeginTag(current, "OBJECT"))            {                node.type = current.Attrs["type"];                current = ReadTag();                while (current != null && !IsEndTag(current, "OBJECT"))                {                    if (IsBeginTag(current, "PARAM"))                    {                        node[current.Attrs["name"]] = current.Attrs["value"];                    }                    current = ReadTag();                }                if (current != null && IsEndTag(current, "OBJECT")) current = ReadTag();                if (current != null && IsEndTag(current, "LI")) current = ReadTag();                //尾随着LI的所有UL中的节点均作为该LI的子节点                while(current != null && IsBeginTag(current, "UL"))                {                    var ul = RenderTag();                    if (ul != null)                    {                        for(var ul_index in ul.Nodes) node.SubNodes.push(ul.Nodes[ul_index]);                    }                }                                return node;            }        }        else if (current != null && IsBeginTag(current, "UL"))        {            var node = {                NodeType: "UL",                Nodes: []            };            current = ReadTag();            while (current != null && !IsEndTag(current, "UL"))            {                var subNode = RenderTag();                if (subNode != null) node.Nodes.push(subNode);            }            if (current != null)            {                current = ReadTag();                return node;            }        }        else        {            current = ReadTag();        }        return null;    }    var roots = [];    this.Render = function()    {        position = 0;        current = ReadTag();        while (current != null)        {            var node = RenderTag();            if (node != null) roots.push(node);        }        current = null;    }    this.GetNodes = function()    {        return roots;    }}

调用ChmHHC的Render方法后,将HHC文件转换成一个数组,保存着所有的节点,其结构与目录的对应关系如下图所示:

在上文中,已经介绍了如何提取出CHM文件中的文件(网页,图片等)以及如何解析目录文件,下一篇文章,将介绍如何使用ISAPI筛选器和IHttpHandler来开发一个在线CHM阅读器。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
代码生成文档工具
hhc.exe制作chm
C#生成CHM文件(外篇使用hha.dll)
电子书籍制作专集-应用篇
CHM格式电子书制作详解
C#:NET4.0中如何使用内存映射操作大文件
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服