分类 文章 下的文章

文本格式

Instiki 支持很多种不同的文本格式,每一个不同的 Web (一个Web可以简单的认为是一套系统上面可以建立多个站点中的一个)可以有自己不同的文本格式。

XHTML 友好的标记格式

对于任何一种使用 XHTML 友好标记格式的页面,系统都会将内容以 application/xhtml+xml 标记发送,这表示,你可以直接在页面插入 SVG 图片,如果你想了解更多关于 SVG 的知识,可以查看一下我的另一篇文章《可缩放矢量图形 - Scalable Vector Graphics (SVG)》,所有下面这些标记格式都是基于 Maruku以及它内置的增强版本的 Markdown 语法

  • Markdown+itex2MML这是Instiki默认使用的文本格式,它使用了 itex2MML ,并且允许你直接在文本中书法 itex 方程式 ,这些方程式会被 MathML 翻译为浏览器可识别的标签,而对于某些不支持MathML的浏览器,MathJax将被用来渲染 MathML。在某些数学公式使用很多页面中可能加载会十分的慢,但是 MathJax 是一个跨浏览器的工具。
  • Markdown+BlahTex/PNG这种文本格式会将你的数学公式编译为 PNG 图片,而不使用 MathML,安装会稍稍复杂一点,而且渲染的效果也没有 MathML的好,但是用户不需要使用支持 MathML 的浏览器即可查看。
  • Markdown如果你不需要数学公式的支持,那么选择这种方式是最好的,这样 $ 这个符号就没有什么特殊的含义了,在前面的两人种格式中,它被用来限定数学公式的范围。

HTML 友好的标记格式

如果你选择了这些格式里面的任何一种,那么内容将被标记为 text/html ,这表示你的网页里面不会有数学公式以及SVG图片,除非必要,我个人不建议你使用这些标记格式。

  • Textile使用 RedCloth 渲染 Textile 文本
  • RDoc支持 RDoc
  • Mixed与 Textile 类似,只是对于一个单独的分行,它不会被解释为 * <br /> *

分类

Instiki可以对所有页面进行归类,要给某个页面加入到一个分类中,只需要在页面中添加下面这一行即可:

category: food

你可以为一个页面添加多个分类,只需要在上面的那一行中写上多个分类即可,分类之间使用英文逗号分开:

category: food,restaurants

你可以将分类放在任何一个位置,但是我们约定将它放在文本的最下方,你还可以将一个页面归入某个类中但是在页面展示时不将其显示出来,只需要下面这样即可:

:category: S5-slideshow

上面这个示例将会让文档成为一个 S5 slideshow 页面.

文件上传

如果你选中了 Allow uploads… 的复选框,那么用户就可以上传文件至服务器,上传的方法是:

  1. 编辑某个需要插入图片的页面,在需要插入图片的地方插入下面这样的代码:
    [[mypic.jpg:pic]]

    或者:
    [[myfile.pdf:file]]

    注意 : 这里面填写的文件名称将是文件上传之后在服务器上面的名称,它并不需要与用户要地的文件名称一样。
  2. 保存页面,上面的代码会生成一个链接,并提示这里需要上传图片,点击上传链接即可打开文件上传页面。
  3. 在文件上传页面中,可以输入一个 Description :
+ 对于图片文件,它将是 &lt;image&gt; 标签的 *alt* 属性
+ 对于其它文件,它将作为提示文本,并且同样也是文件链接文本
  1. 当你上传完成之后,图片、视频、文件等会自动的出现在你的页面上。

如果你想为图片或者文件定义与 Description 不一样的文本,你可以使用下面这样的标记:

[[mypic.jpg|alt text:pic]]

或者:

[[myfile.pdf|link text:file]]

除了上面这种维基的格式,你同样还可以使用 Markdown 风格的标记方式:

![alt text](/mywiki/files/mypic.jpg)

或者:

[link text](/mywiki/files/myfile.pdf)

Wolfram CDF 文件

Wolfram Research 定义了一种文件格式名为 Camputable Document Format,这种文件需要浏览器插件的支持,如果你在文本中输入了像下面这样的标记:

[[Voronoi.cdf| 588 x 380 :cdf]]

那么会像 FlashPlayer一样,Instiki为会指向CDF文件的链接创建一个类似Flash获取插件的链接,它像下面这样的:

http://www.wolfram.com/cdf-player/

上面这个标记中,尺寸是可选的。

文件的管理

你可以在管理员界面中查看所有上传至维基的文件,并且可以直接删除他们,但是这里还有一个更快捷的方法,将:

[[myfile.pdf:file]]

修改为:

[[myfile.pdf:delete]]

保存页面之后,会生成一个:

*Delete myfile.pdf*

的链接,点击该链接,然后输入Web密码,即可删除该文件,这种方法可以被用来替换页面中已有的文件:

  1. 首先像上面这样删除该文件。
  2. 重新添加该页面,再加上下面这行标记:[[myfile.pdf:file]]
  3. 保存页面后作前面说过的上传文件的操作。

搜索

每一个页面顶部的搜索框除了支持普通搜索外,你还可以在这里面输入正则表达式,如果你懂正则,那么这个搜索工具将会帮助你更快的检索到你真正想要的内容,默认的,该搜索工具是大小写不敏感的,关于正则的有关知识,我这里不做任何的讲解,如果你想了解它,建议你从这里开始:http://www.regular-expressions.info/quickstart.html,下面只是几个示例:

  • (bzr|bazaar)
  • init[^w]
  • wikis?word

快捷访问键

绝大多数浏览器都支持通过键盘访问网页中的内容,我们称这为快捷键或者访问键,英文名称为AccessKeys,这些快捷键是由网页中的标签定义的,根据你所使用的操作系统或者浏览器的不同,可以通过下面这些方式访问快捷键:

  • Windows ⇒ Alt + AccessKey
  • Macintosh ⇒ Control + AccessKey
  • Opera 浏览器 ⇒ 先按 Shift+ESC ⇒ 然后按 AccessKey
  • GNOME 下的 FireFox ⇒ Alt + Shift + AccessKey

这里有一点需要注意,在Windows的Internet Explorer(IE)浏览器中,如果某个被设置了快捷链的链接指向另一个页面,那么当你击活该快捷键后,只会将焦点移动到该快捷键所定义的链接上,并不会自动的为你转向该链接所指向的页面,你需要再按一次回车键才能算作是点击。

在Instiki中,定义了下面这些快捷键:

  • Alt-E :编辑当前页面
  • Alt-U :查看当前版本
  • Alt-H :回到首页
  • Alt-A :查看所有页面列表
  • Alt-S :保存当前正在编辑的页面
  • Alt-C :显示所有隐藏的分类;退出当前页面的编辑模式
  • Alt-B :查看上一个版本
  • Alt-F :查看下一个版本
  • Alt-X :导出Wiki
  • Alt-W :编辑Web(当前站点设置)

结语

本文并未对 Instiki 的所有功能及使用方法都作了介绍,仅仅只是对其基本功能的一个说明,我会在更久的使用了它更了解它之后,对其它的一些功能做更深入的说明,比如它的 SVG-EDIT 编辑器(这是一个开源项目,你可以在你的项目中使用),数学公式以及我很喜欢的那个 S3 Slideshow等等。

最近一直在为 Golang Wiki 选择维基引擎,最开始选择的是 DokuWiki 与 MoinMoinWiki ,前者是因为它足够的简单,后者是因为个人对 Python 的喜爱,然后,在网上找寻的过程中知道了 Instiki Wiki Engine,基于Ruby,它让我眼前一亮的是内置的 SVG编辑器 以及 Slideshow功能 ,尤其是前者,太爽了,所以,现在也在自己的本地测试与试用这个系统了。

简单的特性介绍 - Introduction to Instiki

Instiki 提供了基于 itex2MML 的对数学公式的支持,但是要求文本格式为系统默认的 “Markdown + itex2MML” 格式。同时,在每一个维基页面底部都有一个名为 “TeX” 的链接,用户可以通过它将内容导出为 LaTex 格式,不过这个我还没有测试过导出来的文档是不是对中文也很友好。

最让我眼前一亮的就是 “SVG编辑器以及直接将SVG图片插入内容中“ 的功能,它使使得我们可以在编辑内容的时候直接打开一个SVG图片编辑器,完成图片编辑之后,可以选择保存至本地图片库或者远程图片库(这需要提供相关图片库的帐号),更简单的,是可以直接将编辑后的图片插入文本内容中,因为SVG可以使用纯文本描述,所以我们根据就不需要将它保存为文件,如果选择直接插入SVG图片代码,我们还是可以再修改它的,办法就是把整个SVG代码选中,创建SVG图片的按钮就会变成编辑已有图片,点击它就会载入选中的图片进行编辑。

另外一个很不错的功能(但是有可能我会用不着),就是对数据公式的支持(可以说是完美的支持),它并不将我们的公式转为图片,而是使用的MathJax库,以文本的方式展示公式,这个有机会使用得到的时候再好好的去研究一下下。

一个 Instiki 实例可以创建无数个维基站点(在 Instiki 中称之为 “Webs”),每一个站点又都可以被设置为密码保护(使用独立的密码),如果你愿意,还可以将一个密码保护的站点发布一份“只读(Read-Only)”版权,这样其它人可以读取它的内容但是无法对其做出任何修改。

安装与系统需求 - Install & Requirements

在我写这篇文章时,它要求Ruby版本为 1.8.6、1.8.7或者1.9.2,Rubygems版本为 1.3.6或者更高的版本。

Mac OS X - Leopard, Snow Leopard 以及 Lion

Ruby 1.8.7 以及 Rubygems 是跟随开发者工具套件(Developer Tools) 一起被安装的,该套件现在已经可以在 App Store 上面直接下载到,只需要你有一个 Apple ID 即可,在苹果电脑上面安装 Instiki 需要该套件的支持,如果你没有安装,需要先安装它,这根据你的网速可能需要一些时间,尤其是在国内网络情况并不太理想的前提下。

首先将 Rubygems 升级到最新版本:

sudo gem update --system
sudo gem update

如果你的 Rubygems 版本过老的话,可能上面的命令无法成功执行,这需要你手工升级它,详情请移步这里:http://rubygems.org/pages/download ,之后你就可以安装 Instiki 了,关于如何在 Tiger 上面的安装 Instiki ,请移步这里:http://golem.ph.utexas.edu/wiki/instiki/show/Installing+under+MacOSX+Tiger

Linux

在 Debian 或者 Ubuntu 上面安装Instiki,你可以执行下面这行命令安装一些需求包(我安装的时候因为被SVG编辑器完全吸引了,而忘记了安装说明,最后是自己一次一次出错才成功安装的,每出一次错补装一个需求包):

$ sudo apt-get install ruby ruby1.8-dev libopenssl-ruby rake rubygems libsqlite3-ruby1.8 ri1.8 libxslt-dev libxml2-dev libsqlite3-dev swig flex bison

如果你的机器上面都有这些了,那就没必须再安装了,我的电脑上面就是没有 libxslt-ruby 、libsqlite3-dev 以及 libxml2-dev 。

如果你使用的是 Febora 的话,则可以执行下面这行命令来完成需求包的安装:

$ yum install make ruby ruby-devel rubygems sqlite sqlite-devel swig flex bison

然后还需要执行下面这行命令来创建一个软链接:

$ ln -s /usr/lib/libsqlite3.so.0.8.6  /usr/lib/libsqlite3.so
注意 :如果你得到下面这样一个错误信息:

install_gem_spec_stubs': undefined method `loaded_specs' for Gem:Module (NoMethodError)

那表示你的 Rubygems 版本过低,你可以通过下面这两人行命令升级它:

sudo gem install rubygems-update
   sudo update_rubygems

CentOS 是一个 RedHat 家族的Linux发行版,所以,你可以直接像 Fedora 一样做,只是有已知的有两个不一样的错误可能会发成在CentOS上面:

  1. /dev/null is not world-writable ,你需要:
    chmod 777 /dev/null
  2. /dev/urandom is not a "special character device.” ,你需要:
    rm /dev/urandom
    mknod -m 644 /dev/urandom c 1 9
    chown root:root /dev/urandom

对于这上面这个解决办法,你可能需要写进自启动脚本中,因为我们现在所作的任何修改,在系统重新启动之后都会失效。

Windows 系列

饿……没什么想法,我从来不认为这些软件应该被安装在Windows 系统之上,所以,如果你真的有这样的需求,自己去找解决办法吧……

安装 Instiki - Install Instiki

从 http://rubyforge.org/frs/?group_id=186 下载 Instiki 的最新版并解压,如果你想试用最新的开发版本,则可以下载 http://golem.ph.utexas.edu/~distler/code/instiki-svn.tar.gz ,不过开发中的版本可能存在很多的Bug,但是Instiki一直使用的都是开发中的版本,所以,如果有Bug的话,应该也是能在第一时间得到解决的。

如果你选择开发版,你还可以使用 bzr :

bzr branch http://golem.ph.utexas.edu/~distler/code/instiki/svn/ location-of-Instiki/

或者使用 Git :

git clone http://github.com/parasew/instiki.git location-of-Instiki/

之后如果要更新开发版本,只需要使用 [bzr pull]golem.ph.utexas.edu/wiki/instiki/show/Bazaar 或者git pull 即可。

注意 :安装时请讲上面两人行命令中的 location-of-Instiki/ 修改成为你要安装它的目录,同时保证整个路径中没有空格。

运行 Bundler 并启动你的 Instiki 之旅 - Running Bundler and starting Instiki

如果上面的安装或者取得文档都没有错误发生,那么可以在你的 Instiki 主目录中运行下面这行命令了:

ruby bundle install --path vendor/bundle

这将下载并安装其它的一些需求包(sqlite3-ruby/itextomml等),最后:

./instiki --daemon

将启动 Instiki。

打开你的浏览器,并在地址栏中输入:http://localhost:2500 并设置你的第一个维基站点,这很简单,只需要填写维基的名称以及路径和管理它的密码即可。

维护 - Care and Feeding

你可以通过下面这行命令停止 Instiki

% kill pid-of-Instiki

然后 Instiki 还提供了很多的命令行工具用来维护与保养你的 Instiki。

运行:

./instiki

将使用 Mongrel(如果你安装了的话)或者 WEBrick 启动 Instiki,如果你已经安装了 Mongrel,但是却还是想通过 WEBrick 启动 Instiki的话,需要给上面的命令加上 webrick 参数:

./instiki webrick

但是这样的方式启动的 Instiki 将直接在前端运行,所以当你退出终端或者键入 Ctrl + C 之后就会立马停止,如果你想在后端运行,那么你需要像前面那样运行:

./instiki --daemon

或者:

./instiki webrick --daemon

下面还有一些其它的命令行标记:

  • -d, –daemon :以守护进程的方式运行 Instiki
  • -p, –port=Port :定义Instiki监听的端口,默认端口号为 2500
  • -b, –binding=1p :绑定 Instiki 到特定的地址,默认监听 0.0.0.0,即监听所有地址
  • -c, –config=file :使用一个特定的配置文件
  • -u, –debugger :启用Debug模式
  • -e, –environment=name :设定服务器运行的模式(test/development/production),默认为产品模式,即 production
  • -P, –path=/path :绑定一个URL路径给Instiki,默认为 /
  • -h, –help :显示帮助信息,并列出可用的命令

为 Instiki 优化的主机服务 - Dreamhost

Dreamhost 总是为这些开源软件进行各种各样的优化,同样的,在DreamHost上面安装并运行 Instiki 也是十分简单的一件事情,DreamHost 通过 mod_rails 对 Ruby-on-Rails* 提供支持,唯一不足是它只提供了老版本的 sqlite3。

  1. 在DreamHost上面创建一个新的域名虚拟主机(比如 wiki.matrixstack.com)
  2. 在 Specify your web directory 选项中选中 Ruby on Rails Passenger(mod_rails) 选项,并填写路径wiki.matrixstack.com/public

这会在你的家目录中创建一个名为 wiki.matrixstack.com 的目录,登陆 Shell 然后执行下面这些命令:

cd wiki.yourdomain.com
wget http://golem.ph.utexas.edu/~distler/code/instiki-svn.tar.gz
tar --strip-components 1 -xzf instiki-svn.tar.gz
sed '/sqlite3-ruby", :r/s//sqlite3-ruby", "~>1.2.0", :r/' Gemfile > Gemfile.tmp
mv Gemfile.tmp Gemfile
unset GEM_HOME GEM_PATH
ruby bundle

打开浏览器并访问 http://wiki.matrixstack.com对维基进行设置即可。

升级已有的 Instiki 安装

Instiki 的开发还处于活跃期,经常会有新功能被添加或者Bug被修补,所以你肯定时常想升级你现有的Instiki安装,如果你通过 Bazaar 安装,那么:

  1. 运行
    % bzr pull

    或者
    % git pull
  2. 进行所有需要的升级
    % ruby bundle update
    % ruby bundle exec rake upgrade_instiki
  3. 重新启动 Instiki
    % ps -ax | grep instiki
    % kill PID-of-Instiki
    % ./instiki --daemon

如果你是通过 tarball 安装的,那么:

  1. 备份所有上传的文件,它们被保存在 webs/Name-Of-Web/files/ 目录中
  2. 如果你使用默认的 SQLite3 数据库,则需要备份它,它位于 db/production.db.sqlite3 ,如果你使用的是MySQL,则需要备份 config/database.yml 文件
  3. 停止 Instiki
    % ps -ax | grep instiki
    % kill PID-of-Instiki
  4. 下载并解压最新版本
  5. 将第 1 与第 2 步备份的文件重新恢复到对应的目录中
  6. 运行
    % ruby bundle update
    % ruby bundle exec rake upgrade_instiki
  7. 重新启动 Instiki
    % ./instiki --daemon

完成 Instiki 的升级。

安全相关 - Security

软件再好也不可能达到完美的安全,要不然也不会有人站出来说:“永远不要相信用户的输入”了。

以非特权用户运行 Instiki

如果你是一个具有服务器特权的用户,那么我不建议你直接以该用户的身份运行 Instiki,你可以创建一个新的无特权的用户来运行:

  1. 创建一个无特权的没有 shell 访问权限的新用户,我这里就创建一个 instiki 用户
  2. 让该用户仅仅只能访问以下这些必须的文件:
    % sudo chown instiki public secret db db/production.db.sqlite3 config/environment.rb config/database.yml
    % sudo chown -R instiki log storage cache webs tmp
  3. 以 instiki 用户的身份运行 Instiki:
    % sudo -u instiki ./instiki --daemon
  4. 在这种情况下,你可以通过下面这种方式停止 Instiki
    % sudo -u instiki kill pid-of-Instiki

使用 MySQL 数据库替换默认的 SQLite

如果你使用 Sqlite 已经运行你的Instiki一段时间,现在计划转到 MySQL 数据库上去,那么你可以这样做:

  1. 确定你现在运行的Instiki版本大于 0.19,然后执行下面这行命令:
    % ruby bundle exec rake upgrade_instiki

    这会升级你的数据库Schema到最新版本,老版本的数据库Schame可能导致在SQLite3⇒MySQL的转换过程中数据的丢失。
  2. 在 Gemfile 文件的最后面添加下行这一行:
    gem "mysql"

    然后重新执行:
    % ruby bundle

    这将为你安装好 Mysql Gem
  3. 在你的系统环境变量中添加一个新的变量:
- 如果你使用的是 sh ( bash, zsh 等):

        % RAILS_ENV='production'

