将多个HTML文档合并成一个HTML文档

目录

  • 将多个HTML文档合并成一个HTML文档
      • 目录
      • merge_htmlsh脚本
      • 使用说明
      • 对脚本的几处解释

最近在看Vim插件方面的资料,原来一直都只用ctags和cscope看代码,发现看一些大型项目还是不太方便,还是得求助于更多的插件。找了一下发现还是易水老师的《Vi/Vim使用进阶》系列最靠谱,并且还提供pdf下载,于是下载了准备随身携带方便慢慢看。但打开发现有个问题,里面的插图太小了,需要点击小图并联网后才能打开大图,怎么办呢?易水老师也提供了html版本的系列文章下载,于是想可以用修改html版本的文章,替换成大图,再生成pdf,但现在的pdf工具好用的不多,还不支持多个html文档的合并。那能不能自己合并一下再转pdf呢?

于是花点时间研究了下如何合并HTML文档,对于结构完整的HTML文档,合并多个文档的过程是很简单的,就是选一个文档作为主干,将其余HTML文档的”< html>< body> … < /html>< /body>” 标记去掉并塞进主干文档当中。比较麻烦的是按照当需要合并的文档是有章节组织的,这个时候按照章节顺序来合并才有意义。于是又花了点时间研究了下,发现可以从目录中提取出顺序已经定好的章节信息。确定可行后,剩下的就是如何实现了,这个需求很简单,看来用不着专门为这个动用HTML语法的解析器,最快的办法还是用Bash脚本来实现,下面就是实现这个功能的脚本,供有同样需求的朋友参考:

merge_html.sh脚本

#! /bin/bash
# A simple HTML file merge scripts.
# Author: Lewis Liu
# Version: 0.1
# Merge HTML files in given folder and generate a mono html file called "all_in_one.html"
# User can specify an index file which contains a list of all files in chapter order, and
# if not specified, the script will use 'ls' to list all files that need to be merged.
# 

TARGET="all_in_one.html"
#clean result and intermediate files from last run
rm -vf ${TARGET} toc.tag *.sed index.list 

# default html tag tokens
TOKEN_TOC='<div class="toc">'
TOKEN_CHAP_PREFIX='<span class="chapter"><a href="'
TOKEN_CHAP_NAME='[0-9a-zA-Z,\_]+\.html'
TOKEN_BODY_BEG='<body>'
TOKEN_BODY_END='</body>'
TOKEN_HTML_BEG='<html>'
TOKEN_HTML_END='</html>'
# use extended regex
GREP='egrep'
SED='sed'
# debug only
#html_path='html'
#index_page='index.html'

print_usage()
{
    printf  "./merge_html.sh [OPTIONS] html_path [index_page]\n"
    printf  "PARAMETERS:\n"
    printf  "\thtml_path  Path to html files need to be merged.\n"
    printf  "\tindex_page The html file which contains toc(table of contents).\
If not provided, the html files will be merged in the order of 'ls' output.\n"
    printf  "OPTIONS:\n"
    printf  "\t--tok-toc  Overide the toc(table of contents) pattern.\n"
    printf  "\t--tok-prefix  Overide the html file tag prefix pattern. \
This will be used to address the html file name.\n"
    printf  "\t--tok-name  Overide the html file name pattern.\n"
    printf  "\t-h|--help print this help message.\n"

}

# sed script1: remove address token and trailing " 
cat>rm_addr_tok.sed<<EOF
{ 
    s/${TOKEN_CHAP_PREFIX}//g 
    s/\"$//g 
}
EOF
# sed script2: replace thumbnail image file name with the regular one
cat>rm_tb_pic.sed<<EOF
{
    s/\-[0-9]\{3\}x[0-9]\{3\}\.jpg/\.jpg/g
    s/\-[0-9]\{3\}x[0-9]\{3\}\.png/\.png/g
}
EOF
# sed script3: remove html and body begin tag in each chapter
cat>rm_html_tag.sed<<EOF
{
    s;${TOKEN_HTML_BEG};;g
    s;${TOKEN_BODY_BEG};;g
    s;${TOKEN_BODY_END};;g
    s;${TOKEN_HTML_END};;g
}
EOF

# parameters
index_page_default="index.html"
html_path=
index_page=
USE_LS_RESULT=

PARSED_OPT=`getopt -o h --long tok-toc:,tok-prefix:,tok-name:,help\
           -n "$0" -- "$@"`
if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi

eval set -- "$PARSED_OPT"

while true;do
    case "$1" in
        --tok-toc) 
            TOKEN_TOC="$2" 
            echo "TOKEN_TOC: ${TOKEN_TOC}" 
            shift 2 ;;
        --tok-prefix) 
            TOKEN_CHAP_PREFIX="$2"
            echo "TOKEN_CHAP_PREFIX: ${TOKEN_CHAP_PREFIX}" 
            shift 2 ;;
        --tok-name) TOKEN_CHAP_NAME="$2" 
            echo "TOKEN_CHAP_NAME: ${TOKEN_CHAP_NAME}" 
            shift 2 ;;
        -h|--help) print_usage; shift ; exit 0 ;;
        --) shift ; break ;;    # delimter of non-option arguments
        *) echo " Internal error!" ; exit 1 ;;
    esac
