2010年10月9日星期六

SimpleXML介绍

SimpleXML提供了一种简单,直观的方法来处理XML。它只有一个单一类型的类,三个函数和六个方法。

使用SimpleXML

SimpleXMLElement 类是这个扩展中所有操作的核心类。可以用new关键字直接创建这种类,或是使用simplexml_load_file()或 simplexml_load_string()函数返回这种类。本文将使用清单7-1的XML文档来说明如何使用SimpleXML,将此文档命名为 sml.xml。

清单7-1 sml.xml

1.
2. 3. "http://www.oasis-open.org/docbook/xml/4.1.2/docbookx.dtd">
4.
5.
6. SimpleXML in PHP 5
7.
8. Rob
9. Richards
10.

11.
12. 2005
13. Rob Richards
14.

15.

16.
17. Using SimpleXML
18. An example DOM Tree using DocBook.
19.

20.
21. Accessing Elements
22. Elements are accessed as properties
23.
24. 25. 26. $data = '
27. content';
28. $sxe = simplexml_load_string($data);
29. var_dump($sxe);
30. ?>
31. ]]>
32.

33.

34.


创建一个SimpleXMLElement对象

使用new关键字创建

1. $xml = "Content";
2. $sxe = new SimpleXMLElement($xml);//SimpleXMLElement Object ( [node1] => Content )

使用simplexml_load_string()创建

1. $xml = "Content";
2. $sxe = simplexml_load_string($xml);//SimpleXMLElement Object ( [node1] => Content )

如何选择这两种创建SimpleXMLElement的方法呢?simplexml_load_string()提供了更多的函数,比如控制解析选项的能力。如果不需要这些额外的函数的话就可以凭个人爱好选择一种方法。

使用simplexml_load_file()从一个URI创建

1. $sxe = simplexml_load_file("filename.xml");

simplexml_load_string()和simplexml_load_file()都有一个必需的参数和可选的参数。从PHP5.1开始simplexml_load_file()多了一个用来控制解析行为的第三个参数。

1. /* Prototype for PHP 5.0 */
2. simplexml_load_file(string data [, string class_name])
3. /* Prototype for PHP 5.1 */
4. simplexml_load_file(string data [, string class_name [, int options]])

保存XML数据

与DOM扩展一样,SimpleXML也提供了一个用来输出XML内容的方法asXML()。可以用这个方法以字符串或文件形式输出这个文档或文档中的某个节点。

1. $xml = "content";
2. $sxe = new SimpleXMLElement($xml);
3. print $sxe->asXML();
4. $sxe->asXML('test.xml');

输出:

1.
2. content

访问元素节点

在SimpleXML中,可以直接通过元素的名称来访问特定的元素。

访问元素

当一个文档被载入SimpleXML时,文档被看成是一个SimpleXML对象,文档中的所有元素都被看成是该对象的属性。

1. 2. $book = simplexml_load_file('sxml.xml');
3. /* Access the bookinfo child element of the book element */
4. $bookinfo = $book->bookinfo;
5. /* Access the title child element from the bookinfo element */
6. $title = $bookinfo->title;
7. ?>

如果使用DOM来访问title,代码如下

1. $dom = new DOMDocument();
2. $dom->load('sxml.xml');
3. $book = $dom->documentElement;
4. foreach($book->childNodes as $node) {
5. if ($node->nodeName == "bookinfo") {
6. foreach($node->childNodes as $child) {
7. if ($child->nodeName == "title") {
8. $node = $child;
9. break 2;
10. }
11. }
12. }
13. }
14. if ($node) {
15. $title = $node;
16. }

显然SimpleXML对的起它的名字。

访问内容

1. 2. $book = simplexml_load_file('sxml.xml');
3. $bookinfo = $book->bookinfo;
4. $title = $bookinfo->title;
5. /* Object examined with var_dump */
6. var_dump($title);
7. /* Using print with element containing text-only content */
8. print "Title: ".$title."\n";
9. $author = $bookinfo->author;
10. /* Object examined with var_dump */
11. var_dump($author);
12. /* Using print with element containing child elements */
13. print "Author: ".$author."\n";
14. ?>

这段代码检查了两个SimpleXMLElement对象,$author和$title。两者的区别是$author元素有包含子元素而$title元素只包含一个文本节点。
输出如下:
object(SimpleXMLElement)#4 (1) {
[0]=>
string(18) “SimpleXML in PHP 5″
}
Title: SimpleXML in PHP 5
object(SimpleXMLElement)#6 (2) {
[”firstname”]=>
string(3) “Rob”
[”surname”]=>
string(8) “Richards”
}
Author:

检查输出结果可以发现,$title是一个包含有文本内容的SimpleXMLElement对象,索引0表示元素的文本内容,当打印$title时,文本内容将以字符串形式返回。

$author元素有两个子元素,从输出结果可以看出,这些子元素被看成SimpleXMLElement对象的属性,这些属性的值是它们对应的节点包含的内容。用print输出$author时,输出结果是空格和换行符。

如果一个元素无子元素,只包含文本内容,那么可以将此元素所对应的SimpleXMLElement对象视为一个字符串来使用,有些情况下,为了获得以字符串形式返回文本内容,必须执行类型转换操作:

1. $titlecontent = (string) $title;

有子元素的元素所对应的SimpleXMLElement对象将返回该对象直属的文本节点,而不是任一子元素的内容。如果用print输出$author,将得到一个27字符长度,包含空格和换行符的字符。 Neither of the child elements,firstname or surname, nor their content is returned in the string.

为了理解最后一点,可以运行一些下面这段代码:

1. $doc = new SimpleXMLElement('somesubtextthing');
2. print $doc;

使用迭代对象

SimpleXMLElement 对象在大多数情况下是可迭代的,可以用这个特性来访问文档中多个元素名一样的节点,如清单7-1中的para元素。在使用元素名作为属性来访问一个元素时,SimpleXMLElement对象不是一个单一节点的存取器,它实际上是作为属性访问的元素名节点的集合。直接利用元素名作为属性访问实际上访问访问这个集合中的第一个元素。

看下这段代码

1. $book = simplexml_load_file('sxml.xml');
2. $para = $book->chapter->para;
3. print $para."\n";
4. foreach($para AS $node) {
5. print $node."\n";
6. }

这段代码中,$para变量就是para元素的集合,实际上包含了两个元素。如果直接访问$para的话实际上是访问第一个para元素,利用迭代可以看出 $para所包含的内容是两个元素。其中CDATA节点被看做是纯文本的内容,其中包含的空格和换行符都会被如实输出。

用迭代的方式来访问所有的元素显然不太实际,有时候我们希望访问结果集中的某个特定的元素,这时可以使用从0开始的索引来访问这个结果集,例如:

1. $book = simplexml_load_file('sxml.xml');
2. $para = $book->chapter->para[1];
3. print "Content: ".$para."\n";
4. foreach($para AS $node) {
5. print "Iter Content: ".$node."\n";
6. }

运行这段代码会发现foreach循环失效了,这是因为SimpleXML知道你只是在寻找结果集中某个特定的元素,这种情况下对象是不可迭代的。

Caution:使用索引方式访问一个SimpleXMLElement对象会返回一个不可以迭代的对象,因为它是一个单一的元素而不是一个元素集。

访问未知元素

在不知道XML文档的结构的情况下可以利用SimpleXML中的children()方法来返回一个可以迭代方式访问的某个元素的所有子元素的SimpleXML对象。如:

1. $book = simplexml_load_file('sxml.xml');
2. $author = $book->bookinfo->author;
3. $children = $author->children();
4. foreach($children AS $child) {
5. print $child."\n";
6. }

上述代码用children()方法返回了author节点下的所有子元素,然后用foreach循环输出。也可以使用索引方式访问返回的子元素,如echo $children[1];。

理解PHP对象函数

SimpleXMLElement对象的属性是动态的,因为这些属性是由对象实例决定的,而不是由类本身决定的。在PHP中,可以利用get_object_vars()函数来返回某个对象的所有属性,返回的结果是一个包含属性和值的数组,如:

1. $props = get_object_vars($author);
2. foreach ($props AS $name=>$value) {
3. print $name.": ".$value."\n";
4. }