- 如果你使用的是 csh (tcsh 等):

        % setenv RAILS_ENV production
  1. 导出你的文件至 dump/fixtures/.yml*
    % ruby bundle exec rake db:fixtures:export_all
  2. 编辑 config/database.yml ,并修改:
    production:
       adapter: sqlite3
       database: db/production.db.sqlite3

    为:
    production:
      adapter: mysql
      database: your_db_name
      username: your_db_username
      password: your_db_password
      host: 127.0.0.1
      port: 3306
  3. 创建一个新的MySQL数据库,数据库信息与你上面填写的一保持一致:
    % echo "create database your_db_name" | mysql -u your_db_username -p your_db_password
  4. 初始化你的数据库表并导入你的数据:
    % ruby bundle exec rake db:migrate
    % ruby bundle exec rake db:fixtures:import_all

大小写敏感问题

这里可能还有一个小问题需要你注意一下下,那就是在 SQLite3中,字符串是大小写敏感的,这也就是说你可以有一个页面的名称为 Foo ,而另一个页面的名称为 foo ,这使得 Instiki 会将它们分开对待,但是在 MySQL 下面,这个正好是相反的,字符串默认是大小写不敏感的。

这并没有哪一种方法可以说是绝对正确的,只们只是对待事物的方式不一样而已,如果你想在MySQl数据库中也对页面的名称大小写敏感,你可以单独的通过SQL命令修改受该问题影响的字段,根据你自己数据库的字符集编码选择下面命令中的任何一种即可:

ALTER TABLE `pages` MODIFY `name` varchar(255) COLLATE latin1_bin DEFAULT NULL;
ALTER TABLE `wiki_references` MODIFY `referenced_name` varchar(255) COLLATE latin1_bin NOT NULL DEFAULT '';
ALTER TABLE `wiki_files` MODIFY `file_name` varchar(255) COLLATE latin1_bin NOT NULL;

或者

ALTER TABLE `pages` MODIFY `name` varchar(255) COLLATE utf8_bin DEFAULT NULL;
ALTER TABLE `wiki_references` MODIFY `referenced_name` varchar(255) COLLATE utf8_bin NOT NULL DEFAULT '';
ALTER TABLE `wiki_files` MODIFY `file_name` varchar(255) COLLATE utf8_bin NOT NULL;

这个问题我只在使用 Ubuntu 系统的时候出现过,因为很久没有使用其它系统了,所以也不知道是不是都是一样的,会遇到这种问题,这个问题发生的原因是因为系统默认没有开启 sudo 权限,解决办法很简单:

pantao@aliyun:~$ sudo cp /etc/apt/sources.list /etc/apt/sources.list_backup
[sudo] password for pantao:
pantao is not in the sudoers file.  This incident will be reported.
pantao@aliyun:~$ su
Password:
root@aliyun:/home/pantao# chmod u+w /etc/sudoers
root@aliyun:/home/pantao# vi /etc/sudoers
root@aliyun:/home/pantao# chmod u-w /etc/sudoers
root@aliyun:/home/pantao# exit
exitsudo ap
pantao@aliyun:~$ sudo cp /etc/apt/sources.list /etc/apt/sources.list_backup
[sudo] password for pantao:
pantao@aliyun:~$

在上面的代码中,最开始我是要备份软件源,但是因为我不在 sudoers 文件中,所以系统不允许我在 /etc/ 目录下面执行写操作,然后运行 su ,输入 root 用户密码之后切换到超级用户,然后:

  1. 修改 /etc/sudoers 文件权限,让所有者可写
  2. 打开该文件,并在 root ALL=(ALL:ALL) ALL 这一行下面添加:
    pantao  ALL=(ALL) ALL

    请将 pantap 修改成为你自己的用户名
  3. 按 ESC 键退出编辑状态,再输入 :wq 保存修改并退出编辑器
  4. 去掉该文件的写权限
  5. 退出 root 帐户
  6. 现在已经可以正常使用 sudo 了。

最终修改后的文件片段如下:

[...]
# Cmnd alias specification

# User privilege specification
root    ALL=(ALL:ALL) ALL
pantao  ALL=(ALL) ALL
# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL

# Allow members of group sudo to execute any command
%sudo   ALL=(ALL:ALL) ALL
[...]

Beautiful Soup 是一个用来从HTML与XML文档中抓取数据的Python库,它提供了从HTML与XML文档中检索、抓取与修改数据的最傻瓜式的方法,能为我们的开发节省很多时间,本文翻译自 Beautiful Soup Documentation,但并非完全地逐字翻译,如果你想查阅原版英文文档,请点击前面的链接,本文绝大多数示例都是在Python的命令行工具中进行的。

功能演示与快速入门

下面这个摘录自《爱丽丝仙境梦游记》的故事段落是我们最开始要进行处理的HTML文档:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p><b>The Dormouse's story</b></p>

