工作时经常会用到批量文本文件编辑,比如批量改个机器名啊,给所有.csproj文件加上某个xml element啊之类的。平常的做法一般是写个小c#程序扫描文件,读写内容。这些程序80%都是一样的,虽然用C#的StreamReader/Writer IO操作还算方便,每次copy-paste也累。而且很多情况下很小的改动还要建个工程,编译实在太麻烦。于是前段时间趁着假期学了一下sed/awk这对文本操作利器。
刚好老婆最近迷上看网文,叫我写个程序批量下载收藏一些。。。嗯,正好可以尝试一下新学的技能。
需求:从jjwxc.net上下载小说。给定URL,比如:http://www.jjwxc.net/onebook.php?novelid=1234567&chapterid=1 <- chapterid [1..n]
大致研究了一下,这个网页结构还算简单,文章正文都在<div class="noveltext">里,但有一些子div里放着一些链接需要去掉。另外正文里充斥着和背景同色的防拷贝文本,不过非常规律,普通的正则表达式就可以匹配。好了,大体步骤确定了:
- 用wget把url保存到文件
wget -q -Ojjd.htm.gz %URL%
- gzip解压
gzip -df jjd.htm.gz
- iconv转码(gb2312 -> utf-8)
iconv -c -f gb2312 -t utf-8 jjd.htm > jjd.utf8.htm
- sed/awk抓取相应内容,做一下格式转换,最终输出到txt文件
sed "s/>/\n/g" jjd.utf8.htm | awk -f "jjd.awk" | sed "-fjjd.sed" | awk -f "jjd.2.awk" > %OUTFILE%
a. 因为sed/awk都是以行为单位的,为了方便,首先要做的是把放在同一行内的html tag放到不同的行,这样数open tag/close tag就比较容易。这一步用sed把所有的'>'字符变成换行。
/<div class="noveltext"/ {
level = 1
next
}
level == 1 {
print $0
}
level > 0 && /<div/ { level++ }
level > 0 && /<\/div/ { level-- }
awk可以理解成一些if... { } 语句。每个{ }外的条件满足时,执行{ }内的代码。这里一共有四个条件:
1. 如果某一行能匹配正则表达式 /<div class="noveltext"/ (这就是正文所在的div),设level为1。这个level用来跟踪目前html tree的位置。
2. 如果level为1的时候打印本行。level==1就是说在noveltext这个div内,并且不在子div中。如果写成level >= 1就会把子div中的内容也打印出来。
3,4. 进div level +1,出div level -1.
s/<.*$//g
s/^[ ]\+$//g
s/^。.*@$//g
s/^。[0-9a-z]\+$//g
1. 去掉所有html tags
/^$/ {
if(content > 0 && c++ < 1) {
print $0
}
next
}
{
c = 0
content = 1
print $0
}
把所有东西写在.cmd里,删掉临时文件,搞定:) 为了方便易用可以写个自动调用下载所有章节,做个UI什么的,但已经和sed/awk无关啦。
另外也可以不用sed/awk,而是parse xml,然后精确定位正文位置,但是html要转成well formed xml(可以用html tidy)。
用这个程序,如果正文中有>等html encoded字符是不会被decode的,可以用sed在最后做一下替换。但是,得了吧,一般小说都用全角符号,连空格都是全角的!