输出:
firstname: Rob
surname: Richards
这段代码访问的子元素都只包含文本内容,所以返回的数组只包含属性名和值,对于一个包含许多子节点的元素,返回的结果稍微复杂点:

1. $props = get_object_vars($book->bookinfo);
2. var_dump($props);

输出:

array(3) {
[”title”]=>
string(18) “SimpleXML in PHP 5″
[”author”]=>
object(SimpleXMLElement)#4 (2) {
[”firstname”]=>
string(3) “Rob”
[”surname”]=>
string(8) “Richards”
}
[”copyright”]=>
object(SimpleXMLElement)#5 (2) {
[”year”]=>
string(4) “2005″
[”holder”]=>
string(12) “Rob Richards”
}
}

使用DOM互操作

另一种访问未知元素的方法是使用DOM,可以将一个节点导入DOM扩展,然后使用DOM的属性和方法来处理。

1. $book = simplexml_load_file('sxml.xml');
2. $author = $book->bookinfo->author;
3. $children = $author->children();
4. foreach($children AS $child) {
5. /* Import node into DOM, and get nodeName */
6. $element = dom_import_simplexml($child);
7. $name = $element->nodeName;
8. print $name.": ".$child."\n";
9. }

将节点导入DOM扩展时并没有创建一个节点的副本(copy),而是直接访问导入的节点(JIMMY注:这个概念很重要)。

修改内容

利用SimpleXML修改元素内容非常方便,你可以改变或移除树中的某个元素,但是不能直接在树中添加一个元素。要添加一个元素,可以使用DOM的互操作性:

1. $xml = "content";
2. $sxe = new SimpleXMLElement($xml);
3. $dom = dom_import_simplexml($sxe);
4. $dom->appendChild(new DOMElement("node2", "content2"));
5. print $sxe->asXML();

输出:

1.
2. contentcontent2

编辑文本内容

可以利用SimpleXML的属性赋值方法来直接编辑一个元素的内容,要主意的是如果文档中有多个元素名一样的元素,如果没有使用索引来指定要编辑哪个元素时PHP将发出一个警告。如:

1. $book = simplexml_load_file('sxml.xml');
2. /* Modify an unspecified para element where multiple para elements exist */
3. $book->chapter->para = "Removed CDATA";

输出:
Warning: main() [/phpmanual/function.main.html]: Cannot assign to an array of nodes
(duplicate subnodes or attr detected)

必须给para指定索引告诉程序你要编辑哪个元素:

1. $book = simplexml_load_file('sxml.xml');
2. $book->chapter->para[1] = "Removed CDATA";
3. print $book->chapter->asXML();

输出:

1.
2. Acessing Elements
3. Elements are accessed as properties
4. Removed CDATA
5.


这样,第二个para元素的内容被改为Removed CDATA。如果要编辑一个在文档中唯一存在的元素可不必指定索引,直接修改。如修改title:

1. $book = simplexml_load_file('sxml.xml');
2. $book->chapter->title = "New Title";
3. $book->chapter->para[1] = "Removed CDATA";
4. print $book->chapter->asXML();

输出:

1.
2. New Title
3. Elements are accessed as properties
4. Removed CDATA
5.


强烈建议使用索引来编辑元素,除非你对文档的结果非常确定。使用索引来编辑title元素会比较安全,如$book->chapter->title[0] = “New Title”;这行代码用索引[0]指定要编辑第一个title。

编辑有子树的元素

1. $book = simplexml_load_file('sxml.xml');
2. $cholder = $book->bookinfo->copyright->holder;
3. print $cholder->asXML()."\n";
4. $book->bookinfo = "No Book Info";
5. print $book->bookinfo->asXML()."\n";
6. print $cholder->asXML()."\n";

输出:

1. Rob Richards
2. No Book Info
3. Warning: SimpleXMLElement::asXML() [/phpmanual/function.asXML.html]: Node no
4. longer exists in N:\CVS Projects\php5\Debug_TS\booksxe.php on line 7

这段代码中,首先将文档中的holder元素赋值给$cholder变量,然后打印该变量。bookinfo元素包含有title,author和 copyright子树,它的内容被字符串No Book Info代替,从bookinfo的输出结果可以看出,它的子树被清空并且被字符串代替了。接着试图再次打印$cholder变量的XML内容,程序输出一个警告,这个变量依然是一个SimpleXMLElement对象,但它所属的节点在bookinfo元素改变时已经被破坏了。

另一种情况。将bookinfo元素中的子元素用字符串 代替。

1. $book = simplexml_load_file('sxml.xml');
2. $book->bookinfo = "SimpleXML in PHP 5";
3. print $book->bookinfo->asXML()."\n";

如果你认为上述代码将bookinfo中的内容清空后再给bookinfo创建了一个子节点title,那么你错了。输出结果是
SimpleXML in PHP 5
实际上bookinfo元素的子元素都被移除了,但是新赋值的XML数据被转义成文本内容,而不是一个新的子元素。

如果想用一个子树代替另一个子树,可以利用DOM扩展:

1. $book = simplexml_load_file('sxml.xml');
2. $bookinfo = dom_import_simplexml($book->bookinfo);
3. /* 移除bookinfo元素下的所有子元素*/
4. while ($bookinfo->firstChild) {
5. $bookinfo->removeChild($bookinfo->firstChild);
6. }
7. $bookinfo->appendChild(new DOMElement("title", "SimpleXML in PHP 5"));
8. print $book->bookinfo->asXML()."\n";

输出结果:

1.
2. SimpleXML in PHP 5
3.


移除元素

可以用PHP内置函数unset()来将一个元素从树中移除。unset()的参数必须是一个SimpleXMLElement,用属性方法来访问要移除的元素。例如,从chapter节点移除title元素:

1. $book = simplexml_load_file('sxml.xml');
2. $book->chapter->para[1] = "Removed CDATA";
3. unset($book->chapter->title);
4. print $book->chapter->asXML();

上述代码执行后,chapter的结构为:

1.
2.
3. Elements are accessed as properties
4. Removed CDATA
5.


将这个结果与下面代码执行的结果想比较:

1. $book = simplexml_load_file('sxml.xml');
2. $book->chapter->para[1] = "Removed CDATA";
3. $title = $book->chapter->title;
4. unset($title);
5. print $book->chapter->asXML();

输出结果

1.
2. Acessing Elements
3. Elements are accessed as properties
4. Removed CDATA
5.


title元素没有被移除,unset函数只对$title变量作用并没有将title元素从树中移除。

在移除一个元素时必须注意,用索引来指定的特定元素不会被移除:

1. $book = simplexml_load_file('sxml.xml');
2. $book->chapter->para[1] = "Removed CDATA";
3. unset($book->chapter->title[0]);
4. print $book->chapter->asXML();

输出:

1.
2. Acessing Elements
3. Elements are accessed as properties
4. Removed CDATA
5.


如果要移除所有的para元素时可以利用下面代码:

1. $book = simplexml_load_file('sxml.xml');
2. unset($book->chapter->para);
3. print $book->chapter->asXML();

输出:

1.
2. Acessing Elements
3.
4.
5.


问题是如果你只想移除其中的一个para元素时要怎么办。这时可以再次用到DOM扩展:

1. $book = simplexml_load_file('sxml.xml');
2. $chapter = dom_import_simplexml($book->chapter);
3. $node = $chapter->lastChild;
4. while($node) {
5. if ($node->nodeName == "para") {
6. $chapter->removeChild($node);
7. $node = NULL;
8. break;
9. }
10. $node = $node->previousSibling;
11. }
12. print $book->chapter->asXML();

输出:

1.
2. Acessing Elements
3. Elements are accessed as properties
4.
5.


所幸的是PHP5.2开始已经支持删除用索引指定的元素了:

1. $book = simplexml_load_file('sxml.xml');
2. unset($book->chapter->para[1]);
3. print $book->chapter->asXML();

访问属性

读取属性

下面的代码输出了book元素中的lang属性

1. $book = simplexml_load_file('sxml.xml');
2. print $book['lang'];

访问用索引指定的元素的属性:

1. $book = simplexml_load_file('sxml.xml');
2. print $book->chapter[0]['id'];