done
# now processing non-option arguments...
if [[ -z "$@" ]]; then
    read -p "Please specify the folder of html files need to be merged: " html_path
else
    for arg do
        if [[ -d $arg ]]; then
            html_path=$arg
        elif [[ -n $html_path && -f "$html_path/$arg" ]]; then
            IS_HTML=`egrep -o "^$TOKEN_HTML_BEG|$TOKEN_HTML_END$" "$html_path/$arg"`   
            if [[ -z "$IS_HTML" ]]; then
                echo "Not a html file, try again." ; exit 1
            fi
            index_page=$arg
        fi
    done
fi

if [[ -z "$html_path" ]]; then
    echo "No html files path specified. Exit."
    exit 1
elif [[ -z "$index_page" ]]; then
    echo "No index file specified, use 'ls' result instead."
    USE_LS_RESULT=1
fi

if [[ -z $USE_LS_RESULT ]]; then
    echo "Trying to extract TOC from index page... "
    ${GREP} -o "${TOKEN_TOC}" "$html_path/$index_page" > toc.tag
    if [[ -s toc.tag ]]; then
        ${GREP} "${TOKEN_TOC}" $html_path/$index_page |${GREP} -o \
        "${TOKEN_CHAP_PREFIX}${TOKEN_CHAP_NAME}" | ${SED} -f rm_addr_tok.sed > index.list
    else
        # fall back to 'ls' result
        echo "No table of contents found in index page. Will use file order in $html_path"
        # index_page must be excluded from toc ...
        ls -t $html_path |${GREP} -iv "$index_page" > index.list
    fi
else
    # index_page must be excluded from toc ...
    ls -t $html_path |${GREP} -iv "$index_page_default" > index.list
fi

echo "TOC: "
cat index.list

echo "Merging html files ... "
for i in $(< index.list)
do
#   sed -f rm_tb_pic.sed -i "$html_path/$i"
    sed -f rm_html_tag.sed "$html_path/$i" >> ${TARGET}
done
echo "${TOKEN_BODY_END}${TOKEN_HTML_END}" >> ${TARGET} 
echo "Merged result:  ${TARGET}"

使用说明

以易水老师的《Vi/Vim使用进阶》为例,将html文件下载到本地,例如html目录,然后运行

# ./merge_html.sh html index.html

搞定!其中html是待合并html文件所在目录,index.html是提供目录页的html文档。因为文档里除了index.html外第一章也包含了目录,所以就用grep把index.html从待合并文件列表中去掉了。
如果没有提供包含目录的html文件,或者文件里找不到目录信息,脚本就会使用ls命令生成待合并文件列表,并按这个顺序合并所有html文档。
由于易水老师文章的版权要求,这里就不提供转换好的html文档和pdf文档了,有需要的朋友可以下载脚本自己转一下。

对脚本的几处解释

  • 脚本里的第二个sed命令脚本用来将《Vi/Vim使用进阶》中的小图替换成大图,这样方便直接打印。需要的话请将注释去掉

    # sed -f rm_tb_pic.sed -i "$html_path/$i"
  • 脚本使用TOKEN_TOC来匹配目录所在的行,这里有个TODO,就是如果目录是分行写的,匹配就会失败,这个问题留待以后解决。TOKEN_CHAP_PREFIX用来匹配TOC中每个章节的TAG,而TOKEN_CHAP_NAME用来匹配每个章节的标题,这几个TOKEN都可以通过脚本参数手动覆盖。
  • 脚本参考/usr/share/doc/util-linux/examples/getopt-parse.bash脚本,使用了getopt来处理命令行参数。但getopt存在一定的可移植性问题,可以参考stackoverflow上的讨论
  • getopt的使用

    PARSED_OPT=`getopt -o h --long tok-toc:,tok-prefix:,tok-name:,help\
    -n "$0" -- "$@"`

    在Linux上getopt命令是调用GNU C库的getopt函数实现的,所以支持的选项(options)规则完全一样,例如”-“指定单字符选项,后跟”:“表示该选项必须提供参数(argument),”::“表示参数可选。getopt命令本身也按照UNIX的标准处理自己的参数——需要用”- -“将非选项参数和选项隔离开来。所以需要将调用getopt命令的脚本,即merge_html.sh的命令行全部参数作为一个整体”$@“,并且是非选项参数(Non-opt arguments)传给getopt。并且为了保留命令行参数里的特殊字符需要加”“。
    getopt会根据 -o和–long 指定的选项名称和类型对”$@“进行分解,得到的结果保留在PARSED_OPT变量中并将其作为”$@“供后续的while 循环和shift处理。-n ”$0“ 指定merge_html.sh为报错时getopt所使用的身份,因为getopt是给merge_html.sh打工的,所以这样处理后报错的时候显示的就是:”merge_html.sh: invalid option.” 看上去比较符合逻辑。

更多推荐

将多个HTML文档合并成一个HTML文档