将多个HTML文档合并成一个HTML文档
目录
最近在看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.” 看上去比较符合逻辑。