在不知道属性名的情况下可以用attributes()方法来输出属性:

1. $book = simplexml_load_file('sxml.xml');
2. foreach($book->chapter->attributes() AS $attribute) {
3. print $attribute."\n";
4. }

如果要获得位置的属性名,可以使用DOM扩展:

1. $book = simplexml_load_file('sxml.xml');
2. foreach($book->chapter->attributes() AS $attribute) {
3. $att = dom_import_simplexml($attribute);
4. print $att->nodeName."\n";
5. print $attribute."\n";
6. }

修改属性
修改属性的值跟修改元素的值一样,直接对其赋值就可以了:

1. $book = simplexml_load_file('sxml.xml');
2. $book['lang'] = "es";
3. print $book['lang'];

添加一个属性也很简单,如果对一个不存在的属性名进行赋值就给元素创建一个新属性

1. $book = simplexml_load_file('sxml.xml');
2. $book->bookinfo->author->firstname["prefix"] = "Mr.";
3. print $book->bookinfo->author->asXML();

输出:

1.
2. Rob
3. Richards
4.


移除属性

移除属性也用到unset()函数:

1. $book = simplexml_load_file('sxml.xml');
2. $book->bookinfo->author->firstname["prefix"] = "Mr.";
3. print $book->bookinfo->author->firstname->asXML()."\n\n";
4. unset($book->bookinfo->author->firstname["prefix"]);
5. print $book->bookinfo->author->firstname->asXML();

输出:

1. Rob
2.
3. Rob

扩展SimpleXMLElement类

1. class mySXE extends SimpleXMLElement {
2. function appendChild($name, $content) {
3. $dom = dom_import_simplexml($this);
4. $dom->appendChild($dom->ownerDocument->createElement($name, $content));
5. }
6. }

当实例化扩展的类时,文档的每个节点对象的类型都是扩展类的类型。

1. $sxe = new mySXE("");
2. $sxe->node1->appendChild("node2", "content");
3. print $sxe->asXML();

输出:

1.
2. content

使用new方法可以用来处理字符串类型的XML,如果XML保存在一个文件中,那么可以将扩展的类名作为第二个参数传给simplexml_load_string或simplexml_load_file

1. $sxe = simplexml_load_string("", "mySXE");
2. $sxe->node1->appendChild("node2", "content");
3. print $sxe->asXML();

输出结果与用new关键字输出的结果一样。

在SimpleXML中使用命名空间

将清单7-1的内容改为

1.
2.
3.
4. SimpleXML in PHP 5
5.
6. Rob
7. Richards
8.

9.
10. 2005
11. Rob Richards
12.

13.

14.


如果试图用普通的方法来访问元素或属性,你会分析这并不可行,例如:

1. $book = simplexml_load_file('sxmlns.xml');
2. print $book["lang"]."\n";
3. print $book->bookinfo->title."\n";

输出的结果是两行空白。

在访问命名空间节点前,必须使用children()和attributes()方法。这两个方法不仅可以在没有指定参数的时候使用,也可以在指定一个 URI命名空间作为参数使用。如果一个SimleXMLElement对象是从这两个方法返回的,那么你就可以像普通的元素和属性一样访问命名空间下的元素和属性:

1. $book = simplexml_load_file('sxmlns.xml');
2. /* 返回 http://www.example.com/ns2 命名空间下的所有属性 */
3. $bookatts = $book->attributes("http://www.example.com/ns2");
4. print $bookatts["lang"]."\n";
5. /* 返回 http://www.example.com/ns1 命名空间下的所有元素*/
6. $bookns = $book->children("http://www.example.com/ns1");
7. $bookinfo = $bookns->bookinfo;
8. /* 重置命名空间来访问非命名空间的元素 */
9. $nonsbkinfo = $bookinfo->children();
10. print $nonsbkinfo->title."\n";

children ()和attributes()方法可以被看做是过滤器,如果没有参数或者传递一个NULL作为参数,这两个方法将返回非命名空间下的元素或属性;否则将返回特定命名空间下的元素或属性。在重置之前,命名空间仍然起作用并且被子节点继承。例如,使用$bookinfo被设置为命名空间 http://www.example.com/ns1的对象,可以使用 print $bookinfo->author->firstname来但因author中的firstname元素。所有的元素都在命名空间下,因此你在创建$bookinfo对象时不必一直使用children()设置命名空间。

SimpleXML php操作xml的简单入门

