今天收到一封信,里面提出了这样一个有意思的问题:
#include <math.h>
#include <stdio.h>
int main()
{
printf( "%d\n" , pow( 4 , 2 ) ) ;
}
输出是0,
但是
#include <math.h>
#include <stdio.h>
int main()
{
int a = pow( 4 , 2 ) ;
printf( "%d\n , a ) ;
}
输出正确. why?
这个问题很有意思,其实如果把第一个程序改一下,改成
printf( "%d\n" , ( int )pow( 4 , 2 ) ) ;
那么第一个问题的输出也是正确的.
比较一下两种写法,可以发现问题是出在一个转换下,先计算pow(4,2)的值,然后再把它转换成int型的值,最后再用%d输出就正确了,这主要是因为printf()在传递参数的时候不会进行类型转换,而pow()的返回值是一个double型的值!
我们计算一下pow( 4 , 2 ),它的结果是16,然后,我们把它用 double 型来表示,
可知16的double型表示为: 0 0 0 0 0 0 30 40
然后,把它们全部压栈,于是靠近栈顶的4B就是 0 0 0 0
而后编译器调用printf()函数,printf()一分析控制字符串,发现是否%d,它就认为栈中的参数是个整数(4B),于是它就只取出其不意4B来显示,故而结果就是0了~~,
因此,要像得到正确的结果,我们需要让printf()知道,栈中是个double型(8B)的参数,因此,我们应当用:
"%f"而不是"%d"来输出pow()的值
#include <math.h>
#include <stdio.h>
int main()
{
printf( "%d\n" , pow( 4 , 2 ) ) ;
}
输出是0,
但是
#include <math.h>
#include <stdio.h>
int main()
{
int a = pow( 4 , 2 ) ;
printf( "%d\n , a ) ;
}
输出正确. why?
这个问题很有意思,其实如果把第一个程序改一下,改成
printf( "%d\n" , ( int )pow( 4 , 2 ) ) ;
那么第一个问题的输出也是正确的.
比较一下两种写法,可以发现问题是出在一个转换下,先计算pow(4,2)的值,然后再把它转换成int型的值,最后再用%d输出就正确了,这主要是因为printf()在传递参数的时候不会进行类型转换,而pow()的返回值是一个double型的值!
我们计算一下pow( 4 , 2 ),它的结果是16,然后,我们把它用 double 型来表示,
可知16的double型表示为: 0 0 0 0 0 0 30 40
然后,把它们全部压栈,于是靠近栈顶的4B就是 0 0 0 0
而后编译器调用printf()函数,printf()一分析控制字符串,发现是否%d,它就认为栈中的参数是个整数(4B),于是它就只取出其不意4B来显示,故而结果就是0了~~,
因此,要像得到正确的结果,我们需要让printf()知道,栈中是个double型(8B)的参数,因此,我们应当用:
"%f"而不是"%d"来输出pow()的值
补丁下载:
http://www.bo-blog.com/weblog/security-check/
打开方法:
后台->常规管理->bo-blog设置->发言时启用验证码 即可。
刚才重新让服务器商人换回linux,然后再进管理员后台发现一直提示我“验证码不正确”,明明输入的是正确的验证码却提示错误,还是 百度 了下。
我的问题所在是 temp 文件夹的权限问题,也有人说是GD库不支持,还有人说是空间满了。反正俺是找到原因了。
http://www.bo-blog.com/weblog/security-check/
打开方法:
后台->常规管理->bo-blog设置->发言时启用验证码 即可。
刚才重新让服务器商人换回linux,然后再进管理员后台发现一直提示我“验证码不正确”,明明输入的是正确的验证码却提示错误,还是 百度 了下。
我的问题所在是 temp 文件夹的权限问题,也有人说是GD库不支持,还有人说是空间满了。反正俺是找到原因了。
FreeBSD FTP 的架設
當你的 FreeBSD 安裝好後就內建 FTP ,只是預設不啟動它而已, 有人覺得 FreeBSD 內建的 ftpd太陽春,會另外用別的 ftpd 來取代, 常見的替代方案有:proftpd , pureftp ......等,不過要隨時注意更新,因為FTP 程式常常被找到有安全漏洞。
1 以 FreeBSD 內建的 ftpd 提供服務
一般說來,當你安裝好 FreeBSD ,FTP 的服務程式 /usr/libexec/ftpd 就有了
1-1 打開 FTP 服務
FreeBSD 系統的預設 ftpd 提供 daemon 模式(stand alone)和由 inetd 啟動 ftpd 兩種方式
1-1-1 方法一:daemon 模式 (stand alone)
1-1-1-1 立即啟動 ftpd daemon
如果只是要馬上啟動服務只要執行:
/usr/libexec/ftpd -D -l -l
參數說明:
* -D :讓 ftpd 以 daemon 的方式啟動。
* -l :叫 syslogd 記錄每次的連線,用兩次 -l 則可以連使用的動作都記錄
ftpd 還有很多的參數,可以 man ftpd 查看。
1-1-1-2 如何讓 FreeBSD 開機時自動啟動 ftpd
如果只用前面的方法啟動 ftpd daemon,下次系統重新開機後就沒了,為了讓它能自動啟動,我們可以把啟動指令放入 /etc/rc.local 中或是仿 /usr/local/etc/rc.d 的檔案,自己寫個 ftpd.sh 。
例一:
ee /etc/rc.local
在裡面放一行:
/usr/libexec/ftpd -D -l -l
例二:
仿 /usr/local/etc/rc.d 中的檔案,新增一個叫 ftpd.sh 的 script :
ee /usr/local/etc/rc.d/ftpd.sh
裡面放入下面的內容:
ftpd="/usr/libexec/ftpd"
case "$1" in
[ -x ${ftpd} ] && ${ftpd} -D ${ftpd_flag} > /dev/null && echo -n ' ftpd
stop)
;;
echo "Usage: `basename $0` {start|stop}" >&2
esac
exit 0
存好檔後再更改一下檔案的權限,讓它可以執行:
chmod 554 /usr/local/etc/rc.d/ftpd.sh
這樣,當 FreeBSD 開機時就會自動啟動 ftpd ,也可以利用 /usr/local/etc/rc.d/ftpd stop 來停止服務,執行時要加什麼參數就修改 ftpd_flag 那行。
1-1-2 方法二:由 inetd 來啟動 ftpd
這種方式,想當然爾,就是要修改 /etc/inetd.conf 囉:
ee /etc/inetd.conf
先檢查一下 /etc/inetd.conf 中有沒有下面這行:
#ftp stream tcp nowait root /usr/libexec/ftpd ftpd -l
如果像上面那行一樣,開頭是井字號,表示現在 FTP 服務預設是被關閉的。井字號表示註解,不使用,只要把井字號去掉改成下面的樣子:
ftp stream tcp nowait root /usr/libexec/ftpd ftpd -l
存檔後執行下面的指令:
kill -HUP `cat /var/run/inetd.pid`
讓 inetd 重新抓取 /etc/inetd.conf 設定檔就好了
注意:上面的 ` 是 Esc 鍵下面那鍋毛毛蟲的按鍵哦,可別打成單引號
1-2 停止 FTP 服務
如果要停止 FTP 服務,看之前是以 daemon 模式啟動還是 inetd 模式啟動而有所不同。
1-2-1 daemon 模式
daemon 模式可以執行下列指令來終止 ftpd 的程序:
killall ftpd
如果之前是將 ftpd 放在 /etc/rc.local 中來由系統在開機時自動啟動,可以用下面的方法來停止自動提供 FTP 服務。
先打開 /etc/rc.local 來編輯,執行:
ee /etc/rc.local
將檔案中,執行 ftpd 那行的最前面加個井字號,儲存好即可。
1-2-2 inetd 模式
當初是以 inetd 模式來提供服務的,則要修改 inetd 的設定檔 /etc/inetd.conf ,並讓 inetd 重新讀取設定。
先打開 /etc/inetd.conf 來編輯,執行:
ee /etc/inetd.conf
將設定檔中,有 ftpd 那行的前面加上井字號後,儲存設定檔。
讓 inetd 重讀設定檔,執行下面指令:
kill -HUP `cat /var/run/inetd.pid`
如果要確認 FTP 服是否已經停止了,可以執行:
netstat -na
看看下面這行是不是已經消失了:
tcp4 0 0 *.21 *.* LISTEN
1-3 限制使用者只能在自己目錄活動(chroot)
如果沒有特殊設定,使用者用自己的帳號 FTP 到主機後,可以自由的切換任意目錄的,如果不想讓它亂跑則要做以下設定。
1-3-1 方法一:利用 /etc/ftpchroot
FreeBSD 的 ftpd 以 /etc/ftpchroot 來控制哪些人或群組要如何 chroot ,所以我們開啟/新增這個設定檔來編輯。
ee /etc/ftpchroot
在檔案裡面放入我們要管制的人或群組:
gsyan
@staff
上面的設定使得 gsyan, foo 及屬於 staff 群組的人都只能在自己目錄活動。
說明:
小老鼠開頭的表示後面接的名稱為群組。
有方法可以只開放一個帳號不 chroot 其它全部 chroot 嗎?
最近 FreeBSD 內建的 ftpd 在 /etc/ftpchroot 又多了可設定的東東, 如果 man ftpchroot 可以看到說明,就表示可以使用下面的的設定來達到只開放部份帳號不鎖定在個人目錄的目的。
首先開啟 /etc/ftpchroot 來編輯:
ee /etc/ftpchroot
假設 admin 是管理員的帳號,讓 admin 可以在系統中到處游走,就裡面放入下面三行:
admin /
@ www
儲存好就可以用 ftp 連線看看, 上面的設定有底下的效果:
* 第一行設定:admin 登入時會切換到 /
* 第二行設定:匿名登入時則保持原來的方式,只能在帳號指定的公用目錄活動。
* 第三行設定:其它使用者則只能在個人目錄中的 www 資料夾中活動。
第三行應用在 server 有 apache 提供使用者放網頁, 而 apache 設定 UserDir=www 時,以後只要告訴使用者: 『請將做好的網頁直接用自己的帳號 ftp 到主機即可』, 以前都要解釋半天,請他 ftp 後把網頁放到 www 資料夾, 不過,記得先將使用者的 www 目錄先建立好,不然可是會連登入都無法登入哦!!
在 FreeBSD 4.8R 以後的 /etc/ftpchroot 又新增了功能,詳細的設定可以 man ftpchroot (不過,之前的版本沒這鍋 man )。
1-3-2 方法二:利用 /etc/login.conf
這個方法是利用使用者資料庫 (系統密碼檔 /etc/master.passwd) 中 login class 的欄位,來設定使用者隸於的class ,然後在 /etc/login.conf 中設定各 class 在 FTP chroot 的動作為何,在/etc/ftpchroot 不方便設定時,適用於要處理很多人的狀況。
開啟 /etc/login.conf 來修改,執行:
ee /etc/login.conf
然後找到下面 default: 開頭的哪幾行,類似下面的內容:
default:\
:welcome=/etc/motd:\
當你的 FreeBSD 安裝好後就內建 FTP ,只是預設不啟動它而已, 有人覺得 FreeBSD 內建的 ftpd太陽春,會另外用別的 ftpd 來取代, 常見的替代方案有:proftpd , pureftp ......等,不過要隨時注意更新,因為FTP 程式常常被找到有安全漏洞。
1 以 FreeBSD 內建的 ftpd 提供服務
一般說來,當你安裝好 FreeBSD ,FTP 的服務程式 /usr/libexec/ftpd 就有了
1-1 打開 FTP 服務
FreeBSD 系統的預設 ftpd 提供 daemon 模式(stand alone)和由 inetd 啟動 ftpd 兩種方式
1-1-1 方法一:daemon 模式 (stand alone)
1-1-1-1 立即啟動 ftpd daemon
如果只是要馬上啟動服務只要執行:
/usr/libexec/ftpd -D -l -l
參數說明:
* -D :讓 ftpd 以 daemon 的方式啟動。
* -l :叫 syslogd 記錄每次的連線,用兩次 -l 則可以連使用的動作都記錄
ftpd 還有很多的參數,可以 man ftpd 查看。
1-1-1-2 如何讓 FreeBSD 開機時自動啟動 ftpd
如果只用前面的方法啟動 ftpd daemon,下次系統重新開機後就沒了,為了讓它能自動啟動,我們可以把啟動指令放入 /etc/rc.local 中或是仿 /usr/local/etc/rc.d 的檔案,自己寫個 ftpd.sh 。
例一:
ee /etc/rc.local
在裡面放一行:
/usr/libexec/ftpd -D -l -l
例二:
仿 /usr/local/etc/rc.d 中的檔案,新增一個叫 ftpd.sh 的 script :
ee /usr/local/etc/rc.d/ftpd.sh
裡面放入下面的內容:
ftpd="/usr/libexec/ftpd"
case "$1" in
[ -x ${ftpd} ] && ${ftpd} -D ${ftpd_flag} > /dev/null && echo -n ' ftpd
stop)
;;
echo "Usage: `basename $0` {start|stop}" >&2
esac
exit 0
存好檔後再更改一下檔案的權限,讓它可以執行:
chmod 554 /usr/local/etc/rc.d/ftpd.sh
這樣,當 FreeBSD 開機時就會自動啟動 ftpd ,也可以利用 /usr/local/etc/rc.d/ftpd stop 來停止服務,執行時要加什麼參數就修改 ftpd_flag 那行。
1-1-2 方法二:由 inetd 來啟動 ftpd
這種方式,想當然爾,就是要修改 /etc/inetd.conf 囉:
ee /etc/inetd.conf
先檢查一下 /etc/inetd.conf 中有沒有下面這行:
#ftp stream tcp nowait root /usr/libexec/ftpd ftpd -l
如果像上面那行一樣,開頭是井字號,表示現在 FTP 服務預設是被關閉的。井字號表示註解,不使用,只要把井字號去掉改成下面的樣子:
ftp stream tcp nowait root /usr/libexec/ftpd ftpd -l
存檔後執行下面的指令:
kill -HUP `cat /var/run/inetd.pid`
讓 inetd 重新抓取 /etc/inetd.conf 設定檔就好了
注意:上面的 ` 是 Esc 鍵下面那鍋毛毛蟲的按鍵哦,可別打成單引號
1-2 停止 FTP 服務
如果要停止 FTP 服務,看之前是以 daemon 模式啟動還是 inetd 模式啟動而有所不同。
1-2-1 daemon 模式
daemon 模式可以執行下列指令來終止 ftpd 的程序:
killall ftpd
如果之前是將 ftpd 放在 /etc/rc.local 中來由系統在開機時自動啟動,可以用下面的方法來停止自動提供 FTP 服務。
先打開 /etc/rc.local 來編輯,執行:
ee /etc/rc.local
將檔案中,執行 ftpd 那行的最前面加個井字號,儲存好即可。
1-2-2 inetd 模式
當初是以 inetd 模式來提供服務的,則要修改 inetd 的設定檔 /etc/inetd.conf ,並讓 inetd 重新讀取設定。
先打開 /etc/inetd.conf 來編輯,執行:
ee /etc/inetd.conf
將設定檔中,有 ftpd 那行的前面加上井字號後,儲存設定檔。
讓 inetd 重讀設定檔,執行下面指令:
kill -HUP `cat /var/run/inetd.pid`
如果要確認 FTP 服是否已經停止了,可以執行:
netstat -na
看看下面這行是不是已經消失了:
tcp4 0 0 *.21 *.* LISTEN
1-3 限制使用者只能在自己目錄活動(chroot)
如果沒有特殊設定,使用者用自己的帳號 FTP 到主機後,可以自由的切換任意目錄的,如果不想讓它亂跑則要做以下設定。
1-3-1 方法一:利用 /etc/ftpchroot
FreeBSD 的 ftpd 以 /etc/ftpchroot 來控制哪些人或群組要如何 chroot ,所以我們開啟/新增這個設定檔來編輯。
ee /etc/ftpchroot
在檔案裡面放入我們要管制的人或群組:
gsyan
@staff
上面的設定使得 gsyan, foo 及屬於 staff 群組的人都只能在自己目錄活動。
說明:
小老鼠開頭的表示後面接的名稱為群組。
有方法可以只開放一個帳號不 chroot 其它全部 chroot 嗎?
最近 FreeBSD 內建的 ftpd 在 /etc/ftpchroot 又多了可設定的東東, 如果 man ftpchroot 可以看到說明,就表示可以使用下面的的設定來達到只開放部份帳號不鎖定在個人目錄的目的。
首先開啟 /etc/ftpchroot 來編輯:
ee /etc/ftpchroot
假設 admin 是管理員的帳號,讓 admin 可以在系統中到處游走,就裡面放入下面三行:
admin /
@ www
儲存好就可以用 ftp 連線看看, 上面的設定有底下的效果:
* 第一行設定:admin 登入時會切換到 /
* 第二行設定:匿名登入時則保持原來的方式,只能在帳號指定的公用目錄活動。
* 第三行設定:其它使用者則只能在個人目錄中的 www 資料夾中活動。
第三行應用在 server 有 apache 提供使用者放網頁, 而 apache 設定 UserDir=www 時,以後只要告訴使用者: 『請將做好的網頁直接用自己的帳號 ftp 到主機即可』, 以前都要解釋半天,請他 ftp 後把網頁放到 www 資料夾, 不過,記得先將使用者的 www 目錄先建立好,不然可是會連登入都無法登入哦!!
在 FreeBSD 4.8R 以後的 /etc/ftpchroot 又新增了功能,詳細的設定可以 man ftpchroot (不過,之前的版本沒這鍋 man )。
1-3-2 方法二:利用 /etc/login.conf
這個方法是利用使用者資料庫 (系統密碼檔 /etc/master.passwd) 中 login class 的欄位,來設定使用者隸於的class ,然後在 /etc/login.conf 中設定各 class 在 FTP chroot 的動作為何,在/etc/ftpchroot 不方便設定時,適用於要處理很多人的狀況。
開啟 /etc/login.conf 來修改,執行:
ee /etc/login.conf
然後找到下面 default: 開頭的哪幾行,類似下面的內容:
default:\
:welcome=/etc/motd:\
文章开头就列举了那么多联系方式,难免会让大家感觉有点AD的意味,但是不容质疑的是,默默的确有那么丁点的表现欲^_^,虽然有时候过于细致会被人说婆妈,但是幸好这种细致对于编程来说,还是蛮有益的!
从默默自己向别人问怎么学PHP开始,到后来不少人又来问默默怎么学PHP,不管默默是新手,还是老鸟,似乎总是感觉摸不出一条清晰的脉络来,不过,默默既然学会了PHP,那么我走的这条路或多或少的有一定借鉴性。
PHP的背景恐怕就不用默默赘言了,我相信大家选择一种语言,并不是看它的背景和悠久历史,更重要的是看它的实用性,华而不实的语言哪怕是再辉煌的历史,也毕将步向没落,可喜的是PHP经受住了考验,也因此,它确实是一种值得学习的语言。
默默一直是听从别人的经验长大的,也因此在前辈们的经验里让默默少走了许多的弯路,更快的步入了正规,在此向那些我至尽不知道其名字的前辈们道声谢谢,在默默的眼里,帮助不分大小,只要是帮助,总会让默默的心里暖融融的,我想,前辈们帮助我,并不是为了得到我的一句谢谢,更多的是出于一种责任感和对默默的期望,所以我想,只有学好PHP,才能对得起前辈们的汗水。
正如我所说的,默默也终于感觉到了一种责任感,默默不知道自己的经验到底能帮助新手多少,但是默默明白,现在到了履行责任的时候了,我有必要把自己的经验告诉给所有希望学好PHP的人,只有这样才能让中国的PHP不断的进步,不断的发展,在世界上占据一席之地。
默默学习PHP的这段期间,感觉国内的PHP环境越来越成熟,规范也在逐渐的健全,PHPCHINA的成立,标志着与官方直接挂钩的PHP机构在中国正式落户了,在此献上迟到的掌声!
好的,切入正题:
我想在讲述自己的学习方式前,对那些期望能从我的文章中获得有用信息的人说一句心里话:
默默的文章不会对您的学习起到实质性的作用,您能否成功,还得靠自己的,坚持,坚持,再坚持,就是步入成功的不二法门。
我先把我自己学习PHP的过程做一下概括:
(1)熟悉HTML/CSS/JS等网页基本元素,完成阶段可自行制作完整的网页,对元素属性达到熟悉程度
(2)理解动态语言的概念,运做机制,熟悉PHP语法
(3)学习如何将PHP与HTML结合起来完成简单动态页面
(4)接触MYSQL,开始设计数据库程序
(5)不断巩固,摸透大部分PHP常用函数,并可理解OOP,MYSQL优化,以及模板
(6)完成一个功能齐全的动态站点
我的这套线路可能跟许多学习PHP的爱好者不谋而合,这也算是一个循序渐进的学习过程,不过新手不要看到上面的概括就以为学习蛮简单的,默默在此不得不对您稍微泼一下冷水,任何东西其实都不简单,即使是小吃部的烧饼也不是一下子就会做成的。
我先解释一下我的学习思路。
首先,理解网站这一概念之后不难看出,任何网站都是由网页组成的,也就是说想完成网站,必须先学会做网页,因此必须要掌握了HTML,才能为今后制作网站打下基础。
在学习HTML中我想边学边做是最有效的方式,当然这一方式对于学习PHP同样是最有效的。
HTML中的任何元素都要亲自实践,只有明白了什么元素会起到什么效果之后,你才会记忆深刻,而一味的啃书,绝对是不行的,我想大部分新手之所以觉得概念难学,大部分是一个字“懒”,懒是阻止进步的最大敌人,所以克服掉懒的习惯,才能更快的学好一样东西。
也许您在学习PHP的时候只想尽快的开发一个网站,也就会想我做网站,干嘛要学什么网页这些小儿科?不难看出,眼高手低的新手不在少数,这种思想无疑于建造空中楼阁,你不建地基,何来的房顶呢?
OK,掌握静态网页的制作技术是学习开发网站的先决条件,这一点就讲到这里,因为这篇文章不是教程文章,也就不对技术进行深入的刨析了。
我假设你目前已经可以完成一个静态页面了,当然,做的好看难看是另外一说,默默的第一个网页也没好看到哪去,但是“孩子”再丑,咱们做“爹妈”的也不能嫌弃不是?这毕竟是咱的成果。
那么咱们就开始学习动态语言的概念吧,刚一接触动态语言,可能很多人都会蒙了,怎么这乱七八糟的东西,在网页里显示的时候却是另外一码事?其实这并不算乱七八糟,你写的HTML代码不也一样是一堆堆的字符吗?毕竟,代码并不是作为直接输出的,而是经过处理的,说白了,HTML是经过HTML解析器,而 PHP当然也就通过PHP解析器了,跟学习HTML一样的道理,想让任何的解析器完成操作,就必须使用它们专用的语法结构,所以PHP长相奇怪也就不足为奇了。
对于PHP的理解是新手最难迈过的一道门槛,不过你应该感到幸运的是PHP已经最大极限的为了新手而努力了,如果你学过其他的语言,也许会觉得PHP的确相当的简单,但是如果你之前什么都没学过,那么阿弥陀佛,硬着头皮琢磨吧。
书过三遍自然熟,这个简单的道理告诉我们,即使你理解不了PHP,但是也必须先跟它混个脸熟,看,一遍遍的看,看的同时一边琢磨,一边按照它所教的打代码,即使你搞不清楚那些代码到底是干嘛的,但是起码你应该找找感觉。
在一段挣扎之后,聪明的你,显然已经逐渐的开悟了,慢慢的理解了编程的概念,那么祝贺你,你已经迈出了成功的第一步。
搞清楚HTML和PHP的概念,那么PHP和HTML混合编程应该不成问题,在这期间,你完全可以让PHP给你算算一加一等于几,然后在浏览器输出,不要觉得幼稚,这的确是跟阿波罗登月一样,你打的是一小段代码,但是对于你的编程之路,可是迈出了一大步啊!兴奋吧?但是不得不再给你泼点冷水,您还是菜鸟一个。
高兴一段时间就必须继续努力了,接下来就是学习数据库了,MYSQL可算是PHP的黄金搭档了,不过,虽然话是这么说,你也可能恨不得把MYSQL给生吞活剥了,因为这一行一列的东东简直让自己头晕目眩。
头晕归头晕,目眩归目眩,你不可能吃饭的时候咬了自己一下舌头就从此不吃饭了不是?放下畏惧,继续努力,咱们是来征服它的,而不是被它征服的,振奋起来吧同志。
在一番搏斗之后,你终于理解了数据库的概念,而且让你兴奋不已的是你终于可以通过PHP来连接数据库了,这期间你是怎么学会的,我们不去考证了,但是事实证明,你已经可以了。
学会了PHP和数据库的你,无疑是左手拿着MOTOLOLA右手拿着NOKIA,要多潇洒,有多潇洒,哈哈,终于学会了,但是可能这个时候,又会有人不经意的拍拍肩膀对你说:哥们,别高兴的太早,你还是菜鸟,离学会还差着一大截呢!
等到你发奋努力的学会了用PHP成功的插入,删除,更新数据的时候,显然,你已经距离成功指日可待了。
这个时候的你也许是这种状态:
你会HTML吗?会,我能编好几个大表格排板的网页啦!
你会PHP吗?会,我会把一加一的运算写在函数里,然后调用啦!
你会MYSQL吗?会,我会把我的信息在数据库里插入删除啦
那,接下来你该怎么做呢?我觉得,小试一下身手,大概是没问题了,那么交给你个任务,做个留言本吧,这和HELLO WORLD有一比啊!^_^,同是新手面临的第一道关。
花了一段时间,你终于学会把表单的数据插入数据库,然后显示出来了,应该说一个程序的雏形已经诞生了。
但是,你可能瞅瞅东,看看西,人家这个编论坛,那个CMS,还有那啥CRM,我啥时候写一个呢?
不要急,可以说你的马步已经扎的差不多了,接下来就要开始练把势的时候了,如果有条件的话,用笔或者打印一个简易的PHP手册在身上,时不时的摸出来看看,记得,去WC也不能放过(^2^)。
再有条件的话,买本书看看吧,《PHP+MYSQL WEB开发(第三版)》号称圣经级,(也许是个不错的选择(声明:作者没给我啥好处费,我也不是书托,隔着大老远,我连他老兄的面都没见过的说-_-)
巩固了自己的知识,熟悉了PHP和MYSQL开发的要领之后,再回头看你写的那个留言本,你也许会怀疑那真的是你写的吗?当然,如果屋里还有鬼的话,也许是它写的-_-
这个时候,你的留言本应该加入注册以及分页功能了,而如果你更强的话,UI(用户界面)也可以加强,完成之后,感觉是不是特有成就感?不管怎么样,咱好歹是写了一个动态网站程序了,放在自己的网站上耍耍吧,让好朋友来看看,嘿,看咱写的多棒,然后再在网上宣传一下。
几天之后你再打开留言本,哎?哇,一下弹出N多页面!很明显,你的留言本并没有做好安全防范,被人用JS代码小小的耍了一下,我很同情你这个时候的感受,但是没有别的办法了,继续努力吧!
你发奋努力,熟悉了安全方面的问题,然后又设计了一些程序,感觉还不错。
那么接下来,这就算学会啦?NO,NO,NO,还早呢,你至尽还没碰过OOP之类的吧?模板呢?
恩,学!加紧学呀学,学会了这些之后,你又学会了生成静态网页,现在你应该接触一下XML了,恩,XML也了解了,那么AJAX你也得接触接触吧?AJAX完了....然后...
总而言之,你绝对不会发现你全部都学会了,一些真正的强人总会搞出新玩意来丢给你,你不学就落后了,也印证了前人的经验,果然是学无止境啊!
我想通过我的一番YY,你也应该大致熟悉了一些学习过程,也许我的过程和你的有些出路,但是不管怎么样是殊途同归,我写这么多,也只是给大家一个借鉴的机会,至于好与不好,默默不敢打包票^0^
看完之后你发现,罗嗦这么多,对我一点用处没有啊,我知道该怎么学,但是我想如何才能更快的学,一周速成,啊不,24小时速成那种,默默你有没?
我.......我没有,但是2分钟之内把你扁进医院里,我倒是有把握-_-
学东西,永远不要妄想有速成这一说,告诉你了一个方式,但是缺少努力这一环节,那也是白搭。
但是有一点我可以给你保证的就是,你学会了PHP,那么学其他的语言,肯定速成,反过来也一样,如果你之前学过其他的语言,那么学PHP肯定快。
不过语法好学,但是怎么用语法来实现每个人都有每个人的方式,几乎是各有千秋。然而借鉴别人成功的代码,绝对是有益无害,因此,多看那些经过千锤百炼凝出来的经典代码,是进阶的最好方法。
讲了这么多,无非是想说:学习PHP不仅要掌握方法,更多的是付出汗水,我不希望看到中途放弃的人,相信自己,相信自己的选择,更要相信自己的能力,如果自己想放弃,暴力一点的话,就自己抽自己一个嘴巴,然后大吼:别人可以,我为什么就不可以?(是不是有点阎罗教练的味道,默默的确是电影看多了,抽嘴巴是会痛的,各位其实明白这个道理了就行了)
另外要叮嘱各位的是,抵御诱惑,ASP/PHP/JSP/.NET的对比也许会让你无所适从,你也许学了一半PHP,又开始打C#的主意,或者有人说 JAVA很强,这个时候的你绝对不能动摇,哪怕你真想学,也得学会了PHP。然后再学,见异思迁是最不可取的,狗熊掰玉米就是这个道理,如果经常中途放弃,只能是一无所获,还浪费了N多的时间和经历,得不偿失,最重要的是,你会被别人瞧不起,没有人会喜欢和见异思迁的人交朋友,因为这种人太不安分,太不可靠,因此,你必须要强迫自己完成自己的目标,哪怕可能会很难受,也得坚持,毅力就是这么锻炼出来的。
说了这么多,可能大家嫌我烦了,但是默默属于那种平常很沉默,一旦进入状态之后就变的很兴奋,我想尽可能的把我所想的表达出来,但是可惜自己的文字功底有限,效果可能不尽如人意,但是我感觉,把自己的经验分享出来之后感觉很轻松,如释重负的感觉。
最后,我还想说一下,有很多的国人不自信,说过诸如什么语言到了中国就变味,什么中国人不团结,没有团队精神之类的,我反倒觉得那些人鼠目寸光,可悲,可叹,那些人总是把一切的责任推卸的一干二净,却不从自身出发,以身表率,来改变这一状况,反而悲观的叹息,只期望那些人早点醒悟,只有人人都努力,才能进步,而自卑自叹,只会越搞越糟。
其实无论是PHP还是其他任何东西,咱们不学则已,学就要搞出个名堂来,一个人的力量也许微不足道,但是大家都努力,齐心协力,中国人有什么不可以的?咱们不但要赶上,更要超越,要让世界都使用“中国标准”,也许我这么说有人说我痴心妄想,也有人说我只会喊口号,这都无所谓,但是重要的是,我终于把心里的话说了出来,说白了,咱们中国人不缺实力,就缺野心,野心并不是贬义,这里所指的野心,正是指中国人敢于争世界第一的志气。
说了这么多,又跑题了^_^,其实就是鼓励咱们学习PHP的新手,努力吧,中国的发展靠咱们!(把话说大了,各位看官不要见怪!斗胆而言^_^)
嘿嘿!
从默默自己向别人问怎么学PHP开始,到后来不少人又来问默默怎么学PHP,不管默默是新手,还是老鸟,似乎总是感觉摸不出一条清晰的脉络来,不过,默默既然学会了PHP,那么我走的这条路或多或少的有一定借鉴性。
PHP的背景恐怕就不用默默赘言了,我相信大家选择一种语言,并不是看它的背景和悠久历史,更重要的是看它的实用性,华而不实的语言哪怕是再辉煌的历史,也毕将步向没落,可喜的是PHP经受住了考验,也因此,它确实是一种值得学习的语言。
默默一直是听从别人的经验长大的,也因此在前辈们的经验里让默默少走了许多的弯路,更快的步入了正规,在此向那些我至尽不知道其名字的前辈们道声谢谢,在默默的眼里,帮助不分大小,只要是帮助,总会让默默的心里暖融融的,我想,前辈们帮助我,并不是为了得到我的一句谢谢,更多的是出于一种责任感和对默默的期望,所以我想,只有学好PHP,才能对得起前辈们的汗水。
正如我所说的,默默也终于感觉到了一种责任感,默默不知道自己的经验到底能帮助新手多少,但是默默明白,现在到了履行责任的时候了,我有必要把自己的经验告诉给所有希望学好PHP的人,只有这样才能让中国的PHP不断的进步,不断的发展,在世界上占据一席之地。
默默学习PHP的这段期间,感觉国内的PHP环境越来越成熟,规范也在逐渐的健全,PHPCHINA的成立,标志着与官方直接挂钩的PHP机构在中国正式落户了,在此献上迟到的掌声!
好的,切入正题:
我想在讲述自己的学习方式前,对那些期望能从我的文章中获得有用信息的人说一句心里话:
默默的文章不会对您的学习起到实质性的作用,您能否成功,还得靠自己的,坚持,坚持,再坚持,就是步入成功的不二法门。
我先把我自己学习PHP的过程做一下概括:
(1)熟悉HTML/CSS/JS等网页基本元素,完成阶段可自行制作完整的网页,对元素属性达到熟悉程度
(2)理解动态语言的概念,运做机制,熟悉PHP语法
(3)学习如何将PHP与HTML结合起来完成简单动态页面
(4)接触MYSQL,开始设计数据库程序
(5)不断巩固,摸透大部分PHP常用函数,并可理解OOP,MYSQL优化,以及模板
(6)完成一个功能齐全的动态站点
我的这套线路可能跟许多学习PHP的爱好者不谋而合,这也算是一个循序渐进的学习过程,不过新手不要看到上面的概括就以为学习蛮简单的,默默在此不得不对您稍微泼一下冷水,任何东西其实都不简单,即使是小吃部的烧饼也不是一下子就会做成的。
我先解释一下我的学习思路。
首先,理解网站这一概念之后不难看出,任何网站都是由网页组成的,也就是说想完成网站,必须先学会做网页,因此必须要掌握了HTML,才能为今后制作网站打下基础。
在学习HTML中我想边学边做是最有效的方式,当然这一方式对于学习PHP同样是最有效的。
HTML中的任何元素都要亲自实践,只有明白了什么元素会起到什么效果之后,你才会记忆深刻,而一味的啃书,绝对是不行的,我想大部分新手之所以觉得概念难学,大部分是一个字“懒”,懒是阻止进步的最大敌人,所以克服掉懒的习惯,才能更快的学好一样东西。
也许您在学习PHP的时候只想尽快的开发一个网站,也就会想我做网站,干嘛要学什么网页这些小儿科?不难看出,眼高手低的新手不在少数,这种思想无疑于建造空中楼阁,你不建地基,何来的房顶呢?
OK,掌握静态网页的制作技术是学习开发网站的先决条件,这一点就讲到这里,因为这篇文章不是教程文章,也就不对技术进行深入的刨析了。
我假设你目前已经可以完成一个静态页面了,当然,做的好看难看是另外一说,默默的第一个网页也没好看到哪去,但是“孩子”再丑,咱们做“爹妈”的也不能嫌弃不是?这毕竟是咱的成果。
那么咱们就开始学习动态语言的概念吧,刚一接触动态语言,可能很多人都会蒙了,怎么这乱七八糟的东西,在网页里显示的时候却是另外一码事?其实这并不算乱七八糟,你写的HTML代码不也一样是一堆堆的字符吗?毕竟,代码并不是作为直接输出的,而是经过处理的,说白了,HTML是经过HTML解析器,而 PHP当然也就通过PHP解析器了,跟学习HTML一样的道理,想让任何的解析器完成操作,就必须使用它们专用的语法结构,所以PHP长相奇怪也就不足为奇了。
对于PHP的理解是新手最难迈过的一道门槛,不过你应该感到幸运的是PHP已经最大极限的为了新手而努力了,如果你学过其他的语言,也许会觉得PHP的确相当的简单,但是如果你之前什么都没学过,那么阿弥陀佛,硬着头皮琢磨吧。
书过三遍自然熟,这个简单的道理告诉我们,即使你理解不了PHP,但是也必须先跟它混个脸熟,看,一遍遍的看,看的同时一边琢磨,一边按照它所教的打代码,即使你搞不清楚那些代码到底是干嘛的,但是起码你应该找找感觉。
在一段挣扎之后,聪明的你,显然已经逐渐的开悟了,慢慢的理解了编程的概念,那么祝贺你,你已经迈出了成功的第一步。
搞清楚HTML和PHP的概念,那么PHP和HTML混合编程应该不成问题,在这期间,你完全可以让PHP给你算算一加一等于几,然后在浏览器输出,不要觉得幼稚,这的确是跟阿波罗登月一样,你打的是一小段代码,但是对于你的编程之路,可是迈出了一大步啊!兴奋吧?但是不得不再给你泼点冷水,您还是菜鸟一个。
高兴一段时间就必须继续努力了,接下来就是学习数据库了,MYSQL可算是PHP的黄金搭档了,不过,虽然话是这么说,你也可能恨不得把MYSQL给生吞活剥了,因为这一行一列的东东简直让自己头晕目眩。
头晕归头晕,目眩归目眩,你不可能吃饭的时候咬了自己一下舌头就从此不吃饭了不是?放下畏惧,继续努力,咱们是来征服它的,而不是被它征服的,振奋起来吧同志。
在一番搏斗之后,你终于理解了数据库的概念,而且让你兴奋不已的是你终于可以通过PHP来连接数据库了,这期间你是怎么学会的,我们不去考证了,但是事实证明,你已经可以了。
学会了PHP和数据库的你,无疑是左手拿着MOTOLOLA右手拿着NOKIA,要多潇洒,有多潇洒,哈哈,终于学会了,但是可能这个时候,又会有人不经意的拍拍肩膀对你说:哥们,别高兴的太早,你还是菜鸟,离学会还差着一大截呢!
等到你发奋努力的学会了用PHP成功的插入,删除,更新数据的时候,显然,你已经距离成功指日可待了。
这个时候的你也许是这种状态:
你会HTML吗?会,我能编好几个大表格排板的网页啦!
你会PHP吗?会,我会把一加一的运算写在函数里,然后调用啦!
你会MYSQL吗?会,我会把我的信息在数据库里插入删除啦
那,接下来你该怎么做呢?我觉得,小试一下身手,大概是没问题了,那么交给你个任务,做个留言本吧,这和HELLO WORLD有一比啊!^_^,同是新手面临的第一道关。
花了一段时间,你终于学会把表单的数据插入数据库,然后显示出来了,应该说一个程序的雏形已经诞生了。
但是,你可能瞅瞅东,看看西,人家这个编论坛,那个CMS,还有那啥CRM,我啥时候写一个呢?
不要急,可以说你的马步已经扎的差不多了,接下来就要开始练把势的时候了,如果有条件的话,用笔或者打印一个简易的PHP手册在身上,时不时的摸出来看看,记得,去WC也不能放过(^2^)。
再有条件的话,买本书看看吧,《PHP+MYSQL WEB开发(第三版)》号称圣经级,(也许是个不错的选择(声明:作者没给我啥好处费,我也不是书托,隔着大老远,我连他老兄的面都没见过的说-_-)
巩固了自己的知识,熟悉了PHP和MYSQL开发的要领之后,再回头看你写的那个留言本,你也许会怀疑那真的是你写的吗?当然,如果屋里还有鬼的话,也许是它写的-_-
这个时候,你的留言本应该加入注册以及分页功能了,而如果你更强的话,UI(用户界面)也可以加强,完成之后,感觉是不是特有成就感?不管怎么样,咱好歹是写了一个动态网站程序了,放在自己的网站上耍耍吧,让好朋友来看看,嘿,看咱写的多棒,然后再在网上宣传一下。
几天之后你再打开留言本,哎?哇,一下弹出N多页面!很明显,你的留言本并没有做好安全防范,被人用JS代码小小的耍了一下,我很同情你这个时候的感受,但是没有别的办法了,继续努力吧!
你发奋努力,熟悉了安全方面的问题,然后又设计了一些程序,感觉还不错。
那么接下来,这就算学会啦?NO,NO,NO,还早呢,你至尽还没碰过OOP之类的吧?模板呢?
恩,学!加紧学呀学,学会了这些之后,你又学会了生成静态网页,现在你应该接触一下XML了,恩,XML也了解了,那么AJAX你也得接触接触吧?AJAX完了....然后...
总而言之,你绝对不会发现你全部都学会了,一些真正的强人总会搞出新玩意来丢给你,你不学就落后了,也印证了前人的经验,果然是学无止境啊!
我想通过我的一番YY,你也应该大致熟悉了一些学习过程,也许我的过程和你的有些出路,但是不管怎么样是殊途同归,我写这么多,也只是给大家一个借鉴的机会,至于好与不好,默默不敢打包票^0^
看完之后你发现,罗嗦这么多,对我一点用处没有啊,我知道该怎么学,但是我想如何才能更快的学,一周速成,啊不,24小时速成那种,默默你有没?
我.......我没有,但是2分钟之内把你扁进医院里,我倒是有把握-_-
学东西,永远不要妄想有速成这一说,告诉你了一个方式,但是缺少努力这一环节,那也是白搭。
但是有一点我可以给你保证的就是,你学会了PHP,那么学其他的语言,肯定速成,反过来也一样,如果你之前学过其他的语言,那么学PHP肯定快。
不过语法好学,但是怎么用语法来实现每个人都有每个人的方式,几乎是各有千秋。然而借鉴别人成功的代码,绝对是有益无害,因此,多看那些经过千锤百炼凝出来的经典代码,是进阶的最好方法。
讲了这么多,无非是想说:学习PHP不仅要掌握方法,更多的是付出汗水,我不希望看到中途放弃的人,相信自己,相信自己的选择,更要相信自己的能力,如果自己想放弃,暴力一点的话,就自己抽自己一个嘴巴,然后大吼:别人可以,我为什么就不可以?(是不是有点阎罗教练的味道,默默的确是电影看多了,抽嘴巴是会痛的,各位其实明白这个道理了就行了)
另外要叮嘱各位的是,抵御诱惑,ASP/PHP/JSP/.NET的对比也许会让你无所适从,你也许学了一半PHP,又开始打C#的主意,或者有人说 JAVA很强,这个时候的你绝对不能动摇,哪怕你真想学,也得学会了PHP。然后再学,见异思迁是最不可取的,狗熊掰玉米就是这个道理,如果经常中途放弃,只能是一无所获,还浪费了N多的时间和经历,得不偿失,最重要的是,你会被别人瞧不起,没有人会喜欢和见异思迁的人交朋友,因为这种人太不安分,太不可靠,因此,你必须要强迫自己完成自己的目标,哪怕可能会很难受,也得坚持,毅力就是这么锻炼出来的。
说了这么多,可能大家嫌我烦了,但是默默属于那种平常很沉默,一旦进入状态之后就变的很兴奋,我想尽可能的把我所想的表达出来,但是可惜自己的文字功底有限,效果可能不尽如人意,但是我感觉,把自己的经验分享出来之后感觉很轻松,如释重负的感觉。
最后,我还想说一下,有很多的国人不自信,说过诸如什么语言到了中国就变味,什么中国人不团结,没有团队精神之类的,我反倒觉得那些人鼠目寸光,可悲,可叹,那些人总是把一切的责任推卸的一干二净,却不从自身出发,以身表率,来改变这一状况,反而悲观的叹息,只期望那些人早点醒悟,只有人人都努力,才能进步,而自卑自叹,只会越搞越糟。
其实无论是PHP还是其他任何东西,咱们不学则已,学就要搞出个名堂来,一个人的力量也许微不足道,但是大家都努力,齐心协力,中国人有什么不可以的?咱们不但要赶上,更要超越,要让世界都使用“中国标准”,也许我这么说有人说我痴心妄想,也有人说我只会喊口号,这都无所谓,但是重要的是,我终于把心里的话说了出来,说白了,咱们中国人不缺实力,就缺野心,野心并不是贬义,这里所指的野心,正是指中国人敢于争世界第一的志气。
说了这么多,又跑题了^_^,其实就是鼓励咱们学习PHP的新手,努力吧,中国的发展靠咱们!(把话说大了,各位看官不要见怪!斗胆而言^_^)
嘿嘿!
邮件发送和收取是目前网上交流最为重要的途径之一,我们当然很希望自己的PHP程序也能够实现某些商业网站注册程序中采用的方法,即通过邮件方式进行密码(或激活码)发送和资料确认。另一方面,这种方式也是一种反馈用户信息的有效途径。当然,要实现这些功能是离不开邮件服务器的,目前比较流行的Mail服务器(更准确的说是邮件传输代理MTA)有:sendmail、qmail、postfix。至于如何配置其中的pop、 smtp、imap等服务已经超出这篇文章的范围,读者可以参考其他这方面文章。那么好了,我们究竟可以利用PHP来作些什么呢?
1. 简单邮件发送
PHP函数库中有一个mail函数,可以用来进行简单的邮件发送,函数原型为:
boolean mail(string $to, string $subject, string $message, string [$additional]);
$to指定邮件寄送地址,$subject指定邮件标题,$message指定邮件内容,$additional指定邮件的附加头部,例如:
复制PHP内容到剪贴板
PHP代码:
<?php
mail( "ywg_263@263.net", "message from php", "hello, xiaoyz! " );
?>
就可以向 ywg_263@263.net发送一个标题为“message from php” 内容为“hello, xiaoyz!”的邮件,其中的邮件接受人$to可以是多个邮件地址,也就是说可以同时给多个人发送同一份邮件,邮件地址之间用逗号分隔,示例如下:
复制PHP内容到剪贴板
PHP代码:
<?php
$emails = Array( "xiaoyz@birdy.dhs.org", "xiaoyz@hotmail.com" );
mail( implode(",", $emails), "message from php", "hello, xiaoyz!" );
?>
笔者做过的论坛程序中的注册部分就曾经使用过这种方法,不过最后还是采用了一种变通的形式,下文将会具体讲到。其中主要的需求是:当一个用户注册之后,必须得到组管理员的身份确认才能成为论坛的正式会员,我所采用的方法是:用户注册完成提交表单时,先把用户各种注册信息写入数据库,同时把用户的必要信息通过邮件的方式发送给用户所注册组的所有组管理员(如果没有组管理员的话,会给站管理员发送邮件,并告之该组没有组管理员),当然,读者可能会觉得如果有人恶意注册了很多id的话是否会在数据库中造成很多垃圾信息呢?这种考虑是必要的,所以我们需要给出一个策略,提供一个管理界面,来剔除掉这些垃圾,一种简单的方法就是对于超过了给定时期还没有成为正式会员的id一律删除,前提就是必须保证组管理员要在给定时期之内审批这些id,否则会造成误删。读者可以试试上面的代码能否工作,如果没有发送成功,请考虑重新配置邮件服务器的smtp服务。好了,按照上述形式发送的邮件将只是简单的文本形式,如果希望发送一个HTML形式的邮件,就需要知道如何发送MIME形式的邮件了。
2. MIME邮件发送
MIME(Multi- purpose Internet Mail Extensions,多用途Internet邮件扩展) 协议扩展了基于文本的Internet邮件系统,以便可以在消息体中包含二进制附件。MIME信息由正常的Internet文本邮件组成,在文本邮件中包含了一些信息头和格式化过的信息体(用ASCII 码子集表示的附件),这些MIME信息头给出了在邮件中表示附件的特定方法。
刚才通过mail函数发送的邮件接受之后的MIME信息如下(其中的localhost
(localhost[127.0.0.1])表示采用本机上的postfix提供的smtp服务,userid 48表示apache):
[code]
Received: from localhost (localhost [127.0.0.1])
by mx01.263.net (Postfix) with SMTP id E7C8B1DC38A78
for <ywg_263@263.net>; Sat, 8 Dec 2001 20:08:45 +0800 (CST)
Received: by birdy.dhs.org (Postfix, from userid 48)
id 706F3C4923A; Sun, 9 Dec 2001 03:52:26 +0800 (CST)
T ywg_263@263.net
Subject: message from php
Message-Id: <20011208195226.706F3C4923A@birdy.dhs.org>
Date: Sun, 9 Dec 2001 03:52:26 +0800 (CST)
From: apache@birdy.dhs.org (Apache User)
hello, xiaoyz!
可以看出其中的Received、To、Subject、Message-ID、Date、From部分都是信息头(To、Subject信息头分别对应着mail函数中的$to、$subject),而“hello, xiaoyz!”是信息体,如果没有指定Content-Type信息头,则默认为“Content-Type: text/plain;Charset='us-ascii'”。既然如此,我们当然可以用这种方法来发送HTML形式的邮件了(注意:HTML也是文本格式的)!示例如下:
复制PHP内容到剪贴板
PHP代码:
<?php
$to = "ywg_sn@sina.com";
$subject = "html message from php";
$message = "<html><title>html message</title><body bgcolor=#cccccc>
<h1><font color=red>hello, xiaoyz!</font></h1></body></html>";
$additional = "From: [email=ywg_263@263.netnReply]ywg_263@263.netnReply[/email]-[email=Tywg_263@263.netn]Tywg_263@263.netn[/email]
X-Mailer:PHPnX-Priority:2nContent-Type: text/html; charset="GB2312"n
Content-Transfer-Encoding: 7bitnMIME-Version: 1.0";
mail( $to, $subject, $message, $additional );
?>
其中有几个新的信息头:From表示邮件来源地址,Reply-To表示邮件回复地址,X-Mailer表示邮件发送程序,X-Priority表示邮件优先级,Content-Transfer-Encoding表示编码方式。用Outlook Express接受此邮件,我们发现它确实是一封HTML邮件,查看各信息头(在Outlook Express中选择该邮件,点右键,查看“属性”中的“详细信息”),正如上面所述。至此,我们已经可以发送各种文本格式的MIME邮件了,那么又该如何发送各种二进制格式的MIME邮件呢?比如图片;同时又该如何发送具有混合格式的MIME邮件呢?比如HTML和图片,请看第三部分和第四部分。
3. 二进制格式邮件发送
一个JPG图片的MIME信息格式大致如下:
T ywg_sn@sina.com
Subject: jpg picture from xiaoyz
Content-Type: image/jpg; name='picture.jpg'
Content-Transfer-Encoding: base64
Content-Descrīption: xiaoyz's picture
From: 'xiaoyz' <ywg_263@263.net>
...这里是JPG图片的base64编码...
其中,name表示附件的名称,Content-Descrīption表示附件的描述,一般显示为附件的标题。显然,我们必须把从表单提交的JPG图片文件进行base64编码,这个具体该如何实现呢?假定文件上传标签的名称为attach,即,则代码片断如下:
复制PHP内容到剪贴板
PHP代码:
<?php
$fp = fopen( $attach, "r" );
$content = fread($fp, filesize($attach)); //读取文件内容
$content = chunk_split( base64_encode($content) ); //进行base64编码,并在每76个字符后面加上rn
?>
然后只要把HTML格式邮件发送示例中的$to, $subject, $additional信息头部分换成这里对应的信息头,并将其中的$message换成这里的$content,就可以发送JPG图片的附件了,事实上任何类型的文件皆可通过这种方式进行发送。现在我们来看混合格式即多部分信息邮件。
4. 混合格式邮件发送
一个带HTML格式附件的邮件的MIME信息大致如下:
Return-Path: <ywg_263@263.net>
Delivered-T ywg_sn@sina.com
Received: (qmail 20639 invoked from network); 9 Dec 2001 08:04:25 -0000
Received: from unknown (HELO smtp.263.net) (202.96.44.19)
by 202.106.187.149 with SMTP; 9 Dec 2001 08:04:25 -0000
Received: from localhost (localhost [127.0.0.1])
by smtp.263.net (Postfix) with SMTP id B431D1DEBCAAA
for <ywg_sn@sina.com>; Sun, 9 Dec 2001 16:08:44 +0800 (CST)
Message-ID: <004f01c18089$938e83b0$a32869a2@xiaoyz>
From: <ywg_263@263.net>
T <ywg_sn@sina.com>
Subject: multipart MIME
Date: Sun, 9 Dec 2001 16:14:08 +0800
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="----=_NextPart_000_0049_01C180CC.87D17760"
X-Priority: 3
This is a multi-part message in MIME format.
----=_NextPart_000_0049_01C180CC.87D17760
Content-Type: text/plain; charset="gb2312"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
hello, xiaoyz!
------=_NextPart_000_0049_01C180CC.87D17760
Content-Type: text/html; name="index.html"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="index.html"
<html>
<title>html attachment</title>
<body>
hello, xiaoyz!
</body>
</html>
------=_NextPart_000_0049_01C180CC.87D17760--
注意其中的Content-Type信息头,mulitipart/mixed表示这封邮件由多部分组成,boundary指定各部分之间的边界符为 “----=_NextPart_000_0049_01C180CC.87D17760”,我们看到确实存在着三个边界符将邮件分成了三部分:信息头部分,纯文本部分,HTML部分。Content-Disposition告诉邮件程序应该如何显示附件。如果Content-Disposition被设置为attachment,那么邮件程序就不会显示HTML文件的内容,而是显示一个链接;如果设置为inline,则HTML文件的内容直接显示。一般情况下,如果附件是文本格式的(HTML也是文本格式的),Content-Disposition会被设为inline,否则Content- disposition设为attachment。好了,我想现在读者可能已经对邮件的各组成部分及其含义了然于胸了,现在就差如何用PHP来实现混合格式邮件的发送,当然,你可以用PHP来生成全部的信息,现在讨论表单发送的情形,代码如下:
复制PHP内容到剪贴板
PHP代码:
<form name="frm_mail" action="<? print $PHP_SELF; ?>" enctype="multipart/form-data" method="post">
from: <input type="text" name="from"><br>
t <input type="text" name="to"><br>
subject: <input type="text" name="subject"><br>
attach: <input type="file" name="attach"><br>
message: <textarea name="message"></textarea>
<input type="submit" name="send" value="send">
</from>
<?php
if( isset($send) )
{
//定义边界线
$boundary = uniqid( "" );
//生成邮件头
$header = "From: $fromnContent-type: multipart/mixed;boundary="$boundary"nX-Mailer:PHPnX-Priority:3";
//获取上传文件的MIME类型
if( $attach_type )
$mimetype = $attach_type;
else
$mimetype = "application/unknown";
//获取上传文件的名字
$filename = $attach_name;
$filename = $attach_name;
//对上传文件进行编码和切分
$fp = fopen($attach, "r");
$content = fread($fp, filesize($content));
$content = chunk_split( base64_encode($content) );
//生成邮件主体
$body ="
--$boundary
Content-type: text/plain; charset=iso-8859-1
Content-transfer-encoding: 8bit
$message
--$boundary
Content-Type: $mimeType; name=$filename
Content-Disposition: attachment; filename=$filename
Content-Transfer-Encoding: base64
$content
--$boundary--";
mail( $to, $subject, $body, $header );
}
?>
首先通过uniqueid函数得到惟一的边界符,然后得到上传文件的MIME类型,再对上传文件进行base64编码和切分,最后进行组合,得到 mail函数中的$body和$header,并调用mail函数。到现在为止,我丝毫不怀疑您可以作出一个非常漂亮有声有色的注册验证程序了。最后,笔者将来谈谈身份验证中对邮件方式的一种替代方案,其实也很简单,就是使用数据库来保存用户的各种申请通知。具体地说,每当有一个用户填写完注册表单并发送之后,除了在数据库中记录用户的注册信息,同时要在数据库中产生一个申请通知,比如:有一个用户abc申请加入group1组,而通过数据库查询得知 group1组有两个组管理员admin1和admin2,就需要在数据库中加入两条记录,fromwho字段为abc,towho字段分别admin1 和admin2,readed字段为0,表示组管理员还没有查看这个通知。当组管理员登录之后,会对申请通知进行检索,发现存在没有查看的通知,会弹出一个窗口界面供管理员进行审批。管理员通过该申请,则将abc的权限更新为正式会员;拒绝该申请,则将abc的申请通知和用户记录删除。好了,就此打住,请继续关注PHP高级特性讨论系列。
1. 简单邮件发送
PHP函数库中有一个mail函数,可以用来进行简单的邮件发送,函数原型为:
boolean mail(string $to, string $subject, string $message, string [$additional]);
$to指定邮件寄送地址,$subject指定邮件标题,$message指定邮件内容,$additional指定邮件的附加头部,例如:
复制PHP内容到剪贴板
PHP代码:
<?php
mail( "ywg_263@263.net", "message from php", "hello, xiaoyz! " );
?>
就可以向 ywg_263@263.net发送一个标题为“message from php” 内容为“hello, xiaoyz!”的邮件,其中的邮件接受人$to可以是多个邮件地址,也就是说可以同时给多个人发送同一份邮件,邮件地址之间用逗号分隔,示例如下:
复制PHP内容到剪贴板
PHP代码:
<?php
$emails = Array( "xiaoyz@birdy.dhs.org", "xiaoyz@hotmail.com" );
mail( implode(",", $emails), "message from php", "hello, xiaoyz!" );
?>
笔者做过的论坛程序中的注册部分就曾经使用过这种方法,不过最后还是采用了一种变通的形式,下文将会具体讲到。其中主要的需求是:当一个用户注册之后,必须得到组管理员的身份确认才能成为论坛的正式会员,我所采用的方法是:用户注册完成提交表单时,先把用户各种注册信息写入数据库,同时把用户的必要信息通过邮件的方式发送给用户所注册组的所有组管理员(如果没有组管理员的话,会给站管理员发送邮件,并告之该组没有组管理员),当然,读者可能会觉得如果有人恶意注册了很多id的话是否会在数据库中造成很多垃圾信息呢?这种考虑是必要的,所以我们需要给出一个策略,提供一个管理界面,来剔除掉这些垃圾,一种简单的方法就是对于超过了给定时期还没有成为正式会员的id一律删除,前提就是必须保证组管理员要在给定时期之内审批这些id,否则会造成误删。读者可以试试上面的代码能否工作,如果没有发送成功,请考虑重新配置邮件服务器的smtp服务。好了,按照上述形式发送的邮件将只是简单的文本形式,如果希望发送一个HTML形式的邮件,就需要知道如何发送MIME形式的邮件了。
2. MIME邮件发送
MIME(Multi- purpose Internet Mail Extensions,多用途Internet邮件扩展) 协议扩展了基于文本的Internet邮件系统,以便可以在消息体中包含二进制附件。MIME信息由正常的Internet文本邮件组成,在文本邮件中包含了一些信息头和格式化过的信息体(用ASCII 码子集表示的附件),这些MIME信息头给出了在邮件中表示附件的特定方法。
刚才通过mail函数发送的邮件接受之后的MIME信息如下(其中的localhost
(localhost[127.0.0.1])表示采用本机上的postfix提供的smtp服务,userid 48表示apache):
[code]
Received: from localhost (localhost [127.0.0.1])
by mx01.263.net (Postfix) with SMTP id E7C8B1DC38A78
for <ywg_263@263.net>; Sat, 8 Dec 2001 20:08:45 +0800 (CST)
Received: by birdy.dhs.org (Postfix, from userid 48)
id 706F3C4923A; Sun, 9 Dec 2001 03:52:26 +0800 (CST)
T ywg_263@263.net
Subject: message from php
Message-Id: <20011208195226.706F3C4923A@birdy.dhs.org>
Date: Sun, 9 Dec 2001 03:52:26 +0800 (CST)
From: apache@birdy.dhs.org (Apache User)
hello, xiaoyz!
可以看出其中的Received、To、Subject、Message-ID、Date、From部分都是信息头(To、Subject信息头分别对应着mail函数中的$to、$subject),而“hello, xiaoyz!”是信息体,如果没有指定Content-Type信息头,则默认为“Content-Type: text/plain;Charset='us-ascii'”。既然如此,我们当然可以用这种方法来发送HTML形式的邮件了(注意:HTML也是文本格式的)!示例如下:
复制PHP内容到剪贴板
PHP代码:
<?php
$to = "ywg_sn@sina.com";
$subject = "html message from php";
$message = "<html><title>html message</title><body bgcolor=#cccccc>
<h1><font color=red>hello, xiaoyz!</font></h1></body></html>";
$additional = "From: [email=ywg_263@263.netnReply]ywg_263@263.netnReply[/email]-[email=Tywg_263@263.netn]Tywg_263@263.netn[/email]
X-Mailer:PHPnX-Priority:2nContent-Type: text/html; charset="GB2312"n
Content-Transfer-Encoding: 7bitnMIME-Version: 1.0";
mail( $to, $subject, $message, $additional );
?>
其中有几个新的信息头:From表示邮件来源地址,Reply-To表示邮件回复地址,X-Mailer表示邮件发送程序,X-Priority表示邮件优先级,Content-Transfer-Encoding表示编码方式。用Outlook Express接受此邮件,我们发现它确实是一封HTML邮件,查看各信息头(在Outlook Express中选择该邮件,点右键,查看“属性”中的“详细信息”),正如上面所述。至此,我们已经可以发送各种文本格式的MIME邮件了,那么又该如何发送各种二进制格式的MIME邮件呢?比如图片;同时又该如何发送具有混合格式的MIME邮件呢?比如HTML和图片,请看第三部分和第四部分。
3. 二进制格式邮件发送
一个JPG图片的MIME信息格式大致如下:
T ywg_sn@sina.com
Subject: jpg picture from xiaoyz
Content-Type: image/jpg; name='picture.jpg'
Content-Transfer-Encoding: base64
Content-Descrīption: xiaoyz's picture
From: 'xiaoyz' <ywg_263@263.net>
...这里是JPG图片的base64编码...
其中,name表示附件的名称,Content-Descrīption表示附件的描述,一般显示为附件的标题。显然,我们必须把从表单提交的JPG图片文件进行base64编码,这个具体该如何实现呢?假定文件上传标签的名称为attach,即,则代码片断如下:
复制PHP内容到剪贴板
PHP代码:
<?php
$fp = fopen( $attach, "r" );
$content = fread($fp, filesize($attach)); //读取文件内容
$content = chunk_split( base64_encode($content) ); //进行base64编码,并在每76个字符后面加上rn
?>
然后只要把HTML格式邮件发送示例中的$to, $subject, $additional信息头部分换成这里对应的信息头,并将其中的$message换成这里的$content,就可以发送JPG图片的附件了,事实上任何类型的文件皆可通过这种方式进行发送。现在我们来看混合格式即多部分信息邮件。
4. 混合格式邮件发送
一个带HTML格式附件的邮件的MIME信息大致如下:
Return-Path: <ywg_263@263.net>
Delivered-T ywg_sn@sina.com
Received: (qmail 20639 invoked from network); 9 Dec 2001 08:04:25 -0000
Received: from unknown (HELO smtp.263.net) (202.96.44.19)
by 202.106.187.149 with SMTP; 9 Dec 2001 08:04:25 -0000
Received: from localhost (localhost [127.0.0.1])
by smtp.263.net (Postfix) with SMTP id B431D1DEBCAAA
for <ywg_sn@sina.com>; Sun, 9 Dec 2001 16:08:44 +0800 (CST)
Message-ID: <004f01c18089$938e83b0$a32869a2@xiaoyz>
From: <ywg_263@263.net>
T <ywg_sn@sina.com>
Subject: multipart MIME
Date: Sun, 9 Dec 2001 16:14:08 +0800
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="----=_NextPart_000_0049_01C180CC.87D17760"
X-Priority: 3
This is a multi-part message in MIME format.
----=_NextPart_000_0049_01C180CC.87D17760
Content-Type: text/plain; charset="gb2312"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
hello, xiaoyz!
------=_NextPart_000_0049_01C180CC.87D17760
Content-Type: text/html; name="index.html"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="index.html"
<html>
<title>html attachment</title>
<body>
hello, xiaoyz!
</body>
</html>
------=_NextPart_000_0049_01C180CC.87D17760--
注意其中的Content-Type信息头,mulitipart/mixed表示这封邮件由多部分组成,boundary指定各部分之间的边界符为 “----=_NextPart_000_0049_01C180CC.87D17760”,我们看到确实存在着三个边界符将邮件分成了三部分:信息头部分,纯文本部分,HTML部分。Content-Disposition告诉邮件程序应该如何显示附件。如果Content-Disposition被设置为attachment,那么邮件程序就不会显示HTML文件的内容,而是显示一个链接;如果设置为inline,则HTML文件的内容直接显示。一般情况下,如果附件是文本格式的(HTML也是文本格式的),Content-Disposition会被设为inline,否则Content- disposition设为attachment。好了,我想现在读者可能已经对邮件的各组成部分及其含义了然于胸了,现在就差如何用PHP来实现混合格式邮件的发送,当然,你可以用PHP来生成全部的信息,现在讨论表单发送的情形,代码如下:
复制PHP内容到剪贴板
PHP代码:
<form name="frm_mail" action="<? print $PHP_SELF; ?>" enctype="multipart/form-data" method="post">
from: <input type="text" name="from"><br>
t <input type="text" name="to"><br>
subject: <input type="text" name="subject"><br>
attach: <input type="file" name="attach"><br>
message: <textarea name="message"></textarea>
<input type="submit" name="send" value="send">
</from>
<?php
if( isset($send) )
{
//定义边界线
$boundary = uniqid( "" );
//生成邮件头
$header = "From: $fromnContent-type: multipart/mixed;boundary="$boundary"nX-Mailer:PHPnX-Priority:3";
//获取上传文件的MIME类型
if( $attach_type )
$mimetype = $attach_type;
else
$mimetype = "application/unknown";
//获取上传文件的名字
$filename = $attach_name;
$filename = $attach_name;
//对上传文件进行编码和切分
$fp = fopen($attach, "r");
$content = fread($fp, filesize($content));
$content = chunk_split( base64_encode($content) );
//生成邮件主体
$body ="
--$boundary
Content-type: text/plain; charset=iso-8859-1
Content-transfer-encoding: 8bit
$message
--$boundary
Content-Type: $mimeType; name=$filename
Content-Disposition: attachment; filename=$filename
Content-Transfer-Encoding: base64
$content
--$boundary--";
mail( $to, $subject, $body, $header );
}
?>
首先通过uniqueid函数得到惟一的边界符,然后得到上传文件的MIME类型,再对上传文件进行base64编码和切分,最后进行组合,得到 mail函数中的$body和$header,并调用mail函数。到现在为止,我丝毫不怀疑您可以作出一个非常漂亮有声有色的注册验证程序了。最后,笔者将来谈谈身份验证中对邮件方式的一种替代方案,其实也很简单,就是使用数据库来保存用户的各种申请通知。具体地说,每当有一个用户填写完注册表单并发送之后,除了在数据库中记录用户的注册信息,同时要在数据库中产生一个申请通知,比如:有一个用户abc申请加入group1组,而通过数据库查询得知 group1组有两个组管理员admin1和admin2,就需要在数据库中加入两条记录,fromwho字段为abc,towho字段分别admin1 和admin2,readed字段为0,表示组管理员还没有查看这个通知。当组管理员登录之后,会对申请通知进行检索,发现存在没有查看的通知,会弹出一个窗口界面供管理员进行审批。管理员通过该申请,则将abc的权限更新为正式会员;拒绝该申请,则将abc的申请通知和用户记录删除。好了,就此打住,请继续关注PHP高级特性讨论系列。
如果使用 iconv() 函数转换编码就相比比较简单了,不过很多虚拟主机里并不支持这个组件,我在网上找半天,才找到一个gb2312转utf-8的方法,但不能逆向转换。
这个函数如下:
/*******************************
//GB转UTF-8编码
*******************************/
function gb2utf8($gbstr) {
global $CODETABLE;
if(trim($gbstr)=="") return $gbstr;
if(empty($CODETABLE)){
$filename = dirname(__FILE__)."/gb2312-utf8.table";
$fp = fopen($filename,"r");
while ($l = fgets($fp,15))
{ $CODETABLE[hexdec(substr($l, 0, 6))] = substr($l, 7, 6); }
fclose($fp);
}
$ret = "";
$utf8 = "";
while ($gbstr) {
if (ord(substr($gbstr, 0, 1)) > 127) {
$thisW = substr($gbstr, 0, 2);
$gbstr = substr($gbstr, 2, strlen($gbstr));
$utf8 = "";
@$utf8 = u2utf8(hexdec($CODETABLE[hexdec(bin2hex($thisW)) - 0x8080]));
if($utf8!=""){
for ($i = 0;$i < strlen($utf8);$i += 3)
$ret .= chr(substr($utf8, $i, 3));
}
}
else
{
$ret .= substr($gbstr, 0, 1);
$gbstr = substr($gbstr, 1, strlen($gbstr));
}
}
return $ret;
}
//Unicode转utf8
function u2utf8($c) {
for ($i = 0;$i < count($c);$i++)
$str = "";
if ($c < 0x80) {
$str .= $c;
} else if ($c < 0x800) {
$str .= (0xC0 | $c >> 6);
$str .= (0x80 | $c & 0x3F);
} else if ($c < 0x10000) {
$str .= (0xE0 | $c >> 12);
$str .= (0x80 | $c >> 6 & 0x3F);
$str .= (0x80 | $c & 0x3F);
} else if ($c < 0x200000) {
$str .= (0xF0 | $c >> 18);
$str .= (0x80 | $c >> 12 & 0x3F);
$str .= (0x80 | $c >> 6 & 0x3F);
$str .= (0x80 | $c & 0x3F);
}
return $str;
}
因为gb2312都是双字节的,因此转换为utf-8就相对比较简单,但反之有很麻烦了,我尝试了一下:
这样
function utf82gb($utfstr)
{
global $UC2GBTABLE;
$okstr = "";
if(trim($utfstr)=="") return $utfstr;
if(empty($UC2GBTABLE)){
$filename = dirname(__FILE__)."/gb2312-utf8.table";
$fp = fopen($filename,"r");
while($l = fgets($fp,15))
{ $UC2GBTABLE[hexdec(substr($l, 7, 6))] = hexdec(substr($l, 0, 6));}
fclose($fp);
}
$ulen = strlen($utfstr);
for($i=0;$i<$ulen;$i++)
{
if(ord($utfstr[$i])<0x81) $okstr .= $utfstr[$i];
else
{
if($ulen>$i+2)
{
$utfc = substr($utfstr,$i,3);
$c = "";
@$c = dechex($UC2GBTABLE[utf82u_3($utfc)]+0x8080);
if($c!=""){
$okstr .= chr(hexdec($c[0].$c[1])).chr(hexdec($c[2].$c[3]));
}
}
else
{ $okstr .= $utfstr[$i]; }
}
}
$okstr = trim($okstr);
return $okstr;
}
function utf82u_3($c)
{
$n = (ord($c[0]) & 0x1f) << 12;
$n += (ord($c[1]) & 0x3f) << 6;
$n += ord($c[2]) & 0x3f;
return $n;
}
按这种方法,大部份字符也算是能转换成功的了,不过总是有点不妥之处,我把程序改成这样子:
function utf82gb($utfstr)
{
global $UC2GBTABLE;
$okstr = "";
if(trim($utfstr)=="") return $utfstr;
if(empty($UC2GBTABLE)){
$filename = dirname(__FILE__)."/gb2312-utf8.table";
$fp = fopen($filename,"r");
while($l = fgets($fp,15))
{ $UC2GBTABLE[hexdec(substr($l, 7, 6))] = hexdec(substr($l, 0, 6));}
fclose($fp);
}
$okstr = "";
$utfstr = urlencode($utfstr);
$ulen = strlen($utfstr);
for($i=0;$i<$ulen;$i++)
{
if($utfstr[$i]=="%")
{
if($ulen>$i+2){
$hexnext = hexdec("0x".substr($utfstr,$i+1,2));
if($hexnext<127){
$okstr .= chr($hexnext);
$i = $i+2;
}
else{
if($ulen>=$i+9){
$hexnext = substr($utfstr,$i+1,8);
$c = "";
@$c = dechex($UC2GBTABLE[url_utf2u($hexnext)]+0x8080);
if($c!=""){
$okstr .= chr(hexdec($c[0].$c[1])).chr(hexdec($c[2].$c[3]));
}
$i = $i+8;
}
}
}
else
{ $okstr .= $utfstr[$i]; }
}
else if($utfstr[$i]=="+")
$okstr .= " ";
else
$okstr .= $utfstr[$i];
}
$okstr = trim($okstr);
return $okstr;
}
//三字节的URL编码转成的utf8字符转为unicode编码
function url_utf2u($c)
{
$utfc = "";
$cs = split("%",$c);
for($i=0;$i<count($cs);$i++){
$utfc .= chr(hexdec("0x".$cs[$i]));
}
$n = (ord($utfc[0]) & 0x1f) << 12;
$n += (ord($utfc[1]) & 0x3f) << 6;
$n += ord($utfc[2]) & 0x3f;
return $n;
}
这个函数如下:
/*******************************
//GB转UTF-8编码
*******************************/
function gb2utf8($gbstr) {
global $CODETABLE;
if(trim($gbstr)=="") return $gbstr;
if(empty($CODETABLE)){
$filename = dirname(__FILE__)."/gb2312-utf8.table";
$fp = fopen($filename,"r");
while ($l = fgets($fp,15))
{ $CODETABLE[hexdec(substr($l, 0, 6))] = substr($l, 7, 6); }
fclose($fp);
}
$ret = "";
$utf8 = "";
while ($gbstr) {
if (ord(substr($gbstr, 0, 1)) > 127) {
$thisW = substr($gbstr, 0, 2);
$gbstr = substr($gbstr, 2, strlen($gbstr));
$utf8 = "";
@$utf8 = u2utf8(hexdec($CODETABLE[hexdec(bin2hex($thisW)) - 0x8080]));
if($utf8!=""){
for ($i = 0;$i < strlen($utf8);$i += 3)
$ret .= chr(substr($utf8, $i, 3));
}
}
else
{
$ret .= substr($gbstr, 0, 1);
$gbstr = substr($gbstr, 1, strlen($gbstr));
}
}
return $ret;
}
//Unicode转utf8
function u2utf8($c) {
for ($i = 0;$i < count($c);$i++)
$str = "";
if ($c < 0x80) {
$str .= $c;
} else if ($c < 0x800) {
$str .= (0xC0 | $c >> 6);
$str .= (0x80 | $c & 0x3F);
} else if ($c < 0x10000) {
$str .= (0xE0 | $c >> 12);
$str .= (0x80 | $c >> 6 & 0x3F);
$str .= (0x80 | $c & 0x3F);
} else if ($c < 0x200000) {
$str .= (0xF0 | $c >> 18);
$str .= (0x80 | $c >> 12 & 0x3F);
$str .= (0x80 | $c >> 6 & 0x3F);
$str .= (0x80 | $c & 0x3F);
}
return $str;
}
因为gb2312都是双字节的,因此转换为utf-8就相对比较简单,但反之有很麻烦了,我尝试了一下:
这样
function utf82gb($utfstr)
{
global $UC2GBTABLE;
$okstr = "";
if(trim($utfstr)=="") return $utfstr;
if(empty($UC2GBTABLE)){
$filename = dirname(__FILE__)."/gb2312-utf8.table";
$fp = fopen($filename,"r");
while($l = fgets($fp,15))
{ $UC2GBTABLE[hexdec(substr($l, 7, 6))] = hexdec(substr($l, 0, 6));}
fclose($fp);
}
$ulen = strlen($utfstr);
for($i=0;$i<$ulen;$i++)
{
if(ord($utfstr[$i])<0x81) $okstr .= $utfstr[$i];
else
{
if($ulen>$i+2)
{
$utfc = substr($utfstr,$i,3);
$c = "";
@$c = dechex($UC2GBTABLE[utf82u_3($utfc)]+0x8080);
if($c!=""){
$okstr .= chr(hexdec($c[0].$c[1])).chr(hexdec($c[2].$c[3]));
}
}
else
{ $okstr .= $utfstr[$i]; }
}
}
$okstr = trim($okstr);
return $okstr;
}
function utf82u_3($c)
{
$n = (ord($c[0]) & 0x1f) << 12;
$n += (ord($c[1]) & 0x3f) << 6;
$n += ord($c[2]) & 0x3f;
return $n;
}
按这种方法,大部份字符也算是能转换成功的了,不过总是有点不妥之处,我把程序改成这样子:
function utf82gb($utfstr)
{
global $UC2GBTABLE;
$okstr = "";
if(trim($utfstr)=="") return $utfstr;
if(empty($UC2GBTABLE)){
$filename = dirname(__FILE__)."/gb2312-utf8.table";
$fp = fopen($filename,"r");
while($l = fgets($fp,15))
{ $UC2GBTABLE[hexdec(substr($l, 7, 6))] = hexdec(substr($l, 0, 6));}
fclose($fp);
}
$okstr = "";
$utfstr = urlencode($utfstr);
$ulen = strlen($utfstr);
for($i=0;$i<$ulen;$i++)
{
if($utfstr[$i]=="%")
{
if($ulen>$i+2){
$hexnext = hexdec("0x".substr($utfstr,$i+1,2));
if($hexnext<127){
$okstr .= chr($hexnext);
$i = $i+2;
}
else{
if($ulen>=$i+9){
$hexnext = substr($utfstr,$i+1,8);
$c = "";
@$c = dechex($UC2GBTABLE[url_utf2u($hexnext)]+0x8080);
if($c!=""){
$okstr .= chr(hexdec($c[0].$c[1])).chr(hexdec($c[2].$c[3]));
}
$i = $i+8;
}
}
}
else
{ $okstr .= $utfstr[$i]; }
}
else if($utfstr[$i]=="+")
$okstr .= " ";
else
$okstr .= $utfstr[$i];
}
$okstr = trim($okstr);
return $okstr;
}
//三字节的URL编码转成的utf8字符转为unicode编码
function url_utf2u($c)
{
$utfc = "";
$cs = split("%",$c);
for($i=0;$i<count($cs);$i++){
$utfc .= chr(hexdec("0x".$cs[$i]));
}
$n = (ord($utfc[0]) & 0x1f) << 12;
$n += (ord($utfc[1]) & 0x3f) << 6;
$n += ord($utfc[2]) & 0x3f;
return $n;
}
Yar – 并行的RPC框架(Concurrent RPC framework)
有一个扩展实现:
http://www.laruence.com/2012/09/15/2779.html
[ Web Service介绍 ]
Web Service就是为了异构系统的通信而产生的,它基本的思想就是使用基于XML的HTTP 的远程调用提供一种标准的机制,而省去建立一种新协议的需求。目前进行Web Service通信有两种协议标准,一种是XML-RPC,另外一种是SOAP。XML-RPC比较简单,出现时间比较早,SOAP比较复杂,主要是一些需要稳定、健壮、安全并且复杂交互的时候使用。
PHP中集成了XML-RPC和SOAP两种协议的访问,都是集中在xmlrpc扩展当中。另外,在PHP的PEAR中,不管是PHP 4还是PHP 5,都已经默认集成了XML-RPC扩展,而且该扩展跟xmlrpc扩展无关,能够独立实现XML-RPC的协议交互,如果没有xmlrpc扩展,建议使用PEAR::XML-RPC扩展。
我们这里主要是以XML-RPC来简单描述Web Service的交互过程,部分内容来自PHP手册,更详细内容,建议参考手册。
[ 安装xmlrpc扩展 ]
如果你的系统中没有安装xmlrpc的php扩展,那么请正确安装。在Windows平台下,首先把PHP安装目录下的扩展php_xmlrpc.dll放到C:Windows或者C:Winnt目录下,
(PHP4的扩展在C:phpextensions目录中,PHP5的扩展在C:phpext目录中),同时在
在apache 的安装目录下的php.ini中把extension=php_xmlrpc.dll前面的分号";"去掉,然后重
启Web服务器后查看phpinfo()有没有XML-RPC项目就能够确定是否已经正确安装xmlrpc扩展。
rpc_server.php
<?php
/**
* 函数:提供给RPC客户端调用的函数
* 参数:
* $method 客户端需要调用的函数
* $params 客户端需要调用的函数的参数数组
* 返回:返回指定调用结果
*/
function rpc_server_func($method, $params) {
$parameter = $params[0];
if ($parameter == "get")
{
$return = "dikers".$params[0];
}
else
{
$return = "Not specify method or params";
}
return $return;
}
//产生一个XML-RPC的服务器端
$xmlrpc_server = XMLrpc_server_create();
//注册一个服务器端调用的方法rpc_server,实际指向的是rpc_server_func函数
xmlrpc_server_register_method($xmlrpc_server, "rpc_server", "rpc_server_func");
//接受客户端POST过来的XML数据
$request = $HTTP_RAW_POST_DATA;
//执行调用客户端的XML请求后获取执行结果
$xmlrpc_response = xmlrpc_server_call_method($xmlrpc_server, $request, null);
//把函数处理后的结果XML进行输出
header("Content-Type: text/xml");
echo $xmlrpc_response;
//销毁XML-RPC服务器端资源
xmlrpc_server_destroy($xmlrpc_server);
?>
rpc_client.php
<?PHP
/**
* 函数:提供给客户端进行连接XML-RPC服务器端的函数
* 参数:
* $host 需要连接的主机
* $port 连接主机的端口
* $rpc_server XML-RPC服务器端文件
* $request 封装的XML请求信息
* 返回:连接成功成功返回由服务器端返回的XML信息,失败返回false
*/
function rpc_client_call($host, $port, $rpc_server, $request) {
//打开指定的服务器端
$fp = fsockopen($host, $port);
//构造需要进行通信的XML-RPC服务器端的查询POST请求信息
$query = "POST $rpc_server HTTP/1.0nUser_Agent: XML-RPC ClientnHost: ".$host."nContent-Type: text/XMLnContent-Length: ".strlen($request)."nn".$request."n";
//把构造好的HTTP协议发送给服务器,失败返回false
if (!fputs($fp, $query, strlen($query)))
{
$errstr = "Write error";
return false;
}
//获取从服务器端返回的所有信息,包括HTTP头和XML信息
$contents = "";
while (!feof($fp))
{
$contents .= fgets($fp);
}
//关闭连接资源后返回获取的内容
fclose($fp);
//print_r($contents);
return $contents;
}
//构造连接RPC服务器端的信息
$host = "127.0.0.1";
$port = 80;
$rpc_server = "/sample/rpc_server.php";
//http://127.0.0.1/sample/rpc_server.php
//把需要发送的XML请求进行编码成XML,需要调用的方法是rpc_server,参数是get
$request = XMLrpc_encode_request("rpc_server", "get");
//调用rpc_client_call函数把所有请求发送给XML-RPC服务器端后获取信息
$response = rpc_client_call($host, $port, $rpc_server, $request);
//分析从服务器端返回的XML,去掉HTTP头信息,并且把XML转为PHP能识别的字符串
$split = '<?XML version="1.0" encoding="iso-8859-1"?>';
$XML = explode($split, $response);
$xml = $split.array_pop($XML);
//print_r($xml);
$response = xmlrpc_encode($xml);
//输出从RPC服务器端获取的信息
print_r($response);
?>
大致我们上面的例子就是提交一个叫做rpc_server的方法过去,参数是get,然后获取服务器端的返回,服务器端返回的XML数据是:
<?xml version="1.0" encoding="iso-8859-1"?>
<methodResponse>
<params>
<param>
<value>
<string>This data by get method</string>
</value>
</param>
</params>
</methodResponse>
那么我们再通过xmlrpc_decode函数把这个XML编码为PHP的字符串,我们就能够随意处理了,整个Web Service交互完成。
[ 结束语 ]
不管是XML-RPC也好,SOAP也罢,只要能够让我们稳定、安全的进行远程过程的调用,完成我们的项目,那么就算整个Web Service就是成功的。另外,如果可以的话,也可以尝试使用PEAR中的XML-RPC来实现上面类似的操作,说不定会更简单,更适合你使用。
简单的使用XML-RPC进行Web Service交互就完成了,部分代码参考PHP手册,想获取详细信息建议参考手册,如果文章有不正确,请指正。
有一个扩展实现:
http://www.laruence.com/2012/09/15/2779.html
[ Web Service介绍 ]
Web Service就是为了异构系统的通信而产生的,它基本的思想就是使用基于XML的HTTP 的远程调用提供一种标准的机制,而省去建立一种新协议的需求。目前进行Web Service通信有两种协议标准,一种是XML-RPC,另外一种是SOAP。XML-RPC比较简单,出现时间比较早,SOAP比较复杂,主要是一些需要稳定、健壮、安全并且复杂交互的时候使用。
PHP中集成了XML-RPC和SOAP两种协议的访问,都是集中在xmlrpc扩展当中。另外,在PHP的PEAR中,不管是PHP 4还是PHP 5,都已经默认集成了XML-RPC扩展,而且该扩展跟xmlrpc扩展无关,能够独立实现XML-RPC的协议交互,如果没有xmlrpc扩展,建议使用PEAR::XML-RPC扩展。
我们这里主要是以XML-RPC来简单描述Web Service的交互过程,部分内容来自PHP手册,更详细内容,建议参考手册。
[ 安装xmlrpc扩展 ]
如果你的系统中没有安装xmlrpc的php扩展,那么请正确安装。在Windows平台下,首先把PHP安装目录下的扩展php_xmlrpc.dll放到C:Windows或者C:Winnt目录下,
(PHP4的扩展在C:phpextensions目录中,PHP5的扩展在C:phpext目录中),同时在
在apache 的安装目录下的php.ini中把extension=php_xmlrpc.dll前面的分号";"去掉,然后重
启Web服务器后查看phpinfo()有没有XML-RPC项目就能够确定是否已经正确安装xmlrpc扩展。
rpc_server.php
<?php
/**
* 函数:提供给RPC客户端调用的函数
* 参数:
* $method 客户端需要调用的函数
* $params 客户端需要调用的函数的参数数组
* 返回:返回指定调用结果
*/
function rpc_server_func($method, $params) {
$parameter = $params[0];
if ($parameter == "get")
{
$return = "dikers".$params[0];
}
else
{
$return = "Not specify method or params";
}
return $return;
}
//产生一个XML-RPC的服务器端
$xmlrpc_server = XMLrpc_server_create();
//注册一个服务器端调用的方法rpc_server,实际指向的是rpc_server_func函数
xmlrpc_server_register_method($xmlrpc_server, "rpc_server", "rpc_server_func");
//接受客户端POST过来的XML数据
$request = $HTTP_RAW_POST_DATA;
//执行调用客户端的XML请求后获取执行结果
$xmlrpc_response = xmlrpc_server_call_method($xmlrpc_server, $request, null);
//把函数处理后的结果XML进行输出
header("Content-Type: text/xml");
echo $xmlrpc_response;
//销毁XML-RPC服务器端资源
xmlrpc_server_destroy($xmlrpc_server);
?>
rpc_client.php
<?PHP
/**
* 函数:提供给客户端进行连接XML-RPC服务器端的函数
* 参数:
* $host 需要连接的主机
* $port 连接主机的端口
* $rpc_server XML-RPC服务器端文件
* $request 封装的XML请求信息
* 返回:连接成功成功返回由服务器端返回的XML信息,失败返回false
*/
function rpc_client_call($host, $port, $rpc_server, $request) {
//打开指定的服务器端
$fp = fsockopen($host, $port);
//构造需要进行通信的XML-RPC服务器端的查询POST请求信息
$query = "POST $rpc_server HTTP/1.0nUser_Agent: XML-RPC ClientnHost: ".$host."nContent-Type: text/XMLnContent-Length: ".strlen($request)."nn".$request."n";
//把构造好的HTTP协议发送给服务器,失败返回false
if (!fputs($fp, $query, strlen($query)))
{
$errstr = "Write error";
return false;
}
//获取从服务器端返回的所有信息,包括HTTP头和XML信息
$contents = "";
while (!feof($fp))
{
$contents .= fgets($fp);
}
//关闭连接资源后返回获取的内容
fclose($fp);
//print_r($contents);
return $contents;
}
//构造连接RPC服务器端的信息
$host = "127.0.0.1";
$port = 80;
$rpc_server = "/sample/rpc_server.php";
//http://127.0.0.1/sample/rpc_server.php
//把需要发送的XML请求进行编码成XML,需要调用的方法是rpc_server,参数是get
$request = XMLrpc_encode_request("rpc_server", "get");
//调用rpc_client_call函数把所有请求发送给XML-RPC服务器端后获取信息
$response = rpc_client_call($host, $port, $rpc_server, $request);
//分析从服务器端返回的XML,去掉HTTP头信息,并且把XML转为PHP能识别的字符串
$split = '<?XML version="1.0" encoding="iso-8859-1"?>';
$XML = explode($split, $response);
$xml = $split.array_pop($XML);
//print_r($xml);
$response = xmlrpc_encode($xml);
//输出从RPC服务器端获取的信息
print_r($response);
?>
大致我们上面的例子就是提交一个叫做rpc_server的方法过去,参数是get,然后获取服务器端的返回,服务器端返回的XML数据是:
<?xml version="1.0" encoding="iso-8859-1"?>
<methodResponse>
<params>
<param>
<value>
<string>This data by get method</string>
</value>
</param>
</params>
</methodResponse>
那么我们再通过xmlrpc_decode函数把这个XML编码为PHP的字符串,我们就能够随意处理了,整个Web Service交互完成。
[ 结束语 ]
不管是XML-RPC也好,SOAP也罢,只要能够让我们稳定、安全的进行远程过程的调用,完成我们的项目,那么就算整个Web Service就是成功的。另外,如果可以的话,也可以尝试使用PEAR中的XML-RPC来实现上面类似的操作,说不定会更简单,更适合你使用。
简单的使用XML-RPC进行Web Service交互就完成了,部分代码参考PHP手册,想获取详细信息建议参考手册,如果文章有不正确,请指正。
dos2unix [文件名]
修改.vimrc文件,让其支持 gb2312就行
"设定文件编码类型,彻底解决中文编码问题
let &termencoding=&encoding
set fileencodings=utf-8,gbk,ucs-bom,cp936
略微查了一下.vimrc中添加内容的含意,这篇文章有相关解释。
http://blog.dawnh.net/comment.php?type=trackback&entry_id=59
内容如下:
vim中编辑不同编码的文件时需要注意的一些地方
此文讲解的是vim编辑多字节编码文档(中文)所要了解的一些基础知识,注意其没有涉及gvim,纯指字符终端下的vim。
vim编码方面的基础知识:
1,存在3个变量:
encoding----该选项使用于缓冲的文本(你正在编辑的文件),寄存器,Vim 脚本文件等等。你可以把 'encoding' 选项当作是对 Vim 内部运行机制的设定。
fileencoding----该选项是vim写入文件时采用的编码类型。
termencoding----该选项代表输出到客户终端(Term)采用的编码类型。
2,此3个变量的默认值:
encoding----与系统当前locale相同,所以编辑文件的时候要考虑当前locale,否则要设置的东西就比较多了。
fileencoding----vim打开文件时自动辨认其编码,fileencoding就为辨认的值。为空则保存文件时采用encoding的编码,如果没有修改encoding,那值就是系统当前locale了。
termencoding----默认空值,也就是输出到终端不进行编码转换。
由此可见,编辑不同编码文件需要注意的地方不仅仅是这3个变量,还有系统当前locale和、文件本身编码以及自动编码识别、客户运行vim的终端所使用的编码类型3个关键点,这3个关键点影响着3个变量的设定。
如果有人问:为什么我用vim打开中文文档的时候出现乱码?
答案是不确定的,原因上面已经讲了,不搞清楚这3个关键点和这3个变量的设定值,出现乱码是正常的,倒是不出现乱码那反倒是凑巧的。
再来看一下常见情况下这三个关键点的值以及在这种情况下这3个变量的值:
1,locale----目前大部分Linux系统已经将utf-8作为默认locale了,不过也有可能不是,例如有些系统使用中文locale zh_CN.GB18030。在locale为utf-8的情况下,启动vim后encoding将会设置为utf-8,这是兼容性最好的方式,因为内部处理使用utf-8的话,无论外部存储编码为何都可以进行无缺损转换。locale决定了vim内部处理数据的编码,也就是encoding。
2,文件的编码以及自动编码识别----这方面牵扯到各种编码的规则,就不一一细讲了。但需要明白的是,文件编码类型并不是保存在文件内的,也就是说没有任何描述性的字段来记录文档是何种编码类型的。因此我们在编辑文档的时候,要么必须知道这文档保存时是以什么编码保存的,要么通过另外的一些手段来断定编码类型,这另外的手段,就是通过某些编码的码表特征来断定,例如每个字符占用的字节数,每个字符的ascii值是否都大于某个字段来断定这个文件属于何种编码。这种方式vim也使用了,这就是vim的自动编码识别机制了。但这种机制由于编码各式各样,不可能每种编码都有显著的特征来辨别,所以是不可能 100%准确的。对于我们GB2312编码,由于其中文是使用了2个acsii值高于127的字符组成汉字字符的,因此不可能把gb2312编码的文件与 latin1编码区分开来,因此自动识别编码的机制对于gb2312是不成功的,它只会将文件辨识为latin1编码。此问题同样出现在gbk,big5 上等。因此我们在编辑此类文档时,需要手工设定encoding和fileencoding。如果文档编码为utf-8时,一般vim都能自动识别正确的编码。
3,客户运行vim的终端所使用的编码类型----同第二条一样,这也是一个比较难以断定的关键点。第二个关键点决定着从文件读取内容和写入内容到文件时使用的编码,而此关键点则决定vim输出内容到终端时使用的编码,如果此编码类型和终端认为它收到的数据的编码类型不同,则又会产生乱码问题。在 linux本地X环境下,一般终端都认为其接收的数据的编码类型和系统locale类型相符,因此不需关心此方面是否存在问题。但如果牵涉到远程终端,例如ssh登录服务器,则问题就有可能出现了。例如从1台locale为GB2310的系统(称作客户机)ssh到locale为utf-8的系统(称作服务器)并开启vim编辑文档,在不加任何改动的情况下,服务器返回的数据为utf-8的,但客户机认为服务器返回的数据是gb2312的,按照 gb2312来解释数据,则肯定就是乱码了,这时就需要设置termencoding为gb2312来解决这个问题。此问题更多出现在我们的 windows desktop机远程ssh登录服务器的情况下,这里牵扯到不同系统的编码转换问题。所以又与windows本身以及ssh客户端有很大相关性。在 windows下存在两种编码类型的软件,一种是本身就为unicode编码方式编写的软件,一种是ansi软件,也就是程序处理数据直接采用字节流,不关心编码。前一种程序可以在任何语言的windows上正确显示多国语言,而后一种则编写在何种语言的系统上则只能在何种语言的系统上显示正确的文字。对于这两种类型的程序,我们需要区别对待。以ssh客户端为例,我们使用的putty是unicode软件,而secure CRT则是ansi 软件。对于前者,我们要正确处理中文,只要保证vim输出到终端的编码为utf-8即可,就是termencoding=utf-8。但对于后者,一方面我们要确认我们的windows系统默认代码页为cp936(中文windows默认值),另一方面要确认vim设置的termencoding= cp936。
最后来看看处理中文文档最典型的几种情况和设置方式:
1,系统locale是utf-8(很多linux系统默认的locale形式),编辑的文档是GB2312或GBK形式的(Windows记事本默认保存形式,大部分编辑器也默认保存为这个形式,所以最常见),终端类型utf-8(也就是假定客户端是putty类的unicode软件)
则vim打开文档后,encoding=utf-8(locale决定的),fileencoding=latin1(自动编码判断机制不准导致的),termencoding=空(默认无需转换term编码),显示文件为乱码。
解决方案1:首先要修正fileencoding为cp936或者euc-cn(二者一样的,只不过叫法不同),注意修正的方法不是:set fileencoding=cp936,这只是将文件保存为cp936,正确的方法是重新以cp936的编码方式加载文件为:edit ++enc=cp936,可以简写为:e ++enc=cp936。
解决方案2:临时改变vim运行的locale环境,方法是以LANG=zh_CN vim abc.txt的方式来启动vim,则此时encoding=euc-cn(locale决定的),fileencoding=空(此locale下文件编码自动判别功能不启用,所以fileencoding为文件本身编码方式不变,也就是euc-cn),termencoding=空(默认值,为空则等于encoding)此时还是乱码的,因为我们的ssh终端认为接受的数据为utf-8,但vim发送数据为euc-cn,所以还是不对。此时再用命令: set termencoding=utf-8将终端数据输出为utf-8,则显示正常。
2,情况与1基本相同,只是使用的ssh软件为secure CRT类ansi类软件。
vim打开文档后,encoding=utf-8(locale决定的),fileencoding=latin1(自动编码判断机制不准导致的),termencoding=空(默认无需转换term编码),显示文件为乱码。
解决方案1:首先要保证运行secure CRT的windows机器的默认代码页为CP936,这一点中文windows已经是默认设置了。其他的与上面方案1相同,只是要增加一步,:set termencoding=cp936
解决方案2:与上面方案2类似,不过最后一步修改termencoding省略即可,在此情况下需要的修改最少,只要以locale为zh_CN 开启 vim,则encoding=euc-cn,fileencoding和termencoding都为空即为encoding的值,是最理想的一种情况。
可见理解这3个关键点和3个参数的意义,对于编码问题有很大助力,以后就可以随心所欲的处理文档了,同时不仅仅是应用于vim,在其他需要编码转换的环境里,都可以应用类似的思路来处理问题解决问题。
"设定文件编码类型,彻底解决中文编码问题
let &termencoding=&encoding
set fileencodings=utf-8,gbk,ucs-bom,cp936
略微查了一下.vimrc中添加内容的含意,这篇文章有相关解释。
http://blog.dawnh.net/comment.php?type=trackback&entry_id=59
内容如下:
vim中编辑不同编码的文件时需要注意的一些地方
此文讲解的是vim编辑多字节编码文档(中文)所要了解的一些基础知识,注意其没有涉及gvim,纯指字符终端下的vim。
vim编码方面的基础知识:
1,存在3个变量:
encoding----该选项使用于缓冲的文本(你正在编辑的文件),寄存器,Vim 脚本文件等等。你可以把 'encoding' 选项当作是对 Vim 内部运行机制的设定。
fileencoding----该选项是vim写入文件时采用的编码类型。
termencoding----该选项代表输出到客户终端(Term)采用的编码类型。
2,此3个变量的默认值:
encoding----与系统当前locale相同,所以编辑文件的时候要考虑当前locale,否则要设置的东西就比较多了。
fileencoding----vim打开文件时自动辨认其编码,fileencoding就为辨认的值。为空则保存文件时采用encoding的编码,如果没有修改encoding,那值就是系统当前locale了。
termencoding----默认空值,也就是输出到终端不进行编码转换。
由此可见,编辑不同编码文件需要注意的地方不仅仅是这3个变量,还有系统当前locale和、文件本身编码以及自动编码识别、客户运行vim的终端所使用的编码类型3个关键点,这3个关键点影响着3个变量的设定。
如果有人问:为什么我用vim打开中文文档的时候出现乱码?
答案是不确定的,原因上面已经讲了,不搞清楚这3个关键点和这3个变量的设定值,出现乱码是正常的,倒是不出现乱码那反倒是凑巧的。
再来看一下常见情况下这三个关键点的值以及在这种情况下这3个变量的值:
1,locale----目前大部分Linux系统已经将utf-8作为默认locale了,不过也有可能不是,例如有些系统使用中文locale zh_CN.GB18030。在locale为utf-8的情况下,启动vim后encoding将会设置为utf-8,这是兼容性最好的方式,因为内部处理使用utf-8的话,无论外部存储编码为何都可以进行无缺损转换。locale决定了vim内部处理数据的编码,也就是encoding。
2,文件的编码以及自动编码识别----这方面牵扯到各种编码的规则,就不一一细讲了。但需要明白的是,文件编码类型并不是保存在文件内的,也就是说没有任何描述性的字段来记录文档是何种编码类型的。因此我们在编辑文档的时候,要么必须知道这文档保存时是以什么编码保存的,要么通过另外的一些手段来断定编码类型,这另外的手段,就是通过某些编码的码表特征来断定,例如每个字符占用的字节数,每个字符的ascii值是否都大于某个字段来断定这个文件属于何种编码。这种方式vim也使用了,这就是vim的自动编码识别机制了。但这种机制由于编码各式各样,不可能每种编码都有显著的特征来辨别,所以是不可能 100%准确的。对于我们GB2312编码,由于其中文是使用了2个acsii值高于127的字符组成汉字字符的,因此不可能把gb2312编码的文件与 latin1编码区分开来,因此自动识别编码的机制对于gb2312是不成功的,它只会将文件辨识为latin1编码。此问题同样出现在gbk,big5 上等。因此我们在编辑此类文档时,需要手工设定encoding和fileencoding。如果文档编码为utf-8时,一般vim都能自动识别正确的编码。
3,客户运行vim的终端所使用的编码类型----同第二条一样,这也是一个比较难以断定的关键点。第二个关键点决定着从文件读取内容和写入内容到文件时使用的编码,而此关键点则决定vim输出内容到终端时使用的编码,如果此编码类型和终端认为它收到的数据的编码类型不同,则又会产生乱码问题。在 linux本地X环境下,一般终端都认为其接收的数据的编码类型和系统locale类型相符,因此不需关心此方面是否存在问题。但如果牵涉到远程终端,例如ssh登录服务器,则问题就有可能出现了。例如从1台locale为GB2310的系统(称作客户机)ssh到locale为utf-8的系统(称作服务器)并开启vim编辑文档,在不加任何改动的情况下,服务器返回的数据为utf-8的,但客户机认为服务器返回的数据是gb2312的,按照 gb2312来解释数据,则肯定就是乱码了,这时就需要设置termencoding为gb2312来解决这个问题。此问题更多出现在我们的 windows desktop机远程ssh登录服务器的情况下,这里牵扯到不同系统的编码转换问题。所以又与windows本身以及ssh客户端有很大相关性。在 windows下存在两种编码类型的软件,一种是本身就为unicode编码方式编写的软件,一种是ansi软件,也就是程序处理数据直接采用字节流,不关心编码。前一种程序可以在任何语言的windows上正确显示多国语言,而后一种则编写在何种语言的系统上则只能在何种语言的系统上显示正确的文字。对于这两种类型的程序,我们需要区别对待。以ssh客户端为例,我们使用的putty是unicode软件,而secure CRT则是ansi 软件。对于前者,我们要正确处理中文,只要保证vim输出到终端的编码为utf-8即可,就是termencoding=utf-8。但对于后者,一方面我们要确认我们的windows系统默认代码页为cp936(中文windows默认值),另一方面要确认vim设置的termencoding= cp936。
最后来看看处理中文文档最典型的几种情况和设置方式:
1,系统locale是utf-8(很多linux系统默认的locale形式),编辑的文档是GB2312或GBK形式的(Windows记事本默认保存形式,大部分编辑器也默认保存为这个形式,所以最常见),终端类型utf-8(也就是假定客户端是putty类的unicode软件)
则vim打开文档后,encoding=utf-8(locale决定的),fileencoding=latin1(自动编码判断机制不准导致的),termencoding=空(默认无需转换term编码),显示文件为乱码。
解决方案1:首先要修正fileencoding为cp936或者euc-cn(二者一样的,只不过叫法不同),注意修正的方法不是:set fileencoding=cp936,这只是将文件保存为cp936,正确的方法是重新以cp936的编码方式加载文件为:edit ++enc=cp936,可以简写为:e ++enc=cp936。
解决方案2:临时改变vim运行的locale环境,方法是以LANG=zh_CN vim abc.txt的方式来启动vim,则此时encoding=euc-cn(locale决定的),fileencoding=空(此locale下文件编码自动判别功能不启用,所以fileencoding为文件本身编码方式不变,也就是euc-cn),termencoding=空(默认值,为空则等于encoding)此时还是乱码的,因为我们的ssh终端认为接受的数据为utf-8,但vim发送数据为euc-cn,所以还是不对。此时再用命令: set termencoding=utf-8将终端数据输出为utf-8,则显示正常。
2,情况与1基本相同,只是使用的ssh软件为secure CRT类ansi类软件。
vim打开文档后,encoding=utf-8(locale决定的),fileencoding=latin1(自动编码判断机制不准导致的),termencoding=空(默认无需转换term编码),显示文件为乱码。
解决方案1:首先要保证运行secure CRT的windows机器的默认代码页为CP936,这一点中文windows已经是默认设置了。其他的与上面方案1相同,只是要增加一步,:set termencoding=cp936
解决方案2:与上面方案2类似,不过最后一步修改termencoding省略即可,在此情况下需要的修改最少,只要以locale为zh_CN 开启 vim,则encoding=euc-cn,fileencoding和termencoding都为空即为encoding的值,是最理想的一种情况。
可见理解这3个关键点和3个参数的意义,对于编码问题有很大助力,以后就可以随心所欲的处理文档了,同时不仅仅是应用于vim,在其他需要编码转换的环境里,都可以应用类似的思路来处理问题解决问题。
printf的格式控制的完整格式:
% - 0 m.n l或h 格式字符下面对组成格式说明的各项加以说明: ①%:表示格式说明的起始符号,不可缺少。
②-:有-表示左对齐输出,如省略表示右对齐输出。
③0:有0表示指定空位填0,如省略表示指定空位不填。
④m.n:m指域宽,即对应的输出项在输出设备上所占的字符数。N指精度。用于说明输出的实型数的小数位数。为指定n时,隐含的精度为n=6位。
⑤l或h:l对整型指long型,对实型指double型。h用于将整型的格式字符修正为short型。
---------------------------------------
格式字符格式字符用以指定输出项的数据类型和输出格式。
①d格式:用来输出十进制整数。有以下几种用法: %d:按整型数据的实际长度输出。 %md:m为指定的输出字段的宽度。如果数据的位数小于m,则左端补以空格,若大于m,则按实际位数输出。 %ld:输出长整型数据。
②o格式:以无符号八进制形式输出整数。对长整型可以用"%lo"格式输出。同样也可以指定字段宽度用“%mo”格式输出。例: main() { int a = -1; printf("%d, %o", a, a); } 运行结果:-1,177777 程序解析:-1在内存单元中(以补码形式存放)为(1111111111111111)2,转换为八进制数为(177777)8。
③x格式:以无符号十六进制形式输出整数。对长整型可以用"%lx"格式输出。同样也可以指定字段宽度用"%mx"格式输出。
④u格式:以无符号十进制形式输出整数。对长整型可以用"%lu"格式输出。同样也可以指定字段宽度用“%mu”格式输出。
⑤c格式:输出一个字符。
⑥s格式:用来输出一个串。有几中用法 %s:例如:printf("%s", "CHINA")输出"CHINA"字符串(不包括双引号)。 %ms:输出的字符串占m列,如字符串本身长度大于m,则突破获m的限制,将字符串全部输出。若串长小于m,则左补空格。 %-ms:如果串长小于m,则在m列范围内,字符串向左靠,右补空格。 %m.ns:输出占m列,但只取字符串中左端n个字符。这n个字符输出在m列的右侧,左补空格。 %-m.ns:其中m、n含义同上,n个字符输出在m列范围的左侧,右补空格。如果n>m,则自动取n值,即保证n个字符正常输出。
⑦f格式:用来输出实数(包括单、双精度),以小数形式输出。有以下几种用法: %f:不指定宽度,整数部分全部输出并输出6位小数。 %m.nf:输出共占m列,其中有n位小数,如数值宽度小于m左端补空格。 %-m.nf:输出共占n列,其中有n位小数,如数值宽度小于m右端补空格。
⑧e格式:以指数形式输出实数。可用以下形式: %e:数字部分(又称尾数)输出6位小数,指数部分占5位或4位。 %m.ne和%-m.ne:m、n和”-”字符含义与前相同。此处n指数据的数字部分的小数位数,m表示整个输出数据所占的宽度。
⑨g格式:自动选f格式或e格式中较短的一种输出,且不输出无意义的零。
---------------------------------------
关于printf函数的进一步说明:如果想输出字符"%",则应该在“格式控制”字符串中用连续两个%表示,如: printf("%f%%", 1.0/3); 输出0.333333%。
---------------------------------------
对于单精度数,使用%f格式符输出时,仅前7位是有效数字,小数6位.对于双精度数,使用%lf格式符输出时,前16位是有效数字,小数6位.
对于m.n的格式还可以用如下方法表示(例)
char ch[20]; printf("%*.*s\n",m,n,ch);
前边的*定义的是总的宽度,后边的定义的是输出的个数。分别对应外面的参数m和n 。我想这种方法的好处是可以在语句之外对参数m和n赋值,从而控制输出格式。
1.空格作为scanf的参数
在程序员后面的皇后问题源程序中有这样一段代码
要注意我加上去的注释部分
if(k==n)/*找到一个解*/
{
printf("列\t行);
for(i=1;j<=n;j++)
printf("%3d\t%d\n",j,col[j]);
printf("enter a character(Q/q for exit)!\n");
scanf(" %c",&awn);/*注意这里scanf中的空格*/
if(awn==`Q`||awn==`q`)exit(0);
}
.
想必大家都能知道这段代码的功能,也就是说如果用户没有输入退出的条件(q或者Q)那么程序会将下一个皇后解都打出来,直到出现Q/q或者全部解输出完为止,问题就出在这里,scanf里面为什么要有一个空格呢?如果没有运行会是:当输入非退出条件会连续输出两个解,这是为什么?原因就在scanf。
空格在scanf函数中是一个参数,我们都知道c语言的输入输出流有三种(stdio,stdin,stderr),scanf函数是在键盘缓冲区取数据,当我们输入的数据在屏幕上显示的时候,实际上数据还在键盘缓冲区,当用户输入回车键的时候计算机会把缓冲区的数据(包括回车)输入到内存供程序的执行。而参 数%c是接受字符的,当他遇到回车scanf也能正常接收。
假设现在scanf没有空格
分析:现在我们再来看看我们的程序:假设 现在输入y继续执行程序函数scanf正确接收到了y,循环继续,但是下一个字符应该是回车,scanf函数循环接收到的回车!=q/Q,这样循环又继续 一次此时没有数据,那么计算机就等待用户输入数据。就这样用户输入一个数据就会出现两次,这是我们程序员不希望看到的。
解决:解决就是在%c的前面加上一个空格,空格是一个参数,它可以在scanf接收数据之前屏蔽回车(\n),这样就能达到我们的预想效果。
2.输入特定字符集
scanf("%[]",str)可以实现只读入中括号[]中的字符,遇到非指定的字符就停止输入。
例如,scanf("%[0-9]",str);只读入0-9的数字,遇到其它字符返回。scanf("%[^\n]",str),读入回车符外的所有字符,用此语句可以实现输入带空格的字符串。
1.交换两个字符位置
xp
其实就是删除光标当前字符(x),然后再将缓存的字符贴出(p)
2.上下两行调换
ddp
实际就是(dd)删除当前行,(p)后即将缓存的行贴出
3.把文件内容反转
:g/^/m0/ (未通过)
这个貌似没写全啊
4.上下两行合并
J
实用性:高,shift+j
5.删除所有行
dG
错!(d)为删除,(shift+g)到文件尾,因此这命令效用为从光标处删除至文件尾
那么删除到文首呢?对了,如果知道gg可以将光标移至文首,那么dgg便是删除到文首了
类似的还有:
dw -- 删到词尾
db -- 删到词头
daw -- 删除光标所在词(较有用)
如果你用 "c" 代替 "d",这会变成修改命令;而改用 "y",则变成拷贝命令,等等等等。
6.从当前位置删除到行尾
d$
(d)删除,$ (即 shift+4)为行尾,^( 即 shift+6)为行头,依此类推,d^删除至行头
更为简洁的命令为D,对了,就是shift+d,就这么简单
7.从当前位置复制到行尾
y$ 如果要粘贴到其他地方 p 就可以了
同上
由于vi 是建立在 EX 上的 所以 当键入 : 时就来到了 EX 命令状态
8.
:ab string strings
例如 ":ab usa United States of America" ,
当你在文件里插入 usa 时
United States of America 就蹦出来了
其实执行的是":abbreviate"命令,用":unabbreviate"命令解除缩写,要删除全部缩写用":abclear"
9.
:map keys new_seq
定义你当前 键盘命令
键映射,我前面的tips有用到
10.
:set [all]
vi or ex 的编辑状态
如 显示每行 :set nu
set,就是set,不显示行号":se nonu"
11.
在命令状态下,nyy表示拷贝从光标行起的下n行内容,p表示paste,可刚复制的内容粘贴在光标处的
下面。
讲的太不清楚了,n用数字代替,即将后面的指令重复执行n次,yy为复制,nyy复制n行
12.
单个字符替换用r,覆盖多个字符用R,用多个字符替换一个字符用s,整行替换用S
实用性:高,S等于我常用的cc或C
13.
:%s/old_word/new_word/g
这个指令是于在整个文件中替换特定字符串
替换,用法是这样:":起始行,结束行s/搜索串/替换串/gc" 从起始行到结束行,把所有的搜索串替换为替换串,最后那个"c"为开启交互式替换,%为全文
14.光标控制
k:上移 nk 上移n行
j:下移 nj 下移n行
光标移动,还有h:左移 l:右移,不过我更喜欢用上、下、左、右,嘿嘿
将光标移到第n行,按下 mk
将光标移到第m行,按下 "ay'k
即将第n到m的行存到a寄存器,以此类推,b,c........寄存器等
这样就可以将你常用的需要复用的内容粘贴到不同的寄存器中以备用
想粘贴到某处,直接将光标移到某地,按下 'ap 即可,以此类推,b,c........寄存器等
在当前屏幕中
H 跳到第一行
M 跳到中间一行
L 跳到最后一行
当前屏幕的光标控制技巧,助记方法为:Head、Middle、Last
15.
表8-2 删除命令
删除命令操作
d l 删除当前字符(与x命令功能相同)
d 0 删除到某一行的开始位置
d ^ 删除到某一行的第一个字符位置(不包括空格或TA B字符)
d w 删除到某个单词的结尾位置
d 3 w 删除到第三个单词的结尾位置
d b 删除到某个单词的开始位置
d W 删除到某个以空格作为分隔符的单词的结尾位置
d B 删除到某个以空格作为分隔符的单词的开始位置
d 7 B 删除到前面7个以空格作为分隔符的单词的开始位置
d) 删除到某个语句的结尾位置
d 4) 删除到第四个语句的结尾位置
d( 删除到某个语句的开始位置
d } 删除到某个段落的结尾位置
d { 删除到某个段落的开始位置
d 7 { 删除到当前段落起始位置之前的第7个段落位置
d d 删除当前行
d /t e x t 删除从文本中出现" t e x t"中所指定字样的位置,一直向前直到下一个该字样所出现的
位置(但不包括该字样)之间的内容
d fc 删除从文本中出现字符"c"的位置,一直向前直到下一个该字符所出现的位置(包括
该字符)之间的内容
d tc 删除当前行直到下一个字符" c"所出现位置之间的内容
D 删除到某一行的结尾
d $ 删除到某一行的结尾
5 d d 删除从当前行所开始的5行内容
d L 删除直到屏幕上最后一行的内容
d H 删除直到屏幕上第一行的内容
d G 删除直到工作缓存区结尾的内容
d 1 G 删除直到工作缓存区开始的内容
修改命令操作
c l 更改当前字符
c w 修改到某个单词的结尾位置
c 3 w 修改到第三个单词的结尾位置
c b 修改到某个单词的开始位置
c W 修改到某个以空格作为分隔符的单词的结尾位置
c B 修改到某个以空格作为分隔符的单词的开始位置
c 7 B 修改到前面7个以空格作为分隔符的单词的开始位置
c 0 修改到某行的结尾位置
c) 修改到某个语句的结尾位置
c 4) 修改到第四个语句的结尾位置
c( 修改到某个语句的开始位置
c } 修改到某个段落的结尾位置
c { 修改到某个段落的开始位置
c 7 { 修改到当前段落起始位置之前的第7个段落位置
c tc 修改当前行直到下一个字符c所出现位置之间的内容
C 修改到某一行的结尾
c c 修改当前行
5 c c 修改从当前行所开始的5行内容
.重复上一次修改!
表8-4 替换命令
替换命令操作
s 将当前字符替换为一个或多个字符
S 将当前行替换为一个或多个字符
5 s 将从当前字符开始的5个字符替换为一个或多个字符
vi替换使用规则:
:g/s1/s/s2/s3/g
第一个g表示对每一个包括s1的行都进行替换,第二个g表示对每一行包括s1的行所有的s2都用s3替换
s表示替换,s2是要被替换的字符串,他可以和s1相同(如果相同的话用//代替),s3是替换字符串
16.
fx
往右移动到 x 字符上
Fx
往左移动到 x 字符上
tx
往右移动到 x 字符前
Tx
往左移动到 x 字符后
(注意:以上四个命令中,其中x是键入的字符)
;
分号,配合 f 和 t 使用,重复一次
,
逗号,配合 f 和 t 使用,反方向重复一次
如果不结合;与,很不好用,nfx为移到第n个x字符上
17. vi 环境选项 Solaris ksh
noautoindent nomodelines noshowmode
autoprint nonumber noslowopen
noautowrite nonovice tabstop=8
nobeautify nooptimize taglength=0
directory=/var/tmp paragraphs=IPLPPPQPP LIpplpipnpbtags=tags /usr/lib/tags
noedcompatible prompt tagstack
noerrorbells noreadonly term=vt100
noexrc redraw noterse
flash remap timeout
hardtabs=8 report=5 ttytype=vt100
noignorecase scroll=11 warn
nolisp sections=NHSHH HUuhsh+c window=23
nolist shell=/bin/ksh wrapscan
magic shiftwidth=8 wrapmargin=0
mesg noshowmatch nowriteany
For C-Shell:
setenv EXINIT "set nu"
For Bourne or Korn Shell:
EXINIT="set nu"; export EXINIT
For Korn Shell Only (alternate method):
typeset -x EXINIT="set nu"
在 .profile 里设置 vi 的环境选项 , 以上均测试过
以上的查阅vim帮助文档,太多了不一一解释了
18.标记文本
mchar 用字母char标记当前光标的位置
`char 移至char所标记处
'char 移至char标记所在行的开头处
" 移至当前行上一次所在位置(在光标移动之后)――一个双引号
'' 移至当前行上第一次所在位置的行的开头处(在光标移动之后)――两个单引号0.
头两个和最后一个就已经挺好用的了
19.
同时vi多个文件时,CTRL-SHIFT-6回到上一个文件,在本次vi的文件和上次vi的文件之间切换。
但是我发现一个BUG:在用CTRL-SHIFT-6切换到上一个文件后,用:args查看多文件vi状态时,
屏幕底部仍然显示目前vi的是刚才的文件。
(在HP-UX,Solaris,AIX上通过)
也可以使用:
:e#
进行切换
ctrl+shift+6=:e#回到上一个文件,哇哈哈,终于找到了,对应的是:next简写为:n到下个文件,用files貌似更能看清当前打开的文件列表
20.
sco 下VI 要在文本前同样的字符加用
%s/^/要加的内容/g 要在文本后同样的字符加
%s/$/要加的内容/g
汗,这样利用查找替换
21.
如何去掉文本中的 ^M 硬回车?不必用binary传回去再ascii传回来的方式,用shell或者unix语句实现。
cat filename |tr -d '\015' >newfile
不同的unix系统还存在一些其他不同的命令,如:doscp
sed 也可以实现这个功能.
dos2unix filename filename2
反之
unix2dos filename filename2
在vi 中用:$s/^M//g
^是crtl-V crtl-M
我常用的是dos2unix
22.如何在"unix命令行"下将一个文件的某字符串用另一个串换掉
sed 's/string1/string2/gp' file1 > file2
23.将/etc/hosts下所有的地址都ping 2次
1 #/usr/bin/sh
2 #grad /etc/hosts and ping each address
3 cat /etc/hosts|grep -v '^#' | while read LINE
4 do
5 ADDR=`awk '{print $1}'`
6 for MACHINE in $ADDR
7 do
8 ping $MACHINE -n 2
9 done
10 done
24.
到前一个函数[[ ,到下一个函数]] ,括号配对% ,交叉参考Ctrl_] (事先用ctags做索引),回来用e# ` 编辑一个函数:vi -t 函数名 ,编辑加密文本vi -X
%括号匹配我介绍过,:e#回来可以用ctrl+o替代,更优雅
25.
在插入模式下ctrl+p,自动补齐剩余单词,以赖规则:tags,以有的单词等等
还ctrl_n呢
例一、两个常用的指令序列
xp 左右交换光标处两字符的位置。
ddp 上下交换光标处两行的位置。
例二、重复输入同一字符
有时,我们可能想多次输入同一字符,VIM的插入功能可以很好的完成这项工作
命令 80i=^ESC 一次可以输入80个字符= ,当然,80a=^ESC 也可以完成上述功能。
请注意:此处的^ESC表示键盘左上方上的ESC键。
例三、将两个文本数据文件按行逐条合并,并给出标尺
数据文件1内容如下:
1-----
2-----
3-----
数据文件2内容如下:
1=====
2=====
3=====
要求的结果如下:
|--------1---------2---------3---------4---------5
1-----
1=====
|--------1---------2---------3---------4---------5
2-----
2=====
|--------1---------2---------3---------4---------5
3-----
3=====
也许您会说,这还不简单,无非是反复拷贝、粘贴,任何一款文本编辑器都能完成上述功能。可是,如果这两个文件都很大,每个文件都成千上万行,恐怕简单的拷贝、粘贴就难以胜任了。因此,我们所关心的,是找到一种行之有效的方法,把枯燥乏味的工作留给计算机,我们只需发布指令。为达到此目的,请按以下步骤执行:
㈠、将两文件合并,结果如下
1-----
2-----
3-----
1=====
2=====
3=====
㈡、在两文件头尾相接的地方插入标志行,用以区分两个文件,本文采用的是一整行!字符
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
1=====
2=====
3=====
㈢、在标志行的下方输入标尺
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
㈣、执行宏命令脚本merge_2r.vim,即在VIM编辑器中按如下键 :so merge_2r.vim 回车
㈤、按下键盘上的=键,执行的结果如下
|--------1---------2---------3---------4---------5
1-----
1=====
|--------1---------2---------3---------4---------5
2-----
2=====
|--------1---------2---------3---------4---------5
3-----
3=====
|--------1---------2---------3---------4---------5
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
㈥、将最后三行删除,即可得到我们需要的结果
|--------1---------2---------3---------4---------5
1-----
1=====
|--------1---------2---------3---------4---------5
2-----
2=====
|--------1---------2---------3---------4---------5
3-----
3=====
怎么样,简单吗?请大家自己实际尝试一下。下面,我来详细讲解宏命令脚本merge_2r.vim 。
该脚本内容如下:
"--------------------------------------------------------------------
"Macro Function : Merge File1 And File2,Have Ruler in every record
" Date : 2001/12/01
" Author : Yan Shi
"--------------------------------------------------------------------
"1-----
"2----- } Sample File1
"3-----
"!!!!!!!!!!!!!!!!!!!!!!!! Flag Row
"|--------1---------2---------3---------4---------5 Ruler
"1=====
"2===== } Sample File2
"3=====
"--------------------------------------------------------------------
:1
:map = ma/!!!!!^M+:.co 'a-1^M/!!!!!^M2+:.m'a^M+=
前14行每行都以"开始,表明该行是注释行,实际并不执行,只是方便读者阅读,只有最后两行才是真正的代码行。请注意:本例中的^M表示键盘上的回车键,并非^和M两个字符。为了讲述清楚,我把命令行分解开,逐一说明。
首先将第一行置为当前行,然后执行map命令,将一大串VIM指令映像给字符=。这一大串VIM指令共分9步执行:
ma 将数据文件一的第一行标记为a
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
/!!!!!^M 找到标志行,置为当前行
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
+ 光标下移一行,即把标尺行置为当前行
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
:.co 'a-1^M 把标尺行复制到标记行(数据文件一的第一行)的上方
|--------1---------2---------3---------4---------5
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
/!!!!!^M 再次找到标志行,置为当前行
|--------1---------2---------3---------4---------5
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
2+ 光标下移2行,即数据文件二的第一行置为当前行
|--------1---------2---------3---------4---------5
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
:.m'a^M 把数据文件二的第一行移至标记行的下方
|--------1---------2---------3---------4---------5
1-----
1=====
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
2=====
3=====
+ 光标下移一行,即数据文件一的第二行置为当前行
|--------1---------2---------3---------4---------5
1-----
1=====
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
2=====
3=====
= 这一步很关键,是典型的递归调用,重复完成以上步骤
例四、在文件中置入行号
工作中,我们有时希望把行号置入文件中,而VIM提供的功能 :set nu 只能显示行号,不能编辑或将其置入文件当中,下面的宏命令脚本row_num.vim可以完成此项功能。
"------------------------------------------
"Macro Function : Source File Add Row_Num
" Date : 2001/12/01
" Author : Yan Shi
"------------------------------------------
:%s/^/^I/
:$
:let end=line(".")
:1
"------------------------------------------
:let num=1
:while num<=end
:let line=getline(".")
:let temp=substitute(line,$,num,"")
:call setline(".",temp)
:+
:let num=num+1
:endwhile
"------------------------------------------
请注意:本例中的^I表示键盘上的TAB键,并非^和I两个字符。下面,我针对该宏命令脚本逐一讲解。
:%s/^/^I/ 每一行的行首添加一个TAB字符
:$ 到文件的末行
:let end=line(".") 末行的行号 ==〉变量 END,函数line的功能是取得指定行的行号,此处参数"."表示当前行
:1 到文件的首行
"------------------------------------------
:let num=1 1 ==〉计数器
:while num<=end
:let line=getline(".") 取当前行的内容 ==〉变量 LINE
:let line=substitute(line,$,num,"") 在变量 LINE 的前面置入行号
:call setline(".",line) 将变量 LINE 的内容写回当前行
:+ 下移一行
:let num=num+1 计数器加一
:endwhile 循环执行,直到文件结束
"------------------------------------------
真正的高级技巧,不过貌似太高了,一般用不着……
有关正则表达式的使用
UNIX/LINUX 下的很多工具之所以强大、灵活,关键是因为有了正则文法和元字符,这也是VIM乃至UNIX/LINUX系统的精华所在。正因为使用灵活,因此掌握起来比较吃力,如果不是真正理解,实际运用中会出现千奇百怪的错误。因此,有必要对这部分知识多花些气力。下面结合具体实例讲解。
例五、有一文件,包含某外企的中国员工的资料,首先是姓名,然后是两个空格,其次是15位身份证号码。
zhang.fei 430759701022003
diao.chan 651302801225012
guan.yu 342869680413001
xi.shi 120638780214006
liu.bei 210324650708001
现在,有以下问题需要解决:
按照外国人的习惯,应该是名在前,姓在后。因此,文件中的姓名字段需要修改。
姓与名的首字母应该大写。
根据身份证号码,还可以判断出生年月日,将其作为一个新字段添加。
根据身份证号码,可以判断出性别。若为男性,添加male,若为女性,添加female 。
将男女员工分开,男员工在前,女员工在后。
将各字段数据左对齐
最终结果如下:
Fei.Zhang 430759701022003 1970/10/22 male
Yu.Guan 342869680413001 1968/04/13 male
Bei.Liu 210324650708001 1965/07/08 male
-----------------------------------------------
Chan.Diao 651302801225012 1980/12/25 female
Shi.Xi 120638780214006 1978/02/14 female
为了完成上述功能,只需执行脚本employee.vim ,使用方法为 :so employee.vim 回车即可。
脚本内容如下:
:%s/ / /
:%s/\(............\)\( *\)/\1/
:%s/\([A-Za-z][A-Za-z]*\)\(\.\)\([A-Za-z][A-Za-z]*\)/\u\3\2\u\1/
:%s/$/ xxxxxx/
:%s/\([0-9]\{6}\)\([0-9]\{6}\)\([0-9]\{3}\) \(xxxxxx\)/\1\2\3 \2/
:%s/\(..\)\(..\)\(..\)$/19\1\/\2\/\3
:%s/$/ xxxxxx/
:%s/\([0-9]\{14}[13579]\)\(.*\)\(xxxxxx\)/\1\2male /
:%s/\([0-9]\{14}[02468]\)\(.*\)\(xxxxxx\)/\1\2female/
:$
:s/.*/&^M-----------------------------------------------
:g/female/.m$
在这个脚本中,使用了大量的正则表达式,这里仅对涉及到的正则表达式做一简要介绍。有关正则表达式的内容相当多,本文不可能占用大量篇幅叙述,请大家谅解。
% 地址范围符号,代表文件中的所有行,作用等同于地址范围 1,$
. 与任意单字符(换行符除外)匹配,例如 y.s 可以匹配 yas y.s 或 y s 等等。
* 与前一字符的0次或多次出现匹配,例如 y*s 可以匹配 yys yyyyys 或 s 等等。
$ 与行尾匹配。
& 代表模式匹配中出现的字符串,例如 s/abc/&def 是把当前行的abc替换成abcdef 。
[] 匹配[]中出现的字符,例如[abc]匹配字符 a,b 或 c ,[a-zA-Z]匹配所有的英文字符。
\( \) \(和\)之间出现的内容可以由\num来替代。
\1\2\3 替代\(和\)之间出现的内容。
\u 将后续字符串的首字母大写。
\{num} 与前一字符的num次出现匹配。
现在,我们对脚本逐条讲解,希望能帮助大家理解正则文法。
⑴:%s/ / /
将文件中每行出现的2个空格替换为10个空格。
zhang.fei 430759701022003
diao.chan 651302801225012
guan.yu 342869680413001
xi.shi 120638780214006
liu.bei 210324650708001
⑵:%s/\(............\)\( *\)/\1/
保留行首的12个字符,将其余的空格删除,这样,前两个字段就对齐了。
zhang.fei 430759701022003
diao.chan 651302801225012
guan.yu 342869680413001
xi.shi 120638780214006
liu.bei 210324650708001
⑶:%s/\([A-Za-z][A-Za-z]*\)\(\.\)\([A-Za-z][A-Za-z]*\)/\u\3\2\u\1/
将文件中每行出现的雇员姓名互换,并将首字母大写。
Fei.Zhang 430759701022003
Chan.Diao 651302801225012
Yu.Guan 342869680413001
Shi.Xi 120638780214006
Bei.Liu 210324650708001
⑷:%s/$/ xxxxxx/
在每一行的行尾添加2个空格和6个x
Fei.Zhang 430759701022003 xxxxxx
Chan.Diao 651302801225012 xxxxxx
Yu.Guan 342869680413001 xxxxxx
Shi.Xi 120638780214006 xxxxxx
Bei.Liu 210324650708001 xxxxxx
⑸:%s/\([0-9]\{6}\)\([0-9]\{6}\)\([0-9]\{3}\) \(xxxxxx\)/\1\2\3 \2/
将xxxxxx替换成出生年月日。
Fei.Zhang 430759701022003 701022
Chan.Diao 651302801225012 801225
Yu.Guan 342869680413001 680413
Shi.Xi 120638780214006 780214
Bei.Liu 210324650708001 650708
⑹:%s/\(..\)\(..\)\(..\)$/19\1\/\2\/\3
将年月日用/字符分隔,并在年前添加19。
Fei.Zhang 430759701022003 1970/10/22
Chan.Diao 651302801225012 1980/12/25
Yu.Guan 342869680413001 1968/04/13
Shi.Xi 120638780214006 1978/02/14
Bei.Liu 210324650708001 1965/07/08
⑺:%s/$/ xxxxxx/
在每一行的行尾添加2个空格和6个x
Fei.Zhang 430759701022003 1970/10/22 xxxxxx
Chan.Diao 651302801225012 1980/12/25 xxxxxx
Yu.Guan 342869680413001 1968/04/13 xxxxxx
Shi.Xi 120638780214006 1978/02/14 xxxxxx
Bei.Liu 210324650708001 1965/07/08 xxxxxx
⑻:%s/\([0-9]\{14}[13579]\)\(.*\)\(xxxxxx\)/\1\2male /
身份证号码末位是奇数的,将xxxxxx替换成male
Fei.Zhang 430759701022003 1970/10/22 male
Chan.Diao 651302801225012 1980/12/25 xxxxxx
Yu.Guan 342869680413001 1968/04/13 male
Shi.Xi 120638780214006 1978/02/14 xxxxxx
Bei.Liu 210324650708001 1965/07/08 male
⑼:%s/\([0-9]\{14}[02468]\)\(.*\)\(xxxxxx\)/\1\2female/
身份证号码末位是偶数的,将xxxxxx替换成female
Fei.Zhang 430759701022003 1970/10/22 male
Chan.Diao 651302801225012 1980/12/25 female
Yu.Guan 342869680413001 1968/04/13 male
Shi.Xi 120638780214006 1978/02/14 female
Bei.Liu 210324650708001 1965/07/08 male
⑽:$ 到文件的最后一行
⑾:s/.*/&^M-----------------------------------------------
在文件的最末行插入一行 "-" 字符。
Fei.Zhang 430759701022003 1970/10/22 male
Chan.Diao 651302801225012 1980/12/25 female
Yu.Guan 342869680413001 1968/04/13 male
Shi.Xi 120638780214006 1978/02/14 female
Bei.Liu 210324650708001 1965/07/08 male
-----------------------------------------------
⑿:g/female/.m$
将所有的女员工记录移至文件尾。
Fei.Zhang 430759701022003 1970/10/22 male
Yu.Guan 342869680413001 1968/04/13 male
Bei.Liu 210324650708001 1965/07/08 male
-----------------------------------------------
Chan.Diao 651302801225012 1980/12/25 female
Shi.Xi 120638780214006 1978/02/14 female
笔者目前正在为某外资公司从事大型机(IBM S/390)的软件开发,一切工作都在TSO环境中进行。为了对编写的程序进行测试,必须准备测试数据。有过大型机开发经验的人会知道,通过TSO,输入字符型数据还可以,如果要输入16进制数据,操作起来很麻烦。因为16进制数是纵向排列的,输入时既不方便,又很容易错位。怎么解决呢?我尝试了几种办法,实际证明,用VIM最方便。
例六、下列数据 1234567890ABCDEF ,将其变成 13579ACE 24680BDF 的形式,这样,数据就可以很方便的粘贴到TSO环境中了。
下面给出宏命令脚本change_d.vim
"----------------------------------------------------
"Macro Function : Convert Char Arrange Direction
"
" Sample : 40 50 60 ==> 4 5 6
" 0 0 0
" Date : 2001/12/01
" Author : Yan Shi
"----------------------------------------------------
:s/.*/&^M/
:1
:map = malx+$p-`al=
说明如下:
⑴ :s/.*/&^M/ 在数据行下方添加一空行。
⑵ :1 回到文件的首行的首字符。
⑶ :map = malx+$p-`al= 将一大串VIM命令映像给字符=
① ma 将首字符标记为a
② l 光标右移一个字符
③ x 删除光标处字符
④ + 移至下一行
⑤ $ 到行尾
⑥ p 将删除的字符粘贴
⑦ - 回至上一行
⑧ `a 返回到标记字符处
⑨ l 光标右移一个字符
⑩ = 递归调用,重复以上步骤,直到将该行所有的数据处理完。
xp
其实就是删除光标当前字符(x),然后再将缓存的字符贴出(p)
2.上下两行调换
ddp
实际就是(dd)删除当前行,(p)后即将缓存的行贴出
3.把文件内容反转
:g/^/m0/ (未通过)
这个貌似没写全啊
4.上下两行合并
J
实用性:高,shift+j
5.删除所有行
dG
错!(d)为删除,(shift+g)到文件尾,因此这命令效用为从光标处删除至文件尾
那么删除到文首呢?对了,如果知道gg可以将光标移至文首,那么dgg便是删除到文首了
类似的还有:
dw -- 删到词尾
db -- 删到词头
daw -- 删除光标所在词(较有用)
如果你用 "c" 代替 "d",这会变成修改命令;而改用 "y",则变成拷贝命令,等等等等。
6.从当前位置删除到行尾
d$
(d)删除,$ (即 shift+4)为行尾,^( 即 shift+6)为行头,依此类推,d^删除至行头
更为简洁的命令为D,对了,就是shift+d,就这么简单
7.从当前位置复制到行尾
y$ 如果要粘贴到其他地方 p 就可以了
同上
由于vi 是建立在 EX 上的 所以 当键入 : 时就来到了 EX 命令状态
8.
:ab string strings
例如 ":ab usa United States of America" ,
当你在文件里插入 usa 时
United States of America 就蹦出来了
其实执行的是":abbreviate"命令,用":unabbreviate"命令解除缩写,要删除全部缩写用":abclear"
9.
:map keys new_seq
定义你当前 键盘命令
键映射,我前面的tips有用到
10.
:set [all]
vi or ex 的编辑状态
如 显示每行 :set nu
set,就是set,不显示行号":se nonu"
11.
在命令状态下,nyy表示拷贝从光标行起的下n行内容,p表示paste,可刚复制的内容粘贴在光标处的
下面。
讲的太不清楚了,n用数字代替,即将后面的指令重复执行n次,yy为复制,nyy复制n行
12.
单个字符替换用r,覆盖多个字符用R,用多个字符替换一个字符用s,整行替换用S
实用性:高,S等于我常用的cc或C
13.
:%s/old_word/new_word/g
这个指令是于在整个文件中替换特定字符串
替换,用法是这样:":起始行,结束行s/搜索串/替换串/gc" 从起始行到结束行,把所有的搜索串替换为替换串,最后那个"c"为开启交互式替换,%为全文
14.光标控制
k:上移 nk 上移n行
j:下移 nj 下移n行
光标移动,还有h:左移 l:右移,不过我更喜欢用上、下、左、右,嘿嘿
将光标移到第n行,按下 mk
将光标移到第m行,按下 "ay'k
即将第n到m的行存到a寄存器,以此类推,b,c........寄存器等
这样就可以将你常用的需要复用的内容粘贴到不同的寄存器中以备用
想粘贴到某处,直接将光标移到某地,按下 'ap 即可,以此类推,b,c........寄存器等
在当前屏幕中
H 跳到第一行
M 跳到中间一行
L 跳到最后一行
当前屏幕的光标控制技巧,助记方法为:Head、Middle、Last
15.
表8-2 删除命令
删除命令操作
d l 删除当前字符(与x命令功能相同)
d 0 删除到某一行的开始位置
d ^ 删除到某一行的第一个字符位置(不包括空格或TA B字符)
d w 删除到某个单词的结尾位置
d 3 w 删除到第三个单词的结尾位置
d b 删除到某个单词的开始位置
d W 删除到某个以空格作为分隔符的单词的结尾位置
d B 删除到某个以空格作为分隔符的单词的开始位置
d 7 B 删除到前面7个以空格作为分隔符的单词的开始位置
d) 删除到某个语句的结尾位置
d 4) 删除到第四个语句的结尾位置
d( 删除到某个语句的开始位置
d } 删除到某个段落的结尾位置
d { 删除到某个段落的开始位置
d 7 { 删除到当前段落起始位置之前的第7个段落位置
d d 删除当前行
d /t e x t 删除从文本中出现" t e x t"中所指定字样的位置,一直向前直到下一个该字样所出现的
位置(但不包括该字样)之间的内容
d fc 删除从文本中出现字符"c"的位置,一直向前直到下一个该字符所出现的位置(包括
该字符)之间的内容
d tc 删除当前行直到下一个字符" c"所出现位置之间的内容
D 删除到某一行的结尾
d $ 删除到某一行的结尾
5 d d 删除从当前行所开始的5行内容
d L 删除直到屏幕上最后一行的内容
d H 删除直到屏幕上第一行的内容
d G 删除直到工作缓存区结尾的内容
d 1 G 删除直到工作缓存区开始的内容
修改命令操作
c l 更改当前字符
c w 修改到某个单词的结尾位置
c 3 w 修改到第三个单词的结尾位置
c b 修改到某个单词的开始位置
c W 修改到某个以空格作为分隔符的单词的结尾位置
c B 修改到某个以空格作为分隔符的单词的开始位置
c 7 B 修改到前面7个以空格作为分隔符的单词的开始位置
c 0 修改到某行的结尾位置
c) 修改到某个语句的结尾位置
c 4) 修改到第四个语句的结尾位置
c( 修改到某个语句的开始位置
c } 修改到某个段落的结尾位置
c { 修改到某个段落的开始位置
c 7 { 修改到当前段落起始位置之前的第7个段落位置
c tc 修改当前行直到下一个字符c所出现位置之间的内容
C 修改到某一行的结尾
c c 修改当前行
5 c c 修改从当前行所开始的5行内容
.重复上一次修改!
表8-4 替换命令
替换命令操作
s 将当前字符替换为一个或多个字符
S 将当前行替换为一个或多个字符
5 s 将从当前字符开始的5个字符替换为一个或多个字符
vi替换使用规则:
:g/s1/s/s2/s3/g
第一个g表示对每一个包括s1的行都进行替换,第二个g表示对每一行包括s1的行所有的s2都用s3替换
s表示替换,s2是要被替换的字符串,他可以和s1相同(如果相同的话用//代替),s3是替换字符串
16.
fx
往右移动到 x 字符上
Fx
往左移动到 x 字符上
tx
往右移动到 x 字符前
Tx
往左移动到 x 字符后
(注意:以上四个命令中,其中x是键入的字符)
;
分号,配合 f 和 t 使用,重复一次
,
逗号,配合 f 和 t 使用,反方向重复一次
如果不结合;与,很不好用,nfx为移到第n个x字符上
17. vi 环境选项 Solaris ksh
noautoindent nomodelines noshowmode
autoprint nonumber noslowopen
noautowrite nonovice tabstop=8
nobeautify nooptimize taglength=0
directory=/var/tmp paragraphs=IPLPPPQPP LIpplpipnpbtags=tags /usr/lib/tags
noedcompatible prompt tagstack
noerrorbells noreadonly term=vt100
noexrc redraw noterse
flash remap timeout
hardtabs=8 report=5 ttytype=vt100
noignorecase scroll=11 warn
nolisp sections=NHSHH HUuhsh+c window=23
nolist shell=/bin/ksh wrapscan
magic shiftwidth=8 wrapmargin=0
mesg noshowmatch nowriteany
For C-Shell:
setenv EXINIT "set nu"
For Bourne or Korn Shell:
EXINIT="set nu"; export EXINIT
For Korn Shell Only (alternate method):
typeset -x EXINIT="set nu"
在 .profile 里设置 vi 的环境选项 , 以上均测试过
以上的查阅vim帮助文档,太多了不一一解释了
18.标记文本
mchar 用字母char标记当前光标的位置
`char 移至char所标记处
'char 移至char标记所在行的开头处
" 移至当前行上一次所在位置(在光标移动之后)――一个双引号
'' 移至当前行上第一次所在位置的行的开头处(在光标移动之后)――两个单引号0.
头两个和最后一个就已经挺好用的了
19.
同时vi多个文件时,CTRL-SHIFT-6回到上一个文件,在本次vi的文件和上次vi的文件之间切换。
但是我发现一个BUG:在用CTRL-SHIFT-6切换到上一个文件后,用:args查看多文件vi状态时,
屏幕底部仍然显示目前vi的是刚才的文件。
(在HP-UX,Solaris,AIX上通过)
也可以使用:
:e#
进行切换
ctrl+shift+6=:e#回到上一个文件,哇哈哈,终于找到了,对应的是:next简写为:n到下个文件,用files貌似更能看清当前打开的文件列表
20.
sco 下VI 要在文本前同样的字符加用
%s/^/要加的内容/g 要在文本后同样的字符加
%s/$/要加的内容/g
汗,这样利用查找替换
21.
如何去掉文本中的 ^M 硬回车?不必用binary传回去再ascii传回来的方式,用shell或者unix语句实现。
cat filename |tr -d '\015' >newfile
不同的unix系统还存在一些其他不同的命令,如:doscp
sed 也可以实现这个功能.
dos2unix filename filename2
反之
unix2dos filename filename2
在vi 中用:$s/^M//g
^是crtl-V crtl-M
我常用的是dos2unix
22.如何在"unix命令行"下将一个文件的某字符串用另一个串换掉
sed 's/string1/string2/gp' file1 > file2
23.将/etc/hosts下所有的地址都ping 2次
1 #/usr/bin/sh
2 #grad /etc/hosts and ping each address
3 cat /etc/hosts|grep -v '^#' | while read LINE
4 do
5 ADDR=`awk '{print $1}'`
6 for MACHINE in $ADDR
7 do
8 ping $MACHINE -n 2
9 done
10 done
24.
到前一个函数[[ ,到下一个函数]] ,括号配对% ,交叉参考Ctrl_] (事先用ctags做索引),回来用e# ` 编辑一个函数:vi -t 函数名 ,编辑加密文本vi -X
%括号匹配我介绍过,:e#回来可以用ctrl+o替代,更优雅
25.
在插入模式下ctrl+p,自动补齐剩余单词,以赖规则:tags,以有的单词等等
还ctrl_n呢
例一、两个常用的指令序列
xp 左右交换光标处两字符的位置。
ddp 上下交换光标处两行的位置。
例二、重复输入同一字符
有时,我们可能想多次输入同一字符,VIM的插入功能可以很好的完成这项工作
命令 80i=^ESC 一次可以输入80个字符= ,当然,80a=^ESC 也可以完成上述功能。
请注意:此处的^ESC表示键盘左上方上的ESC键。
例三、将两个文本数据文件按行逐条合并,并给出标尺
数据文件1内容如下:
1-----
2-----
3-----
数据文件2内容如下:
1=====
2=====
3=====
要求的结果如下:
|--------1---------2---------3---------4---------5
1-----
1=====
|--------1---------2---------3---------4---------5
2-----
2=====
|--------1---------2---------3---------4---------5
3-----
3=====
也许您会说,这还不简单,无非是反复拷贝、粘贴,任何一款文本编辑器都能完成上述功能。可是,如果这两个文件都很大,每个文件都成千上万行,恐怕简单的拷贝、粘贴就难以胜任了。因此,我们所关心的,是找到一种行之有效的方法,把枯燥乏味的工作留给计算机,我们只需发布指令。为达到此目的,请按以下步骤执行:
㈠、将两文件合并,结果如下
1-----
2-----
3-----
1=====
2=====
3=====
㈡、在两文件头尾相接的地方插入标志行,用以区分两个文件,本文采用的是一整行!字符
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
1=====
2=====
3=====
㈢、在标志行的下方输入标尺
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
㈣、执行宏命令脚本merge_2r.vim,即在VIM编辑器中按如下键 :so merge_2r.vim 回车
㈤、按下键盘上的=键,执行的结果如下
|--------1---------2---------3---------4---------5
1-----
1=====
|--------1---------2---------3---------4---------5
2-----
2=====
|--------1---------2---------3---------4---------5
3-----
3=====
|--------1---------2---------3---------4---------5
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
㈥、将最后三行删除,即可得到我们需要的结果
|--------1---------2---------3---------4---------5
1-----
1=====
|--------1---------2---------3---------4---------5
2-----
2=====
|--------1---------2---------3---------4---------5
3-----
3=====
怎么样,简单吗?请大家自己实际尝试一下。下面,我来详细讲解宏命令脚本merge_2r.vim 。
该脚本内容如下:
"--------------------------------------------------------------------
"Macro Function : Merge File1 And File2,Have Ruler in every record
" Date : 2001/12/01
" Author : Yan Shi
"--------------------------------------------------------------------
"1-----
"2----- } Sample File1
"3-----
"!!!!!!!!!!!!!!!!!!!!!!!! Flag Row
"|--------1---------2---------3---------4---------5 Ruler
"1=====
"2===== } Sample File2
"3=====
"--------------------------------------------------------------------
:1
:map = ma/!!!!!^M+:.co 'a-1^M/!!!!!^M2+:.m'a^M+=
前14行每行都以"开始,表明该行是注释行,实际并不执行,只是方便读者阅读,只有最后两行才是真正的代码行。请注意:本例中的^M表示键盘上的回车键,并非^和M两个字符。为了讲述清楚,我把命令行分解开,逐一说明。
首先将第一行置为当前行,然后执行map命令,将一大串VIM指令映像给字符=。这一大串VIM指令共分9步执行:
ma 将数据文件一的第一行标记为a
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
/!!!!!^M 找到标志行,置为当前行
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
+ 光标下移一行,即把标尺行置为当前行
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
:.co 'a-1^M 把标尺行复制到标记行(数据文件一的第一行)的上方
|--------1---------2---------3---------4---------5
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
/!!!!!^M 再次找到标志行,置为当前行
|--------1---------2---------3---------4---------5
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
2+ 光标下移2行,即数据文件二的第一行置为当前行
|--------1---------2---------3---------4---------5
1-----
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
1=====
2=====
3=====
:.m'a^M 把数据文件二的第一行移至标记行的下方
|--------1---------2---------3---------4---------5
1-----
1=====
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
2=====
3=====
+ 光标下移一行,即数据文件一的第二行置为当前行
|--------1---------2---------3---------4---------5
1-----
1=====
2-----
3-----
!!!!!!!!!!!!!!!!!!!!!!!!
|--------1---------2---------3---------4---------5
2=====
3=====
= 这一步很关键,是典型的递归调用,重复完成以上步骤
例四、在文件中置入行号
工作中,我们有时希望把行号置入文件中,而VIM提供的功能 :set nu 只能显示行号,不能编辑或将其置入文件当中,下面的宏命令脚本row_num.vim可以完成此项功能。
"------------------------------------------
"Macro Function : Source File Add Row_Num
" Date : 2001/12/01
" Author : Yan Shi
"------------------------------------------
:%s/^/^I/
:$
:let end=line(".")
:1
"------------------------------------------
:let num=1
:while num<=end
:let line=getline(".")
:let temp=substitute(line,$,num,"")
:call setline(".",temp)
:+
:let num=num+1
:endwhile
"------------------------------------------
请注意:本例中的^I表示键盘上的TAB键,并非^和I两个字符。下面,我针对该宏命令脚本逐一讲解。
:%s/^/^I/ 每一行的行首添加一个TAB字符
:$ 到文件的末行
:let end=line(".") 末行的行号 ==〉变量 END,函数line的功能是取得指定行的行号,此处参数"."表示当前行
:1 到文件的首行
"------------------------------------------
:let num=1 1 ==〉计数器
:while num<=end
:let line=getline(".") 取当前行的内容 ==〉变量 LINE
:let line=substitute(line,$,num,"") 在变量 LINE 的前面置入行号
:call setline(".",line) 将变量 LINE 的内容写回当前行
:+ 下移一行
:let num=num+1 计数器加一
:endwhile 循环执行,直到文件结束
"------------------------------------------
真正的高级技巧,不过貌似太高了,一般用不着……
有关正则表达式的使用
UNIX/LINUX 下的很多工具之所以强大、灵活,关键是因为有了正则文法和元字符,这也是VIM乃至UNIX/LINUX系统的精华所在。正因为使用灵活,因此掌握起来比较吃力,如果不是真正理解,实际运用中会出现千奇百怪的错误。因此,有必要对这部分知识多花些气力。下面结合具体实例讲解。
例五、有一文件,包含某外企的中国员工的资料,首先是姓名,然后是两个空格,其次是15位身份证号码。
zhang.fei 430759701022003
diao.chan 651302801225012
guan.yu 342869680413001
xi.shi 120638780214006
liu.bei 210324650708001
现在,有以下问题需要解决:
按照外国人的习惯,应该是名在前,姓在后。因此,文件中的姓名字段需要修改。
姓与名的首字母应该大写。
根据身份证号码,还可以判断出生年月日,将其作为一个新字段添加。
根据身份证号码,可以判断出性别。若为男性,添加male,若为女性,添加female 。
将男女员工分开,男员工在前,女员工在后。
将各字段数据左对齐
最终结果如下:
Fei.Zhang 430759701022003 1970/10/22 male
Yu.Guan 342869680413001 1968/04/13 male
Bei.Liu 210324650708001 1965/07/08 male
-----------------------------------------------
Chan.Diao 651302801225012 1980/12/25 female
Shi.Xi 120638780214006 1978/02/14 female
为了完成上述功能,只需执行脚本employee.vim ,使用方法为 :so employee.vim 回车即可。
脚本内容如下:
:%s/ / /
:%s/\(............\)\( *\)/\1/
:%s/\([A-Za-z][A-Za-z]*\)\(\.\)\([A-Za-z][A-Za-z]*\)/\u\3\2\u\1/
:%s/$/ xxxxxx/
:%s/\([0-9]\{6}\)\([0-9]\{6}\)\([0-9]\{3}\) \(xxxxxx\)/\1\2\3 \2/
:%s/\(..\)\(..\)\(..\)$/19\1\/\2\/\3
:%s/$/ xxxxxx/
:%s/\([0-9]\{14}[13579]\)\(.*\)\(xxxxxx\)/\1\2male /
:%s/\([0-9]\{14}[02468]\)\(.*\)\(xxxxxx\)/\1\2female/
:$
:s/.*/&^M-----------------------------------------------
:g/female/.m$
在这个脚本中,使用了大量的正则表达式,这里仅对涉及到的正则表达式做一简要介绍。有关正则表达式的内容相当多,本文不可能占用大量篇幅叙述,请大家谅解。
% 地址范围符号,代表文件中的所有行,作用等同于地址范围 1,$
. 与任意单字符(换行符除外)匹配,例如 y.s 可以匹配 yas y.s 或 y s 等等。
* 与前一字符的0次或多次出现匹配,例如 y*s 可以匹配 yys yyyyys 或 s 等等。
$ 与行尾匹配。
& 代表模式匹配中出现的字符串,例如 s/abc/&def 是把当前行的abc替换成abcdef 。
[] 匹配[]中出现的字符,例如[abc]匹配字符 a,b 或 c ,[a-zA-Z]匹配所有的英文字符。
\( \) \(和\)之间出现的内容可以由\num来替代。
\1\2\3 替代\(和\)之间出现的内容。
\u 将后续字符串的首字母大写。
\{num} 与前一字符的num次出现匹配。
现在,我们对脚本逐条讲解,希望能帮助大家理解正则文法。
⑴:%s/ / /
将文件中每行出现的2个空格替换为10个空格。
zhang.fei 430759701022003
diao.chan 651302801225012
guan.yu 342869680413001
xi.shi 120638780214006
liu.bei 210324650708001
⑵:%s/\(............\)\( *\)/\1/
保留行首的12个字符,将其余的空格删除,这样,前两个字段就对齐了。
zhang.fei 430759701022003
diao.chan 651302801225012
guan.yu 342869680413001
xi.shi 120638780214006
liu.bei 210324650708001
⑶:%s/\([A-Za-z][A-Za-z]*\)\(\.\)\([A-Za-z][A-Za-z]*\)/\u\3\2\u\1/
将文件中每行出现的雇员姓名互换,并将首字母大写。
Fei.Zhang 430759701022003
Chan.Diao 651302801225012
Yu.Guan 342869680413001
Shi.Xi 120638780214006
Bei.Liu 210324650708001
⑷:%s/$/ xxxxxx/
在每一行的行尾添加2个空格和6个x
Fei.Zhang 430759701022003 xxxxxx
Chan.Diao 651302801225012 xxxxxx
Yu.Guan 342869680413001 xxxxxx
Shi.Xi 120638780214006 xxxxxx
Bei.Liu 210324650708001 xxxxxx
⑸:%s/\([0-9]\{6}\)\([0-9]\{6}\)\([0-9]\{3}\) \(xxxxxx\)/\1\2\3 \2/
将xxxxxx替换成出生年月日。
Fei.Zhang 430759701022003 701022
Chan.Diao 651302801225012 801225
Yu.Guan 342869680413001 680413
Shi.Xi 120638780214006 780214
Bei.Liu 210324650708001 650708
⑹:%s/\(..\)\(..\)\(..\)$/19\1\/\2\/\3
将年月日用/字符分隔,并在年前添加19。
Fei.Zhang 430759701022003 1970/10/22
Chan.Diao 651302801225012 1980/12/25
Yu.Guan 342869680413001 1968/04/13
Shi.Xi 120638780214006 1978/02/14
Bei.Liu 210324650708001 1965/07/08
⑺:%s/$/ xxxxxx/
在每一行的行尾添加2个空格和6个x
Fei.Zhang 430759701022003 1970/10/22 xxxxxx
Chan.Diao 651302801225012 1980/12/25 xxxxxx
Yu.Guan 342869680413001 1968/04/13 xxxxxx
Shi.Xi 120638780214006 1978/02/14 xxxxxx
Bei.Liu 210324650708001 1965/07/08 xxxxxx
⑻:%s/\([0-9]\{14}[13579]\)\(.*\)\(xxxxxx\)/\1\2male /
身份证号码末位是奇数的,将xxxxxx替换成male
Fei.Zhang 430759701022003 1970/10/22 male
Chan.Diao 651302801225012 1980/12/25 xxxxxx
Yu.Guan 342869680413001 1968/04/13 male
Shi.Xi 120638780214006 1978/02/14 xxxxxx
Bei.Liu 210324650708001 1965/07/08 male
⑼:%s/\([0-9]\{14}[02468]\)\(.*\)\(xxxxxx\)/\1\2female/
身份证号码末位是偶数的,将xxxxxx替换成female
Fei.Zhang 430759701022003 1970/10/22 male
Chan.Diao 651302801225012 1980/12/25 female
Yu.Guan 342869680413001 1968/04/13 male
Shi.Xi 120638780214006 1978/02/14 female
Bei.Liu 210324650708001 1965/07/08 male
⑽:$ 到文件的最后一行
⑾:s/.*/&^M-----------------------------------------------
在文件的最末行插入一行 "-" 字符。
Fei.Zhang 430759701022003 1970/10/22 male
Chan.Diao 651302801225012 1980/12/25 female
Yu.Guan 342869680413001 1968/04/13 male
Shi.Xi 120638780214006 1978/02/14 female
Bei.Liu 210324650708001 1965/07/08 male
-----------------------------------------------
⑿:g/female/.m$
将所有的女员工记录移至文件尾。
Fei.Zhang 430759701022003 1970/10/22 male
Yu.Guan 342869680413001 1968/04/13 male
Bei.Liu 210324650708001 1965/07/08 male
-----------------------------------------------
Chan.Diao 651302801225012 1980/12/25 female
Shi.Xi 120638780214006 1978/02/14 female
笔者目前正在为某外资公司从事大型机(IBM S/390)的软件开发,一切工作都在TSO环境中进行。为了对编写的程序进行测试,必须准备测试数据。有过大型机开发经验的人会知道,通过TSO,输入字符型数据还可以,如果要输入16进制数据,操作起来很麻烦。因为16进制数是纵向排列的,输入时既不方便,又很容易错位。怎么解决呢?我尝试了几种办法,实际证明,用VIM最方便。
例六、下列数据 1234567890ABCDEF ,将其变成 13579ACE 24680BDF 的形式,这样,数据就可以很方便的粘贴到TSO环境中了。
下面给出宏命令脚本change_d.vim
"----------------------------------------------------
"Macro Function : Convert Char Arrange Direction
"
" Sample : 40 50 60 ==> 4 5 6
" 0 0 0
" Date : 2001/12/01
" Author : Yan Shi
"----------------------------------------------------
:s/.*/&^M/
:1
:map = malx+$p-`al=
说明如下:
⑴ :s/.*/&^M/ 在数据行下方添加一空行。
⑵ :1 回到文件的首行的首字符。
⑶ :map = malx+$p-`al= 将一大串VIM命令映像给字符=
① ma 将首字符标记为a
② l 光标右移一个字符
③ x 删除光标处字符
④ + 移至下一行
⑤ $ 到行尾
⑥ p 将删除的字符粘贴
⑦ - 回至上一行
⑧ `a 返回到标记字符处
⑨ l 光标右移一个字符
⑩ = 递归调用,重复以上步骤,直到将该行所有的数据处理完。
有人说,你是个好人。我不知道好人和坏人的界定是什么。
看了晃晃悠悠的blog,感觉自己真的很落后,甚至保守。思想上有太多的束缚,需要事情不想去想,不敢想,也想不到。连大嫂都说,我是比较传统的男人。因此,先嘲笑一下自己,嘲笑自己工作在有着最新锐客户群的互联网WEB2.0公司。
与大哥大嫂晃晃悠悠不一样,我来自农村。
农村人从小就朴实,更不懂得追求新锐;从小就懂得劳动的艰辛,懂得“粒粒皆辛苦”的真真涵义;从小就懂得同情弱者,懂得帮助他人。农村人从小就羡慕城里人看起来丰富的物质生活,羡慕他们不用面朝黄土背朝天的种地就能挣钱的工作,羡慕他们下雨时不用光着脚丫子踩着可能有刺的泥土……,因此,为了过上和城里人一样的生活,农村的小孩从小就只有一个梦想,一个很简单的梦想:走出农村。
农村人考上大学了,从此变成了“非农”,家里亲戚都来祝贺。大学里,农村孩子与城里的孩子生活在了一起。开始他们相互帮助,农村孩子一个月200元生活费,省着花,尽量少出去吃喝,因为他们知道省下的100元钱,还可以留给家里买化肥;因此,城里孩子开始瞧不起农村孩子,说他们小气;农村孩子也看不惯城里孩子,说他们是花花公子。
毕业了,农村人进城工作了。他们努力工作,努力学习。他们不会去酒吧,不会去歌舞厅……,他们还是省吃俭用,因为他们知道,有钱了但不能忘本。因此,在真正的城里人看来他们显得落后,显得老土。因此,非农的他们还是农村人。
看了晃晃悠悠的blog,感觉自己真的很落后,甚至保守。思想上有太多的束缚,需要事情不想去想,不敢想,也想不到。连大嫂都说,我是比较传统的男人。因此,先嘲笑一下自己,嘲笑自己工作在有着最新锐客户群的互联网WEB2.0公司。
与大哥大嫂晃晃悠悠不一样,我来自农村。
农村人从小就朴实,更不懂得追求新锐;从小就懂得劳动的艰辛,懂得“粒粒皆辛苦”的真真涵义;从小就懂得同情弱者,懂得帮助他人。农村人从小就羡慕城里人看起来丰富的物质生活,羡慕他们不用面朝黄土背朝天的种地就能挣钱的工作,羡慕他们下雨时不用光着脚丫子踩着可能有刺的泥土……,因此,为了过上和城里人一样的生活,农村的小孩从小就只有一个梦想,一个很简单的梦想:走出农村。
农村人考上大学了,从此变成了“非农”,家里亲戚都来祝贺。大学里,农村孩子与城里的孩子生活在了一起。开始他们相互帮助,农村孩子一个月200元生活费,省着花,尽量少出去吃喝,因为他们知道省下的100元钱,还可以留给家里买化肥;因此,城里孩子开始瞧不起农村孩子,说他们小气;农村孩子也看不惯城里孩子,说他们是花花公子。
毕业了,农村人进城工作了。他们努力工作,努力学习。他们不会去酒吧,不会去歌舞厅……,他们还是省吃俭用,因为他们知道,有钱了但不能忘本。因此,在真正的城里人看来他们显得落后,显得老土。因此,非农的他们还是农村人。
当汪峰的《北京北京》忧郁苍凉的旋律击打我的耳膜直至内心深处的时候,我能感觉到眼角有一点湿润。 “ 人们在挣扎中相互告慰和拥抱, 寻找着追逐着奄奄一息的碎梦”,和多年以前的那首《晚安,北京》一样,汪峰最直接地触摸到了城市里瓢泊的人们无奈和失落交织,希望与失落并存的心灵深处,让人为之动容为之叹息。
每座城市都有生命,它们象亲人一样张开怀抱,包容着我们所有的情感;每座城市都有故事,不同的街景里演绎着许多相似的故事情节,悄然在记忆中凝固。城市和人的话题是现代社会最牵动人心的一个话题。北京如是,我们这座小城又何尝不是。
我向往过北京,我曾在过北京,我离开了北京。尽管最终没有在北京飘的经历,但那种独自一人在一个陌生的城市打拼的心路历程是相似的。在每一个打拼的地方,所有的人都会留下眼泪、欢笑、失落、痛苦还有希望和彷惶。在光荣和梦想一步步近了又一步步远了的爱恨交织的过往岁月里,我们一直在路上,奔波、徘徊、犹豫、挣扎、憧憬、向往……
《北京 北京》
汪峰
当我走在这里的每一条街道
我的心似乎从来都不能平静
除了发动机的轰鸣和电气之音
我似乎听到了它烛骨般的心跳
我在这里欢笑我在这里哭泣
我在这里活着也在这儿死去
我在这儿祈祷 我在这儿迷惘
我在这儿寻找也在这儿失去
北京 北京
咖啡馆与广场有三个街区
就象霓虹灯到月亮的距离
人们在挣扎中相互告慰和拥抱
寻找着追逐着奄奄一息的碎梦
我们在这里欢笑
我们在这里哭泣
我们在这里活着
也在这儿死去
我们在这儿祈祷
我们在这儿迷惘
我们在这儿寻找
也在这儿失去
北京 北京
如果有一天我不得不离去
我希望人们把我埋在这里
在这儿我能感觉到我的存在
在这儿有太多让我眷恋的东西
我在这里欢笑
我在这里哭泣
我在这里活着
也在这儿死去
我在这儿祈祷
我在这儿迷惘
我在这儿寻找
也在这儿失去
北京 北京
晚安,北京
汪峰
我将在今夜的雨中睡去
伴着国产压路机的声响
伴着伤口迸裂的巨响
在今夜的雨中睡去
晚安北京
晚安所有未眠的人们
风会随子夜的钟声北去
带着街上乞讨的男孩
带着路旁破碎的轮胎
随子夜的钟声北去
晚安北京
晚安所有未眠的人们
晚安北京
晚安所有孤独的人们
我曾在许多的夜晚失眠
倒在城市梦幻的空间
倒在自我虚设的洞里
在疯狂的边缘失眠
晚安北京
晚安所有未眠的人们
我沉得越来越有些疲倦
听着隔壁提琴的抽泣
喝着世事煮沸的肉汤
越来越有些疲倦
晚安北京
晚安所有未眠的人们
晚安北京
晚安所有孤独的人们
每座城市都有生命,它们象亲人一样张开怀抱,包容着我们所有的情感;每座城市都有故事,不同的街景里演绎着许多相似的故事情节,悄然在记忆中凝固。城市和人的话题是现代社会最牵动人心的一个话题。北京如是,我们这座小城又何尝不是。
我向往过北京,我曾在过北京,我离开了北京。尽管最终没有在北京飘的经历,但那种独自一人在一个陌生的城市打拼的心路历程是相似的。在每一个打拼的地方,所有的人都会留下眼泪、欢笑、失落、痛苦还有希望和彷惶。在光荣和梦想一步步近了又一步步远了的爱恨交织的过往岁月里,我们一直在路上,奔波、徘徊、犹豫、挣扎、憧憬、向往……
《北京 北京》
汪峰
当我走在这里的每一条街道
我的心似乎从来都不能平静
除了发动机的轰鸣和电气之音
我似乎听到了它烛骨般的心跳
我在这里欢笑我在这里哭泣
我在这里活着也在这儿死去
我在这儿祈祷 我在这儿迷惘
我在这儿寻找也在这儿失去
北京 北京
咖啡馆与广场有三个街区
就象霓虹灯到月亮的距离
人们在挣扎中相互告慰和拥抱
寻找着追逐着奄奄一息的碎梦
我们在这里欢笑
我们在这里哭泣
我们在这里活着
也在这儿死去
我们在这儿祈祷
我们在这儿迷惘
我们在这儿寻找
也在这儿失去
北京 北京
如果有一天我不得不离去
我希望人们把我埋在这里
在这儿我能感觉到我的存在
在这儿有太多让我眷恋的东西
我在这里欢笑
我在这里哭泣
我在这里活着
也在这儿死去
我在这儿祈祷
我在这儿迷惘
我在这儿寻找
也在这儿失去
北京 北京
晚安,北京
汪峰
我将在今夜的雨中睡去
伴着国产压路机的声响
伴着伤口迸裂的巨响
在今夜的雨中睡去
晚安北京
晚安所有未眠的人们
风会随子夜的钟声北去
带着街上乞讨的男孩
带着路旁破碎的轮胎
随子夜的钟声北去
晚安北京
晚安所有未眠的人们
晚安北京
晚安所有孤独的人们
我曾在许多的夜晚失眠
倒在城市梦幻的空间
倒在自我虚设的洞里
在疯狂的边缘失眠
晚安北京
晚安所有未眠的人们
我沉得越来越有些疲倦
听着隔壁提琴的抽泣
喝着世事煮沸的肉汤
越来越有些疲倦
晚安北京
晚安所有未眠的人们
晚安北京
晚安所有孤独的人们
#include <unistd.h>
int daemon(int nochdir,int noclose)
在创建精灵进程的时候,往往需要将精灵进程的工作目录修改为"/"根目录
并且将标准输入,输出和错误输出重定向到/dev/null
daemon的作用就是当参数nochdir为0时,将根目录修改为工作目录
noclose为0时,做输入,输出以及错误输出重定向到/dev/null
执行成功返回0
错误返回-1
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
转载请注明链接: http://hi.baidu.com/kkernel/blog/item/b1a55c1ea7d37d034134172f.html
守护进程最重要的特性是后台运行;其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的;最后,守护进程的启动方式有其特殊之处------它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。
总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别,因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。
其实,在标准的Linux系统环境下的库函数中已经提供了daemon()函数,如下:
[guowenxue@centos6 mobiled]$ man daemon
DAEMON(3) Linux Programmer’s Manual DAEMON(3)
NAME
daemon - run in the background
SYNOPSIS
#include <unistd.h>
int daemon(int nochdir, int noclose);
下面我们将针对Linux平台和uClinux平台来讲解守护编程的编写细节:
标准Linux下的守护进程的编写:
void daemonize_linux(int nochdir, int noclose)
{
int retval, fd;
int i;
在创建daemon进程时,父进程在子进程退出之前退出,这时子进程会被Init进程(pid=1)领养,所以这里如果进程的parent pid是1的话,说明他已经在后台运行了。
/* already a daemon */
if (1 == getppid())
return;
/* fork error */
retval = fork();
if (retval < 0)
_exit(0);
为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止, 让Daemon在子进程中后台执行。如果这时父进程在子进程前退出,那么init进程将领养子进程。
/* parent process exit */
if (retval > 0)
_exit(0);
这里主要是脱离控制终端,登录会话和进程组 。必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于 一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。 控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们 ,使之不受它们的影响。方法是在这里调用setsid()使进程成为会话组长:
/* obtain a new process session group */
setsid();
if (!noclose)
{
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源, 造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:
/* close all descriptors */
for (i = getdtablesize(); i >= 0; --i)
close(i);
因为子进程在后台运行,他们不需要标准输入,标准输出,标准出错.这时我们可以将他们全部重定向到/dev/null文件下去
fd = open("dev/null", O_RDWR); /* Redirect Standard input [0] to /dev/null */
dup(fd); /* Redirect Standard output [1] to /dev/null */
dup(fd); /* Redirect Standard error [2] to /dev/null */
}
进程从创建它的父进程那里继承了文件创建掩码。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩码掩模清除:
umask(0);
进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录
改变到根目录 。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmp
if (!nochdir)
chdir("/");
return;
}
uCLinux下的守护进程编写:
/*由于uClinux不支持fork(),只支持vfork()的特性,同时在一些库中也没有提供像标准linux系统中能提供的daemon()函数,所以我们必须自己来实现uclinux操作系统下的daemon()函数:
这里我们主要是通过,第一次vfork()让父进程结束,同时让子进程再次运行一次代码来实现。所以这里的argc和argv参数主要使用在子进程中再次以同样的参数来运行自己。
*/
void daemon_ucLinux (int noclose, int argc, char **argv)
{
int iRet = 0;
int fd = -1;
5, 父进程是init进程,说明已在后台运行,这是孙子进程加载运行程序了
if (1 == getppid ()) /* already a daemon*/
{
setsid (); /* set new process group */
if (!noclose)
{
/*Close all the file description.*/
for (iRet = getdtablesize (); 0 <= iRet; --iRet)
{
fd = iRet;
close(fd);
}
fd = open("/dev/null", O_RDWR); /*Redirect Standard input [0] to /dev/null */
dup (fd); /*Redirect Standard output [1] to /dev/null */
dup (fd); /*Redirect Standard error [2] to /dev/null */
}
umask (027); /* set newly created file mode mask */
chdir("/"); // change running directory
return ;
}
else /*1, 第一次启动该程序的时候,系统将运行到这里*/
{
iRet = vfork ();
/*2, 通过vfrok()创建子进程,由于vfork()会导致父进程阻塞直到子进程推出或调用exec()函数,所以这里父进程并没有结束*/
if (0 > iRet) _exit (1); /* fork error */
if (0 < iRet) _exit (0); /* parent exists */
3, 再次调用vfork()产生孙子进程,再让子进程结束,这时上面的父进程就会退出,释放终端,子进程则阻塞在vfork()上,程序跑到后台了。
/*Child process(daemon) continue and fork again.*/
iRet = vfork ();
if (0 > iRet) _exit (1); /* fork error */
if (0 < iRet) _exit (0); /* parent exists */
4, 孙子进程调用execv()函数重新装载程序运行,子进程将不再阻塞在vfork()上,这时子进程也退出了。孙子进程被init进程领养,他的Parent PID将变成1
/* Parent exit, so child process parent pid should be init process(pid is 1) */
execv (argv[0], argv); /*Run the pograme again.*/
_exit (1); /*If execv() returned, then something error happend.*/
}
}
http://fanqiang.chinaunix.net/program/netpro/2001-04-19/1904.shtml
int daemon(int nochdir,int noclose)
在创建精灵进程的时候,往往需要将精灵进程的工作目录修改为"/"根目录
并且将标准输入,输出和错误输出重定向到/dev/null
daemon的作用就是当参数nochdir为0时,将根目录修改为工作目录
noclose为0时,做输入,输出以及错误输出重定向到/dev/null
执行成功返回0
错误返回-1
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
转载请注明链接: http://hi.baidu.com/kkernel/blog/item/b1a55c1ea7d37d034134172f.html
守护进程最重要的特性是后台运行;其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的;最后,守护进程的启动方式有其特殊之处------它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。
总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别,因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。
其实,在标准的Linux系统环境下的库函数中已经提供了daemon()函数,如下:
[guowenxue@centos6 mobiled]$ man daemon
DAEMON(3) Linux Programmer’s Manual DAEMON(3)
NAME
daemon - run in the background
SYNOPSIS
#include <unistd.h>
int daemon(int nochdir, int noclose);
下面我们将针对Linux平台和uClinux平台来讲解守护编程的编写细节:
标准Linux下的守护进程的编写:
void daemonize_linux(int nochdir, int noclose)
{
int retval, fd;
int i;
在创建daemon进程时,父进程在子进程退出之前退出,这时子进程会被Init进程(pid=1)领养,所以这里如果进程的parent pid是1的话,说明他已经在后台运行了。
/* already a daemon */
if (1 == getppid())
return;
/* fork error */
retval = fork();
if (retval < 0)
_exit(0);
为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止, 让Daemon在子进程中后台执行。如果这时父进程在子进程前退出,那么init进程将领养子进程。
/* parent process exit */
if (retval > 0)
_exit(0);
这里主要是脱离控制终端,登录会话和进程组 。必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于 一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。 控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们 ,使之不受它们的影响。方法是在这里调用setsid()使进程成为会话组长:
/* obtain a new process session group */
setsid();
if (!noclose)
{
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源, 造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:
/* close all descriptors */
for (i = getdtablesize(); i >= 0; --i)
close(i);
因为子进程在后台运行,他们不需要标准输入,标准输出,标准出错.这时我们可以将他们全部重定向到/dev/null文件下去
fd = open("dev/null", O_RDWR); /* Redirect Standard input [0] to /dev/null */
dup(fd); /* Redirect Standard output [1] to /dev/null */
dup(fd); /* Redirect Standard error [2] to /dev/null */
}
进程从创建它的父进程那里继承了文件创建掩码。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩码掩模清除:
umask(0);
进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录
改变到根目录 。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmp
if (!nochdir)
chdir("/");
return;
}
uCLinux下的守护进程编写:
/*由于uClinux不支持fork(),只支持vfork()的特性,同时在一些库中也没有提供像标准linux系统中能提供的daemon()函数,所以我们必须自己来实现uclinux操作系统下的daemon()函数:
这里我们主要是通过,第一次vfork()让父进程结束,同时让子进程再次运行一次代码来实现。所以这里的argc和argv参数主要使用在子进程中再次以同样的参数来运行自己。
*/
void daemon_ucLinux (int noclose, int argc, char **argv)
{
int iRet = 0;
int fd = -1;
5, 父进程是init进程,说明已在后台运行,这是孙子进程加载运行程序了
if (1 == getppid ()) /* already a daemon*/
{
setsid (); /* set new process group */
if (!noclose)
{
/*Close all the file description.*/
for (iRet = getdtablesize (); 0 <= iRet; --iRet)
{
fd = iRet;
close(fd);
}
fd = open("/dev/null", O_RDWR); /*Redirect Standard input [0] to /dev/null */
dup (fd); /*Redirect Standard output [1] to /dev/null */
dup (fd); /*Redirect Standard error [2] to /dev/null */
}
umask (027); /* set newly created file mode mask */
chdir("/"); // change running directory
return ;
}
else /*1, 第一次启动该程序的时候,系统将运行到这里*/
{
iRet = vfork ();
/*2, 通过vfrok()创建子进程,由于vfork()会导致父进程阻塞直到子进程推出或调用exec()函数,所以这里父进程并没有结束*/
if (0 > iRet) _exit (1); /* fork error */
if (0 < iRet) _exit (0); /* parent exists */
3, 再次调用vfork()产生孙子进程,再让子进程结束,这时上面的父进程就会退出,释放终端,子进程则阻塞在vfork()上,程序跑到后台了。
/*Child process(daemon) continue and fork again.*/
iRet = vfork ();
if (0 > iRet) _exit (1); /* fork error */
if (0 < iRet) _exit (0); /* parent exists */
4, 孙子进程调用execv()函数重新装载程序运行,子进程将不再阻塞在vfork()上,这时子进程也退出了。孙子进程被init进程领养,他的Parent PID将变成1
/* Parent exit, so child process parent pid should be init process(pid is 1) */
execv (argv[0], argv); /*Run the pograme again.*/
_exit (1); /*If execv() returned, then something error happend.*/
}
}
http://fanqiang.chinaunix.net/program/netpro/2001-04-19/1904.shtml