<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p>...</p>
"""

上面的代码并没有进行合理的缩进,那是因为我最开始想像大家展示一下 Beautiful Soup提供给我们的第一个有用的功能——美化代码结构:

>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup(html_doc)
>>> print(soup.prettify())
<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>
 <body>
  <p>
   <b>
    The Dormouse's story
   </b>
  </p>
  <p>
   Once upon a time there were three little sisters; and their names were
   <a href="http://example.com/elsie" id="link1">
    Elsie
   </a>
   ,
   <a href="http://example.com/lacie" id="link2">
    Lacie
   </a>
   and
   <a href="http://example.com/tillie" id="link3">
    Tillie
   </a>
   ;
and they lived at the bottom of a well.
  </p>
  <p>
   ...
  </p>
 </body>
</html>

下面的这些示例则展示了如何通过Beautiful Soup获取HTML文档中的数据:

>>> soup.title
<title>The Dormouse's story</title>
>>> soup.title.name
'title'
>>> soup.title.string
u"The Dormouse's story"
>>> soup.title.parent.name
'head'
>>> soup.p
<p><b>The Dormouse's story</b></p>
>>> soup.p['class']
['title']
>>> soup.a
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.find(id='link3')
<a href="http://example.com/tillie" id="link3">Tillie</a>

还有一个我们用得最多的功能那就是从一个网页中获取所有网址,来看看Beautiful Soup做这个事情有多简单:

>>> for link in soup.find_all('a'): print(link.get('href'))
...
http://example.com/elsie
http://example.com/lacie
http://example.com/tillie

另一个用得最多的功能就是从某一个元素中获取文本内容:

>>> print(soup.get_text())
The Dormouse's storyThe Dormouse's story
Once upon a time there were three little sisters; and their names were
Elsie,
Lacie and
Tillie;
and they lived at the bottom of a well.
...
>>> print(soup.p.get_text())
The Dormouse's story

安装 Beautiful Soup

看了上面的这些功能演示是不是已经心动了,心动不如行动,先安装Beautiful Soup吧。

如果你使用的是Debian或者Ubuntu Linux,你可以使用系统的包管理工具直接安装:

$ apt-get install python-bs4

Beautiful Soup 4以在PyPi中发布了,所以,如果你不能使用系统的包管理工具安装它,那也可以使用Python的安装工具 easy_install 或者 pip 安装,在PyPi中的包名称为 beautifulsoup4,该包能同时工作于 Python 2.x 与 Python 3.x。

$ easy_install beautifulsoup4

或者

$ pip install beautifulsoup4
如果你使用的某一个软件是基于 Beautiful Soup 3 的,那么你可能需要了解一下怎么安装它,查阅:http://www.crummy.com/software/BeautifulSoup/bs3/documentation.html以获得更多的帮助信息,但是如果你是准备基于Beautiful Soup 创建新的应用,那么我强烈建议你使用 Beautiful Soup 4。

如果你的机器上面连 easy_install 或者 pip 工具都没有,那你还可以直接下载安装包使用包里的setup.py 安装,下载地址为:http://www.crummy.com/software/BeautifulSoup/download/4.x/,下载完成之后,将其解压至某个目录下,运行:

$ python setup.py install

如果还是安装失败,那要么你可以选择检查是哪里出了问题,再尝试安装,或者,最简单的办法,Beautiful Soup 4所使用的许可证允许你将它的所有代码直接包装进你自己的应用中,这样就不再需要安装它了。

Beautiful Soup 4是在 Python2.7与Python3.2上开发的,但是它应该是可以运行在其它版本中的。

安装之后的一些问题

Beautiful Soup包本身是 Python 2 的代码,当你安装至Python 3之后,它会自动转换成为Python 3代码,但是如果你不安装该包,代码则不会被转换,在Windows机器上会报出你安装了错误的版本。

  • 如果你得到 ImportError “No module named HTMLParser” ,则表示你在 Python 3中运行Python 2版本的代码。
  • 如果你得到 ImportError “No module named html.parser” ,则表示你在 Python 2中运行Python 3版本的代码。
  • 如果上面两个错误你都有,那最好的办法是删除所有现有安装,然后重新安装一次试试。
  • 如果你得到 SyntaxError “Invalid syntax” on the line ROOT_TAG_NAME = u'[document]' ,你需要转换Python 2的代码至Python 3代码,你可以通过安装脚本来转换: $ python3 setup.py install

或者使用Python 的 2to3 转换脚本进行:

$ 2to3-3.2 -w bs4

安装解析器

Beautiful Soup 支持Python标准库中的HTML的解析器,但是你同样还可以使用很多其它的第三方解析器,比如 lxml parser 。根据你的系统不同,你可以使用下面中的某一种方式安装 lxml:

$ apt-get install python-lxml
$ easy_install lxml
$ pip install lxml

如果你使用 Python 2,另一个纯Python的HTML解析器 html5lib parser 也是很不错的选择,它可以像浏览器一样的解析HTML文档,用下面的任何一种方法安装它:

$ apt-get install python-html5lib
$ easy_install html5lib
$ pip install html5lib

下表简单地说明了每一种解析器优点与缺点:

解析器 典型用法 优点 缺点
Python标准库 html.parser BeautifulSoup(markup, “html.parser”) 内置,不错的速度,宽容(2.7.3 与 3.2 ) 不是太宽容
lxml HTML parser BeautifulSoup(markup, “lxml”) 非常的快 , 宽容 依赖于外部C库
lxml XML parser BeautifulSoup(markup, [“lxml”, “xml”]) , BeautifulSoup(markup, “xml”) 非常的快,只支持正确的XML文档 依赖于第三方C库
html5lib BeautifulSoup(markup, html5lib) 宽容,像浏览器一样解析页面,操作HTML5文档 非常慢,依赖第三方库,仅支持 Python 2

如果可以的话,我建议你安装 lxml 库,如果你使用的是 2.7.3或者3.2.2以前的版本,建议你使用 lxml 或者 html5lib,早期版本的Python的HTML Parser并不是非常的好。

注意:不同的解析器会为其一份文档创建不同的 tree 。

创建 Soup

要解析一个文档,你可以将该文档的所有字符串或者一个打开的文件对象传送给BeautifulSoup构造器:

from bs4 import BeautifulSoup

soup = BeautifulSoup(open('index.html'))

soup = BeautifulSoup('<html>data</html>')

首先,文档会被转换为Unicode编码,HTML 元素也会被转换为Unicode字符串。

>>> BeautifulSoup("这是中文")
<html><body><p>这是中文</p></body></html>

Beautiful Soup 接着使用一个可用的解析器分析文档,如果不是特别指定的话,它会默认使用一个HTML解析器。

对象类型

Beautiful Soup 将HTML文档转换成为一个 Python 对象树,但是你只需要处理这下面这四种类型的对象即可。

标签 (Tag)

一个 Tag 对象对应于 XML 或 HTML 文档中的某一个标签:

>>> soup = BeautifulSoup('<b>Extremely bold</b>')
>>> tag = soup.b
>>> type(tag)
<class 'bs4.element.Tag'>

Tag对象有许多的方法与属性可供我们使用,这些会在本文下方专门的章节做介绍。

名称(Name)

任何一个Tag都有一个名称,通过 .name 属性访问:

>>> tag.name
'b'

如果你修改了它的名称,那么此次修改会体现到任何一个由Beautiful Soup 创建的HTML元素上:

>>> tag.name = 'blockquote'
>>> tag
<blockquote>Extremely bold</blockquote>

属性(Attributes)

一个Tag可以有不限数量的属性,如 <b class=“boldest”> 就有一个名为 class 的属性,它的值为boldest ,你可以像字典一样访问Tag的属性,比如:

>>> tag['class']
['boldest']

如果你想获取整个Tag的所有属性的字典,可以使用 .attrs

>>> tag.attrs
{'class': ['boldest']}

你可以添加、删除或者修改Tag的属性:

>>> tag['class'] = 'verybold'
>>> tag['id'] = 1
>>> tag
<blockquote id="1">Extremely bold</blockquote>

>>> del tag['class']
>>> del tag['id']
>>> tag
<blockquote>Extremely bold</blockquote>

多值的属性

HTML 4 定义了一些可以有多个值的属性,HTML 5又移除了一些,但是最后却又定义了更多的新的,最常见的可多值的属性是 class ,其它的还有比如 rel , rev , accept-charset , headers 以及accesskey 等,Beautiful Soup将多值以列表(list)的方式进行处理:

>>> css_soup = BeautifulSoup('<p>Strikeout</p>')
>>> css_soup.p['class']
['body', 'strikeout']
>>> css_soup = BeautifulSoup('<p>Strikeout</p>')
>>> css_soup.p['class']
['body']

如果一个属性,在文档中看似是一个多值的,但是却没有在任何一个HTML标准中被定义为多值属性,则 Beautiful Soup 会按标准执行,即将其按单个值进行处理:

>>> id_soup = BeautifulSoup('<p id="my id"></p>')
>>> id_soup.p['id']
'my id'

当Tag重新转为HTML代码时,多个值会被合并:

>>> rel_soup = BeautifulSoup('<p>Back to the <a rel="index">Home Page</a></p>')
>>> rel_soup.a['rel']
['index']
>>> rel_soup.a['rel'] = ['index', 'contents']
>>> print(rel_soup.p)
<p>Back to the <a rel="index contents">Home Page</a></p>

但是如果你使用的是 xml 解析器,那么不存在多值属性:

>>> xml_soup = BeautifulSoup('<p></p>', 'xml')
>>> xml_soup.p['class']
u'body strikeout'

可操纵字符串(NavigableString)

Beautiful Soup 将 Tag 中的字符串内容称之为 NavigableString :

>>> tag.string
u'Extremely bold'
>>> type(tag.string)
<class 'bs4.element.NavigableString'>

NavigableString 与 Unicode 字符类型,当然它还提供有很多特殊的功能,这些我也会在下面的章节中作详细介绍,你可以使用 unicode() 将其转换为 Unicode 字符:

>>> unicode_string = unicode(tag.string)
>>> unicode_string
u'Extremely bold'
>>> type(unicode_string)
<type 'unicode'>

你不能直接编辑字符,但是你可以使用一个新字符替换它:

>>> tag.string.replace_with("No langer bold")
u'Extremely bold'
>>> tag
<blockquote>No langer bold</blockquote>

BeautifulSoup 对象

BeautifulSoup 对象则是文档本身,很多时候你可以像Tag的那样操作它,但是因为它并不是一个真正的Tag,所以它没有名称,也没有属性,但是我们经常需要获取它的名称,所以,它有一个名称,为 [document]

>>> soup.name
u'[document]'

注释以及其它一些字符

Tag , NavigableString 以及 BeautifulSoup 提供了几乎所有我们在HTML或者XML文档中可见的内容,但是还有一些特殊的内容则没有被它们包括在里面,比如你最常需要考虑的是其注释:

>>> markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
>>> soup = BeautifulSoup(markup)
>>> comment = soup.b.string
>>> type(comment)
<class 'bs4.element.Comment'>

Comment 仅仅只是一个特殊的 NavigableString :

>>> comment
u'Hey, buddy. Want to buy a used parser?'

当一个 Comment 对象以 HTML 元素出现的时候,它有自己的特殊格式:

>>> print(soup.b.prettify())
<b>
 <!--Hey, buddy. Want to buy a used parser?-->
</b>

Beautiful Soup 同样定义了 XML 文档中会出现的各种各样的元素,比如 CData ,ProcessingInstruction , Declaration 以及 Doctype 等,像 Comment 一样,所有这些类都是NavigableString 的子类,但是都有它们自己的特性,下面这是一个 CData 示例:

>>> from bs4 import CData
>>> cdata = CData("A CDATA block")
>>> comment.replace_with(cdata)
u'Hey, buddy. Want to buy a used parser?'
>>> print(soup.b.prettify())
<b>
 <![CDATA[A CDATA block]]>
</b>

浏览树对象 (Navigating the tree)

“三姐妹”的HTML文档又来了:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p><b>The Dormouse's story</b></p>

<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p>...</p>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)

我还是将使用这个文档来演示如何在树对象中游走。

往内在子元素中检索(Going Down)

Tag 可以包含字符串或者其它 Tag,这些被包裹的元素称为 Tag 的子元素,Beautiful Soup 提供了很多属性用来在 Tag 的子元素中进行导航或者遍历子元素。

注意:Beautiful Soup 字符串没有上面说的这些任何一种属性,因为字符串无法包含子元素。

使用 Tag 名称

最简单的检索树的方法是使用标签名称,如果你想获取 <head> 标签 ,只需要: soup.head :

>>> soup.head
<head><title>The Dormouse's story</title></head>
>>> soup.title
<title>The Dormouse's story</title>

你可以像上面这一样一直往后加添加标签的名称,比如我想获取 <body> 中的 <b> ,则可以使用 soup.body.b :

>>> soup.body.b
<b>The Dormouse's story</b>

使用标签浏览的局限在于我们总是只能得到第一个为该名称的子标签:

>>> soup.a
<a href="http://example.com/elsie" id="link1">Elsie</a>

如果你想得到所有某一个名称的标签,你需要使用搜索树的方法,比如 find_all() :

>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

.contents 与 .children

标签的所有子元素可以通过一个名为 .contents 的列表访问到:

>>> head_tag = soup.head
>>> head_tag
<head><title>The Dormouse's story</title></head>
>>> head_tag.contents
[<title>The Dormouse's story</title>]
>>> title_tag = head_tag.contents[0]
>>> title_tag
<title>The Dormouse's story</title>
>>> title_tag.contents
[u"The Dormouse's story"]

BeautifulSoup 对象只有一个子元素,即 <html> 标签:

>>> len(soup.contents)
1
>>> soup.contents[0].name
'html'

字符串没有任何 .contents ,因为它无法包裹其它数据:

>>> text = title_tag.contents[0]
>>> text.contents
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/bs4/element.py", line 667, in __getattr__
    self.__class__.__name__, attr))
AttributeError: 'NavigableString' object has no attribute 'contents'

除了通过列表 .contents 来访问子元素外,你还可能通过遍历访问它们,使用 .children :

>>> for child in title_tag.children: print(child)
...
The Dormouse's story

.descendants

descendants 是后代的意思,它不像 .contents 或者 .children 只能访问到父元素的下一级子元素,它能访问到父元素内的所有子元素,比如:

>>> head_tag.contents
[<title>The Dormouse's story</title>]

我们只得到了一个子元素,但是:

>>> for child in head_tag.descendants: print(child)
...
<title>The Dormouse's story</title>
The Dormouse's story

可以看到,当得到的子元素有其自己的子元素,那么也会一并的遍历到,这是因为 <head> 虽然只有一个子元素 <title> ,但是 <title> 也有一个子元素,这个子元素同样是 <head> 的后代:

>>> len(list(soup.children))
1
>>> len(list(soup.descendants))
23

.string

如果一个标签有且只有一个子元素,而该子元素是一个 NavigableString ,那么可以直接使用 .string访问到这个子元素:

>>> title_tag.string
u"The Dormouse's story"

如果一个元素有且仅有一个子元素,而该子元素同样有且仅有一个子元素并且该子元素是一个NavigableString ,那么该字符同样是最开始父元素的 .string :

>>> head_tag.contents
[<title>The Dormouse's story</title>]
>>> head_tag.string
u"The Dormouse's story"

但是如果一个元素有多个字符的话,因为我们无法通过一个属性来指定多个值,所以我们直接将该元素的 .string 设定为 None

>>> print(soup.html.string)
None

.strings 与 stripped_strings

像上面说过的,我们无法使用 .string 名称指定多个值,但是却可以使用 .strings 访问到,它是一个生成器(Generator):

>>> for string in soup.strings: print(repr(string))
...
u"The Dormouse's story"
u"The Dormouse's story"
u'n'
u'Once upon a time there were three little sisters; and their names weren'
u'Elsie'
u',n'
u'Lacie'
u' andn'
u'Tillie'
u';nand they lived at the bottom of a well.'
u'n'
u'...'

我在上面的示例中使用了 repr 函数是因为我想告诉你 .strings 会将任何字符串都囊括在内,这在很多时候是不需要的,所以还提供了另一个生成器 .stripped_strings :

>>> for string in soup.stripped_strings: print(repr(string))
...
u"The Dormouse's story"
u"The Dormouse's story"
u'Once upon a time there were three little sisters; and their names were'
u'Elsie'
u','
u'Lacie'
u'and'
u'Tillie'
u';nand they lived at the bottom of a well.'
u'...'

所有空字符串包括只有换行符等等都被去除掉了。

向外在父元素中检索(Going Up)

就像“族谱”一样,任何一个标签一定有它的父元素:

.parent

你可以访问通过 .parent 访问一个标签的父元素,在上面的示例HTML文档中, <head> <title> 的父元素:

>>> title_tag.parent
<head><title>The Dormouse's story</title></head>

顶层的元素的父元素就像Linux系统中的根目录 / 一样,就是 BeautifulSoup 对象本身:

>>> html_tag = soup.html
>>> type(html_tag.parent)
<class 'bs4.BeautifulSoup'>

而 BeautifulSoup 对象的父元素则被定义为 None :

>>> print(soup.parent)
None

.parents

就像 .descendants 可以遍历到一个元素的所有子元素一样,你还可以使用 .parents 遍历一个元素的所有父元素:

>>> link = soup.a
>>> link
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> for parent in link.parents:
...     if parent is None:
...         print(parent)
...     else:
...         print(parent.name)
...
p
body
html
[document]

同等级兄弟元素中平行检索(Going Sideways)

考虑一下下面这个HTML文档:

>>> sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></b></a>")
>>> print(sibling_soup.prettify())
<html>
 <body>
  <a>
   <b>
    text1
   </b>
   <c>
    text2
   </c>
  </a>
 </body>
</html>

<b> 与 <c> 是具有同一个父元素的同一级标签,我们称这两个元素为兄弟元素,它们处于文档结构中的同一级,它们之间的这种关系同样可以被用来进行内容的检索。

.next_sibling 与 .previous_sibling

你可以使用 .next_sibling 与 .previous_sibling 检索下一个与上一个元素:

>>> sibling_soup.b.next_sibling
<c>text2</c>
>>> sibling_soup.c.previous_sibling
<b>text1</b>

因为 .next_sibling 与 .previous_sibling 都只是在同一级的兄弟元素中检索,所以,<b> 有 .next_sibling 但是因为它前面没有兄弟元素,所以它的 .previous_sibling 为 None,而 <c> 有 .previous_sibling 但是后面没有兄弟元素,所以它的 .next_sibling 为 None:

>>> print(sibling_soup.b.previous_sibling)
None
>>> print(sibling_soup.c.next_sibling)
None

“text1” 与 “text2” 却不是兄弟元素,因为他们没有同一个父元素:

>>> sibling_soup.b.string
u'text1'
>>> print(sibling_soup.b.string.next_sibling)
None

在真实的文档中, .next_sibling 与 .previous_sibling 得到的一般总是一个空白字符串,回到前面三姐妹的那个示例HTML文档中:

<a href="http://example.com/elsie" id="link1">Elsie</a>
<a href="http://example.com/lacie" id="link2">Lacie</a>
<a href="http://example.com/tillie" id="link3">Tillie</a>

你可以会认为第一个 <a> 的下一个元素是 Lacie,但是并不是这样的:

>>> link = soup.a
>>> link
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> link.next_sibling
u',n'
>>> link.next_sibling.next_sibling
<a href="http://example.com/lacie" id="link2">Lacie</a>

这是因为空白也是一个子元素,空白字符串不代表,没有。

.next_siblings 与 .previous_siblings

你可以使用 .next_siblings 与 .previous_siblings 来遍历某一个元素所有前面的兄弟元素或者所有它后面的兄弟元素:

>>> for sibling in soup.a.next_siblings: print(repr(sibling))
...
u',n'
<a href="http://example.com/lacie" id="link2">Lacie</a>
u' andn'
<a href="http://example.com/tillie" id="link3">Tillie</a>
u';nand they lived at the bottom of a well.'
>>> for sibling in soup.find(id='link3').previous_siblings: print(repr(sibling))
...
u' andn'
<a href="http://example.com/lacie" id="link2">Lacie</a>
u',n'
<a href="http://example.com/elsie" id="link1">Elsie</a>
u'Once upon a time there were three little sisters; and their names weren'

根据文档解析器的解析流程进行检索(Going back and forth)

返回来看看最开始的那个 三姐妹 的示例:

<html><head><title>The Dormouse's story</title></head>
<p><b>The Dormouse's story</b></p>

HTML 解析器对上面这段HTML文档的解析可以以一个一个的事件来表示:

  1. 打开一个 <html> 标签
  2. 打开一个 <head> 标签
  3. 打开一个 <title> 标签
  4. 添加一个字符串 “The Dormouse's story”
  5. 关闭 <title> 标签
  6. 关闭 <head> 标签
  7. 打开一个 <p> 标签
  8. ……

BeautifulSoup 提供工具可以让你根据这种流程对文档进行检索

.next_element 与 .previous_element

.next_element 属性指向当前元素的下一个元素,看上去它与 .next_sibling 似乎有些一样,但是其实是完全不同的两码子事儿,比如在三姐妹的示例中, 最后一个 <a> 的 .next_sibling 是一个字符串:

>>> last_a = soup.find('a', id = 'link3')
>>> last_a
<a href="http://example.com/tillie" id="link3">Tillie</a>
>>> last_a.next_sibling
u';nand they lived at the bottom of a well.'

但是 .next_element 却不一样,根据上面所说的章节的说明, 它的值应该是 Tillie 这个词:

>>> last_a.next_element
u'Tillie'

简单来说,next_sibling 或者 previous_sibling 是根据标签或者元素在文档中的最终位置来定义的,但是 .next_element 与 previous_element 则是根据元素的解析流程来定义的。

.previous_element 与 .next_element 正好相反,它表示在当前元素前面被解析的元素:

>>> last_a.previous_element
u' andn'
>>> last_a.previous_element.next_element
<a href="http://example.com/tillie" id="link3">Tillie</a>
>>> last_a
<a href="http://example.com/tillie" id="link3">Tillie</a>

.next_elements 与 .previous_elements

到现在为止你应该找到规律了吧, .next_elements 与 .previous_elements 检索当前元素的所有后面被解析的以及前面被解析的元素:

>>> for element in last_a.next_elements: print(repr(element))
...
u'Tillie'
u';nand they lived at the bottom of a well.'
u'n'
<p>...</p>
u'...'

在树中搜索(Searching the tree)

Beautiful Soup 定义了许多用来搜索解析树的方法,但是所有这些方法都差不多,我将着重花时间来解释两人个使用频率最多的方法 find() 与 find_all() ,其它的方法基本上都是使用同样的参数定义,所以我将对其它的只作简单的介绍。

同样的,我们还是使用三姐妹的示例:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p><b>The Dormouse's story</b></p>

<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p>...</p>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)

通过传递一个过滤器到某一个方法(比如 find_all() ),你可以获取到文档中的任何元素。

过滤器类型(Kinds of filters)

在对 find_all() 或者其它相类似的方法进行详细的说明前,我将先向你展示可以传递给这些方法的不同的过滤器类型,这些过滤器在整个搜索API中一次又一次的出现,你可以基于一个标签的名称,或者它的属性,或者一个文本字符串等等创建过滤器。

一个字符串

我简单的过滤器就是一个字符串,通过传递字符串到搜索方法中,Beautiful Soup将返回所有包含该字符的标签的元素:

>>> soup.find_all('b')
[<b>The Dormouse's story</b>]
>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

如果你传递的是一个 byte string, Beautiful Soup 会先将其编码为 UTF-8 字符串,所以你可以直接传递Unicode 字符串。

一个正则表达式

如果你传递一个正则表达式对象, Beautiful Soup 将使用它的 match() 方法对文档进行过滤,下面这段代码将搜索文档中所有以 b 开头的标签的元素:

>>> import re
>>> for tag in soup.find_all(re.compile('^b')): print(tag.name)
...
body
b

下面这段示例代码则搜索所有标签名称中包含 t 的元素:

>>> for tag in soup.find_all(re.compile('t')): print(tag.name)
...
html
title

一个列表

如果你传递的是一个列表,那么 Beautiful Soup 会返回“符合其中任何一个过滤器要求的”元素,下面这段代码将搜索到所有的 <b> 与 <a> 标签:

>>> soup.find_all(['a','b'])
[<b>The Dormouse's story</b>, <a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

True

True 将匹配文档中的所有标签,但是不包括字符串内容:

>>> for tag in soup.find_all(True): print(tag.name)
...
html
head
title
body
p
b
p
a
a
a
p

一个函数

如果上面的所有过滤器中没有一个能满足你的要求,那么你还可以提供自己的过滤函数,该函数需要接收一个标签作为参数,而且只允许接收这一个参数,它应该返回 True 或者 FalseTrue 表示匹配, False 表示不匹配。

下面这个示例函数将匹配所有具有 class 属性但是不没有 id 属性的元素:

>>> def has_class_but_no_id(tag):
...     return tag.has_key('class') and not tag.has_key('id')
...

我们使用这个函数对当前文档进行一些搜索过滤:

>>> soup.find_all(has_class_but_no_id)
[<p><b>The Dormouse's story</b></p>, <p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, <p>...</p>]
>>> for tag in soup.find_all(has_class_but_no_id): print(tag.name)
...
p
p
p

下面我们再定义一个示例函数,它将过滤出所有前后被字符串包裹的元素:

>>> from bs4 import NavigableString
>>> def surrounded_by_strings(tag):
...     return (isinstance(tag.next_element, NavigableString) and isinstance(tag.previous_element, NavigableString))
...
>>> for tag in soup.find_all(surrounded_by_strings): print tag.name
...
p
a
a
a
p

find_all()

函数签名: find_all(name, attrs, recursive, text, limit, kwargs)

find_all() 方法是根据标签的 “ 父-子 ” 关系对文档进行检索,并返回当前匹配过滤器的所有当前元素的子元素,在前面的一些示例中我已经使用过这个函数了,这里再列出一些更多该函数的示例:

>>> soup.find_all('title')
[<title>The Dormouse's story</title>]
>>> soup.find_all('p', 'title')
[<p><b>The Dormouse's story</b></p>]
>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.find_all(id='link2')
[<a href="http://example.com/lacie" id="link2">Lacie</a>]
>>> import re
>>> soup.find(text=re.compile('sisters'))
u'Once upon a time there were three little sisters; and their names weren'

上面这些示例中,有一些与我之前使用的是一样的,但是有一些却是刚刚才使用的新的,比如 text与 id 这两人个参数分别都表示什么?为什么 find_all('p','title') 返回一个 class 为 title 的 <p> 元素?下面让我们来详细的看看传递给 find_all() 方法的参数。

name 参数

传递一个 name 参数,代表你告诉 Beautiful Soup 你只需要名称为参数所指定的标签,文本字符串以及未匹配名称的标签都将直接忽略。

简单的用法示例:

>>> soup.find_all('title')
[<title>The Dormouse's story</title>]

关键字参数

如果一个函数未定义的关键字参数被传递给 find_all() 方法,那么该关键字的键将作为标签的属性进行搜索,而匹配规则是标签的该属性为该关键字参数的值,所以,当我们传递 id=“link2” 时,会匹配到 id 属性值为 link2 的元素:

>>> soup.find_all(id="link2")
[<a href="http://example.com/lacie" id="link2">Lacie</a>]

你可以使用一个字符串、正则表达式、列表、一个函数或者一个 True 值来过滤,比如下面的代码找到所有设定了 id 属性的元素:

>>> soup.find_all(id=True)
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

同样的,你还可以一次性通过多个属性对元素进行过滤:

>>> soup.find_all(href = re.compile('elsie'), id = 'link1' )
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

以 CSS 类为参数搜索

因为 class 是Python的一个关键字,所以我们无法以上面关键字参数的方式通过 class 属性过滤,但是我们有一个替代方案,那就是使用 class_ :

>>> soup.find_all('a', class_='sister')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

这个时候 class_ 就与前面的关键字参数无异,你同样可以使用一个字符、正则表达式等等的作为该参数的值:

>>> soup.find_all(class_=re.compile('itl'))
[<p><b>The Dormouse's story</b></p>]
>>> def has_six_characters(css_class):
...     return css_class is not None and len(css_class) == 6
...
>>> soup.find_all(class_=has_six_characters)
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
注意: class 是一个可多值的属性,当匹配任何其中一个值是,都会算成功匹配,如下示例代码:
>>> css_soup = BeautifulSoup('<p></p>')
>>> css_soup.find_all('p', class_='strikeout')
[<p></p>]
>>> css_soup.find_all('p', class_='body')
[<p></p>]

你也同样可以将完整的 class 值传递给 find_all() :

>>> css_soup.find_all('p', class_='body strikeout')
[<p></p>]

但是对于上例中,如果将两个类返过来写就不能匹配了:

>>> css_soup.find_all('p', class_='strikeout body')
[]

这是很不方便的一件事情,如果我们要通过 class 来搜索元素,这样的方法还需要 class 名称的顺序是统一的,但是我们可以写一个函数:

>>> css_soup = BeautifulSoup('<p></p><p>Another One</p>')
>>> css_soup.find_all('p', class_='body strikeout')
[<p></p>]
>>> css_soup.find_all('p', class_='strikeout body')
[<p>Another One</p>]

对于 class ,Beautiful Soup 还提供了一个快捷方式,那就是 attrs ,传递一个字符串给 attrs 将以 class 对元素进行过滤:

>>> soup.find_all('a', 'sister')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

你同样也可以传递一个正则表达式、函数或者 True,等参数,但是不允许使用字典类型的数据,这些都和使用 class_ 关键字的效果是一样的。

>>> soup.find_all('p', re.compile('itl'))
[<p><b>The Dormouse's story</b></p>]

如果传递的是一个字典的话,它不再仅仅只是当作 class 搜索了,而是以字典中的键为属性名称,值为属性值进行搜索:

>>> soup.find_all(href=re.compile('elsie'), id = 'link1')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]
>>> soup.find_all(attrs={'href': re.compile('elsie'), 'id': 'link1'})
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

使用 text 参数

除了对属性进行过滤外,还可以通过文本内容对元素进行过滤,使用的关键字就是 text ,你可以传递一个字符串、正则表达式、列表、函数或者一个 True 值,下面是一些简单的示例:

>>> soup.find_all(text='Elsie')
[u'Elsie']
>>> soup.find_all(text=['Tillie', 'Elsie', 'Lacie'])
[u'Elsie', u'Lacie', u'Tillie']
>>> soup.find_all(text=re.compile('Dormouse'))
[u"The Dormouse's story", u"The Dormouse's story"]
>>> def is_the_only_string_within_a_tag(s):
...     '''Return True if this string is the only child of its parent tag.'''
...     return (s == s.parent.string)
...
>>> soup.find_all(text=is_the_only_string_within_a_tag)
[u"The Dormouse's story", u"The Dormouse's story", u'Elsie', u'Lacie', u'Tillie', u'...']

虽然 text 是用来搜索字符串的,但是你可以通过绑定一个标签名称来搜索标签, Beautiful Soup 会搜索所有 .string 值匹配你所指定的字符串的元素:

>>> soup.find_all('a', text='Elsie')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

使用 limit 参数

limit 参数可以限定返回的结果集元素的数量,当一个文档十分大的时候,这个参数将十分有用,它所做的工作就像SQL中的 LIMIT 一样,在我们的示例文档中,有三个链接,但是我现在想将结果限定为两个:

>>> soup.find_all('a', limit=2)
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>]

使用 recursive 参数

通常的,当你从一个标签中呼叫 find_all() 方法时,它会搜索该函数的子元素,以及子元素的子元素……但是如果你将 recursive 参数设置为 False ,它将只匹配它自己的第一级子元素,而子元素的子元素则直接跳过:

>>> soup.html.find_all('title')
[<title>The Dormouse's story</title>]
>>> soup.html.find_all('title', recursive = False)
[]

下面这是我们示例的HTML代码片段:

<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>
...

<title> 虽然是 <html> 标签的子元素,但是并不是它的直属子元素,所以我们将搜索不到。

Beautiful Soup 提供了很多基于树的搜索方法,这些方法基本上都使用与 find_all() 同样的参数,唯一不同的是 recursive 参数只能被使用在 find_all() 与 find() 这两人个方法中,其它的方法都不支持该参数。

像呼叫 find_all() 一样呼叫一个标签

因为 find_all() 是我们使用得多的 Beautiful Soup 搜索API,所以这里还提供了一个呼叫该方法的快捷方式,如果你将一个 Beautiful Soup 对象或者一个 Tag 对象当作一个函数来对待,那么你得到的结果将和使用该对象呼叫 find_all() 方法得到的结果一样:

>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

下面这两人个也是一样的:

>>> soup.title.find_all(text=True)
[u"The Dormouse's story"]
>>> soup.title(text=True)
[u"The Dormouse's story"]

find()

函数签名: find(name, attrs, recursive, text, kwargs)

find_all() 搜索呼叫它的对象的所有内容,但是有时候你仅仅只想搜索一个结果,如果你知道整个HTML文档只有一个 <body> 元素,那么搜索整个文档完全是一种浪费,除了每一次你都指定一个limit=1 这个参数外,你还可以使用一个更简单的方法: find() 。

>>> soup.find_all('title')
[<title>The Dormouse's story</title>]
>>> soup.find('title')
<title>The Dormouse's story</title>

这两者之间唯一的区别是 find_a;;() 返回一个结果集(不管结果集中有多少元素),而 find() 只返回匹配到的一个结果,如果未找到任何结果,那么 find_all() 会返回一个空的列表,但是 find() 则返回None。

>>> print(soup.find_all('nothing'))
[]
>>> print(soup.find('nothing'))
None

如果你还记得 soup.head.title 这种方式的检索的话,那么你应该发现,这两者之间得到的结果是一样的,这是因为它本身就是调用的 find() 接口:

>>> soup.head.title
<title>The Dormouse's story</title>
>>> soup.find('head').find('title')
<title>The Dormouse's story</title>

find_parents() 与 find_parent()

函数签名: find_parents(name, attrs, text, limit, kwargs)

函数签名: find_parent(name, attrs, text, kwargs)

在本文的前面部分已经有很大的篇幅来介绍 find_all() 与 find() 两人个函数了,下面还有十个函数是 Beautiful Soup 提供的搜索API,五个类似于 find_all() ,另外五个类似于 find() 。这些函数之间的唯一区别是他们搜索的范围不同。

首先让我们来看看 find_parents() 与 find_parent() :

>>> a_string.find_parents('a')
[<a href="http://example.com/lacie" id="link2">Lacie</a>]
>>> a_string.find_parents('p')
[<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;

and they lived at the bottom of a well.

] >>> len(a_string.find_parents('p')) 1 >>> print(a_string.find_parents('p', 'title')) []

 

因为 <p> 与 三个 <a> 中的某一个是 a_string 的父元素,所以我们都能搜索得到结果,但是因为 class 为 title 的 <p> 元素并不是a_string的父元素,所以我们这里得不到结果,而 find_parent() 则类似 find() 仅仅只返回一个结果。

>>> a_string.find_parent()
<a href="http://example.com/lacie" id="link2">Lacie</a>
>>> a_string.find_parent('p')
<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

你可能已经与我们前面说过的 .parent 与 .parents 属性联系上的,这两人个属性与我们刚才所了解的find_parent() 与 find_parents() 的关联是很紧密的,这两人个函数本质上就是使用这两个属性进行遍历。

find_next_siblings() 与 find_next_sibling()

函数签名: find_next_siblings(name, attrs, text, limit, kwargs)

函数签名: find_next_sibling(name, attrs, text, kwargs)

这两个函数对 .next_siblings 生成器进行遍历以搜索:

>>> first_link = soup.a
>>> first_link
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> first_link.find_next_siblings('a')
[<a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> first_link.find_next_sibling('a')
<a href="http://example.com/lacie" id="link2">Lacie</a>

find_previous_siblings() 与 find_previous_sibling()

函数签名: find_previous_siblings(name, attrs, text, limit, kwargs)

函数签名: find_previous_sibling(name, attrs, text, kwargs)

这两个函数对 .previous_siblings 进行遍历以搜索:

>>> last_link = soup.find('a', id='link3')
>>> last_link
<a href="http://example.com/tillie" id="link3">Tillie</a>
>>> last_link.find_previous_siblings('a')
[<a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/elsie" id="link1">Elsie</a>]
>>> last_link.find_previous_sibling('a')
<a href="http://example.com/lacie" id="link2">Lacie</a>

find_all_next() 与 find_next()

函数签名: find_all_next(name, attrs, text, limit, kwargs)

函数签名: find_next(name, attrs, text, kwargs)

这两个函数对 .next_elements 进行遍历以搜索:

>>> first_link = soup.a
>>> first_link
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> first_link.find_all_next(text=True)
[u'Elsie', u',n', u'Lacie', u' andn', u'Tillie', u';nand they lived at the bottom of a well.', u'n', u'...']
>>> first_link.find_next('p')
<p>...</p>

find_all_previous() 与 find_previous()

函数签名: find_all_previous(name, attrs, text, limit, kwargs)

函数签名: find_previous(name, attrs, text, kwargs)

这两个函数对 .next_previous 进行遍历以搜索:

>>> last_link = soup.find('a', id='link3')
>>> last_link
<a href="http://example.com/tillie" id="link3">Tillie</a>
>>> last_link.find_all_previous('p')
[<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, <p><b>The Dormouse's story</b></p>]
>>> last_link.find_previous('title')
<title>The Dormouse's story</title>

CSS 选择器

CSS 选择器是一个十分强大的工具,Beautiful Soup 也提供了 CSS selector standard 定义的很多选择器,你仅仅只需要将选择器作为一个字符串传递给 .select() 即可:

你可以搜索标签:

>>> soup.select("title")
[<title>The Dormouse's story</title>]

或者标签中所有的子元素标签:

>>> soup.select('body a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

或者搜索某一个标签的下一层标签:

>>> soup.select('head > title')
[<title>The Dormouse's story</title>]
>>> soup.select('p > a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.select('body > a')
[]

通过 CSS class 搜索标签:

>>> soup.select('.sister')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

>>> soup.select('[class~=sister]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

或者通过ID搜索:

>>> soup.select('#link1')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]
>>> soup.select('a#link1')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

通过检测一个标签的属性是否存在搜索:

>>> soup.select('a[href]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

或者通过指定标签属性的值来搜索:

>>> soup.select('a[href="http://example.com/elsie"]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]
>>> soup.select('a[href^="http://example.com/"]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.select('a[href$="tillie"]')
[<a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.select('a[href*=".com/el"]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

匹配语言代码:

>>> multilingual_markup = """
...  <p lang="en">Hello</p>
...  <p lang="en-us">Howdy, y'all</p>
...  <p lang="en-gb">Pip-pip, old fruit</p>
...  <p lang="fr">Bonjour mes amis</p>
... """
>>> multilingual_soup = BeautifulSoup(multilingual_markup)
>>> multilingual_soup.select('p[lang|=en]')
[<p lang="en">Hello</p>, <p lang="en-us">Howdy, y'all</p>, <p lang="en-gb">Pip-pip, old fruit</p>]

该功能只要求用户有一定的CSS选择器知识即可使用。

修改树(Modifying the tree)

Beautiful Soup 的主要任务被设计为从现有文档中搜索与检索数据,但是你同样可以对文档的对象树进行修改并将其保存到一新的HTML或者XML文档中。

修改标签名称及属性

在本文的前面部分我就已经使用该功能做过示例,你可以重新为一个标签命名,修改一个标签的属性的值或者添加新属性以及删除已有数据等等:

>>> soup = BeautifulSoup('<b>Extremely bold</b>')
>>> tag = soup.b
>>> tag.name = 'blockquote'
>>> tag
<blockquote>Extremely bold</blockquote>
>>> tag['class'] = 'verybold'
>>> tag
<blockquote>Extremely bold</blockquote>
>>> tag['id'] = 1
>>> tag
<blockquote id="1">Extremely bold</blockquote>
>>> del tag['class']
>>> tag
<blockquote id="1">Extremely bold</blockquote>
>>> del tag['id']
>>> tag
<blockquote>Extremely bold</blockquote>

修改标签的 .string

如果你设置一个标签的 .string 属性,那么它的 contents 值将被修改为你设定的 .string 值:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> tag = soup.a
>>> tag.string = "New link text."
>>> tag
<a href="http://example.com/">New link text.</a>
注意:如果一个标签包含很多子标签,那么你使用这种方法修改 .string 值将会使用该值替代原来的所有内容,包括它的子标签等等。

追加- append()

你可以通过 Tag.append() 来添加标签的内容,它就像在Python List上使用 .append() 一样:

>>> soup = BeautifulSoup("<a>Foo</a>")
>>> soup.a.append("Bar")
>>> soup
<html><body><a>FooBar</a></body></html>
>>> soup.a.contents
[u'Foo', u'Bar']

BeautifulSoup.new_string() 与 .new_tag()

如果你需要向文档中添加字符串,你可以使用 append() 方法,或者你可以使用工厂方法BeautifulSoup.new_string() :

>>> soup = BeautifulSoup("<b></b>")
>>> tag = soup.b
>>> tag.append("Hello")
>>> new_string = soup.new_string(" there")
>>> tag.append(new_string)
>>> tag
<b>Hello there</b>
>>> tag.contents
[u'Hello', u' there']

如果你想插入一个完整的新的标签而不仅仅是往标签中插入内容,那最好的办法是使用BeautifulSoup.new_tag() 方法:

>>> soup = BeautifulSoup("<b></b>")
>>> original_tag = soup.b
>>> new_tag = soup.new_tag("a", href="http://www.example.com")
>>> original_tag.append(new_tag)
>>> original_tag
<b><a href="http://www.example.com"></a></b>
>>> new_tag.string = 'Link Text.'
>>> original_tag
<b><a href="http://www.example.com">Link Text.</a></b>

该方法仅仅只有第一个参数——标签名称是必须的。

插入- insert()

Tag.insert() 就像 Tag.append() 一样,不同在于它不仅仅只是将新标签添加到父元素 .contents 属性的最后,而可以将新标签添加到任何一个你所指定的位置,它就像 Python list中的 insert() 函数一样:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> tag = soup.a
>>> tag.insert(1, "but did not endorse ")
>>> tag
<a href="http://example.com/">I linked to but did not endorse <i>example.com</i></a>
>>> tag.contents
[u'I linked to ', u'but did not endorse ', <i>example.com</i>]

在前面或者后面插入 - insert_before() 与 insert_after()

insert_before() 方法会在标签或者字符串的前面立马插入新内容:

>>> soup = BeautifulSoup("<b>stop</b>")
>>> tag = soup.new_tag("i")
>>> tag.string = "Don't"
>>> soup.b.string.insert_before(tag)
>>> soup.b
<b><i>Don't</i>stop</b>

insert_after() 则是在标签或字符的后面立马插入新内容:

>>> soup.b.i.insert_after(soup.new_string(" ever "))
>>> soup.b
<b><i>Don't</i> ever stop</b>
>>> soup.b.contents
[<i>Don't</i>, u' ever ', u'stop']

清除- clear()

Tag.clear() 移除一个标签的所有内容:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> tag = soup.a
>>> tag.clear()
>>> tag
<a href="http://example.com/"></a>

extract()

PageElement.extract() 将一个标签从对象树中完全移除,并返回被移除的标签本身:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> i_tag = soup.i.extract()
>>> a_tag
<a href="http://example.com/">I linked to </a>
>>> i_tag
<i>example.com</i>
>>> print(i_tag.parent)
None

现在你得到了两人个根对象 BeautifulSoup 对象和另一个 extract 出来的对象,你可以继续对这个对象使用同样的方法:

>>> my_string = i_tag.string.extract()
>>> my_string
u'example.com'
>>> print(my_string.parent)
None
>>> i_tag
<i></i>

decompose()

decompose() 方法将一个标签从树中移除,然后 完全的删除它以及它的内容 。

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> soup.i.decompose()
>>> a_tag
<a href="http://example.com/">I linked to </a>

replace_with()

PageElement.replace_with() 删除一个标签或者一个字符串,然后使用一个你指定的标签或者字符串代替原来被删除的标签或者字符的位置:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> new_tag = soup.new_tag("b")
>>> new_tag.string = "example.net"
>>> a_tag.i.replace_with(new_tag)
<i>example.com</i>
>>> a_tag
<a href="http://example.com/">I linked to <b>example.net</b></a>

replace_with() 返回被移除的标签或者字符,这使得你可以在稍后重新将其加入文档或者插入到其它地方去。

wrap()

PageElement.wrap() 将你给定的元素使用你指定的标签包裹,它返回新的容器:

>>> soup = BeautifulSoup("<p>I wish I was bold.</p>")
>>> soup.p.string.wrap(soup.new_tag("b"))
<b>I wish I was bold.</b>
>>> soup.p.wrap(soup.new_tag("div"))
<div><p><b>I wish I was bold.</b></p></div>

unwrap()

Tag.unwrap() 是 wrap() 的反向操作,它使用标签中的内容来替换标签本身:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> a_tag.i.unwrap()
<i></i>
>>> a_tag
<a href="http://example.com/">I linked to example.com</a>

与 replace_with() 一样,unwrap() 返回替换前的原始标签。

输出(Output)

美化后输出

prettify() 方法将一个 Beautiful Soup 解析树格式化为一个美化的 Unicode 字符,每一个标签将独占一行,并进行合理的缩进:

    >>> str(soup)
    '<html><body><a href="http://example.com/">I linked to <i>example.com</i></a></body></html>'

昨天晚上申请的一个新域名,其实是有一个好的网站的想法,发扬我想到就做的优良传统,立马就注册了这个相应的域名, codinuts.com 为 *coding nuts* 的全体词,意思是 “*我为编码狂*”, 网站程序准备使用刚刚开始接触的 Golang 进行开发,数据库现在还没有想好使用什么,我其实现在正好也在研究 Amazon AWS,所以,数据库准备使用 AWS RDS,静态文件准备使用AWS S3,而网站的VPS则使用AWS EC2,恩,是一个不错的搭配。

该网站的功能简单来说就是:用户提出实际问题,用户用编码的方式解决实际问题。任何一个实际问题都可以让任何用户来解决,他们可以将他的解决方法公布出来,而每一个解决方法又可以被别的用户优化。更具体的东西,等以后再慢慢地说, 这里只是记录一下下想法,如果你发现这篇文章的发布时间已经是很久以前了,不防看看 Codinuts.com 是否已经上线了……

在 Unix Shell 中, 命令替换(Command Substitution) 是一个非常强大的概念,它被用来将某一个命令的输出作为参数直接插入到另一个命令中,如下例中,我将 date 命令的值赋于 *$today$ 变量,然后再将其输出:

cox@Cox:~$ today=$(date)
cox@Cox:~$ echo "$today"
2012年 11月 21日 星期三 01:39:31 CST

在上面的示例中,我们可以把两人行命令简化为一行:

cox@Cox:~$ echo $(date)
2012年 11月 21日 星期三 01:57:20 CST

或者像下面这样更复杂的使用:

cox@Cox:~$ echo "今天是 $(date +%A),现在时间是 $(date +%H:%M)"
今天是 星期三,现在时间是 01:59

这个示例中, echo 命令调用了 date 命令两个,第一次打印出星期几,第二次打印出当前时间,上面出现的三个命令的顺序是:

第一个 date &rArr; 第二个 date &rArr; echo

当然了,上面只是示例,其实我们有更简单的办法来打印出同样的信息:

cox@Cox:~$ date "+今天是 %A,现在时间是 %H:%M"
今天是 星期三,现在时间是 02:02

但是有一个问题需要注意,嵌套命令的结果会被进行 单词分割,除非所有的内容都被包裹在一个 双引号 中,否则这会导致有些时候你得到的并不是你自己想的,比如下面这行命令:

cox@Cox:~$ ls | grep Ubuntu
Ubuntu One
cox@Cox:~$ for i in $(ls | grep Ubuntu); do echo $i; done
Ubuntu
One

看到他们的不同了没有?我的当前目录下面本身是有一个名为 Ubuntu One 的目录,但是第二个命令本身的意途是想遍历当前目录中所有名称包含 Ubuntu 的文件,但是 Ubuntu One 却被当作两个来处理了,这是因为这个目录中有一个空格的存在,如前面所说的,结果会被进行单词分割,空格使得这本身是一个目录的名称被分割为两个了,对于这个问题,应该使用下面这样的命令去实现:

cox@Cox:~$ for i in Ubuntu*; do echo $i; done
Ubuntu One

命令是可以无限制嵌套的,比如下面这个示例:

cox@Cox:~$ echo $(echo (这是第一层嵌套($(echo 这是第二层嵌套))))
(这是第一层嵌套(这是第二层嵌套))

替换命令可以创建 Subshell ,这很有用,因为 subshell 所作的任何修改都只对它本身有效,不会污染程序的全局环境,比如下面这个示例:

cox@Cox:~$ var=$(cd ../../usr/bin/; pwd)
cox@Cox:~$ echo "$var"
/usr/bin
cox@Cox:~$ pwd
/home/cox

在命令替换中,父命令会去除子命令的输出结果中所有的换行:

cox@Cox:~$ ls -lh | grep Ubuntu
drwxrwxr-x 7 cox  cox      4.0K 11月 20 19:16 Ubuntu One
drwxrwxr-x 2 cox  cox      4.0K 11月 21 02:20 Ubuntu Two
cox@Cox:~$ echo $(ls -lh | grep Ubuntu)
drwxrwxr-x 7 cox cox 4.0K 11月 20 19:16 Ubuntu One drwxrwxr-x 2 cox cox 4.0K 11月 21 02:20 Ubuntu Two

通用性

$(command) 命令可以在 KornShellBASH以及PosixShell 中使用,老一些的 shell (比如BourneShell)则需要使用返引号"",形式是 *command*。

就像我以前说过的,我所想要的一个博客系统应该是最简单的,我只需要发布一些文章和几个静态页面而已,WordPress或者Drupal这种东西实在是太大了,所以我一直都是使用TextPattern作为我的博客发布系统的,从今天开始,我开始转到一个基本Python的纯静态发布系统 - Felix Felicis

静态发布是我最喜欢的方式(没有之一),我喜欢的网站发布系统应该是这样的,内容可以以最简单的方式管理,根据模板发布成为纯HTML页面,之后需要动态调用的(比如说评论需要服务器端处理)使用一个统一的接口(这似乎Movable Type是最合适的,但是它实在是太太大了),而现在这个就完全满足了我的要求。

今天一个下午的时间写了一个同步工具,我只需要在本地发布,然后该同步工具就会自动的将发布得到的HTML页面、CSS/JS文件、图片资源等等同步更新到我的一个S3桶里面去,而我的域名因为绑定到这个桶,所以就有了你现在看到的这个博客了,关于如何绑定域名到S3的桶,请看我前面专门介绍该问题的文章《Amazon S3的域名绑定》

我的同步工具现在还只是实现了将更新上至S3桶中,但是还没有任何其它优化,比如如果文件没有更新就不上传,如果本地已经不存在的,在S3服务器也同步删除掉那个对象等等功能,不过都还在开发中,应该今天晚上就会搞定的吧,刚刚开始使用S3没多久,还有很多事情需要做啊……评论我已经使用了DISQUS的服务,这样一来就省得我再在自己的服务器上写评论系统了,而且这个东西用起来并不比我写的差(应该说是好得多吧……)。

现在更新文章的方式再简单不过了:

  1. 在本地相应的目录中(比如: */home/cox/cox.im/content*)创建一个新文件(我取名为:*My Blog Is Using Static Publish System.md*)。
  2. 打开该文件,以下面这样的格式输入内容:
    # 我的博客现在已经开始使用静态发布系统进行更新了
    
    - url_title: my-blog-is-using-static-publishing-system
    - date: 2012-11-19 17:36:36
    - public: true
    - tags: Blog, Python, Amazon, S3, CMS, Boto
    - category: tech
    
    --------------------------------------------------------
    
    就像我以前说过的,我所想要的一个博客系统应该是最简单的,我只需要发布一些文章和几个
    静态页面而已,WordPress或者Drupal这种东西实在是太大了,所以我一直都是使用TextPattern
    作为我的博客发布系统的,从今天开始,我开始转到一个基本Python的纯静态发布系统。
  3. 保存该文件,在当前目录(*settings.py*)文件所在目录,执行下面命令:
    $ liquidluck build -v
  4. 重新发布完成之后,就可以使用Tornado服务器进行预览:
    $ liquidluck server
  5. 打开 http://127.0.0.1:8000 即可预览刚才的更新了,如果都没有问题,则执行我的同步工具:
    $ python sync.py
  6. 同步完成之后(这需要一些时间,但是如果是在服务器上面更新的话,那应该会快很多),即可打开自己的博客查看更新了http://cox.im/.

上面这是一种特定的格式,在第一个—————-*之前的所有内容都属于文章的Meta信息,之后的就是文章正文,Meta信息的格式就是上面这种的,我给本文定义了 *url_title*、*date*等属性,这些并不全都是必要的,但是 *date 如果有的话就称之为文章(博客文章),如果没有这个属性的话就是页面(静态页面是不需要跟踪时间的)。所有的这些属性都是可以在模板中使用的,模板引擎使用的是 *jinja2*。

我准备把现在使用的这个系统进行一些修改,增加一些功能,比如发布的时候可以选择发布在本地服务器、FTP发布、SHELL登陆远程服务器发布、S3发布等等,同时还需要增加一些其它更通用的细节功能吧,根本自己的需求来修改……

我一直使用的都是 MarkDown,使用起来很方便,当然 Felix Felicis 系统本身还支持reStructuredText,你也可以自己扩展适合你自己使用习惯的MarkUp Language。

这么算一下,已经有一个星期没有离开店子和仓库了,现在的生活就是这么简单,早上八九点醒来开始一天的工作,中午吃饭,再吃晚饭,再到晚上八九点,开始学习/写代码,晚上两人三点煮个饭拌点饭扫光,啊,不错,一天就这么过去了。

感觉得到自己在进步,虽然不知道这进步能给我带来什么,但是总不停在原地要好,虽然不在IT这个行业,但是一直还是放不下这个东西,或许这样更好,兴趣永远都还是我的兴趣,我可以学习一切我以前没有时间和精力去学习的东西,当然,不是为了工作,而仅仅只是兴趣。

只是感觉自己落后这个行业太远太远,所以,只能更加努力啊……

  • Python还是我使用的最主要的语言
  • 开始使用Bash脚本并开发了一些实用的服务器工作
  • 开始使用Golang
  • 开始使用Amazon AWS服务
  • 还是在使用Linode 的VPS服务
  • 还是回到Textpattern,最简单的才是我感觉最好的
  • 接了两人个小项目,不过最后的这一个钱还没收到
  • ……

一不小心又把博客域名给换了,以前的*note.costony.com*和*cox.antusoft.com*都会自动的转身到现在的这个新地址,对于使用 *cox.antusoft.com*转向过来的都可以对应到与cox.antusoft.com上面相同的文章上面,但是从note.costony.com上面转向过来的就不一定了,因为数据库使用的不是一样的。

其实我并不是一个米农,我只是想找一个最短的域名,自己好好地写写博客,但是在网上混了这么多年,也一直没有找到,或者说找到了要么别人注册着在用了,要么是我等草根买不起的,比如那个chenxi.com别人一出价就是四十万,按我现在的这点收入要不吃不喝100个月,可能买得起吗?

另外,虽然我一直最喜欢的网站内容发布方式是 Movable Type的那种纯静态的发布,但是因为它实在是太大了,我只是想写个博客,在Movable Type里面还得先建一个网站,然后在网站上面再建一个博客,太麻烦了,我还是一个ID控,这个让我对WordPress是直接Pass掉了的(虽然我也用过,但那都是以前的版本),现在的WordPress让我完全无法控制ID。

现在正在研究Golang以及Amazon AWS相关的产品,发现自己又落后了不少,得努力补习上来啊,博客的服务器转移到了 Amazon EC2上面,应该是可以免费使用一年吧,不过不知道像我这种资源乱用的人是不是每个月都会超出使用限制,甚至现在那个限制是多少我都没有认真的去研究过,反正应该不多吧。

另外,我现在正在准备把博客的前端界面换一下下,感觉还是有点儿太复杂了,换个更简单的。

Amazon EC2 的默认用户名为 *ec2-user*,在默认情况下,我们是没有办法修改这个用户的名称的,原因是这样的:1) 我们只能使用该用户登陆服务器;2) 登陆之后就无法修改它的用户名,但是我们还是有别的办法来修改它的。

直接不管它,创建一个新用户

这个方法其实足够了,创建了一个新的 EC2 Instance之后,使用下面的命令修改 root 帐户的密码:

[ec2-user@coxantu ~]$ sudo passwd root

这会提示你输入新密码以及重复输入一次,之后使用下面命令创建一个新用户:

[ec2-user@coxantu ~]$ su
密码:
[root@coxantu ec2-user]# useradd antu -p password

之后如下命令,修改 */etc/ssh/sshd_config* 文件

[root@coxantu ec2-user]# vi /etc/ssh/sshd_config

找到下面这部分并作相应的修改:

# To disable tunneled clear text passwords, change to no here!
#PasswordAuthentication yes

修改为

# To disable tunneled clear text passwords, change to no here!
PasswordAuthentication yes

之后重启 *sshd* 服务:

[root@coxantu ec2-user]# /etc/init.d/sshd restart
停止 sshd:                                                [确定]
正在启动 sshd:                                            [确定]

之后我们再退出,重新使用密码认证的方式就可以登陆服务器了,比如:

cox@CoxStation:~$ ssh cox@ec2-23-22-97-113.compute-1.amazonaws.com
cox@ec2-23-22-97-113.compute-1.amazonaws.com's password:

   __|  __|_  )
   _|  (     /   Amazon Linux AMI
  ___|___|___|

https://aws.amazon.com/amazon-linux-ami/2012.09-release-notes/
There are 3 security update(s) out of 29 total update(s) available
Run "sudo yum update" to apply all updates.
[cox@coxantu ~]$

这个时候我们也没有必要再去删除前面那一个默认用户了,当然,如果你想你的系统“干干净净”的,那你直接删除它即可。

rdesktop 可以在Linux中连接Windows远程桌面,不过像我这种天天挂Q的人还是很不方便,毕竟要么在远程桌面和Linux桌面中切换,要么在那个远程桌面的小窗口里面上Q,而QQ的Linux版本用起来还是很不爽,所以,在前面的文章里面带过 Seamless 这个事情之后,我还是在今天好好的研究了一下下这个东西。

如果你也和我一样想使用这个东西,可以从「Cendio 网站」下载,不过我在网上找了很多资料都直接提供了 Seamless的下载地址,但是这个地址却总是下载不了,会转向到http://www.cendio.com/seamlessrdp/,在这里面只能注册自己的邮箱地址然后下载 ThinLinc 软件的完整版,总共有90Mb,对于只想使用 seamlessrdp 的我来说太大了,那个小东西只有几百Kb,为了方便大家,我把这个提取出来再发布了,可以从「http://dl.antusoft.com/tools/thinlinc/tl-wts-tools.exe」这个地址下载,它是一个安装文件,在Windows桌面安装之后,会一同安装 “*seamlessrdpshell.exe*”工具。

如果你使用的是默认的安装,那么该文件的路径为:“*C:Program FilesThinLincWTSToolsseamlessrdpshell.exe*”,这个地址需要记住,因为我们在后面会使用到它。

完成Windows端的软件安装之后,我们现在只需要在Linux端执行 rdesktop 时加入一个 -s 参数即可,当然,还需要指定要打开的Windows软件的路径,比如我现在想打开我仓库的二号机的远程桌面,并且运行“*D:Program FilesBAISON_PGBS3000+CLIENT_CSCKBSBusiness.exe*”程序,那么可以使用下面这行命令:

rdesktop -s "C:Program FilesThinLincWTSToolsseamlessrdpshell.exe D:Program FilesBAISON_PGBS3000+CLIENT_CSCKBSBusiness.exe" 192.168.1.5 -u <Windows Username> -p <Windows Password>

这会在桌面的右下角打开一个默认大小的窗口,并且只运行 BSBusiness.exe 程序(它会自动打开)。(将 修改为你的Windows登陆用户名, 修改为你的帐户密码即可)。

但是这还不是很好,我需要它的窗口能大一些,而且不想放在右下角,一出来就能在我想要的位置,比如正中间(或者左上角),我们可以将上面的命令做下面这样的修改:

rdesktop -g 1400x900 -s "C:Program FilesThinLincWTSToolsseamlessrdpshell.exe D:Program FilesBAISON_PGBS3000+CLIENT_CSCKBSBusiness.exe" 192.168.1.5 -u <Windows Username> -p <Windows Password>

这会打开一个尺寸为 1400px * 900px 大小的窗口,其窗口的右下角还是对在桌面的右下角,这你应该看到,参数 -g 1400x900 指定了打开了桌面的大小,或许你还可以再修改成为下面这个样子:

rdesktop -g 1400x900+100 -s "C:Program FilesThinLincWTSToolsseamlessrdpshell.exe D:Program FilesBAISON_PGBS3000+CLIENT_CSCKBSBusiness.exe" 192.168.1.5 -u <Windows Username> -p <Windows Password>

这个时候你看到窗口,它已经不在右下角了,而是以它的左上角为定位点,距桌面上边距离为0,而左边距离为100px进行定位,我们还可以再加一个参数,用来修改窗口的上边与桌面上边的距离:

rdesktop -g 1400x900+100+100 -s "C:Program FilesThinLincWTSToolsseamlessrdpshell.exe D:Program FilesBAISON_PGBS3000+CLIENT_CSCKBSBusiness.exe" 192.168.1.5 -u <Windows Username> -p <Windows Password>

这还不是我想要的结果,我想要的是没有Windows桌面的出现,我想让程序就像是安装在Linux系统中一样,这个时候我们可以使用下面这样的命令:

rdesktop -A -s "C:Program FilesThinLincWTSToolsseamlessrdpshell.exe D:Program FilesBAISON_PGBS3000+CLIENT_CSCKBSBusiness.exe" 192.168.1.5 -u <Windows Username> -p <Windows Password>

这只是把 -g 这个参数去掉,再加上了一个 -A 这个参数,结果应该看到只有程序窗口,不再有桌面背景之类的了。

该版本增加了开源的PHP内容管理系统的安装,系统默认提供了由安图软件提供的开源软件源中精选的一些开源PHP内容管理系统,用户还可以自己为该脚本增加支持的CMS系统,只需要增加remote_apps 变量的项目即可,具体方法请见脚本说明。

您可以直接使用下列命令获得该脚本:

wget http://dl.antusoft.com/tools/server-setup-tool/sst-v0.3.sh

或者直接点击下载链接下载到本地

#!/bin/bash
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

# Check if the user if root
if [ $(id -u) != "0" ]; then
    echo "ERROR: You must login as root ro run this script!"
    exit 1
fi

#clear

# default global variables
declare -A vars
declare -A apps
# do you want to upgrade the server first before set it up?
vars=(
[upgrade_server]="n"
# the default website meta information
[default_website_owner]="cox"
[default_website_domain]="_"
[default_website_error_log]="n"
[default_website_access_log]="n"
[default_website_applications]=""
[default_website_error_log]="error.log"
[default_website_access_log]="access.log"
[default_website_log]="y"
# mysql database's root password
[mysql_root_password]="root"
# install nginx extras or not
[install_nginx_extras]="y"
[install_common_cgi_support]="y"
[install_perl_support]="y"
# install phpmyadmin to the document root of default website or not?
[install_phpmyadmin]="y"
[install_movabletype]="n"
)
export vars

apps=(
[apps_server_root]="http://dl.antusoft.com/"
[apps_server_root2]="http://dl.coin.so/"
[phpmyadmin]="tools/phpmyadmin/phpMyAdmin-3.5.3-all-languages.tar.gz"
[wordpress]="cms/wordpress/wordpress-3.4.2-zh_CN.tar.gz"
[wordpress_en]="cms/wordpress/wordpress-3.4.2.tar.gz"
[drupal]="cms/drupal/drupal-7.17.tar.gz"
[openatrium]="cms/drupal/dist/openatrium-6.x-1.5-core.tar.gz"
[dokuwiki]="cms/dokuwiki/dokuwiki-2012-10-13.tgz"
[textpattern]="cms/textpattern/textpattern-4.5.2.tar.gz"
[movabletype]="cms/movabletype/MT-5.2.tar.gz"
[mediawiki]=""
)

remote_apps=(
# you can add you own cms list here
# one record per line
# format:
# [CMS_NAME_KEY]="CMS_REMOTE_DOWNLOAD_URL"
# e.g.
[mediawiki]="http://download.wikimedia.org/mediawiki/1.20/mediawiki-1.20.0.tar.gz"
)

export remote_apps
export apps

# hightlight echo
function hightlight() {
    echo -e "33[40;31;5m $1 33[0m "
}

# separate line
function separate_line() {
if [ "$1" = "b" ]; then
echo "================================================================="
else
echo "-----------------------------------------------------------------"
fi
}

# function set variable to set vars
function set_variable() {
    separate_line
    # if not variable name
    if [ "$1" = "" ]; then
        hightlight "ERROR: no variable name!"
        return
    fi
    # if got a value
    if [ "$2" != "" ]; then
        vars[$1]=$2
    fi
    echo "Set the value of variable: $1"
    # echo the description of this value
    echo $3
    # get new value from user input
    read -p "current value is $(hightlight ${vars[$1]}) ): " v
    if [ "$v" != "" ]; then
        vars[$1]=$v
        echo "$1 => $v"
    else
        echo "no changes, default value will be used."
        echo "$1 => ${vars[$1]}"
    fi
}

function declare_variable() {
if [ "$2" != "" ]; then
    echo $2
else
    echo $1
fi
}

function check_user() {
echo $(cat /etc/passwd|grep $1|wc -l)
}

function create_user() {
if [ "$(check_user $1)" = "0" ]; then
useradd $1
fi
}

function copyright() {
separate_line b
echo "Server Setup Tool for Ubuntu - by Cox"
separate_line
echo "Version: 0.3"
echo "Modified: 2012/11/16"
echo "Url: http://cox.antusoft.com/project/ubuntu-server-setup-tool/"
separate_line
echo "Author: Cox Antu"
echo "Email: cox # antusoft.com"
echo "Url: http://cox.antusoft.com/"
echo "Mobile: 183-7488-8188"
echo "QQ: 54778899"
separate_line b
echo ""
}

function help() {
echo "-h --help for help"
echo "-s --setup for setup"
separate_line
}

function update_server() {
apt-get update
if [ "${vars[upgrade_server]}" = "y" ]; then
    echo "upgrading your server..."
    stty -echo
    echo y | apt-get upgrade
    stty echo
fi
separate_line
}

function install_mysql() {
echo "Installing mysql for you..."
echo y | apt-get install mysql-client mysql-server
separate_line
}

function install_nginx() {
echo "Installing nginx for you..."
if [ "${vars[install_nginx_extras]}" = "y" ]; then
    echo "You are going to install nginx-extras"
    echo y | apt-get install nginx-extras
else
    echo y | apt-get install nginx
fi
separate_line
}

function install_php() {
echo "Installing php support for nginx..."
echo y | apt-get install 
  php5 php5-fpm php5-mysql php5-curl php5-gd php5-idn 
  php-pear php5-imagick php5-imap php5-mcrypt php5-memcache 
  php5-mhash php5-ming php5-ps php5-pspell php5-recode 
  php5-snmp php5-sqlite php5-tidy php5-xmlrpc php5-xsl 
  php5-json

/etc/init.d/php5-fpm restart
separate_line
}

function php_socket() {
if [ -S /var/run/php5-fpm.sock ]; then
echo "unix:/var/run/php5-fpm.sock"
else
echo "127.0.0.1:9000"
fi
}

function install_common_cgi_support() {
echo "Installing common cgi support for nginx..."
echo y | apt-get install fcgiwrap
separate_line
}

function install_perl_support() {
echo "Installing perl support for nginx"
echo y | apt-get install 
  dbconfig-common libarchive-zip-perl libcache-perl 
  libclass-data-inheritable-perl libclass-errorhandler-perl 
  libclass-inspector-perl libclass-load-perl libclass-singleton-perl 
  libclass-trigger-perl libcommon-sense-perl libconvert-binhex-perl 
  libcrypt-dh-gmp-perl libdata-objectdriver-perl libdatetime-perl 
  libdatetime-timezone-perl libdbd-pg-perl libdbd-sqlite3-perl 
  libfile-nfslock-perl libgd-gd2-perl libheap-perl libimage-size-perl 
  libio-stringy-perl libipc-run-perl libjcode-pm-perl libjson-perl 
  liblist-moreutils-perl liblucene-queryparser-perl libmath-round-perl 
  liblwp-authen-wsse-perl libmime-charset-perl libmime-encwords-perl 
  libmodule-runtime-perl libnet-openid-common-perl libio-pty-perl 
  libossp-uuid-perl libossp-uuid16 libpackage-deprecationmanager-perl 
  libpackage-stash-perl libpackage-stash-xs-perl libparams-classify-perl 
  libparams-util-perl libparams-validate-perl libpq5 libsoap-lite-perl 
  libsub-install-perl libtask-weaken-perl libtext-simpletable-perl 
  libtry-tiny-perl liburi-fetch-perl libxml-atom-perl 
  libxml-xpath-perl libjson-xs-perl libxml-libxslt-perl 
  libdata-optlist-perl libdatetime-locale-perl libfcgi-perl 
  libmime-tools-perl libtheschwartz-perl libnet-openid-consumer-perl
separate_line
}

function setup_website_directly() {
# $1 => website domain name
# $2 => website owner
# $3 => applications list for install
# $4 => log or not to log
# $5 => error log
# $6 => access log
domain=$(declare_variable ${vars[default_website_domain]} $1)
owner=$(declare_variable ${vars[default_website_owner]} $2)
applications=$(declare_variable ${vars[default_website_applications]} $3)
log=$(declare_variable ${vars[default_website_log]} $4)
error=$(declare_variable ${vars[default_website_error_log]} $5)
access=$(declare_variable ${vars[default_website_access_log]} $6)

conf_file="/etc/nginx/sites-available/$domain"
listen="80"
server_name="$domain"
domain_root="/home/$owner/websites/$domain"
root="$domain_root/public"
logs_root="$domain_root/logs"

error_log="$logs_root/$error"
access_log="$logs_root/$access"
touch error_log
touch access_log
error_log="error_log $logs_root/$error"
access_log="access_log $logs_root/$access"
# if not to log the website
if [ "$log" = "n" ]; then
    error_log="#$error_log"
    access_log="#access_log"
fi
php_fastcgi_pass="$(php_socket)"

create_user $owner

mkdir -p $root
mkdir -p $logs_root

chown $owner:www-data -R $domain_root
chmod g+w -R $domain_root

cat >>$root/info.php<<EOF
<?php phpinfo(); ?>
EOF

# create configuration file
if [ -f $conf_file ]; then
    cp $conf_file $conf_file.bak
    cat /dev/null > $conf_file
fi

cat >>$conf_file<<EOF
server {
  listen 80;
  server_name $server_name;
  root $root;
  $error_log;
  $access_log;
  index index.php index.html index.htm;

  # php
  location ~ .php$ {
      try_files $uri = 404;
      fastcgi_pass $php_fastcgi_pass;
      fastcgi_index index.php;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      include fastcgi_params;
  }

  # perl cgi movable type
  location ~ ^/mt.*.cgi {
    # the exact pattern is required fo MT.
    fastcgi_split_path_info ^(/mt.*.cgi)(.*)$;

    fastcgi_pass unix:/var/run/fcgiwrap.socket;
    include fastcgi_params;

    # MT related
    fastcgi_param PERL5LIB $document_root/lib;
    fastcgi_param MT_HOME $document_root;
    fastcgi_param MT_CONFIG $document_root/mt-config.cgi;

    # fcgiwrap related
    fastcgi_param SCRIPT_NAME $document_root$fastcgi_script_name";
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name";
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
  }

}
EOF

cur_dir="$(pwd)"
cd /etc/nginx/sites-enabled/
if [ ! -L $domain ]; then
ln -s $conf_file
fi
/etc/init.d/nginx restart

echo "Setup website for $domain succeed!"
separate_line
}

# applications installer
function application_installer() {
# $1 => application name
# $2 => application domain name
# $3 => application owner
# $4 => application subfolder

app=$(declare_variable "textpattern" $1)
domain=$(declare_variable ${vars[default_website_domain]} $2)
owner=$(declare_variable ${vars[default_website_owner]} $3)
subfolder=$(declare_variable "" $4)

if [ "$1" == "" ]; then
  read -p "Do you want to select an application? (default to $app): " na
  if [ "$na" != "" ]; then
    app=$na
  fi
  read -p "Do you want to select an domain of this app? (default to $domain): " nd
  if [ "$nd" != "" ]; then
    domain=$nd
  fi
fi

read -p "do you want to install the application in a sub folder of your website
root? (default to n ):(y/N) " sf
if [ "$sf" != "" ]; then
subfolder=$app
fi

uri="${apps[$app]}"
url="${apps[apps_server_root]}$uri"
if [ "$uri" = "" ]; then
url="${remote_apps[$app]}"
fi
conf="/etc/nginx/sites-available/$domain"
root="/home/$owner/websites/$domain/public"

echo "URI: $uri"
echo "URL: $url"
echo "CONF: $conf"
echo "ROOT: $root"

# if the domain has been used
if [ -f "$conf" ]; then
hightlight "the domain has been used, do you want to choose another one?"
echo "If you want to select a new domain, just type it here,"
echo "or you can just pass this note and use this domain, if so, "
echo "i will make a backup of your exist website"
read -p "(default to $domain)" new_domain
  if [ "$new_domain" != "" ]; then
  # copy exist website
  # this will be separated to a standalone function
  # mv "$conf" "$root/$domain.conf"
  # cd "/home/$owner"
  # echo "backup exist website"
  # tar zcvf "$domain.backup.tar.gz" "/home/$owner/websites/$domain"
  # mv "$root" "$root.old"
  # setup_website_directly $domain $owner ""
    domain=$new_domain
    setup_website_directly $domain $owner ""
  fi
else
  setup_website_directly $domain $owner
fi

cd $root
wget -c $url

to="/tmp/application_installer/$app"
if [ ! -d $to ]; then
mkdir -p $to
fi

tar zxvf "$root/$(basename $url)" -C $to

sub=""
# how many files or folders?
dirs=($(ls $to))
if [ "${#dirs[@]}" = "1" ]; then
from="$to/${dirs[0]}/*"
else
from="$to/*"
fi
if [ "$subfolder" = "" ]; then
to="$root/"
else
to="$root/$subfolder/"
fi
if [ ! -d $to ]; then
mkdir -p $to
fi

echo "mv $from $to"
mv $from $to
echo "Install $app succeed"

echo "you can open your browser and visit : http://$domain/ to run
the application..."

}

function setup_website() {

read -p "(domain name): " domain
read -p "(owner): " owner
read -p "(applications): " applications
read -p "(log Y/n): " log
read -p "(error log): " error_log
read -p "(access log): " access_log

setup_website_directly $domain $owner $applications $log $error_log $access_log

}

function setup() {
copyright

# upgrade the server or not
set_variable upgrade_server "" "If you want to upgrade the server
 before set it up, please choose y"

# install common nginx or with extras
set_variable install_nginx_extras "" "Do you want to install the extra
 functions of nginx?"

# set the server's default domain name
set_variable default_website_domain "" "This should not be a domain you
 want to use in your pub site."

# set the owner of the default website
set_variable default_website_owner "" "The website's content will be
 stored in his home folder"

# set website logs
set_variable default_website_log "" "If you do not want to log your
  website, pleast choose n."
set_variable default_website_error_log
set_variable default_website_access_log

# install perl support or not?
set_variable install_perl_support "" "If you want to run perl
 applications in your server, you should choose y to install perl
 support for nginx server."

separate_line b

# update or upgrade the server
update_server

# install mysql server the client
install_mysql

# install nginx or nginx-extras
install_nginx

# install php and some extra libs
install_php
# install perl support or not
if [ "${vars[install_perl_support]}" = "y" ]; then
    install_perl_support
    install_common_cgi_support
fi

setup_website_directly
application_installer
# $1 => website domain name
# $2 => website owner
# $3 => applications list for install
# $4 => log or not to log
# $5 => error log
# $6 => access log
}

# ==================================================================== #
# Start the program

case "$1" in
--help|-h)
help
;;
--setup-website|-sw)
setup_website
;;
--setup|-s|--install|-i)
setup
;;
--install-app|-ia)
application_installer
;;
*)
setup
esac

一直都是使用的Linode的VPS,不过慢慢的,发现很不划算,我的网站访问量不是很大,但是图片量很大,比如我女儿的相册,上传的基本上都是原图或者大图,现在光图片就已经达到一个多G了,而总是出现的问题是除了VPS的空间容量不够用了之外,其它的资源都是有余很多的,所以我就想直接把静态文件都保存到Amazon S3服务上去,而我的服务器仅仅只做为程序的运行平台,而它的价格则低得多。

Amazon提供的费用计算器中粗略计算一了一下,差不多也就$12左右,我预定存储数据量为20Gb,每一个月的出流量为100Gb(这需要11.88美金,而其实我一个月也就几个G的出流量,按现在的实际情况来算的话,也就0.5美金不到,其它的都没什么钱了),从这个可以看出来,如果我的出流量不多的话,其实只需要5美金不到就足够了。

但是有一个问题是,Amazon并没有提供一种方法让你可以直接绑定自己的域名,所以,还是需要我们自己通过别的办法来实现这个,其实也是十分简单的事情,我们将使用到域名的CNAME记录,具体操作是这样的:

  1. 确定要绑定的域名,比如我这里是安图软件的文件下载站,域名使用 *download.antusoft.com*。
  2. 登陆 Amazon S3 控制台,创建一个新的*Buckets*,这里需要注册,名称选择你上面确定的域名,所以,我需要创建的Buckets的名称应该为:*download.antusoft.com*。
  3. 在本地创建两个文件 index.html 与 error.html ,并上传至刚才创建的 Bucket 的根目录中,并且设置这两个文件可以给公众访问(点文件上点击鼠标右键-> Make Public 或者选中文件,点击上方工具条中的 Actions,在下拉菜单中选择 Make Public)
  4. 打开改才创建的 Buckets 的属性设置面板,找到 Website 标签,将 Enable 复选框钩上(即开启网站服务),之后你还需要指定该网站的主索引文件和错误文件,我使用的分别是:
+ Index Document : *index.html*
+ Error Document : *error.html*
  1. 设置 download.antusoft.com 这个域名的CNAME记录至Amazon S3为你提供的那一个 URL 地址上,之后等待域名生效即可。

生效之后我们就可以使用 http://download.antusoft.com 这个地址来访问你刚才的S3 Bucket,整个过程我们其实可以把 Bucket 当作是一台静态文件存储服务器,我们只需要能通过我们的域名访问到这台“服务器”即可,而 Amazon S3 会将你域名的 CNAME 记录直接绑定到名为这个域名的Bucket上面。

今天在写我的 Ubuntu Server Setup Tool 时,因为在本地的环境下面 php5-fpm 默认监听 *127.0.0.1*,但是在我的 Linode VPS 上面,安装之后却默认监听 *unix:/var/run/php5-fpm.sock*,应该是版本不同的原因吧,所以我需要有一种办法,让程序知道是应该使用哪种监听方式(我们需要把这个写进 Nginx 的配置文件的),这个时候就用到我现在要说的这些东西了。

先来解决我的问题:我应该使用哪种监听方式?

因为,如果是以 Unix Socket 的方式监听的话,那肯定是需要有一个 Socket 文件的,而以IP+端口的方式却没有,所以,我现在可以把问题简单的转化为,是否存在一个相关的 Socket 文件,对于我现在要解决的这个问题,其实就是:是否存在 /var/run/php5-fpm.sock 文件,这样一来问题自然而然的就已经解决了:

#!/bin/bash
LISTEN_METHOD="127.0.0.1:9000"
if [ -S /var/run/php5-fpm.sock ]; then
  LISTEN_METHOD="unix:/var/run/php5-fpm.sock"
fi
echo $LISTEN_METHOD

执行包含上面这段代码的可执行文件,就可以知道是哪一直方式了(这个只是 apt-get 安装之后默认配置的位置,如果你修改了php5-fpm 的 Socket 文件的路径,那么这个需要修改一下下(不过如果你都知道修改了,那么我想你也应该知道使用哪种方式了吧,只是对于我的自动安装脚本来说,还是要自动选择比较好,就像上面这样的。

判断文件是否存在

要判断一个文件是否存在其实是很简单的一件事情,只需要像下面这样:

if [ ! -f /path/to/file ]; then
    touch /path/to/file
fi

在上面的示例中,会先判断 /path/to/file* 这个文件是否存在,如果不存在,则创建它,这里我们使用了“非”,即“!*”符号,如果去掉这个符号,那么就是“如果文件存在就怎么样”。

在上面的判断语句中 *-f* 是一个参数,它表示“文件存在,并且是一个普通文件”,与该参数具有同样功能的还有很多其它参数,但是不管参数蛤才能,后面部分都是你想判断的文件的路径。

与文件判断相关的参数

下面这些参数都是我们可能在 Shell 脚本中使用得到的:

参数|意义 -|- -a|文件存在 -b|文件存在并且是一个特殊的块文件 -c|文件存在并且是一个特殊字符文件 -d|文件存在并且是一个目录 -e|文件存在(与 -a 是一样的) -f|文件存在并且是一个普通文件 -g|文件存在,并且有setgid(2)设置 -G|文件存在,并且属于当前进程相同的组 -k|文件存在并且设置了sticky 标记 -L|文件存在,并且是一个动态链接 -O|文件存在,并且与当前进程属于相同的用户ID -p|文件存在,并且是先入先出(FIFO)的特殊文件或者命名管道 -r|文件存在,并且是当前进程可读取的 -s|文件存在,并且文件大小大于0 -S|文件存在,并且是一个Socket -t|文件描述符是对外开放的,并且关联一个终端设备 -u|文件存在,并且有 setuid(2) -w|文件存在,并且是当前进程可写的 -x|文件存在,并且是当前进程可执行的 -n|字符串长度不为0 -o|指定的选项已经设置 -z|字符串长度为0

上表中,-n*,-o,-z*是用来判断字符串或者选项的。

如果我们想直接在终端中判断而不使用 if 判断,那么我们还可以使用*test*命令,比如:

test -S /var/run/php5-fpm.sock

上面这样并不会显示任何内容,但是我们可以这样:

root@li485-100:~# test -S /var/run/php5-fpm.sock && echo "exist" || echo "Not exist"
exist
root@li485-100:~# test -S /var/run/php5-fpm.socket && echo "exist" || echo "Not exist"
Not exist

由于Nginx没有对CGI的原生支持,但是他同样还是可以运行类似Movable Type这样的程序(包括其它的一些基于CGI的程序),我们要使用到的中具就是 *fcgiwrap*,它就像是一个代理一样,把FCGI请求转发为CGI请求,所以,使用它,我们不需要修改一行程序代码。

首先需要安装 fcgiwrap:

root@CoxStation:~# aptitude install fcgiwrap

fcgiwrap* 可以以一个系统守护进程运行,并以一个Unix Socket文件访问(/var/run/fcgiwrap.socket)。现在我想让Movable Type的管理端通过 http://mt.csmumu.net访问到,下面这个就是完整的 server 配置代码。

server {
    server_name mt.csmumu.net;
    access_log /home/cox/logs/mt.csmumu.net.access.log;
    error_log /home/cox/logs/mt.csmumu.net.error.log;
    root   /home/cox/websites/mt.csmumu.net/public;

    location ~ ^/mt.*.cgi {
        ## The exact pattern is required for MT.
        fastcgi_split_path_info ^(/mt.*.cgi)(.*)$;

        include fastcgi_params;
        fastcgi_pass unix:/var/run/fcgiwrap.socket;

        ## MT-related
        fastcgi_param PERL5LIB $document_root/lib;
        fastcgi_param MT_HOME $document_root/;
        fastcgi_param MT_CONFIG $document_root/mt-config.cgi;

        ## fcgiwrap-related
        fastcgi_param SCRIPT_NAME     $document_root$fastcgi_script_name";
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name";
        fastcgi_param PATH_INFO       $fastcgi_path_info;
        fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
    }
}

感谢 CSS3 给我们带来了这么多的惊喜,这才让我可以完成今天的这个小作品,不使用任何 JavaScript 代码,实现可循环的幻灯片,它还带渐变等动画。不过这似乎是我对 CSS3 的一种乱用吧,因为比起使用 JavaScript 的各种库来说,使用 CSS3 来实现麻烦得多,而且它还有严重的浏览器兼容性问题,尤其是在神一样的国度,闲话少扯,下面入正题。

HTML 结构

整个 Slider 的HTML结构十分简单,就和我们平常写的 HTML 没有什么两样,下面是整个 Slider 的 HTML 代码:

<div>
  <div>
    <ul>
      <li>
        <a href="#"><img src="image-1.jpg" alt="First Image" /></a>
        <div>
          <h3>第一个幻灯片</h3>
          <p>这里一个幻灯片的介绍文字,您以前应该看到过很多这样的东西了吧?</p>
        </div>
      </li>
      <li>
        <a href="#"><img src="image-2.jpg" alt="Second Image" /></a>
        <div>
          <h3>第二个幻灯片</h3>
          <p>这里一个幻灯片的介绍文字,您以前应该看到过很多这样的东西了吧?</p>
        </div>
      </li>
      <li>
        <a href="#"><img src="image-3.jpg" alt="Third Image" /></a>
        <div>
          <h3>第三个幻灯片</h3>
          <p>这里一个幻灯片的介绍文字,您以前应该看到过很多这样的东西了吧?</p>
        </div>
      </li>
      <li>
        <a href="#"><img src="image-4.jpg" alt="Fourth Image" /></a>
        <div>
          <h3>第四个幻灯片</h3>
          <p>这里一个幻灯片的介绍文字,您以前应该看到过很多这样的东西了吧?</p>
        </div>
      </li>
      <li>
        <a href="#"><img src="image-5.jpg" alt="Fifth Image" /></a>
        <div>
          <h3>第五个幻灯片</h3>
          <p>这里一个幻灯片的介绍文字,您以前应该看到过很多这样的东西了吧?</p>
        </div>
      </li>
    </ul>
  </div><!-- /.slides -->
  <div></div><!-- /.progress-bar -->
</div><!-- /.slider -->
  1. div class=“slider”

    这是我们的幻灯片的主容器,整个幻灯片的所有内容都被放在该容器里面,我们将使用它*暂停动画的运行*。
  2. div class=“slides”

    我们所有的幻灯片都放在该容器中,在该容器中,存在一个无序列表,该列表的每一项目就是一张幻灯片。
  3. li

    一张幻灯片,我制作的这个幻灯片中包括一张图片,一个标题以及一段简短的介绍文字,这些都是我们最常用的格式。
  4. div class=“tooltip”

    这里面是幻灯片的标题以及介绍文字,你还可以根据需要添加一些你自己的的内容,比如让标题成为一个链接指向某一个网页。
  5. div class=“progress-bar”

    进度条,展示幻灯片动画的进度。

现在我们可以开始编辑 CSS 文件了。

 CSS 样式

我将所有的 CSS 样式都保存在名为 css3.css 的文件中。

.slider

我们使用这个类来创建幻灯片的整体结构:

.slider {
  font-size: 10px;
  background-color: #000;
  border: .5em solid #ddd;
  box-shadow: .1em .1em .5em rgba(0.0.0.0.7);
  height: 32em;
  width: 68em;
  margin: 5em auto 0;
  overflow: visible;
  position: relative;
}

.slides

从我们以前使用 JavaScript 实现的幻灯片的经验中,应该知道,我们总是需要有一个容器,它能完整的显示一个幻灯片,但同时它还需要可以隐藏掉没有激活的幻灯片,在我们的这个小项目中,.slides就是完成这个任务的。

.slides {
  overflow: hidden;
  height: 32em;
}

.slides ul & .slides li

最后我们还需要去除掉 ul 与 li 的默认样式,并且把所有的幻灯片都安排到一个初始位置,这个位置应该方便我们动画的完成,还不能让未激活的幻灯片显示出来。

.slides ul {
  margin: 0;
  padding: 0;
  list-style: none;
  position: relative;
}

.slides li {
  width: 68em;
  height: 32em;
  position: absolute;
  top: -32.5em;
}

CSS3 帧动画

在继续完成 CSS 代码之前,我们需要有一些关于当前这个小幻灯片的一些约定,这些约定也是我们需要使用 CSS3 去实现的,在本未例中,共有五张幻灯片,我准备按5秒一张的速度执行该幻灯片,那么总计将需要25秒完成整个幻灯片,但是我们需要知道 *1秒的时间能执行多少帧*。

所以,让我们做一个小小的算术题:

  1. 我们总个幻灯片使用的总幻灯片数目:*5*;
  2. 每一张幻灯片动画执行的时间:*5秒*;
  3. 整个幻灯片动画需要执行的时间:5张 x 5秒 = *25秒*;
  4. 每一秒动画执行的帖数为:155 / 25 = *4帧*。

现在我们可以来继续完成 CSS 了,我们将给所有幻灯片都设置为无限循环,因为每一张幻灯片都是在完成它自己的动画,是互不干扰的。

.slides li:first-child {
  animation: first 25s linear infinite;
}
.slides li:nth-child(2) {
  animation: second 25s linear infinite;
}
.slides li:nth-child(3) {
  animation: third 25s linear infinite;
}
.slides li:nth-child(4) {
  animation: fourth 25s linear infinite;
}
.slides li:nth-child(5) {
  animation: fifth 25s linear infinite;
}

现在我们已经为每一个幻灯片设定了一个专属于它的动画(CSS animation 的第一个参数即它所绑定的动画的名称),我们现在只需要再定义每一个名称所指定的动画的实现流程即可(注意:运行必须要先定义才能使用,所以我们应该把动画的定义放在 CSS 文件的最前面)。

/* ANIMATION */
@keyframes first {
  0%  { top:0px; }
  4%  { top:0px; }
  16% { top:0px; opacity:1; z-index:0; }
  20% { top:32.5em; opacity:0; z-index:0; }
  21% { top:-32.5em; opacity:0; z-index:-1; }
  92% { top:-32.5em; opacity:0; z-index:0; }
  96% { top:-32.5em; opacity:0; }
  100%{ top:0px; opacity:1; }

}
@keyframes second {
  0%  { top:-32.5em; opacity:0; }
  16% { top:-32.5em; opacity:0; }
  20% { top:0px; opacity:1; }
  24% { top:0px; opacity:1; }
  36% { top:0px; opacity:1; z-index:0; }
  40% { top:32.5em; opacity:0; z-index:0; }
  41% { top:-32.5em; opacity:0; z-index:-1; }
  100%{ top:-32.5em; opacity:0; z-index:-1; }
}
@keyframes third {
  0%  { top:-32.5em; opacity:0; }
  36% { top:-32.5em; opacity:0; }
  40% { top:0px; opacity:1; }
  44% { top:0px; opacity:1; }
  56% { top:0px; opacity:1; }
  60% { top:32.5em; opacity:0; z-index:0; }
  61% { top:-32.5em; opacity:0; z-index:-1; }
  100%{ top:-32.5em; opacity:0; z-index:-1; }
}
@keyframes fourth {
  0%  { top:-32.5em; opacity:0; }
  56% { top:-32.5em; opacity:0; }
  60% { top:0px; opacity:1; }
  64% { top:0px; opacity:1; }
  76% { top:0px; opacity:1; z-index:0; }
  80% { top:32.5em; opacity:0; z-index:0; }
  81% { top:-32.5em; opacity:0; z-index:-1; }
  100%{ top:-32.5em; opacity:0; z-index:-1; }
}
@keyframes fifth {
  0%  { top:-32.5em; opacity:0; }
  76% { top:-32.5em; opacity:0; }
  80% { top:0px; opacity:1; }
  84% { top:0px; opacity:1; }
  96% { top:0px; opacity:1; z-index:0; }
  100%{ top:32.5em; opacity:0; z-index:0; }
}

到现在为止,我们的幻灯片已经可以正常的工作了,那么接下面,把 .tooltip 和 .progress-bar 的问题也解决掉那就完美了。

进度条

进度条我准备放在幻灯片的最下面,它是一个从左至右慢慢被颜色填充的长方形容器,它的 CSS 样式如下:

@keyframes progressbar {
  0%, 20%, 40%, 60%, 80%, 100% { width:0%; opacity:0; }
  4%, 24%, 44%, 64%, 84% { width:0%; opacity:0.3; }
 16%, 36%, 56%, 76%, 96% { width:100%; opacity:0.7; }
 17%, 37%, 57%, 77%, 97% { width:100%; opacity:0.3; }
 18%, 38%, 58%, 78%, 98% { width:100%; opacity:0; }
}

[...]

.progress-bar {
  position: absolute;
  bottom: 0;
  width: 100%;
  height: .5em;
  background-color: #000;
  animation: progressbar 25s ease-out infinite;
}

标题与介绍

幻灯片本身已经完成啦,现在我们再来给他的标题与介绍做一些细节上面的处理。

.tooltip {
    position: absolute;
    width: 100%;
    left: -100%;
    padding: .5em 0;
    bottom: 1em;
    color: #f0f0f0;
  background-color: rgba(0,0,0,0.7);
  transition: all 0.3s ease-in-out;
}

.slider:hover .tooltip {
  left: 0;
}

.tooltip h3 {
    padding-left: .5em;
  font-size: 1.6em;
  margin: 0;
}

.tooltip p {
  padding-left: .8em;
  margin: 0;
}

到现在为止我们的幻灯片已经完成做完了,最终的效果你可以下载本示例的打包文件在自己的电脑中测试,下载链接在本文的下方。

常见问题

  1. 我下载了打包文件可以运行,但是自己按照本文的代码写的去无法运行,这是怎么回事儿?

    这个问题很好理解啊,transition, animation 这些东西到现在都还没见着哪个浏览器直接支持,而是使用了自家的指令,比如对于 Chrome浏览器我们应该使用:*-webkit-animation*,但是对于火狐我们却又得使用 *-moz-animation*,但是在本文的文章中,我并没有把这每一个都写出来,而是直接写了 *animation*,所以,如果你接照我的文章来做的话,你需要自己加各种浏览器的不同版本的指令才行,IE的话直接跳过吧……

从05年使用Bluehost的虚拟主机开始,就知道可以通过服务器来达到翻山跃岭的目的,以前都是使用虚拟主机,然后配上主机服务器提供的Shell帐户在本地创建一个代理来达实现,记得那个时候用得最多的就是DreamHost的,当然,那还是一个流行着合租虚拟主机的时代。

从09年开始就慢慢地都转到VPS上面来了,到现在我已经只使用VPS了,以前最常使用的是CentOS作为服务器系统,后来慢慢地全部转到Ubuntu上面来了,现在又正准备转到openSUSE上面去,玩什么就转什么吧,毕竟我不是一个IT行业中人了,什么东西精不精对我来说没有太大的影响了,完全是自己的兴趣了。

时代不同,山还是那座山,岭还是那道岭,有些时间走不过还是得借一些工具,以致于本身因为安全考虑的VPN产品现在成了跳出牢笼的工具,一直以来我也是那种过不过都无所谓的人,所以,虽然我的服务器都在国外,我也从来都没有做过什么不符合政策的事情来,只是现在还是感觉,多走出去看看对自己的个人成长还是有好处的,所以,我还是想着,哪天还是整整这个吧,哪知一整,整去了我半条命。

闲话多说无益,解决不了的问题还是要解决,所以,还是回到正题来吧,不过首先申明,我这不是什么教程,纯粹只是记录,有可能我今天真的解决了我应该要解决的问题,但是并不一定就能成为你们的参考。

操作平台的选择

我是准备在openSUSE上面架设的,可是因为我还有两台Ubuntu的VPS。所以我还得解决Ubuntu的问题(好像这个更好解决一些?也许是因为可参考的资料多得多吧),软件的话我也就没啥选择了的,大家都在说openvpn,就这个了,其实我自己个人是没法儿去选择什么的,因为我对这一块是完全不通,所以只能随大流,而这里面的道道也就只能硬着头皮去学了。

使用openvpn

这个安装在两种系统里面都不难,比如下面这样的:

yast2 -i openvpn

或者在 Ubuntu 下面:

apt-get install openvpn

(*题外话*:我一朋友安装这个东西安装了一天,使用的是源码安装,唉,我还是没那么有勇气去玩源码安装,还是使用这种最简单的办法吧!)

配置openVPN

就是这个东西整死我了,尤其是对于我这种什么基础知识都没有的人来说就更是难上加难了啊,从昨天晚上8点多到现在,我还是没有搞定,现在也只能再继续去摸索了。

openSUSE下面的安装与配置

我还是先使用 openSUSE 吧,因为我更喜欢这个破东西一点。

安装 openVPN

我们可以先通过*zypper se*命令来查找一下下我们的软件源里面是否已经有openvpn了,运行下面这样的命令:

cox:~ # zypper se openvpn

这将会得到一个结果集,它在终端里面是一个表格,我们可以很轻松地看到 openvpn 这七个字母在那里啊,所以,我再直接安装了:

cox:~ # yast2 -i openvpn

包构建安装工具能为我们解决很多事情,比如包的依赖关系等。

复制需要的目录

我们现在需要把 */usr/share/openvpn* 复制到 */etc/* 目录下面,yast2 安装在安装 openvpn 是会创建一个 */etc/init.d/openvpn* 的启动脚本,该脚本会自动的搜索 */etc/openvpn* 中的配置文件:

cox:~ # cp -r /usr/share/openvpn /etc/
cox:~ # cd /etc/openvpn/
cox:/etc/openvpn # ls
easy-rsa
cox:/etc/openvpn #

创建主CA与KEY文件

现在我们修改工作目录到 */etc/openvpn/easy-rsa/2.0/* 下,修改 *./vars* 文件:

# These are the default values for fields
# which will be placed in the certificate.
# Don't leave any of these fields blank.
export KEY_COUNTRY="CN"
export KEY_PROVINCE="HN"
export KEY_CITY="ChangSha"
export KEY_ORG="AntuSoftInc"
export KEY_EMAIL="cox@antusoft.com"
export KEY_EMAIL=cox@antusoft.com
export KEY_CN=antusoft
export KEY_NAME=cox
export KEY_OU=antusoft
export PKCS11_MODULE_PATH=changeme
export PKCS11_PIN=1234

保存之后执行下面这样的命令:

cox:/etc/openvpn/easy-rsa/2.0 # . ./vars
NOTE: If you run ./clean-all, I will be doing a rm -rf on /etc/openvpn/easy-rsa/2.0/keys
cox:/etc/openvpn/easy-rsa/2.0 # ./clean-all
cox:/etc/openvpn/easy-rsa/2.0 # ./build-ca
Generating a 1024 bit RSA private key
.................................................++++++
..++++++
[......]
cox:/etc/openvpn/easy-rsa/2.0 #

创建服务器证书与key文件

在 */etc/openvpn/easy-rsa/2.0* 目录中执行下面这条命令来创建服务器端的证书与 key 文件:

cox:/etc/openvpn/easy-rsa/2.0 # ./build-key-server server
Generating a 1024 bit RSA private key
[,,,]
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated
cox:/etc/openvpn/easy-rsa/2.0 #

创建客户端证书与key文件

与服务器证书创建在同样的一个目录中,运行下面这样的命令:

cox:/etc/openvpn/easy-rsa/2.0 # ./build-key-server server
Generating a 1024 bit RSA private key
[......]
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated
cox:/etc/openvpn/easy-rsa/2.0 #

创建Diffie-Hellman (DH) 文件

使用下面这样的命令创建Diffie-Hellman文件:

cox:/etc/openvpn/easy-rsa/2.0 # ./build-dh
Generating DH parameters, 1024 bit long safe prime, generator 2
This is going to take a long time
....................+...++*++*++*
cox:/etc/openvpn/easy-rsa/2.0 #

现在我们可以进入 *./vars* 目录,可以看到刚才生成的所有这些证书文件与密钥文件了:

cox:/etc/openvpn/easy-rsa/2.0 # cd ./keys/
cox:/etc/openvpn/easy-rsa/2.0/keys # ls
01.pem  ca.key      client.key  index.txt.attr      serial      server.csr
02.pem  client.crt  dh1024.pem  index.txt.attr.old  serial.old  server.key
ca.crt  client.csr  index.txt   index.txt.old       server.crt
cox:/etc/openvpn/easy-rsa/2.0/keys #

这些文件分别是:

  • *ca.crt*-服务端与客户端的根证书
  • *ca.key*-登陆机器专用的CA密钥文件
  • dh1024.pem*-服务器端DH文件,格式为*dh.pem
  • *server.crt*与*server.key*-服务器端证书与密钥文件(名称就是我们生成它时输入的)
  • *client.crt*与*client.key*-客户端证书与密钥文件(名称也是我们创建时指定的)

服务器端配置文件

安装了 openvpn 之后,会自动创建一个示例配置文件,它被保存在 */usr/share/docs/packages/openvpn/sample-config-files/* 目录中,我们为了简单,就不再自己全新的创建配置文件了,而是把这个示例配置文件复制到 */etc/openvpn* 目录稍作修改即可:

cox:/etc/openvpv # cp /usr/share/doc/packages/openvpn/sample-config-files/server.conf ./

编辑该文件,作如下所示的修改:

  • port 1194
  • proto udp
  • ca /etc/openvpn/easy-rsa/2.0/keys/ca.crt
  • cert /etc/openvpn/easy-rsa/2.0/keys/server.crt
  • key /etc/openvpn/easy-rsa/2.0/keys/server.key
  • dh /etc/openvpn/easy-rsa/2.0/keys/dh1024.pem
  • dev tun
  • server 10.8.0.0 255.255.255.0

修改完成之后,运行服务器端:

cox:/etc/openvpn # /etc/init.d/openvpn start
redirecting to systemctl
cox:/etc/openvpn #

客户端配置文件

与服务器端配置文件一样,客户端也有一个示例文件被保存在 server.conf 示例文件同一个目录下面,我们现在进入我现在的系统中,并且将前面生成的这些文件都保存到本地客户端电脑中的:*/home/cox/vpnconf*目录中。

需要创建一个地方:

remote 50.116.15.100 1194

上面的IP地址为无端服务器地址,如果你的和我不一样,那么就改成你的即可,商品是我们的服务器的配置文件里面定义,默认是1194我没有做任何修改,如果你做了什么修改,也需要改成你自己设置的端口号。

测试是否能成功连接上

root@Cox:/home/cox/vpnconf# openvpn ./client.conf
[...]
Fri Nov  9 11:16:37 2012 Initialization Sequence Completed
.

当出现了 *Initialization Sequence Completed*的时候,使用 *ifconfig* 查看到下面这样的信息:

tun0      Link encap:未指定  硬件地址 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
          inet 地址:10.8.0.6  点对点:10.8.0.5  掩码:255.255.255.255
          UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1500  跃点数:1
          接收数据包:1 错误:0 丢弃:0 过载:0 帧数:0
          发送数据包:2 错误:0 丢弃:0 过载:0 载波:0
          碰撞:0 发送队列长度:100
          接收字节:52 (52.0 B)  发送字节:104 (104.0 B)

再用一个更加直观的办法来检测一下,我的服务器IP地址是 50.116.15.100,我在服务器上面安装Nginx服务器,直接使用这个服务器IP地址是可以访问的,而现在我使用 http://10.8.0.1/也能访问到这个服务器了,因为对于我的桌面电脑与服务器来说,他们现在已经是点对点处在同一个网里面了。

Mac用户的客户端解决办法

不用多说了吧,就使用TunnelBlick即可,这个使用方法其实很简单,只需要把上面我们的 vpnconf目录成为一个特别的名称,比如:

AhaInsight.tblk

然后,把*client.crt*与*client.key*两个文件的名称都改为*AhaInsight*即可。

在你安装了TunnelBlick软件之后,以 .tblk 后缀结尾的目录会被视为一个TunnelBlick包文件,直接双击它即可把这个配置添加到TunnelBlick中去。

Ubuntu中OpenVPN的安装与

其实与在 OpenSUSE中是一样的,只是好像就easy-rsa/server.conf/client.conf这几个目录和文件的保存位置不一样,其它的都是一样的,自己玩吧。

通过openVPN共享服务器上网

其实我前面整那么多,发现对于我来说是没有任何用处的,我真正的需求在本文的最前面就已经说得很清楚了:

我要翻山跃岭,跃过长江大河,飘洋过海去看看外面的世界

而现在,我只是从黄花机场飞到了New York,但是却没签证,出不了机场的大门,啊,这不算去外面的世界看了,所以,我还需要做更多的事情,一些能真正让我《自由地飞翔》的事情,也让《我飞得更高》的事情,什么事?我现在还不知道,只能继续来摸索了。

我的一点点理解是这样的,要实现我所想实现的目标,那么可能需要满足下面这两个条件:

  1. 服务器端允许客户通过它访问外网
  2. 客户端要自己知道它是要通过服务器访问外网

把这样的想法转换成具体的实现,那么可能是这样的:

  1. 我打开某个网站
  2. 客户端告诉服务器哥要访问这个网站了
  3. 服务器把这些请求发送出去(这个时候服务器就像我们的TP-Link了吧)
  4. 服务器再把回复过来的东西发到客户端这里

啊,这些个都只是我的想法,不过也没有去看过相关的资料,还不知道是不是按着这样的思路下面,有时间再找找一些资料再来好好整整这个东西,至少现在VPN已经是连上了,至少,哥现在已经坐上灰机了。饿,想想不对,我只是买到机票了而已……

使用PPTP

实然之间,把脑子里面的水给掉跳了,发现原来还有PPTP这等东西,对于我们这种电脑白痴来讲,这东西应该简单得多吧,所以,我就又试了试使用PPTP,于是乎,我把VPS又重新给安装了,现在回到一个空白的系统中来。

安装PPTP

在 Ubuntu 下面的话,像下面这样的安装PPTP的服务端程序:

root@ubuntu:~# apt-get install pptpd

在SuSE中的话,这种事情还是交给 yast吧:

suse:~ # yast2 -i pptpd

哦,对了,这个是服务器端,如果要安装客户端的话,请使用:

suse:~ # yast2 -i pptp

而对于 Ubuntu,pptp客户端的名称应该是 pptp-linux

安装好了之后,又回到让人头痛的事情上面来了。

配制PPTP服务器端

PPTP我看了看说明,好像比OpenVPN简单得多啊。

创建用户及密码

用来登陆 PPTPD 服务器的用户与其登陆密码是保存在 */etc/ppp/chap-secrets* 文件中的,我们先进入到这个 */etc/ppp* 目录来,再修改这个文件:

root@ubuntu:~# cd /etc/ppp
root@ubuntu:/etc/ppp# ls
chap-secrets  ip-up      ipv6-down.d  options      pppoe_on_boot
ip-down       ip-up.d    ipv6-up      pap-secrets  resolv
ip-down.d     ipv6-down  ipv6-up.d    peers
root@ubuntu:/etc/ppp# emacs chap-secrets

看上面列出的第一个文件,就是我们需要修改的文件,打开它之后会发现,其实这个文件里面除了被注释掉的各种各样的说明之外,什么配置都没有,就当这里是一个空数据库吧,用来存储用户帐户与密码的空数据库,但是可以看到最后的一行标明了我们应该怎么写这个文件,如下所示:

#client         hostname        <password>      192.168.1.1
cox     pptpd       iamcox  *

对于上面我添加的这一行的意思是这样的:

  • cox : 用户名
  • iamcox : 登陆使用的密码明文

其它的就暂时不去管吧,如果要改也就只需要改这两样就成了。

添加DNS配置

我想使用Google的“死吧!”DNS服务,它的服务器IP地址是:*8.8.8.8*,在我的系统(openSUSE 12.2)里面需要修改 */etc/ppp/options* 文件,你的可能不一样,但是位置是一样的,有可能叫作:*pptpd-options*吧:

root@ubuntu:/etc/ppp# emacs options
[...]
ms-dns 8.8.8.8
[...]
netmask 255.255.255.0
[...]

修改PPTP的配置

PPTP的配置文件一般都是保存在 */etc* 这个目录的 *pptpd.conf* 文件中,打开并像下面这样的编辑它:

root@ubuntu:/etc/ppp# emacs /etc/pptpd.conf
localip 10.8.0.1
remoteip 10.8.1.100-199

修改完成之后启动 PPTPD:

root@ubuntu:/etc/ppp# /etc/init.d/pptpd start

配制并测试PPTP客户端

在客户端,我们需要先安装PPTP,安装方法在上面已经介绍了,这里又不再说了。

手工添加 /etc/ppp/peers/pptpd 文件

注意啦,注意啦,*/etc/ppp/peers/pptpd* 是一个文件,而不是一个目录,使用 *touch*命令也行,或者使用 *vi /etc/ppp/peers/pptpd* 或 *emacs /etc/ppp/peers/pptpd* 即可,其内容如下:

pty "pptp 50.116.15.100 -nolaunchpppd"
name cox
remotename pptpd
require-mppe-128
file /etc/ppp/options.pptp
ipparam pptpd

修改 *chap-secrets*(同服务器)

如果你本地电脑和我一样,与服务器用的是一样的话,那么这里和服务器上面是一样的。

测试连接

我们使用下面这种Debug方式来测试连接:

root@Cox:/etc/ppp# pon pptpd debug dump logfd 2 nodetach
[...]
rcvd [IPCP ConfAck id=0x2 <compress VJ 0f 01> <addr 10.8.0.100>]
local  IP address 10.8.0.1
remote IP address 10.8.0.100
Script /etc/ppp/ip-up started (pid 15997)
Script /etc/ppp/ip-up finished (pid 15997), status = 0x0

出现上面这样的信息,就证明我的客户端已经成功的连接上服务器了。现在使用 *ifconfig* 来查看一下下:

ppp0      Link encap:点对点协议
          inet 地址:192.168.1.234  点对点:192.168.0.234  掩码:255.255.255.255
          UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1496  跃点数:1
          接收数据包:5 错误:0 丢弃:0 过载:0 帧数:0
          发送数据包:5 错误:0 丢弃:0 过载:0 载波:0
          碰撞:0 发送队列长度:3
          接收字节:62 (62.0 B)  发送字节:68 (68.0 B)

配置IP转发

修改 */etc/sysctl.conf*文件,即去掉下面这一行前面的注释即可:

net.ivp4.ip_forward=1

修改完成之后重新加载IP转发配置:

root@ubuntu:/var/log# sysctl -p

 设置网络地址翻译

修改 iptables :

root@ubuntu:/var/log# iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

上述设置在重起后会丢失,因此需要修改 */etc/rc.local*,在 exit 0之前添加以下语句:

iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

一些小问题

  1. 修改了 */etc/pptpd.conf* 文件之后出现:*Long config file line ignored.*错误

    这个问题整死我了,一直找不到原因,后来,无意之间发现,*/etc/pptpd.conf* 文件内容最后面必须保留有至少一个空行,否则就会出现这个问题,而这个问题的结果就是, */etc/init.d/pptpd start*无法成功执行。
  2. 连上VPN之后,但是却没法打开网站

    这个一般是因为服务器没有开启 53 DNS 查询端口,使用下面命令开启即可:
    iptables -A INPUT -p udp --dport 53 -j ACCEPT
    iptables -A OUTPUT -p udp --sport 53 -j ACCEPT

一直没有找到怎么从官方下载安装包哪里有得下,所以,还是直接使用了 *apt-get* 完成安装:

sudo apt-get install flashplugin-nonfree
sudo cp /usr/lib/flashplugin-installer/libflashplayer.so /usr/lib/chromium-browser/plugins
chromium-browser --enable-plugins

另外,Flash中文乱码解决方法: 终端命令:sudo gedit /etc/fonts/conf.d/49-sansserif.conf 将倒数第4行内容替换成:文泉驿正黑。

今天一整天的时间,就花在了这个显示器的检测上面了,从昨天开始就一直在准备着把Windows系统全部下掉,转而到Linux环境下,最开始我选择的是openSUSE,因为我的服务器也准备就使用这个系统,但是却被显示器的检测浪费了一整天的时间,最开始很自然的去找驱动安装,但是后来发现,不是驱动的问题,而是我的显示器根本就不能自动地向操作系统发送其信息(可能还是与驱动有关吧)。

在openSUSE下面整了整整一天没有任何收获的情况下,我试着重新安装了Ubuntu,这个是熟悉一些吧,几分钟时间搞定这个问题,下面是我的整个解决过程:

  1. 使用 xrandr 命令列出所有当前能检测到的分辨率,比如我的计算机安装完Ubuntu之后,是下面这样的一个情况:
    cox@Cox:~$ xrandr
    Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 8192 x 8192
    VGA1 connected 1024x768+0+0 (normal left inverted right x axis y axis) 0mm x 0mm
        1024x768       60.0
        800x600        60.3     56.2
        848x480        60.0
        640x480        59.9

    从上面的信息中我们可以看到,我现在只识别到上面四种分辨率,现在使用的是1024x768的。
  2. 现在使用 xrandr 新添加显示模式(分辨率可以自己定,但是不要大于显示器的最大分辨率),我建议使用最佳分辨率,比如我的显示器的最佳分辨率是:1920x1080,那么就添加这个分辨率的即可,这里可能你会不知道怎么添加显示模式,其实很简单,还有一个名为 cvt 的工具,它会根据你所提供的分辨率来创建一个可用的显示模式:
    cox@Cox:~$ cvt 1920 1080
    # 1920x1080 59.96 Hz (CVT 2.07M9) hsync: 67.16 kHz; pclk: 173.00 MHz
    Modeline "1920x1080_60.00"  173.00  1920 2048 2248 2576  1080 1083 1088 1120 -hsync +vsync

    默认的屏幕刷新率是 60.0Hz。
  3. 将得到的显示模式使用 xrandr 命令添加:
    cox@Cox:~$ su
    root@Cox:~# xrandr --newmode "1920x1080_60.00"  173.00  1920 2048 2248 2576  1080 1083 1088 1120 -hsync +vsync
    root@Cox:~# xrandr --addmode VGA1 1920x1080_60.00
    root@Cox:~# xrandr --output VGA1 --mode 1920x1080_60.00

    上面四行命令的作用分别为:
    1. 以Root身份登陆
    2. 创建新的显示模式
    3. 添加新的显示模式
    4. 设置刚才创建的显示模式为当前使用的模式
  4. 有可能每次开机之后都又不能使用设定好的分辨库了,那我们可以将上述命令添加到X图形界面的启动命令中去,使用如下命令:
    sudo gedit /etc/gdm/Lnit/Default

    将上面后三个步骤的所有命令都复制到新打开的文件中,然后再在最后面加上下面这一行命令:
    /sbin/initctl -q emit login-session-start DISPLAY_MANAGER=gdm
  5. 如果开机之后任务栏显示不正常,则可以使用下面脚本关闭并重新启动任务栏来纠正:
    gconftool-2 --shutdown
    rm -rf ~/.gconf/apps/panel
    pkill gnome-panel

去年十一月一号考的理论,到今天为止一年堆28天,终于算是个司机了,虽然驾照还需要三天左右才寄到手里,不过也已经是确定了,本来应该是刚好一年就能拿到的,上次考科目三是11月1号,如果过的话就是刚好一年的时间,但是没过,没过的原因很简单,感觉自己开得太好了,所以就连靠边停车都没有打转向灯了。

唉,以前的事情就不用再想啦,这种算是一辈子的事情,以后就永远都是一个司机了,也应该庆祝一下……不过没有什么可庆祝的……

准备慢慢的把自己的台式机的操作系统从Windows转移到Linux下面(OpenSUSE或者也可能是Ubuntu),所以最近总是在寻找一些Windows下必做的工作软件在Linux下面的替代软件,其实说实施,除了店里面使用的百胜BS3000+这个ERP系统必须使用Windows外,其它的都已经没有什么需要在Windows下面工作的了,而这个软件我可以直接连程连接上服务器使用,或者在Linux下面安装VirtualBox虚拟机安装Windows系统也可以。

如果使用远程连接的话,我一直使用TeamViewer也可以解决问题,另外我希望使用的就是一个在Linux下面连接Windows远程桌面的软件了 RDesktop,该软件是直接源码发行的,我写这篇文章时最近版本是1.7.1,可能你下载的版本与我这里使用的不一样,但是安装和使用方法是一样的。

使用下面命令进行安装:

tar zxvf rdesktop-1.7.1.tar.gz
cd rdesktop-1.7
./configure
make
make install
make clean

如果你使用的是Ubuntu/Debian,还可以像下面这样的安装:

apt-get install rdesktop

对于CentOS还可以这样安装:

yum install rdesktop

安装完成之后,会添加一个 rdesktop 命令,使用方法为:

rdesktop [options] server[:port]

例如我的服务器地址是 *192.168.1.10*,并且要以32位色彩(本地连接的速度是很快的,所以使用了更高的颜色),那么像下面这样的:

rdesktop -f -a 32 192.168.1.10

关于 rdesktop 更多的使用方法可以运行下面这行命令:

man rdesktop

Seamlessrdp

这里还有一个很有趣的使用方法,那就是让RDesktop结合Seamlessrdp使用,Seamlessrdp可以让你在Linux直接执行Windows桌面中的应用程序,就好像直接运行在Linux的源生程序一样,其实现方法如下:

  1. 下载Seamlessrdp,并将其提取到 C:seamlessrdp 目录中。
  2. 在 Linux 按下面方式执行 rdesktop(我这里想要直接打开IE浏览器):
    rdesktop -A -s "C:seamlessrdpseamlessrdpshell.exe C:Program FilesInternet Exploreriexplore.exe" <SERVER IP>:3389 -u administrator -p <ADMINISTRATOR PASSWORD>

    说明*:将 * 替换为你服务器的IP地址,把 替换为你Windows系统的登陆密码。

关于 Seamlessrdp 的使用,还可以查看一下下Ubuntu Wiki

安装过程中经常遇到的问题

  • Could not find X Window System headers/libraries To specify paths manually, use the options –x-includes and –x-libraries”

    解决办法
    # yast2 -i

    然后 software->Software Management->serch xorg-x11-devel,然后install
  • “configure: error: no acceptable C compiler found in $PATH See config.log' for more details.”

    解决办法
    # yast2 -i gcc
  • “ERROR: Could not find OpenSSL headers/libraries. To specify a path manually, use the Cwith-openssl option”

    解决办法
    # yast2 -i openssl-devel

    或者
    # yast2 -i libopenssl-devel

一些链接

删除了运行了100多天之后滴 Ubuntu 服务器之后,Linode里面还有40美金的余额没用完(这个是9月份的时候那个Linode的活动送的,没法儿取出来,也不能转移到其它的Linode帐户里面去,只能消费完),这段时间就尝试着玩玩各种各样的Linux发行版吧,基本上都是Linode提供的,最开始就选择OpenSUSE是因为昨天看到腾讯的开放平台提供的服务器基本上都是OpenSUSE的。

OpenSUSE还只是在北京的时候看公司里面一同事玩过,后来也就只是在自己的虚拟机里面装过一两回,不过从来没有深入的去研究过,我玩Linux基本上的作用都是做网站的服务器之用,所以,最开始我就会去了解如何在某一个Linux发行版上面更好的配置安装LEMP环境。

在本文中,我使用 opensuse.antusoft.com 作为服务器的域名,IP地址还是旧服务器的 50.115.16.100,这可能与你的情况有些不一样,不过也不影响整个安装。

安装 MySQL 5

首先,我们像下面这样的安装 MySQL 5:

yast2 -i mysql mysql-client mysql-community-server

接着我们为 MySQL 创建系统启动链接(这样一来,MySQL 就会随着系统启动而启动),再启动 MySQL 服务器:

chkconfig -f --add mysql
/etc/init.d/mysql start

我们可以像下面这样的来确定是否已经运行成功:

netstat -tap | grep mysql

这可能会输出像下面这样的内容:

cox: ~ # netstat - tap | grep mysql
tcp     0    0    *:mysql     *:*    LISTEN    8924/mysqld
cox: ~ #

如果没有出现类似上面这样的信息,你可能需要打开 /etc/my.cnf 然后注释掉 skip-networking 选项:

[...]
#skip-networking
[...]

修改配置之后,重起你的 MySQL 服务器

/etc/init.d/mysql restart

(如果你遇到*You do not have a valid vim binary package installed. Please install either “vim”, “vim-enhanced” or “gvim.”*,请运行下面这行命令来安装 VIM 之后再尝试修改。

yast2 -i vim

运行下面这行命令来设置 MySQL 服务器的 root 帐户密码:

mysql_secure_installation

否则的话,任何人都可以访问你的 MySQL 数据库。进行安装设置时,脚本会询问你一些问题,你只需要回答是(Y)或者否(n),下面是我的选择:

  • Enter current password for root ( enter for none): –> 直接回车
  • Set root password? [Y/n] –> Y
  • Remove anonymouse users? [Y/n] –> Y
  • Disallow root login remotely? [Y/n] –> Y
  • Remove test database and access to it? [Y/n] –> Y
  • Reload privilege tables now? [Y/n] –> Y

安装 Nginx

我先使用 search 或者 se 搜索最新的 Nginx 软件包:

cox:~ # zypper se nginx

现在得到的最新的Nginx版本是 Nginx-1.0,我也就安装这个版本的了,像下面这样安装它:

yast2 -i nginx-1.0

与 MySQL 一样创建 Nginx 的启动链接,然后启动 Nginx

chkconfig -f --add nginx
/etc/init.d/nginx start

启动之后,我们就可以访问 http://50.116.15.100 访问,Nginx 会返回一个 403 Forbidden 错误,这证明Nginx已经安装成功,这是因为在 OpenSUSE 12中,Nginx 的默认文档根目录是: /src/www/htdocs,而这个目录下面默认又是没有索引页的。

安装 PHP5

我们可以让 PHP5通过 PHP-FPM 在Nginx下工作(PHP-FPM:FastCGI Process Manager)是一个添加了一些特别功能的PHP的FastCGI实现,像下面这样安装:

yast2 -i php5-fpm

在运行 PHP-FPM 之前,我们将 /etc/php5/fpm/php-fpm.conf.default 修改为 */etc/php5/fpm/php-fpm.conf*:

mv /etc/php5/fpm/php-fpm.conf.default /etc/php5/fpm/php-fpm.conf

然后打开 /etc/php5/fpm/php-fpm.conf :

vi /etc/php5/fpm/php-fpm.conf

然后 error_log 为 /var/log/php-fpm.log 同时去掉 pm.min_spare_servers 与 pm.max_spare_servers 两个选项前的注释并修改为下面这样的值:

[...]
error_log = /var/log/php-fpm.log
[...]
pm.min_spare_servers = 5
[...]
pm.max_spare_servers = 35
[....]

保存刚才所作的修改,然后创建系统启动链接,并系统 PHP-FPM:

chkconfig -f --add php-fpm
/etc/init.d/php-fpm start

PHP-FPM 是一个由 /etc/init.d/php-fpm 创建的守护进程,它运行一个 FastCGI 服务器并监听 9000 端口,你可以看到下面这样的信息:

cox:~ # netstat -tapn
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      9473/nginx
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      3407/sshd
tcp        0      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      9912/php-fpm.conf)
tcp        0      0 0.0.0.0:3306            0.0.0.0:*               LISTEN      8924/mysqld
tcp        0      0 50.116.15.100:22        113.240.81.181:16158    ESTABLISHED 8184/0
tcp        0    300 50.116.15.100:22        113.240.81.181:18023    ESTABLISHED 9937/1
tcp        0      0 :::22                   :::*                    LISTEN      3407/sshd

配置 Nginx

Nginx的配置都由 /etc/nginx/nginx.conf 文件定义,我们现在打开它:

vi /etc/nginx/nginx.conf

Nginx的文档是十分简单的,你可以Google一下如何对Nginx进行配置,在我的这个网站里面,我的配置是下面这样的:

首先,我提高了 worker_processes 到5,同时让 keepalive_timeout 为 2

[...]
worker_processes 5;
[...]
    keepalive_timeout 2;
[...]

在Nginx中,虚拟主机是在 server {} 容器中定义的,我修改了默认的虚拟主机为下面这样的:

[...]
    server {
        listen       80;
        server_name  _;
        #charset koi8-r;
        #access_log  /var/log/nginx/host.access.log  main;
        location / {
            root   /srv/www/htdocs/;
            index  index.php index.html index.htm;
        }
        #error_page  404                  /404.html;
        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /srv/www/htdocs/;
        }
        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ .php$ {
        #    proxy_pass   http://127.0.0.1;
        #}
        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        location ~ .php$ {
            root               /srv/www/htdocs;
            try_files $uri =404;
            fastcgi_pass   127.0.0.1:9000;
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME  /srv/www/htdocs$fastcgi_script_name;
            include            fastcgi_params;
        }
        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        location ~ /.ht {
            deny  all;
        }
    }
[...]
  • server_name _; 让服务器监听任何没有在其它虚拟主机中定义了的域名(当然你还可以指定为某一个特定的域名,比如: *opensuse.antusoft.com*)。
  • 在 location / 部分,我给 index 添加了一个 index.php ,这让Nginx知道最先以 index.php 为索引页,同时 root /srv/www/htdocs 表示该虚拟主机的根目录是 */srv/www/htdocs*。
  • 最重要的部分就是 location ~ .php$ {} 部分,修改 root 为虚拟主机的根目录,还需要确定fastcgi_param 那一行已经修改成为了 *fastcgi_param SCRIPT_FILENAME /srv/www/htdocs$fastcgi_script_name;*,否则有可能 Nginx 找不到脚本文件

保存所做的修改,然后重新启动 Nginx

/etc/init.d/nginx restart

接着我们创建一个 info.php 文件,并保存到 /srv/www/htdocs 目录中:

vi /srv/www/htdocs/info.php

它的内容是:

<?php
    phpinfo();
?>

现在我们打开 http://50.116.15.100/info.php 即可看到下面这样的画面了:

安装 PHP5 的MySQL 支持

要让PHP支持MySQL,我们只需要安装 php5-mysql 包即可,我在这里还安装了一些其它的模块,这些都是很有可能在我的应用里面会用到的,像下面这样:

yast2 -i php5-mysql php5-bcmath php5-bz2 php5-calendar php5-ctype php5-curl php5-dom php5-ftp php5-gd php5-gettext php5-gmp php5-iconv php5-imap php5-ldap php5-mbstring php5-mcrypt php5-odbc php5-openssl php5-pcntl php5-pgsql php5-posix php5-shmop php5-snmp php5-soap php5-sockets php5-sqlite php5-sysvsem php5-tokenizer php5-wddx php5-xmlrpc php5-xsl php5-zlib php5-exif php5-pear php5-sysvmsg php5-sysvshm

安装完成之后我们需要重新启动 PHP-FPM:

/etc/init.d/php-fpm restart

重新启动之后再去查看 http://50.116.15.100/info.php,向下滚动网页直到 Modules 章节,你应该就可以看到现在PHP所支持的模块了,当然,包括了MySQL。

测试环境是否满足我的需要

我一般使用 phpMyAdmin 来管理数据库,同时使用 WordPressDrupalTextPattern 等 基于PHP 语言的CMS程序,我这里就安装了 phpMyAdmin:

一些链接

从昨天开始就在做新服务器的配置,到现在全部转移完成,花了一个晚上哈,只是昨天晚上还是睡了觉,也没有必要通宵搞,最主要是这一次我使用了自己修改过后的LEMP安装SHELL脚本,对 LNMP.ORG 的脚本做了自己必要的优化,同时对 vhost.sh 也做了文件也做了一些修改。

现在的 vhost.sh 脚本在创建虚拟主机的时候,会询问该网站是属于哪一个用户的(我现在还没有做用户是否存在的判断,准备以后有时间再加上,如果不存在的话就创建一个新用户),现在还是只允许 root 用户使用,我是准备以后让我服务器上面的所有用户都可以直接使用它。比如:名为 cox 的用户使用的话会直接在 /home/cox/websites/ 目录下面创建网站目录,最终的目录结构可能是: /home/cox/websites/cox.antusoft.com/public 。

另外,我的习惯是所有内容都转移之后,关闭旧服务器,测试是否所有网站都能正常运行,如果可以的话就将旧服务器完全关闭,并删除,旧服务器也已经成功运行了102天了,Linode的VPS还是不错滴:

以前小时候时常看到老一辈人玩这个打山棋,它的棋盘简单,棋子更简单,随便一个地方都能很快速的画出来,而棋子则更简单了,没有任何特别要求,只需要下棋的双方所使用的棋子能被区分开来即可,所以,我吃了个香蕉就把香蕉皮弄成一小片儿的,他吃了个桔子就把桔子皮弄成一小片儿的然后再画个三层方框的棋盘就能下棋了。

打山棋棋盘样式
╋++++++++╋++++++++╋
++       +       ++
+ +      +      + +
+  ╋+++++╋+++++╋  +
+  ++    +    ++  +
+  + +   +   + +  +
+  +  ╋++╋++╋  +  +
+  +  +     +  +  +
╋++╋++╋     ╋++╋++╋
+  +  +     +  +  +
+  +  ╋++╋++╋  +  +
+  + +   +   + +  +
+  ++    +    ++  +
+  ╋+++++╋+++++╋  +
+ +      +      + +
++       +       ++
╋++++++++╋++++++++╋

上面的就是一个打山棋的棋盘(我是使用“+”来画出来的)的样式,粗的加号“╋”表示摆棋位,在这些位置可以摆放棋子,两人对局,其详细的走棋规则为:

  • 下棋双方先摆棋,一人一着只到将整个棋盘所有棋位都摆满;
  • 摆满后后摆棋子者先走棋;
  • 摆棋子时,逢三子(一方的)连成一线,称为打山;
  • 打山后用自己的棋子压住对方棋子,称为压包;
  • 在摆完棋子走棋前,所有压包都需要拿开;
  • 走棋过程中未发生打山,先无走棋位的一方就是36担;
  • 走棋过程中发生打山,则可以拿掉对方一粒棋子(最多也只允许拿掉一个棋子); +走棋过程中,如遇移动一子可能时形成两山(即同时出现两个一方的三个棋子连成一线),称之为过根;
  • 任意摆方两子,如果这两子中间有三个空位,则从任一个棋子数起第三个棋位(即中间位)为关键根,或者根位;
  • 被拿掉所有棋子的一方输棋。
打山棋根位
☆++++++++◇++++++++◇
++       +       ++
+ +      +      + +
+  △+++++╋+++++╋  +
+  ++    +    ++  +
+  + +   +   + +  +
+  +  ╋++╋++╋  +  +
+  +  +     +  +  +
◇++╋++╋     △++╋++╋
+  +  +     +  +  +
+  +  ◇++◇++☆  +  +
+  + +   +   + +  +
+  ++    +    ++  +
+  ╋+++++╋+++++◇  +
+ +      +      + +
++       +       ++
◇++++++++╋++++++++◇

如上棋盘所未:左下与右上角两个棋子之间总共有三个棋位,“☆”所处的位置即为根位,如果“△”处的棋子也是“◇”一方的棋子,那么,它往左上角移动一位即可同时形成两山,即过根。