DOM是Document Object Model文档对象模型的缩写。根据W3C DOM规范(http://www.w3.org/DOM/),DOM是一种与浏览器,平台,语言无关的接口,使得你可以访问页面其他的标准组件。

说到对xml文件的操作,简单的入门的话无非是: 读取文件,写文件,修改文件

首先说读取文件,我们可以使用两种方法,一种是dom方式,一种是php提供的SimpleXMLElement对象。

我的xml文件中的内容如下:



Great American Novel
This is a science fiction!
Science Novel


名字叫做test1.xml

$xmlstr = 'test1.xml';
//加载xml文件(load a xml file)
//$xml = simplexml_load_file($xmlstr); //这也是一种简单可行的方法

//the third parameter means the first parameter is url or not
$xml = new SimpleXMLElement($xmlstr,NULL,TRUE);
//以属性的方式读取
//echo $xml->book[0]->title;
//循环度读取元素
foreach ($xml->book as $book ){
//echo $book->plot."
";
}

//修改文本节点信息
$xml->book[0]->define = "self define!";
//插入节点
$mybook = $xml->addChild('book');
$mybook->addChild('title','Anna');
$mybook->addChild('plot','About Anna');
$mybook->addChild('define','added by me!');

echo $xml->asXML(); //输出xml文件内容
代码很清晰,不用更多解释了。下面的是使用dom对象读取的方法:

$dom = new DOMDocument('1.0');
$books = $dom->getElementsByTagName('book'); //注意这里获取的类型是DOMNodelist
$book = $books->item(0); //这里获取的类型是DOMNode
$title = $book->getElementsByTagName('title');
echo $title->item(0)->nodeValue; //输出节点信息

这里有点难理解,关键是DOMNodelist和DOMNode这两种类型。 nodelist就像一个数组,是我们用getElementsByTagName获取到的,它具有item方法,item后面可以跟上index来指明是访问那个node,有点类似与数组但是用法完全不一样。node的话,具有nodeValue这个属性,可是使用它来访问node的具体内容。 对于nodelist,一般都倾向与使用迭代来便利,用法如下:
foreach($books as $book){ //$books是nodelist类型的,$book是node类型的
//具体操作
}

读文件完成了,下面是写文件:

//创建一个dom对象
$dom = new DOMDocument('1.0');
//创建根节点
$books = $dom->appendChild($dom->createElement('books'));
//创建子节点
$book = $books->appendChild($dom->createElement('book'));
//创建子节点
$title = $book->appendChild($dom->createElement('title'));
//写入文本内容
$title->appendChild($dom->createTextNode('Great American Novel'));
//格式化输出
$dom->formatOutput = true;
//保存xml到文件
$dom -> save('test1.xml'); // save as file

最后是修改文件,其实也很简单,打开以后读取node的值,修改,然后保存就可以了。

//读取根节点
$root = $dom->getElementsByTagName('books');
//创建一个book节点
$mybook = $dom->createElement('book');
//添加到根节点
$root->item(0)->appendChild($mybook);
//创建title节点
$title = $dom->createElement('title');
//向title节点写入文本内容
$title->appendChild($dom->createTextNode('The deviche code'));
//修改文本内容
$title->nodeValue='my novel';
//添加到book节点
$mybook->appendChild($title);
//保存xml文件
$dom->save($xml_path);

修改文本的时候有两种方法,个人感觉第二种直接写nodeValue更好一些。 掌握了这三种操作,简单的xml操作基本就没有问题了。


这里有所有你想要的:
DOMNodelist讲解: http://www.phpchina.com/manual/php/function.dom-domnodelist-item.html
phpsimplexml官方帮助文档: http://us3.php.net/manual/en/book.simplexml.php

2010年10月2日星期六

举例说明数据库一、二、三及BCNF范式

数据库的设计范式是数据库设计所需要满足的规范,满足这些规范的数据库是简洁的、结构明晰的,同时,不会发生插入(insert)、删除(delete)和更新(update)操作异常。反之则是乱七八糟,不仅给数据库的编程人员制造麻烦,而且面目可憎,可能存储了大量不需要的冗余信息。

设计范式是不是很难懂呢?非也,大学教材上给我们一堆数学公式我们当然看不懂,也记不住。所以我们很多人就根本不按照范式来设计数据库。

实质上,设计范式用很形象、很简洁的话语就能说清楚,道明白。本文将对范式进行通俗地说明,并以笔者曾经设计的一个简单论坛的数据库为例来讲解怎样将这些范式应用于实际工程。

范式说明

第一范式(1NF):数据库表中的字段都是单一属性的,不可再分。这个单一属性由基本类型构成,包括整型、实数、字符型、逻辑型、日期型等。

例如,如下的数据库表是符合第一范式的:

字段1 字段2 字段3 字段4


而这样的数据库表是不符合第一范式的:

字段1 字段2 字段3 字段4
字段3.1 字段3.2



很显然,在当前的任何关系数据库管理系统(DBMS)中,傻瓜也不可能做出不符合第一范式的数据库,因为这些DBMS不允许你把数据库表的一列再分成二列或多列。因此,你想在现有的DBMS中设计出不符合第一范式的数据库都是不可能的。

第二范式(2NF):数据库表中不存在非关键字段对任一候选关键字段的部分函数依赖(部分函数依赖指的是存在组合关键字中的某些字段决定非关键字段的情况),也即所有非关键字段都完全依赖于任意一组候选关键字。

假定选课关系表为SelectCourse(学号, 姓名, 年龄, 课程名称, 成绩, 学分),关键字为组合关键字(学号, 课程名称),因为存在如下决定关系:

(学号, 课程名称) → (姓名, 年龄, 成绩, 学分)

这个数据库表不满足第二范式,因为存在如下决定关系:

(课程名称) → (学分)

(学号) → (姓名, 年龄)

即存在组合关键字中的字段决定非关键字的情况。

由于不符合2NF,这个选课关系表会存在如下问题:

(1) 数据冗余:

同一门课程由n个学生选修,"学分"就重复n-1次;同一个学生选修了m门课程,姓名和年龄就重复了m-1次。

(2) 更新异常:

若调整了某门课程的学分,数据表中所有行的"学分"值都要更新,否则会出现同一门课程学分不同的情况。

(3) 插入异常:

假设要开设一门新的课程,暂时还没有人选修。这样,由于还没有"学号"关键字,课程名称和学分也无法记录入数据库。

(4) 删除异常:

假设一批学生已经完成课程的选修,这些选修记录就应该从数据库表中删除。但是,与此同时,课程名称和学分信息也被删除了。很显然,这也会导致插入异常。

把选课关系表SelectCourse改为如下三个表:

学生:Student(学号, 姓名, 年龄);

课程:Course(课程名称, 学分);

选课关系:SelectCourse(学号, 课程名称, 成绩)。

这样的数据库表是符合第二范式的, 消除了数据冗余、更新异常、插入异常和删除异常。

另外,所有单关键字的数据库表都符合第二范式,因为不可能存在组合关键字。

第三范式(3NF):在第二范式的基础上,数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖则符合第三范式。所谓传递函数依赖,指的是如果存在"A → B → C"的决定关系,则C传递函数依赖于A。因此,满足第三范式的数据库表应该不存在如下依赖关系:

关键字段 → 非关键字段x → 非关键字段y

假定学生关系表为Student(学号, 姓名, 年龄, 所在学院, 学院地点, 学院电话),关键字为单一关键字"学号",因为存在如下决定关系:

(学号) → (姓名, 年龄, 所在学院, 学院地点, 学院电话)

这个数据库是符合2NF的,但是不符合3NF,因为存在如下决定关系:

(学号) → (所在学院) → (学院地点, 学院电话)

即存在非关键字段"学院地点"、"学院电话"对关键字段"学号"的传递函数依赖。

它也会存在数据冗余、更新异常、插入异常和删除异常的情况,读者可自行分析得知。

把学生关系表分为如下两个表:

学生:(学号, 姓名, 年龄, 所在学院);

学院:(学院, 地点, 电话)。

这样的数据库表是符合第三范式的,消除了数据冗余、更新异常、插入异常和删除异常。

鲍依斯-科得范式(BCNF):在第三范式的基础上,数据库表中如果不存在任何字段对任一候选关键字段的传递函数依赖则符合第三范式。

假设仓库管理关系表为StorehouseManage(仓库ID, 存储物品ID, 管理员ID, 数量),且有一个管理员只在一个仓库工作;一个仓库可以存储多种物品。这个数据库表中存在如下决定关系:

(仓库ID, 存储物品ID) →(管理员ID, 数量)

(管理员ID, 存储物品ID) → (仓库ID, 数量)

所以,(仓库ID, 存储物品ID)和(管理员ID, 存储物品ID)都是StorehouseManage的候选关键字,表中的唯一非关键字段为数量,它是符合第三范式的。但是,由于存在如下决定关系:

(仓库ID) → (管理员ID)

(管理员ID) → (仓库ID)

即存在关键字段决定关键字段的情况,所以其不符合BCNF范式。它会出现如下异常情况:

(1) 删除异常:

当仓库被清空后,所有"存储物品ID"和"数量"信息被删除的同时,"仓库ID"和"管理员ID"信息也被删除了。

(2) 插入异常:

当仓库没有存储任何物品时,无法给仓库分配管理员。

(3) 更新异常:

如果仓库换了管理员,则表中所有行的管理员ID都要修改。

把仓库管理关系表分解为二个关系表:

仓库管理:StorehouseManage(仓库ID, 管理员ID);

仓库:Storehouse(仓库ID, 存储物品ID, 数量)。

这样的数据库表是符合BCNF范式的,消除了删除异常、插入异常和更新异常。

范式应用

我们来逐步搞定一个论坛的数据库,有如下信息:

(1) 用户:用户名,email,主页,电话,联系地址

(2) 帖子:发帖标题,发帖内容,回复标题,回复内容

第一次我们将数据库设计为仅仅存在表:


用户名 email 主页 电话 联系地址 发帖标题 发帖内容 回复标题 回复内容

这个数据库表符合第一范式,但是没有任何一组候选关键字能决定数据库表的整行,唯一的关键字段用户名也不能完全决定整个元组。我们需要增加"发帖ID"、"回复ID"字段,即将表修改为:

用户名 email 主页 电话 联系地址 发帖ID 发帖标题 发帖内容 回复ID 回复标题 回复内容

这样数据表中的关键字(用户名,发帖ID,回复ID)能决定整行:

(用户名,发帖ID,回复ID) → (email,主页,电话,联系地址,发帖标题,发帖内容,回复标题,回复内容)

但是,这样的设计不符合第二范式,因为存在如下决定关系:

(用户名) → (email,主页,电话,联系地址)

(发帖ID) → (发帖标题,发帖内容)

(回复ID) → (回复标题,回复内容)

即非关键字段部分函数依赖于候选关键字段,很明显,这个设计会导致大量的数据冗余和操作异常。

我们将数据库表分解为(带下划线的为关键字):

(1) 用户信息:用户名,email,主页,电话,联系地址

(2) 帖子信息:发帖ID,标题,内容

(3) 回复信息:回复ID,标题,内容

(4) 发贴:用户名,发帖ID

(5) 回复:发帖ID,回复ID

这样的设计是满足第1、2、3范式和BCNF范式要求的,但是这样的设计是不是最好的呢?

不一定。

观 察可知,第4项"发帖"中的"用户名"和"发帖ID"之间是1:N的关系,因此我们可以把"发帖"合并到第2项的"帖子信息"中;第5项"回复"中的"发 帖ID"和"回复ID"之间也是1:N的关系,因此我们可以把"回复"合并到第3项的"回复信息"中。这样可以一定量地减少数据冗余,新的设计为:

(1) 用户信息:用户名,email,主页,电话,联系地址

(2) 帖子信息:用户名,发帖ID,标题,内容

(3) 回复信息:发帖ID,回复ID,标题,内容

数据库表1显然满足所有范式的要求;

数据库表2中存在非关键字段"标题"、"内容"对关键字段"发帖ID"的部分函数依赖,即不满足第二范式的要求,但是这一设计并不会导致数据冗余和操作异常;

数据库表3中也存在非关键字段"标题"、"内容"对关键字段"回复ID"的部分函数依赖,也不满足第二范式的要求,但是与数据库表2相似,这一设计也不会导致数据冗余和操作异常。

由此可以看出,并不一定要强行满足范式的要求,对于1:N关系,当1的一边合并到N的那边后,N的那边就不再满足第二范式了,但是这种设计反而比较好!

对于M:N的关系,不能将M一边或N一边合并到另一边去,这样会导致不符合范式要求,同时导致操作异常和数据冗余。
对于1:1的关系,我们可以将左边的1或者右边的1合并到另一边去,设计导致不符合范式要求,但是并不会导致操作异常和数据冗余。

结论

满足范式要求的数据库设计是结构清晰的,同时可避免数据冗余和操作异常。这并意味着不符合范式要求的设计一定是错误的,在数据库表中存在1:1或1:N关系这种较特殊的情况下,合并导致的不符合范式要求反而是合理的。

在我们设计数据库的时候,一定要时刻考虑范式的要求。

第一范式,第二范式,第三范式,BCNF

第一范式 1NF

属性不可再分割,符合原子性。

没什么好解释的,地球人都明白

第二范式 2NF

在1NF的基础上:

不允许出现有field部分依赖于主键(或者说依赖于主键的一部分)

官方说法:数据库表中不存在非关键字段对任一候选关键字段的部分函数依赖(部分函数依赖指的是存在组合关键字中的某些字段决定非关键字段的情况),也即所有非关键字段都完全依赖于整组候选关键字。

Allen解释一下:比如一张表是(A, B, C, D),其中(A, B)是主键,如果存在B->C就违反了2NF,因为C只需要主键的一部分就可以被决定了

第三范式 3NF

在2NF的基础上:

不允许出现可传递的依赖关系(transitive dependencies)

官方说法:在第二范式的基础上,数据表中如果不存在非关键字段对关键字段的传递函数依赖则符合第三范式。所谓传递函数依赖,指的是如果存在"A → B → C"的决定关系,则C传递函数依赖于A

很容易理解,Kiza同学说这是人类的常识。

Boyce-Codd范式 BCNF

在3NF的基础上:

不允许出现有主键的一部分被主键另一部分或者其他部分决定

另一种说法:Left side of every non-trivial FD must contains a key

每个非平凡FD的左边必须有一个主键

BCNF,表中的每个决定因子是候选键。如果只有一个候选键,则3NF和BCNF相同

网友们通常喜欢用的一个例子:

假设仓库管理关系表为StorehouseManage(仓库ID, 存储物品ID, 管理员ID, 数量),且有一个管理员只在一个仓库工作;一个仓库可以存储多种物品。这个数据库表中存在如下决定关系:

   (仓库ID, 存储物品ID) →(管理员ID, 数量)

   (管理员ID, 存储物品ID) → (仓库ID, 数量)

所以,(仓库ID, 存储物品ID)和(管理员ID, 存储物品ID)都是StorehouseManage的候选关键字,表中的唯一非关键字段为数量,它是符合第三范式的。但是,由于存在如下决定关系:

   (仓库ID) → (管理员ID)

   (管理员ID) → (仓库ID)

也就是说,(仓库ID, 存储物品ID)这个主键中的仓库ID可以被管理员ID决定,同样(管理员ID, 存储物品ID)中管理员ID也可以被仓库ID决定,所以此表应该拆分。

3NF->BCNF的步骤:

1. 一个表,从所给的FD中,找出一个FD左边没有key的

2. 给这个FD的右边补全属性(field)。

3. 此时得到两个表,一个是第2步中得到的FD中的表,另一个是第2步中FD的所有左边 及 其他所有没有在这个FD中出现的属性组成的表

4. 投影总的FD分别到两个表

如果仍然有表violation,继续分解

例如:

表(Name, Location, Application, Provider, FavAppl)

FD: Name->Location, Name->FavAppl, Application->Provider

Key: Name, Application

1. Name->Location的左边没有key。

2. 将其补充。Name->Location, Name->FavAppl,因此得到表(Name, Location, FavAppl)

3. 一个表是(Name, Location, FavAppl)。另一个表,”第2步中FD的所有左边“是指Name,“其他所有没有在这个FD中出现的属性”是指Application和

Provider。因此第二个表是(Name, Application, Provider)

4. 第二个表中,Application->Provider的左边没有key,继续分解第二个表

最终结果,原表分为了(Name, Location, Provider),(Application, Provider),(Application, Name)

总结:

1NF

| 消除非主属性对码的部分依赖

2NF

| 消除非主属性对码的传递依赖

3NF

| 消除主属性对码的部分和传递依赖

BCNF

参考资料:

http://blog.csdn.net/kkdelta/archive/2009/04/11/4064986.aspx

《Beginning Database Design》 By Wrox

2010年9月19日星期日

java/OutputTest

/*

* OutputTest.java

*

* Created on June 1, 2004, 12:31 PM

*/

//comment the package line out to run in the default package.

import java.math.BigInteger;

import java.util.Calendar;

import java.util.Date;

/**

*

* @author cpresser

*/

public class OutputTest {

/**

* @param args the command line arguments

*/

public static void main(String[] args) {

// Set up some variables with different types of data.

String word = "Information";

int integer = 24;

double real = 12*Math.PI;

BigInteger bigInt = new BigInteger("123456789012345678901234567890123456789012345678901234567890");

Date today = Calendar.getInstance().getTime();

//a series of output statements to show some of the functionality of the printf method.

//The comments below indicate the output from each section:

System.out.printf("String output tests:\n");

System.out.printf("A string-------------------------------|%s|\n", word);

System.out.printf("An upper-case string-------------------|%^s|\n", word);

System.out.printf("A string in 15 spaces------------------|%15s|\n", word);

System.out.printf("A string left justified in 15 spaces---|%-15s|\n", word);

System.out.printf("A string within 5 characters-----------|%.5s|\n", word);

System.out.printf("A string in a space of 5 to 7 chars----|%5.7s|\n", word);

System.out.printf("\n");

/* OUTPUT:

String output tests:

A string-------------------------------|Information|

An upper-case string-------------------|INFORMATION|

A string in 15 spaces------------------| Information|

A string left justified in 15 spaces---|Information |

A string within 5 characters-----------|Infor|

A string in a space of 5 to 7 chars----|Informa|

*/

System.out.printf("Integer output tests:\n");

System.out.printf("An integer-----------------------------|%d|\n", integer);

System.out.printf("An integer as an octal-----------------|%o|\n", integer);

System.out.printf("An integer as hex----------------------|%x|\n", integer);

System.out.printf("\n");

/* OUTPUT:

Integer output tests:

An integer-----------------------------|24|

An integer as an octal-----------------|30|

An integer as hex----------------------|18|

*/

System.out.printf("Double output tests:\n");

System.out.printf("A double-------------------------------|%f|\n", real);

System.out.printf("A double in scientific notation--------|%e|\n", real);

System.out.printf("A double with precision of 2-----------|%.2f|\n", real);

System.out.printf("A double with precision of 10----------|%.10f|\n", real);

System.out.printf("A double as a hex number---------------|%a|\n", real);

System.out.printf("\n");

/* OUTPUT:

Double output tests:

A double-------------------------------|37.699112|

A double in scientific notation--------|3.769911e+01|

A double with precision of 2-----------|37.70|

A double with precision of 10----------|37.6991118431|

A double as a hex number---------------|0x1.2d97c7f3321d2p5|

*/

System.out.printf("BigInteger output test:\n");

System.out.printf("A BigInteger---------------------------|%d|\n", bigInt);

System.out.printf("\n");

/* OUTPUT:

BigInteger output test:

A BigInteger---------------------------|123456789012345678901234567890123456789012345678901234567890|

*

*/

System.out.printf("Date output tests:\n"); //see multiple arguments section for others

System.out.printf("A Date object as a string--------------|%s|\n", today);

System.out.printf("A formatted Date object----------------|%tc|\n", today);

System.out.printf("An ISO8602 format Date object----------|%tF|\n", today);

System.out.printf("Time in 12 hour format ----------------|%tr|\n", today);

System.out.printf("Time in 24 hour format-----------------|%tR|\n", today);

System.out.printf("Time in 24 hour format w/ seconds -----|%tT|\n", today);

System.out.printf("Date-----------------------------------|%tD|\n", today);

System.out.printf("\n");

/* OUTPUT:

Date output tests:

A Date object as a string--------------|Tue Jun 01 15:25:41 EDT 2004|

A formatted Date object----------------|Tue Jun 01 15:25:41 EDT 2004|

An ISO8602 format Date object----------|2004-06-01|

Time in 12 hour format ----------------|03:25:41 PM|

Time in 24 hour format-----------------|15:25|

Time in 24 hour format w/ seconds -----|15:25:41|

Date-----------------------------------|06/01/04|

*

*/

System.out.printf("Multiple Argument tests:\n");

System.out.printf("%1$s %2$d, %1$s %3$d, %1$s %4$d\n", "label", 1, 2, 3);

System.out.printf("Date mmddyyyy--------------------------|%1$tm%1$td%1$tY|\n", today);

/* OUTPUT:

Multiple Argument tests:

label 1, label 2, label 3

Date mmddyyyy--------------------------|06012004|

*

*/

//There are many other formats/conversions see java.util.Formatter for details

}

}

2010年7月29日星期四

Apache无法解析PHP代码的问题-转载

一天安装Apache 和PHP, Apache的版本是httpd-2.0.55.tar.gz,PHP的版本是php-5.2.6.tar.gz,
首先安装APACHE,
./configure --prefix=/usr/local/apache2 --enable-so --enable-module=so
make
make install

安装PHP
./configure --prefix=/usr/local/php --with-apxs2=/usr/local/apache2/bin/apxs --with-config-file-path=/usr/local/lib --with-sybase=/opt/sybase/OCS-15_0/ --with-mysql --enable-dbase --enable-zip
make
install
安装完PHP后将安装源文件中的 cp php.ini-dist /usr/local/lib/php.ini

写个测试文件test.php ,文件的内容为 ,在IE中运行提示下载test.php,出现这种情况说明APACHE无法解析PHP, 在APACHE中的配置文件httpd.conf中添加AddType application/x-httpd-php .php,重启APACHE,还是出现同样的错误。而且APACHE的配置文件已经加载了PHP的模块LoadModule php5_module modules/libphp5.so。最后发现是由于APACHE的配置文件缺少如下的配置:

AddType application/x-httpd-php .php
AddType application/x-httpd-php .php .phtml .php3
AddType application/x-httpd-php-source .phps


因为我之前的 AddType application/x-httpd-php .php 在加在 中间,有问题,必须加在 中间,重启APACHE后可以正常解析PHP


原帖发表在:http://blog.chinaunix.net/u/24215/showart_2000512.html

2010年7月19日星期一

Putty

Putty

随着Linux在服务器端应用的普及,Linux系统管理越来越依 赖于远程。在各种远程登录工具中,Putty是

出色的工具之一。

Putty是一个免费的、Windows 32平台下的telnet、rlogin和ssh客户端,但是功能丝毫不逊色于商业的

telnet类工具。用它来远程管理Linux十分 好用,其主要优点如下:

◆ 完全免费;

◆ 在Windows 9x/NT/2000下运行的都非常好;

◆ 全面支持ssh1和ssh2;

◆ 绿色软件,无需安装,下载后在桌面建个快捷方式即可使用;

◆ 体积很小,仅364KB (0.54 beta版本);

◆ 操作简单,所有的操作都在一个控制面板中实现。

使用简介:

把 Putty下载到机器上,双击putty.exe,就出现如图1的配置界面。选择“Session”,在“Host Name

(or IP address)”输入框中输入欲访问的主机名或IP,比如server1或192.168.9.4。端口号(Port)根

据使用 的协议有所区别,ssh默认使用22,telnet默认使用23,rlogin默认使用513。在“Protocol”单

选栏中选择使用 的协议,一般是telnet或ssh,这取决于服务器提供的服务。在“Saved Session”输入

栏中输入任务的名字,单击 “Save”按钮,就可以把任务配置保存起来了。

配置完成后单击“Open”按钮,出现如图2的登录界面,就可以使用Putty连接 Linux主机了。

关于putty的一些使用中常见的问题

一、在Windows中使用putty登录FreeBsd系统 时,能够输入用户名,但是输入密码后按回车键半天无反应
。

答:设置过程如下:

1. 在/etc/ssh/sshd_config中添加如下内容,使普通用户可以通过SSH登录:

AllowUsers 普通用户帐号

UseDNS no

2. 将希望使用su命令的用户加入到wheel组中,在/etc/group中作如下修改

wheel:*:0:root, 普通用户帐号

二、在login as: 后输入用户名,J04222035@cs8.xmu.edu.cn's password: 光标不动。即使输入正确的

密码也是没有反应?

答:这是因为putty不像telent那样会直接显示密码,而是不显示密 码,只要你输入的密码是正确的,

即使不显示只要输完密码后直接回车即可进入,不用在乎光标不动。

终于轮到 Pagent 出场了,双击一下 Pagent.exe,嗯,没反应?再双击一下,咦?出来个提示,说已经运行了。
0 && image.height>0){if(image.width>=700){this.width=700;this.height=image.height*700/image.width;}}" title="Pagent is already running" style="width: 246px; height: 150px;" src="http://docs.google.com/File?id=ajbgz6fp3pjh_62f2tqn3">
看看右下角吧,在这 里呢
0 && image.height>0){if(image.width>=700){this.width=700;this.height=image.height*700/image.width;}}" title="Pagent" style="width: 212px; height: 54px;" src="http://docs.google.com/File?id=ajbgz6fp3pjh_63fgtwg5">
双击一下 Pagent 的图标,出来这样一个界面。很简洁的,Add Key 是添加私钥,Remove Key 是把选中的私钥从 Pagent 中卸载了。
0 && image.height>0){if(image.width>=700){this.width=700;this.height=image.height*700/image.width;}}" title="Pagent GUI" style="width: 524px; height: 375px;" src="http://docs.google.com/File?id=ajbgz6fp3pjh_64f8qrq8">
好,现在单击 Add Key 按钮添加私钥,我把这个演示用的私钥保存到 C:\ 了
0 && image.height>0){if(image.width>=700){this.width=700;this.height=image.height*700/image.width;}}" title="选择私钥" style="width: 586px; height: 434px;" src="http://docs.google.com/File?id=ajbgz6fp3pjh_65dc39fw">
然后会出现输入密钥 口令的对话框,输入正确的密钥后,单击 OK
0 && image.height>0){if(image.width>=700){this.width=700;this.height=image.height*700/image.width;}}" title="输入私钥的口令" style="width: 239px; height: 148px;" src="http://docs.google.com/File?id=ajbgz6fp3pjh_66cwj3cc">
这时,在 Pagent 的窗口中就能看到我们的私钥已经装载上去了。
0 && image.height>0){if(image.width>=700){this.width=700;this.height=image.height*700/image.width;}}" title="察看已经装载的私钥" style="width: 524px; height: 375px;" src="http://docs.google.com/File?id=ajbgz6fp3pjh_67hf58c5">
现在打开 PuTTY,选择先前保存的 Session,双击一下。只要自动登录用户名设置上,主机上改帐户的 authorized_keys 文件里也有匹配的公钥。哈哈,发现没有?完全不需要口令,我们已经登录到系统了。
在 Pagent 的图标上点右键,也可以快捷的选择已经保存的会话。
0 && image.height>0){if(image.width>=700){this.width=700;this.height=image.height*700/image.width;}}" title="Pagent 的快捷菜单" style="width: 318px; height: 506px;" src="http://docs.google.com/File?id=ajbgz6fp3pjh_68g5fstk">
建议大家把 Pagent 放到启动组里面,这样每次一开机,Pagent 自动运行,然后我们只需要把私钥装载一下,然后我们就可以一直享受自动登录系统的乐趣了。再配合上以后会讲到的 Plink、PSCP 这些,我们可以实现很多工作的自动化进行。完全不需要每次输入用户名、口令、输入又长又多的命令,再做一些烦躁的文件备份,最后还得记得注销系统,难道不 觉得麻烦么?这一切烦恼很快就会远离我们了,继续往下看吧。

用 SSH 来传输文件

PuTTY 提供了两个文件传输工具

  • PSCP (PuTTY Secure Copy client)
  • PSFTP (PuTTY SFTP client)

PSCP 通过 SSH 连接,在两台机器之间安全的传输文件,可以用于任何 SSH(包括 SSH v1、SSH v2) 服务器。
PSFTP 则是 SSH-2 中新增的特性,使用的是新的 SFTP 协议,使用上与传统的 FTP 类似。事实上 PSCP 如果发现 SFTP 可用,PSCP就会使用 SFTP 协议来传输文件,否则还是 SCP 协议。PSFTP 与 PSCP 相比,PSFTP 的优点是可以与服务器进行交互,遍历服务器上的文件系统,在一个会话中上传或下载多个文件。而 PSCP 只能一次传输一个文件,传输完毕后立刻终止会话。

PSCP 的使用

在控制台直接执行 pscp 可以看到帮助

C:\>pscp
PuTTY Secure Copy client
Release 0.58
Usage: pscp [options] [user@]host:source target
pscp [options] source [source...] [user@]host:target
pscp [options] -ls [user@]host:filespec
Options:
-V print version information and exit
-pgpfp print PGP key fingerprints and exit
-p preserve file attributes
-q quiet, don't show statistics
-r copy directories recursively
-v show verbose messages
-load sessname Load settings from saved session
-P port connect to specified port
-l user connect with specified username
-pw passw login with specified password
-1 -2 force use of particular SSH protocol version
-4 -6 force use of IPv4 or IPv6
-C enable compression
-i key private key file for authentication
-batch disable all interactive prompts
-unsafe allow server-side wildcards (DANGEROUS)
-sftp force use of SFTP protocol
-scp force use of SCP protocol

C:\>

可以看出 PSCP 的使用是很简单的,把常用的几个选项说一下:

  • -q 安静模式,传输文件时什么也不显示,否则会显示出文件的传输进度,默认是关闭的
  • -P port 指定服务器的 SSH 端口,注意这个是大写字母 P,默认是 -P 22,如果主机的 SSH 端口就是 22,就不用指定了
  • -l user 指定以哪个用户的身份登录主机,如果没有指定,则 PSCP 会在 PuTTY 保存的同名 Session 中获得默认的用户名称。用户名称也可以和主机名称写在一起,用 @ 分割开,比如:username@server
  • -pw passwd 指定登录时所用的口令为 passwd
  • -i keyfile 就是指定登录时所用的密钥文件
  • 最后面指定的主机名也可以是 PuTTY 中保存的 Session 名称。比如我们在 PuTTY 中保存了一个名为 foobarserver 的会话,而我们所在的网络又的确没有名为 foobarserver 的主机名称。而在这个 foobarserver 会话中保存的主机名称是 demo-server,保存的自动登录的用户是 taylor。那么用命令
    pscp c:\autoexec.bat foobarserver:backup/
    就把 本地的 c:\autoexec.bat 复制到了主机 demo-server 上的用户 taylor 所在的主目录下的 backup 子目录中(这个路径可能是 /home/taylor/backup


所以 PSCP 大致用法的例子就是:
pscp -P 22 -i c:\path\your-private-key.ppk -C username@server:/remote/path/
下面还是用一些实例来说明会比较简单一些:
把本地的 C:\path\foo.txt 复制到远程主机 192.168.6.200 的 /tmp 目录下

pscp c:\path\foo.txt 192.168.6.200:/tmp

把本地的 C:\path\foo.txt 复制到主机 192.168.6.200 的 /tmp 目录下,但是以主机上的用户 taylor 的权限执行

pscp c:\path\foo.txt taylor@192.168.6.200:/tmp

或者是

pscp -l taylor c:\path\foo.txt 192.168.6.200:/tmp

把本地的 C:\path\foo.txt 传送到主机 192.168.6.200 的 /tmp 目录下,但是主机的 SSH 端口是 3122

pscp -P 3122 c:\path\foo.txt 192.168.6.200:/tmp

把本地的 C:\path\foo.txt 复制到主机 192.168.6.200 的用户 taylor 的主目录下

pscp c:\path\foo.txt taylor@192.168.6.200:.

把主机 192.168.6.200 上的用户 taylor 主目录下的所有 *.tgz 文件拷贝到本地的 c:\backup 目录中,如果 SSH 版本是 SSH v1,那这个命令就会出错。

pscp taylor@192.168.6.200:*.tgz c:\backup

再来看看 PSFTP

在控制台执行命令 psftp -h,可以得到 psftp 的帮助

C:\>psftp -h
PuTTY Secure File Transfer (SFTP) client
Release 0.58
Usage: psftp [options] [user@]host
Options:
-V print version information and exit
-pgpfp print PGP key fingerprints and exit
-b file use specified batchfile
-bc output batchfile commands
-be don't stop batchfile processing if errors
-v show verbose messages
-load sessname Load settings from saved session
-l user connect with specified username
-P port connect to specified port
-pw passw login with specified password
-1 -2 force use of particular SSH protocol version
-4 -6 force use of IPv4 or IPv6
-C enable compression
-i key private key file for authentication
-batch disable all interactive prompts

C:\>

用法与 PSCP 大同小异,虽然有个 -load 选项,其实这个没啥用,后面用主机名的时候,与 PSCP 一样直接用上会话名称就可以了。
用 PSFTP 登录到服务器上以后,操作与 FTP 差不多,这里简单的说一下吧:

  • open 登录主机
    open [username@] [port]
    比 如:
    • open taylor@demo-server 3022
      就是以用户 taylor 的身份,登陆到主机 demo-server 上,SSH 端口是 3022
    • open demo-server
      登陆 demo-server,这里的 demo-server 可以是PuTTY 中已经保存的会话名称,也可以是主机的名称,如果主机名称与会话名称相同,以会话名称为准。
  • close 关闭 SFTP 连接
    这个没啥说的,close 就关闭了 SFTP 连接
  • quit 结束本次的 SFTP 会话
    也没啥用法,就是关闭了 PSFTP 这个程序
  • help [command] 帮助
    直接打 help 就可以看到帮助指令,后面指定上 一个命令就可以查看该命令的帮助,比如: help open
  • cd [directory] 改变当前目录
  • pwd 察看当前目录
  • lcd [directory] 改变本地目录
  • lpwd 察看本地当前目录
  • get [-r] 从服务器下载一个文件/目录,这个命令不能用通配符,参数 -r 可以递归下载整个目录
  • put [-r] [dest] 把文件/目录上传到服务器,这个命令不能用通配符,参数 -r 可以递归上传整个目录
  • mget [-r] 从服务器下载一批文件/目录,可以用通配符,-r 的含义与 get 一样
  • mget [-r] [dest] 把一批文件/目录上传到服务器,可以用通配符,-r 的含义与 put 一样
  • reget [-r] 从服务器续传下载一个文件/目录,这个命令不能用通配符,-r 的含义与 get 一样
  • reput [-r] [dest] 把一批文件/目录续传上传到服务器,这个命令不能用通配符,-r 的含义与 put 一样
  • dir [directory] 列目录
  • ls 和 dir 一样
  • chmod [file|directory] 改变文件的权限,与 Unix 的 chmod 命令类似
  • del 删除文件,要注意的是 del 只能删除文件
  • rm 与 del 一样
  • mkdir 创建一个目录
  • rmdir 删除一个空目录,只有空目录才可以被删除
  • mv 改名/移动。如果源和目的都是文件或目录,则是改名。如果目的是目录的话,则是移动。
  • ! 在本地命令前加一个感叹号,就可以直接执行

其他可选的 SFTP 客户端

FileZilla : http://filezilla.sf.net
WinSCP : http://www.winscp.net

用 Plink 更方便快捷的执行远程主机上的命令

Plink 是 PuTTY 的命令行连接工具,主要用于自动化工作的处理。
直接在控制台执行 plink,可以看到 Plink 的帮助

C:\>plink
PuTTY Link: command-line connection utility
Release 0.58
Usage: plink [options] [user@]host [command]
("host" can also be a PuTTY saved session name)
Options:
-V print version information and exit
-pgpfp print PGP key fingerprints and exit
-v show verbose messages
-load sessname Load settings from saved session
-ssh -telnet -rlogin -raw
force use of a particular protocol
-P port connect to specified port
-l user connect with specified username
-batch disable all interactive prompts
The following options only apply to SSH connections:
-pw passw login with specified password
-D [listen-IP:]listen-port
Dynamic SOCKS-based port forwarding
-L [listen-IP:]listen-port:host:port
Forward local port to remote address
-R [listen-IP:]listen-port:host:port
Forward remote port to local address
-X -x enable / disable X11 forwarding
-A -a enable / disable agent forwarding
-t -T enable / disable pty allocation
-1 -2 force use of particular protocol version
-4 -6 force use of IPv4 or IPv6
-C enable compression
-i key private key file for authentication
-m file read remote command(s) from file
-s remote command is an SSH subsystem (SSH-2 only)
-N don't start a shell/command (SSH-2 only)

C:\>

看上去 Plink 的使用方法、参数与PSCP、PSFTP都很类似。

  • -P port 指定服务器的 SSH 端口,注意这个是大写字母 P,默认是 -P 22,如果主机的 SSH 端口就是 22,就不用指定了
  • -l user 指定以哪个用户的身份登录主机,如果没有指定,则 PSCP 会在 PuTTY 保存的同名 Session 中获得默认的用户名称。用户名称也可以和主机名称写在一起,用 @ 分割开,比如:username@server
  • -pw passwd 指定登录时所用的口令为 passwd
  • -i keyfile 就是指定登录时所用的密钥文件
  • -m file 如果执行的命令很多的话,可以把命令写到文件中,然后用这个参数来指定

还是用一些实际的例子来说明一下 Plink 吧
还记得前面说到 PuTTY 的自动执行命令那个配置么?在说到那个配置的时候,我们演示了一个简单的 Tomcat 重新启动的命令,这个命令是要写在 PuTTY 的 Remote command 里面去。现在我们用 Plink 来实现同样的功能:
假设连接的主机是 192.168.6.200,SSH 的端口是 3022,用户是 taylor:

plink -P 3022 taylor@192.168.6.200 export CATALINA_HOME="~/apache-tomcat-5.5.17";export JAVA_HOME="~/jdk1.5.0_07";export PATH=$JAVA_HOME/bin;$PATH ; cd $CATALINA_HOME/bin;./shutdown.sh;./startup.sh;tail -f $CATALINA_HOME/logs/catalina.out

如果在 PuTTY 中保存了一个名为 192.168.6.200 的会话,注意,这个会话的名称与主机 IP 一样,在会话中已经正确保存了端口 3022,指定了默认的用户是 taylor,现在这个命令就可以简化为:

plink 192.168.6.200 export CATALINA_HOME="~/apache-tomcat-5.5.17";export JAVA_HOME="~/jdk1.5.0_07";export PATH=$JAVA_HOME/bin;$PATH ; cd $CATALINA_HOME/bin;./shutdown.sh;./startup.sh;tail -f $CATALINA_HOME/logs/catalina.out

用 date 命令查看一下主机上的时间,并且格式化输出:

plink 192.168.6.200 date "+%F %T"

大家实际执行一下命令看看,会发现,这个命令并没有返回我们期望的结果,而是返回了一个错误:

C:\>plink 192.168.6.200 date "+%F %T"
date: too many non-option arguments: %T
Try `date --help' for more information.

可是在服务器上直接执行命令 date "+%F %T",的确是正确无误的,哪里出了问题呢?这是因为Windows的控制台会把两个双引号之间的字符串作为一个参数传递给被执行的程序,而不会把双引号 也传递给程序。我们做这样一个小小的实验来说明一下这个问题:
比如在 c:\tmp 文件夹里建立三个文件夹,名称分别为:"foo"、"bar"、"foo bar"。然后在 foo 这个文件夹里面建立一个名为“foo.log”的空文件,在“bar”这个文件夹里建立一个名为“bar.log”的空文件,在“foo bar”这个文件夹里建立一个名为“foo-bar.log”的空文件。
然后在控制台下进入 c:\tmp 这个文件夹,执行如下命令:

dir foo bar

结果是列出“foo bar”这个文件夹里的内容,还是分别列出“foo”和“bar”文件夹里的东西呢?正确答案是后者。
要想正确列出 “foo bar”文件夹里的东西,就需要用双引号把"foo bar"引起来

C:\tmp>dir foo bar
Volume in drive C is System
Volume Serial Number is 9C51-A51C

Directory of C:\tmp\foo

2006-11-22 09:48 .
2006-11-22 09:48 ..
2006-11-16 11:58 0 foo.log
1 File(s) 0 bytes

Directory of C:\tmp\bar

2006-11-22 09:48 .
2006-11-22 09:48 ..
2006-11-16 11:58 0 bar.log
1 File(s) 0 bytes
2 Dir(s) 1,107,345,408 bytes free

C:\tmp>dir "foo bar"
Volume in drive C is System
Volume Serial Number is 9C51-A51C

Directory of C:\tmp\foo bar

2006-11-22 09:48 .
2006-11-22 09:48 ..
2006-11-16 11:58 0 foo-bar.log
1 File(s) 0 bytes
2 Dir(s) 1,107,345,408 bytes free

C:\tmp>

说到这里,就会明白上面的那个命令 plink 192.168.6.200 date "+%F %T" 其实在主机上执行的真实命令是 date +%F %T,而不是命令行中指定的 date "+%F %T"。不过还好,Windows 的控制台可不认得单引号,所以上面那个命令的正确用法应该是:

c:\>plink 192.168.6.200 date '+%F %T'
2006-11-22 09:39:57

我经常需要登录到服务器上把 ADSL 重新拨号,可以把下面的命令写到一个文本文件中,比如保存到了 C:\adsl-restart.command.txt

echo "stoping..."
/sbin/adsl-stop
echo "starting..."
/sbin/adsl-start
echo "done."
/sbin/adsl-status

然后执行如下命令:

plink -m c:\adsl-restart.command.txt root@192.168.6.251

我经常要查看 Tomcat 的运行日志

plink taylor@192.168.6.200 tail -f ~/apache-tomcat-5.5.17/logs/catalina.out

每天都要看服务器上的剩余空间,就用这个命令:

plink taylor@192.168.6.200 df -k

假设 www.chaifeng.com 连接着另外一个网段 10.204.26.0,有台内网IP 为 10.204.26.21 的 Solaris 8主机只能用 telnet 登录,为了防止被监听,我们可以用 Plink 建立一个隧道,隧道开放 120 秒钟,如果隧道没有被使用,就自动断开连接,然后我们在本地就可以用命令 telnet localhost 2623 的安全登录那台 Solaris 8 主机了。

plink -L 2623:10.204.26.21:23 www.chaifeng.com sleep 120

在主机 www.chaifeng.com 上正在运行着 tor,默认的监听地址是 127.0.0.1:9050,用 Plink 建立一个隧道,然后浏览器上配置代理服务器为 127.0.0.1,端口是 9050,这样就能够安全的使用 tor 代理了,不用担心从我们的机器到主机 www.chaifeng.com 有被监听的可能了。

plink -C -N -L 9050:127.0.0.1:9050 taylor@www.chaifeng.com

结合上 PSCP 我们还可以完成文件的每天备份

plink taylor@192.168.6.200 tar jcf $(date '+documents.%F.tar.bz2') ~/documents
pscp taylor@192.168.6.200:$(date '+documents.%F.tar.bz2') c:\backup\
plink taylor@192.168.6.200 rm -f $(date '+documents.%F.tar.bz2')

如果把这些常用的操作写成批处理文件,到时候要重启一下 Tomcat,或者马上察看一下 Tomcat 日志,再或者只是要把 ADLS 重新拨号以下,只需要用鼠标一双击这个批处理文件,稍等一下就自动完成了。不比你打开 PuTTY,登录到服务器上,然后再一个一个的执行命令,最后还得注销来的方便快捷么?再