第 25 章 创建与数据库、Web及电子邮件相关的脚本

本章内容

  • 编写数据库shell脚本
  • 在脚本中使用互联网
  • 在脚本中发送电子邮件

到目前为止,我们已经讲述了shell脚本的很多特性。不过这还不够!要想提供先进的特性,还得利用shell脚本之外的高级功能,例如访问数据库、从互联网上检索数据以及使用电子邮件发送报表。本章将为你展示如何在脚本中使用这三个Linux系统中的常见功能。

25.1 MySQL 数据库

shell脚本的问题之一是持久性数据。你可以将所有信息都保存在shell脚本变量中,但脚本运行结束后,这些变量就不存在了。有时你会希望脚本能够将数据保存下来以备后用。
过去,使用shell脚本存储和提取数据需要创建一个文件,从其中读取数据、解析数据,然后将数据存回到该文件中。在文件中搜索数据意味着要读取文件中的每一条记录进行查找。现在由于数据库非常流行,将shell脚本和有专业水准的开源数据库对接起来非常容易。Linux中最流行的开源数据库是MySQL。它是作为Linux-Apache-MySQL-PHP(LAMP)服务器环境的一部分而逐渐流行起来的。许多互联网Web服务器都采用LAMP来搭建在线商店、博客和其他Web应用。本节将会介绍如何在Linux环境中使用MySQL数据库创建数据库对象以及如何在shell脚本中使用这些对象。

25.1.1 使用 MySQL

绝大多数Linux发行版在其软件仓库中都含有MySQL服务器和客户端软件包,这使得在Linux 系统中安装完整的MySQL 环境简直小菜一碟。图25-1 展示了Ubuntu Linux 发行版中的Add Software(添加软件)功能。

第 25 章 创建与数据库、Web及电子邮件相关的脚本 - 图1

图25-1 在Ubuntu Linux系统上安装MySQL服务器
搜索到mysql-server包之后,只需要选择出现的mysql-server条目就可以了,包管理器会下载并安装完整的MySQL(包括客户端)软件。没什么比这更容易的了!
通往MySQL数据库的门户是mysql命令行界面程序。本节将会介绍如何使用mysql客户端程序与数据库进行交互。

  1. 连接到服务器

mysql客户端程序允许你通过用户账户和密码连到网络中任何地方的MySQL数据库服务器。默认情况下,如果你在命令行上输入mysql,且不加任何参数,它会试图用Linux登录用户名连接运行在同一Linux系统上的MySQL服务器。
大多数情况下,这并不是你连接数据库的方式。通常还是创建一个应用程序专用的账户比较安全,不要用MySQL服务器上的标准用户账户。这样可以针对应用程序用户实施访问限制,即便应用程序出现了偏差,在必要时你也可以删除或重建。可以使用-u参数指定登录用户名。

  1. $ mysql -u root p
  2. Enter password:
  3. Welcome to the MySQL monitor. Commands end with ; or \g.
  4. Your MySQL connection id is 42
  5. Server version: 5.5.38-0ubuntu0.14.04.1 (Ubuntu)
  6. Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.
  7. Oracle is a registered trademark of Oracle Corporation and/or its
  8. affiliates. Other names may be trademarks of their respective
  9. owners.
  10. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
  11. mysql>

-p参数告诉mysql程序提示输入登录用户输入密码。输入root用户账户的密码,这个密码要么是在安装过程中,要么是使用mysqladmin工具获得的。一旦登录了服务器,你就可以输入命令。

  1. mysql命令

mysql程序使用两种不同类型的命令:

  • 特殊的mysql命令
  • 标准SQL语句

mysql程序使用它自有的一组命令,方便你控制环境和提取关于MySQL服务器的信息。这些命令要么是全名(例如status),要么是简写形式(例如\s)。你可以从mysql命令提示符中直接使用命令的完整形式或简形式。

mysql> \s
--------------
mysql Ver 14.14 Distrib 5.5.38, for debian-linux-gnu (i686) using readline 6.3
Connection id: 43
Current database:
Current user: root@localhost
SSL: Not in use
Current pager: stdout
Using outfile: ''
Using delimiter: ;
Server version: 5.5.38-0ubuntu0.14.04.1 (Ubuntu)
Protocol version: 10
Connection: Localhost via UNIX socket
Server characterset: latin1
Db characterset: latin1
Client characterset: utf8
Conn. characterset: utf8
UNIX socket: /var/run/mysqld/mysqld.sock
Uptime: 2 min 24 sec
Threads: 1 Questions: 575 Slow queries: 0 Opens: 421 Flush tables: 1
Open tables: 41 Queries per second avg: 3.993
--------------
mysql>

25
mysql程序实现了MySQL服务器支持的所有标准SQL(Structured Query Language,结构化查询语言)命令。mysql程序实现的一条很棒的SQL命令是SHOW命令。你可以利用这条命令提取MySQL服务器的相关信息,比如创建的数据库和表。

mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
+--------------------+
2 rows in set (0.04 sec)
mysql> USE mysql;
Database changed
mysql> SHOW TABLES;
+---------------------------+
| Tables_in_mysql |
+---------------------------+
| columns_priv |
| db |
| func |
| help_category |
| help_keyword |
| help_relation |
| help_topic |
| host |
| proc |
| procs_priv |
| tables_priv |
| time_zone |
| time_zone_leap_second |
| time_zone_name |
| time_zone_transition |
| time_zone_transition_type |
| user |
+---------------------------+
17 rows in set (0.00 sec)
mysql>

在这个例子中,我们用SQL命令SHOW来显示当前在MySQL服务器上配置过的数据库,然后 用SQL命令USE来连接到单个数据库。mysql会话一次只能连一个数据库。
你会注意到,在每个命令后面我们都加了一个分号。在mysql程序中,分号表明命令的结束。如果不用分号,它会提示输入更多数据。

mysql> SHOW
-> DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
+--------------------+
2 rows in set (0.00 sec)
mysql>

在处理长命令时,这个功能很有用。你可以在一行输入命令的一部分,按下回车键,然后在下一行继续输入。这样一条命令可以占任意多行,直到你用分号表明命令结束。

说明 本章中,我们用大写字母来表示SQL命令,这已经成了编写SQL命令的通用方式,但mysql 程序支持用大写或小写字母来指定SQL命令。

  1. 创建数据库

MySQL服务器将数据组织成数据库。数据库通常保存着单个应用程序的数据,与用这个数据库服务器的其他应用互不相关。为每个shell脚本应用创建一个单独的数据库有助于消除混淆, 避免数据混用。
创建一个新的数据库要用如下SQL语句。

CREATE DATABASE name;

非常简单。当然,你必须拥有在MySQL服务器上创建新数据库的权限。最简单的办法是作为root用户登录MySQL服务器。

$ mysql -u root –p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 42
Server version: 5.5.38-0ubuntu0.14.04.1 (Ubuntu)
Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> CREATE DATABASE mytest;
Query OK, 1 row affected (0.02 sec)
mysql>

可以使用SHOW命令来查看新数据库是否创建成功。

mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| mytest |
+--------------------+
3 rows in set (0.01 sec)
mysql>

好了,它已经成功创建了。现在你可以创建一个新的用户账户来访问新数据库了。

  1. 创建用户账户

到目前为止,你已经知道了如何用root管理员账户连接到MySQL服务器。这个账户可以完全控制所有的MySQL服务器对象(就和Linux的root账户可以完全控制Linux系统一样)。
在普通应用中使用MySQL的root账户是极其危险的。如果有安全漏洞或有人弄到了root用户账户的密码,各种糟糕事情都可能发生在你的系统(以及数据)上。

为了阻止这种情况的发生,明智的做法是在MySQL上创建一个仅对应用中所涉及的数据库有权限的独立用户账户。可以用GRANT SQL语句来完成。

mysql> GRANT SELECT,INSERT,DELETE,UPDATE ON test.* TO test IDENTIFIED
by 'test';
Query OK, 0 rows affected (0.35 sec)
mysql>

这是一条很长的命令。让我们看看命令的每一部分都做了什么。
第一部分定义了用户账户对数据库有哪些权限。这条语句允许用户查询数据库数据(select 权限)、插入新的数据记录以及删除和更新已有数据记录。
test.*项定义了权限作用的数据库和表。这通过下面的格式指定。

database.table

正如在这个例子中看到的,在指定数据库和表时可以使用通配符。这种格式会将指定的权限作用在名为test的数据库中的所有表上。
最后,你可以指定这些权限应用于哪些用户账户。grant命令的便利之处在于,如果用户账户不存在,它会创建。identified by部分允许你为新用户账户设定默认密码。
可以直接在mysql程序中测试新用户账户。


$ mysql mytest -u test –p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 42
Server version: 5.5.38-0ubuntu0.14.04.1 (Ubuntu)
Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>

第一个参数指定使用的默认数据库(mytest)。如你所见,-u选项定义了登录的用户,-p用来提示输入密码。输入test用户账户的密码后,你就连到了服务器。
现在已经有了数据库和用户账户,可以为数据创建一些表了。

  1. 创建数据表

MySQL是一种关系数据库(relational database)。在关系数据库中,数据按照字段、记录和表进行组织。数据字段是信息的单个组成部分,比如员工的姓或工资。记录是相关数据字段的集合,比如员工ID号、姓、名、地址和工资。每条记录都代表一组数据字段。
表含有保存相关数据的所有记录。因此,你会使用一个叫作Employees的表来保存每个员工的记录。
要在数据库中新建一张新表,需要用SQL命令CREATE TABLE。

$ mysql mytest -u root -p
Enter password:
mysql> CREATE TABLE employees (
-> empid int not null,
-> lastname varchar(30),
-> firstname varchar(30),
-> salary float,
-> primary key (empid));
Query OK, 0 rows affected (0.14 sec)
mysql>

首先要注意,为了新建一张表,我们需要用root用户账户登录到MySQL上,因为test用户没有新建表的权限。接下来,我们在mysql程序命令行上指定了test数据库。不这么做的话,就需要用SQL命令USE来连接到test数据库。

警告 在创建新表前,很重要的一点是,要确保你使用了正确的数据库。另外还要确保使用管理员用户账户(MySQL中的root用户)登录来创建表。

表中的每个数据字段都用数据类型来定义。MySQL和PostgreSQL数据库支持许多不同的数据类型。表25-1列出了其中较常见的一些数据类型。
表25-1 MySQL的数据类型

数据类型 描 述
char 定长字符串值
varchar 变长字符串值
int 整数值
float 浮点值
boolean 布尔类型true/false值
date YYYY-MM-DD格式的日期值
time HH:mm:ss格式的时间值
timestamp 日期和时间值的组合
text 长字符串值
BLOB 大的二进制值,比如图片或视频剪辑

empid数据字段还指定了一个数据约束(data constraint)。数据约束会限制输入什么类型数据可以创建一个有效的记录。not null数据约束指明每条记录都必须有一个指定的empid值。
最后,primary key定义了可以唯一标识每条记录的数据字段。这意味着每条记录中在表中都必须有一个唯一的empid值。
创建新表之后,可以用对应的命令来确保它创建成功了,在mysql中是用show tables命令。

mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| employees |
+----------------+
1 row in set (0.00 sec)
mysql>

有了新建的表,现在你可以开始保存一些数据了。下一节将会介绍应该怎么做。

  1. 插入和删除数据

毫不意外,你需要使用SQL命令INSERT向表中插入新的记录。每条INSERT命令都必须指定数据字段值来供MySQL服务器接受该记录。
SQL命令INSERT的格式如下。

INSERT INTO table VALUES (...)

每个数据字段的值都用逗号分开。

$ mysql mytest -u test -p
Enter password:
mysql> INSERT INTO employees VALUES (1, 'Blum', 'Rich', 25000.00);
Query OK, 1 row affected (0.35 sec)

上面的例子用-u命令行选项以mytest用户账户登录。
INSERT命令会将指定的数据写入表中的数据字段里。如果你试图添加另外一条包含相同的empid数据字段值的记录,就会得到一条错误消息。

mysql> INSERT INTO employees VALUES (1, 'Blum', 'Barbara', 45000.00);
ERROR 1062 (23000): Duplicate entry '1' for key 1

但如果你将empid的值改成唯一的值,那就没问题了。

mysql> INSERT INTO employees VALUES (2, 'Blum', 'Barbara', 45000.00);
Query OK, 1 row affected (0.00 sec)

现在表中应该有两条记录了。
如果你需要从表中删除数据,可以用SQL命令DELETE,但要非常小心。
DELETE命令的基本格式如下。

DELETE FROM table;

其中table指定了要从中删除记录的表。这个命令有个小问题:它会删除该表中所有记录。
要想只删除其中一条或多条数据行,必须用WHERE子句。WHERE子句允许创建一个过滤器来指定删除哪些记录。可以像下面这样使用WHERE子句。

DELETE FROM employees WHERE empid = 2;

这条命令只会删除empid值为2的所有记录。当你执行这条命令时,mysql程序会返回一条消息来说明有多少个记录符合条件。

mysql> DELETE FROM employees WHERE empid = 2;
Query OK, 1 row affected (0.29 sec)

跟期望的一样,只有一条记录符合条件并被删除。

  1. 查询数据

一旦将所有数据都放入数据库,就可以开始提取信息了。
所有查询都是用SQL命令SELECT来完成。SELECT命令非常强大,但用起来也很复杂。
SELECT语句的基本格式如下。

SELECT datafields FROM table

datafields参数是一个用逗号分开的数据字段名称列表,指明了希望查询返回的字段。如果你要提取所有的数据字段值,可以用星号作通配符。
你还必须指定要查询的表。要想得到有意义的结果,待查询的数据字段必须对应正确的表。默认情况下,SELECT命令会返回指定表中的所有记录。

mysql> SELECT * FROM employees;
+-------+----------+------------+--------+
| empid | lastname | firstname | salary |
+-------+----------+------------+--------+
| 1 | Blum | Rich | 25000 |
| 2 | Blum | Barbara | 45000 |
| 3 | Blum | Katie Jane | 34500 |
| 4 | Blum | Jessica | 52340 |
+-------+----------+------------+--------+
4 rows in set (0.00 sec)
mysql>

可以用一个或多个修饰符定义数据库服务器如何返回查询数据。下面列出了常用的修饰符。

  • WHERE:显示符合特定条件的数据行子集。
  • ORDER BY:以指定顺序显示数据行。
  • LIMIT:只显示数据行的一个子集。

WHERE子句是最常用的SELECT命令修饰符。它允许你指定查询结果的过滤条件。下面是一个 使用WHERE子句的例子。

mysql> SELECT * FROM employees WHERE salary > 40000;
+-------+----------+-----------+--------+
| empid | lastname | firstname | salary |
+-------+----------+-----------+--------+
| 2 | Blum | Barbara | 45000 |
| 4 | Blum | Jessica | 52340 |
+-------+----------+-----------+--------+
2 rows in set (0.01 sec)
mysql>

现在你可以看到将数据库访问功能添加到shell脚本中的强大之处了!只要使用几条SQL命令和mysql程序就可以轻松应对你的数据管理需求。下一节将会介绍如何将这些功能引入shell脚本。

25.1.2 在脚本中使用数据库

现在你已经有了一个可以正常工作的数据库,终于可以将精力放回shell脚本编程了。本节将会介绍如何用shell脚本同数据库交互。

  1. 登录到服务器

如果你为自己的shell脚本在MySQL中创建了一个特定的用户账户,那你需要使用mysql命 令,以该用户的身份登录。实现的方法有好几种,其中一种是使用-p选项,在命令行中加入密码。

mysql mytest -u test –p test

不过这并不是一个好做法。所有能够访问你脚本的人都会知道数据库的用户账户和密码。要解决这个问题,可以借助mysql 程序所使用的一个特殊配置文件。mysql 程序使用
$HOME/.my.cnf文件来读取特定的启动命令和设置。其中一项设置就是用户启动的mysql会话的默认密码。
要想在这个文加中设置默认密码,只需要像下面这样。

$ cat .my.cnf
[client]
password = test
$ chmod 400 .my.cnf
$

可以使用chmod命令将.my.cnf文件限制为只能由本人浏览。现在可以在命令行上测试一下。

$ mysql mytest -u test
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 44
Server version: 5.5.38-0ubuntu0.14.04.1 (Ubuntu)
Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>

棒极了!这样就不用在shell脚本中将密码写在命令行上了。

  1. 向服务器发送命令

在建立起到服务器的连接后,接着就可以向数据库发送命令进行交互。有两种实现方法:

  • 发送单个命令并退出;
  • 发送多个命令。

要发送单个命令,你必须将命令作为mysql命令行的一部分。对于mysql命令,可以用-e 选项。

$ cat mtest1
#!/bin/bash
# send a command to the MySQL server
MYSQL=$(which mysql)
$MYSQL mytest -u test -e 'select * from employees'
$ ./mtest1
+-------+----------+------------+---------+
| empid | lastname | firstname | salary |
+-------+----------+------------+---------+
| 1 | Blum | Rich | 25000 |
| 2 | Blum | Barbara | 45000 |
| 3 | Blum | Katie Jane | 34500 |
| 4 | Blum | Jessica | 52340 |
+-------+----------+------------+---------+
$

数据库服务器会将SQL命令的结果返回给shell脚本,脚本会将它们显示在STDOUT中。
如果你需要发送多条SQL命令,可以利用文件重定向(参见第15章)。要在shell脚本中重定向多行内容,就必须定义一个结束(end of file)字符串。结束字符串指明了重定向数据的开始和结尾。
下面的例子定义了结束字符串及其中数据。

$ cat mtest2
#!/bin/bash
# sending multiple commands to MySQL
MYSQL=$(which mysql)
$MYSQL mytest -u test <<EOF
show tables;
select * from employees where salary > 40000;
EOF
$ ./mtest2
Tables_in_test
employees
empid lastname firstname salary
2 Blum Barbara 45000
4 Blum Jessica 52340
$

shell会将EOF分隔符之间的所有内容都重定向给mysql命令。mysql命令会执行这些命令行, 就像你在提示符下亲自输入的一样。用了这种方法,你可以根据需要向MySQL服务器发送任意多条命令。但你会注意到,每条命令的输出之间没有没有任何分隔。在25.2.3节中,你会看到如何解决这个问题。
25

说明 你应该也注意到了,当使用输入重定向时,mysql程序改变了默认的输出风格。mysql程序检测到了输入是重定向过来的,所以它只返回了原始数据而不是在数据两边加上ASCII 符号框。这非常有利于提取个别的数据元素。

当然,并不是只能从数据表中提取数据。你可以在脚本中使用任何类型的SQL命令,比如INSERT语句。

$ cat mtest3
#!/bin/bash
# send data to the table in the MySQL database
MYSQL=$(which mysql)
if [ $# -ne 4 ]
then
  echo "Usage: mtest3 empid lastname firstname salary"
else
  statement="INSERT INTO employees VALUES ($1, '$2', '$3', $4)"
  $MYSQL mytest -u test << EOF
  $statement
EOF
  if [ $? -eq 0 ]
  then
    echo Data successfully added
  else
    echo Problem adding data
  fi
fi
$ ./mtest3
Usage: mtest3 empid lastname firstname salary
$ ./mtest3 5 Blum Jasper 100000
Data added successfully
$
$ ./mtest3 5 Blum Jasper 100000
ERROR 1062 (23000) at line 1: Duplicate entry '5' for key 1
Problem adding data
$

这个例子演示了使用这种方法的一些注意事项。在指定结束字符串时,它必须是该行唯一的内容,并且该行必须以这个字符串开头。如果我们将EOF文本缩进以和其余的if-then缩进对齐, 它就不会起作用了。
注意,在INSERT语句里,我们在文本值周围用了单引号,在整个INSERT语句周围用了双引号。一定不要弄混引用字符串值的引号和定义脚本变量文本的引号。
还有,注意我们是怎样使用$?特殊变量来测试mysql程序的退出状态码的。它有助于你判断命令是否成功执行。
将这些命令的结果发送到STDOUT并不是管理和操作数据最简单的方法。下一节将会为你展 示一些技巧,帮助脚本获取从数据库中检索到的数据。

  1. 格式化数据

mysql命令的标准输出并不太适合提取数据。如果要对提取到的数据进行处理,你需要做一些特别的操作。本节将会介绍一些技巧来帮你从数据库报表中提取数据。
提取数据库数据的第一步是将mysql命令的输出重定向到一个环境变量中。这允许你在其他命令中使用输出信息。这里有个例子。

$ cat mtest4
#!/bin/bash
# redirecting SQL output to a variable
MYSQL=$(which mysql)
dbs=$($MYSQL mytest -u test -Bse 'show databases')
for db in $dbs
do
  echo $db
done
$ ./mtest4
information_schema
test
$

这个例子在mysql程序的命令行上用了两个额外参数。-B选项指定mysql程序工作在批处理模式运行,-s(silent)选项用于禁止输出列标题和格式化符号。
通过将mysql命令的输出重定向到一个变量,此例可以逐步输出每条返回记录里的每个值。
mysql程序还支持另外一种叫作可扩展标记语言(Extensive Markup Language,XML)的流行格式。这种语言使用和HTML类似的标签来标识数据名和值。
对于mysql程序,可以用-X命令行选项来输出。

$ mysql mytest -u test -X -e 'select * from employees where empid = 1'
<?xml version="1.0"?>
<resultset statement="select * from employees">
<row>
<field name="empid">1</field>
<field name="lastname">Blum</field>
<field name="firstname">Rich</field>
<field name="salary">25000</field>
</row>
</resultset>
$

通过使用XML,你能够轻松标识出每条记录以及记录中的各个字段值。然后你就可以使用标准的Linux字符串处理功能来提取需要的数据。
25

25.2 使用 Web

通常在考虑shell脚本编程时,最不可能考虑到的就是互联网了。命令行世界看起来往往跟丰富多彩的互联网世界格格不入。但你可以在shell脚本中非常方便的利用一些工具访问Web以及其他网络设备中的数据内容。
作为一款于1992年由堪萨斯大学的学生编写的基于文本的浏览器,Lynx程序的历史几乎和互联网一样悠久。因为该浏览器是基于文本的,所以它允许你直接从终端会话中访问网站,只不过Web页面上的那些漂亮图片被替换成了HTML文本标签。这样你就可以在几乎所有类型的Linux
终端上浏览互联网了。图25-2展示了Lynx的界面。
第 25 章 创建与数据库、Web及电子邮件相关的脚本 - 图2

图25-2 使用Lynx浏览Web页面
Lynx使用标准键盘按键浏览网页。链接会在Web页面上以高亮文本的形式出现。使用向右方向键可以跟随一个链接到下一个Web页面。
你可能想知道如何在shell脚本中使用图形化文本程序。Lynx程序还提供了一个功能,允许你将Web页面的文本内容转储到STDOUT中。这个功能非常适合用来挖掘Web页面中包含的数据。本 节将会介绍如何在shell脚本中用Lynx程序提取网站中的数据。

25.2.1 安装 Lynx

尽管Lynx程序有点古老,但它的开发仍然很活跃。在本书写作时,Lynx的最新版本是2010 年6月发布的2.8.8,新版本正在研发中。鉴于它在shell脚本程序员中十分流行,许多Linux发行版都将它作为默认程序安装。
如果你正在用一个不带Lynx程序的Linux系统,请检查一下该发行版的安装包。大多数情况下,你都能在那里找到Lynx包并轻松地安装好。
如果发行版没有提供Lynx包,或者你想用最新版的,可以从lynx.isc.org网站上下载源码并编译(假定你已经在Linux系统上安装了C开发库)。参考第9章获取有关如何编译并安装源码包的相关信息。

说明 Lynx程序使用了Linux中的curses文本图形库。大多数发行版会默认安装这个库。如果你的发行版没有安装,在尝试编译Lynx前先参考你的发行版的安装指南来安装curses库。

下一节将会介绍如何在命令行上使用lynx命令。

25.2.2 lynx 命令行

lynx命令行命令极其擅长从远程网站上提取信息。当用浏览器查看Web页面时,你只是看到了传送到浏览器中信息的一部分。Web页面由三种类型的数据组成:

  • HTTP头部
  • cookie
  • HTML内容

HTTP头部提供了连接中传送的数据类型、发送数据的服务器以及采用的连接安全类型的相关信息。如果你发送的是特殊类型的数据,比如视频或音频剪辑,服务器会将其在HTTP头部中标示出来。Lynx程序允许你查看Web页面会话中发送的所有HTTP头部。
如果你浏览过Web页面,对Web页面cookie一定不会陌生。网站用cookie存储有关网站的访问数据,以供将来使用。每个站点都能存储信息,但只能访问它自己设置的信息。lynx命令提供了一些选项来查看Web服务器发送的cookie,还可以接受或拒绝服务器发过来的特定cookie。
Lynx程序支持三种不同的格式来查看Web页面实际的HTML内容:

  • 在终端会话中利用curses图形库显示文本图形;
  • 文本文件,文件内容是从Web页面中转储的原始数据;
  • 文本文件,文件内容是从Web页面中转储的原始HTML源码。

对于shell脚本,原始数据或HTML源码可是一座金山。一旦你获得了从网站上检索到的信息, 就能轻松地从中提取每一条信息。
如你所见,Lynx程序将它的本职工作发挥到了极致。但随之而来的是复杂性,尤其是对命令行参数来说。Lynx程序是你在Linux世界中遇到的较复杂的程序之一。
lynx命令的基本格式如下。

lynx options URL

其中URL是你要连接的HTTP或HTTPS地址,options则是一个或多个选项。这些选项可以在Lynx与远程网站交互时改变它的行为。许多命令行参数定义了Lynx的行为,可以用来控制全屏模式下的Lynx,允许在浏览Web页面时对其进行定制。
在正常的浏览环境中,你通常会发现有几组命令行参数非常有用。你不用每次使用Lynx时都在命令行上将这些参数输入一遍,Lynx提供了一个通用配置文件来定义Lynx的基本行为。我们将在下一节中讨论这个配置文件。

25.2.3 Lynx 配置文件

lynx 命令会从配置文件中读取大量的参数设置。默认情况下,这个文件位于 /usr/local/lib/lynx.cfg,不过有许多Linux发行版将其改放到了/etc目录下(/etc/lynx.cfg)(Ubuntu发行版将lynx.cfg放到了/etc/lynx-curl目录中)。
lynx.cfg配置文件将相关的参数分组到不同的区域中,这样更容易找到参数。配置文件中条目的格式为:

PARAMETER:value

其中PARAMETER是参数的全名(通常都是用大写字母,但也不总是如此),value是跟参数关联的值。
浏览一下这个文件,你会发现许多参数都跟命令行参数类似,比如ACCEPT_ALL_COOKIES 参数就等同于设置了-accept_all_cookies命令行参数。
还有一些配置参数功能类似,但名称不同。FORCE_SSL_COOKIES_SECURE配置文件参数设 置可以用-force_secure命令行参数给覆盖掉。
你还会发现少数配置参数并没有对应的命令行参数。这些值只能在配置文件中设定。
最常见的你不能在命令行上设置的配置参数是代理服务器。有些网络(尤其是公司网络)使用代理服务器作为客户端浏览器和目标网站的桥梁。客户端浏览器不能直接向远程Web服务器发送HTTP请求,而是必须将它们的请求发到代理服务器上,然后由代理服务器将请求转发给远程Web服务器,获取结果,再将结果回传给客户端浏览器。
虽然这看起来像在浪费时间,但它是保护客户端不受互联网上危险侵害的重要功能。代理服务器可以过滤不良内容和恶意代码,甚至可以发现钓鱼网站(为了获取用户数据,假扮他人的流氓服务器)。代理服务器还可以帮助降低网络带宽的使用,因为它缓存了经常浏览的Web页面并将其直接返回给客户端,而不用再从原始地址处下载页面。
用来定义代理服务器的配置参数有:

http_proxy:http://some.server.dom:port/
https_proxy:http://some.server.dom:port/
ftp_proxy:http://some.server.dom:port/
gopher_proxy:http://some.server.dom:port/
news_proxy:http://some.server.dom:port/
newspost_proxy:http://some.server.dom:port/
newsreply_proxy:http://some.server.dom:port/
snews_proxy:http://some.server.dom:port/
snewspost_proxy:http://some.server.dom:port/
snewsreply_proxy:http://some.server.dom:port/
nntp_proxy:http://some.server.dom:port/
wais_proxy:http://some.server.dom:port/
finger_proxy:http://some.server.dom:port/
cso_proxy:http://some.server.dom:port/
no_proxy:host.domain.dom

你可以为任何Lynx支持的网络协议定义不同的代理服务器。NO_PROXY参数是逗号分隔的网站列表。对于列表中的这些网站,不希望使用代理服务器直接访问。这些通常都是不需要过滤的内部网站。

25.2.4 从 Lynx 中获取数据

在shell脚本中使用Lynx时,大多数情况下你只是要提取Web页面中的某条(或某几条)特定信息。完成这个任务的方法称作屏幕抓取(screen scraping)。在屏幕抓取过程中,你要尝试通过编程寻找图形化屏幕上某个特定位置的数据,这样你才能获取它并在脚本中使用。
用lynx进行屏幕抓取的最简单办法是用-dump选项。这个选项不会在终端屏幕上显示Web
页面。相反,它会将Web页面文本数据直接显示在STDOUT上。

$ lynx -dump http://localhost/RecipeCenter/
The Recipe Center
"Just like mom used to make"
Welcome
[1]Home
[2]Login to post
[3]Register for free login
_____________________________________________________________
[4]Post a new recipe

每个链接都由一个标号标定,Lynx在Web页面数据后显示了所有标号所指向的地址。
在从Web页面中获得了所有文本数据之后,你可能已经知道我们会从工具箱中取出什么工具来提取数据了。没错,就是我们的老朋友sed编辑器和gawk程序(参见第19章)。
首先,让我们找一些有意思的数据来收集。Yahoo!天气页面是找出全世界任何地区当前气候的不错来源。每个位置都用一个单独的URL来显示该城市的天气信息(你可以在浏览器中打开该站点并输入你的城市信息来获取所在地的特定URL)。查看伊利诺伊州芝加哥市的天气情况的 lynx命令如下:

lynx -dump http://weather.yahoo.com/united-states/illinois/chicago-2379574/

这条命令会从页面中转储出很多的数据。第一步是找到你需要的准确信息。要做到这点,需将lynx命令的输出重定向到一个文件中,然后在文件中查找数据。执行了前面的命令后,我们在输出文件中找到了这段文本。

Current conditions as of 1:54 pm EDT
Mostly Cloudy
  Feels Like:
    32 °F
  Barometer:
    30.13 in and rising
  Humidity:
    50%
  Visibility:
    10 mi
  Dewpoint:
    15 °F
  Wind:
    W 10 mph

这都是你需要的关于当前天气的所有信息。但这段输出中有个小问题。你会注意到,数字都是在标题下面一行的。只提取单独的数字有些困难。第19章讨论过如何处理这样的问题。
解决这一问题的关键是先写一个能查找数据标题的sed脚本。找到之后,你就可以到正确的行中提取数据了。很幸运,这个例子中我们所需要的数据就是那些文本行。这里应该只用sed脚本就能解决了。如果在同一行中还有其他文本,就需要使用gawk工具来过滤出我们需要的数据。
首先,你需要创建一个sed脚本来查找表示地点的文本,然后跳到下一行来获取描述当前天气状况的文本并打印出来。输出芝加哥天气的脚本如下。

$ cat sedcond
/IL, United States/{
n
p
}
$

地址指明了要查找的行。如果sed命令找到了,n命令就会跳到下一行,然后p命令会打印当前行的内容,也就是描述该城市当前天气状况的文本。
下一步,你需要一段sed脚本来查找文本Feels Like,并打印出下一行的温度。

$ cat sedtemp
/Feels Like/{
p
}
$

漂亮极了。现在你可以在shell脚本中用这两个sed脚本。首先将Web页面的lynx输出放入一个临时文件中,然后对Web页面数据使用这两个sed脚本,提取所需的数据。下面的例子演示了具体的做法。

$ cat weather
#!/bin/bash
# extract the current weather for Chicago, IL
URL="http://weather.yahoo.com/united-states/illinois/chicago-2379574/"
LYNX=$(which lynx)
TMPFILE=$(mktemp tmpXXXXXX)
$LYNX -dump $URL > $TMPFILE
conditions=$(cat $TMPFILE | sed -n -f sedcond)
temp=$(cat $TMPFILE | sed -n -f sedtemp | awk '{print $4}')
rm -f $TMPFILE
echo "Current conditions: $conditions"
echo The current temp outside is: $temp
$ ./weather
Current conditions: Mostly Cloudy
The current temp outside is: 32 °F
$

天气脚本会连接到指定城市的Yahoo!天气页面,将Web页面保存到一个文件中,提取对应的文本,删除临时文件,然后显示天气信息。这么做的好处在于,一旦你从网站上提取到了数据, 就可以随心所欲地处理它,比如创建一个温度表。可以创建一个每天运行的cron任务(参见第16 章)来跟踪当天的温度。

警告 互联网无时不刻不在发生变化。如果你花费了几个小时找到了Web页面上数据的精确位置,而几个星期后却发现数据已经不在了,脚本也没法工作了,不必感到惊讶。事实上, 很有可能上面这个例子在你阅读本书时已经无法工作了。重要的是要知道从Web页面提取数据的过程。这样你就可以将原理运用到任何情形中。

25.3 使用电子邮件

随着电子邮件的普及,现在几乎每个人都有一个邮件地址。正因如此,人们通常更期望通过邮件接收数据而不是看文件或打印出的资料。在shell脚本编程中也是如此。如果你通过shell脚本生成了报表,大多数情况下都要用电子邮件的形式将结果发送给他人。
可用来从shell脚本中发送电子邮件的主要工具是Mailx程序。不仅可以用它交互地读取和发送消息,还可以用命令行参数指定如何发送消息。

说明 在你安装包含Mailx程序的mailutils包之前,有些Linux发行版还会要求你安装邮件服务器包(例如sendmail或Postfix)。

Mailx程序发送消息的命令行的格式为:

mail [-eIinv] [-a header] [-b addr] [-c addr] [-s subj] to-addr

mail命令使用表25-2中列出的命令行参数。
表25-2 Mailx命令行参数

参 数 描 述
-a 指定额外的SMTP头部行
-b 给消息增加一个BCC:收件人
-c 给消息增加一个CC:收件人
-e 如果消息为空,不要发送消息
-i 忽略TTY中断信号
-I 强制Mailx以交互模式运行
-n 不要读取/etc/mail.rc启动文件
-s 指定一个主题行
-v 在终端上显示投递细节

正如表25-2中所示,你完全可以使用命令行参数来创建整个电子邮件消息。唯一需要添加的就是消息正文。
要这么做的话,你需要将文本重定向给mail命令。下面这个简单的例子演示了如何直接在命令行上创建和发送电子邮件消息。

$ echo "This is a test message" | mailx -s "Test message" rich

Mailx程序将来自echo命令的文本作为消息正文发送。这提供了一个从shell脚本发送消息的简单途径。下面是一个简单的例子。

$ cat factmail
#!/bin/bash
# mailing the answer to a factorial
MAIL=$(which mailx)
factorial=1
counter=1
read -p "Enter the number: " value
while [ $counter -le $value ]
do
  factorial=$[$factorial * $counter]
  counter=$[$counter + 1]
done
echo The factorial of $value is $factorial | $MAIL -s "Factorial
answer" $USER
echo "The result has been mailed to you."

这段脚本不会假定Mailx程序位于标准位置。它使用which命令来确定mail程序在哪里。
在计算出阶乘函数的结果后,shell脚本使用mail命令将这个消息发送到用户自定义的$USER 环境变量,这应该是运行这个脚本的人。

$ ./factmail
Enter the number: 5
The result has been mailed to you.
$

你只需要查看邮件,看看是否收到回信。

$ mail
"/var/mail/rich": 1 message 1 new
>N 1 Rich Blum Mon Sep 1 10:32 13/586 Factorial answer
?
Return-Path: <rich@rich-Parallels-Virtual-Platform>
X-Original-To: rich@rich-Parallels-Virtual-Platform
Delivered-To: rich@rich-Parallels-Virtual-Platform
Received: by rich-Parallels-Virtual-Platform (Postfix, from userid 1000)
id B4A2A260081; Mon, 1 Sep 2014 10:32:24 -0500 (EST)
Subject: Factorial answer
To: <rich@rich-Parallels-Virtual-Platform>
X-Mailer: mail (GNU Mailutils 2.1)
Message-Id: <20101209153224.B4A2A260081@rich-Parallels-Virtual-Platform>
Date: Mon, 1 Sep 2014 10:32:24 -0500 (EST)
From: rich@rich-Parallels-Virtual-Platform (Rich Blum)
The factorial of 5 is 120
?

在消息正文中只发送一行文本有时会不方便。通常,你需要将整个输出作为电子邮件消息发送。这种情况总是可以将文本重定向到临时文件中,然后用cat命令将输出重定向给mail程序。
下面是一个在电子邮件消息中发送大量数据的例子。

$ cat diskmail
#!/bin/bash
# sending the current disk statistics in an e-mail message
date=$(date +%m/%d/%Y)
MAIL=$(which mailx)
TEMP=$(mktemp tmp.XXXXXX)
df -k > $TEMP
cat $TEMP | $MAIL -s "Disk stats for $date" $1
rm -f $TEMP

diskmail程序用date命令(采用了特殊格式)得到了当前日期,找到Mailx程序的位置后创建了一个临时文件。接着用df命令显示了当前磁盘空间的统计信息(参见第4章),并将输出重定向到了那个临时文件。
然后它使用第一个命令行参数作为目的地地址,使用当前日期作为邮件主题,将临时文件重定向到mail命令。在运行这个脚本时,你不会看到任何命令行输出。

$ ./diskmail rich

但如果你检查邮件,你就会看到发出的消息。

$ mail
"/var/mail/rich": 1 message 1 new
>N 1 Rich Blum Mon Sep 1 10:35 19/1020 Disk stats for 09/01/2014
?
Return-Path: <rich@rich-Parallels-Virtual-Platform>
X-Original-To: rich@rich-Parallels-Virtual-Platform
Delivered-To: rich@rich-Parallels-Virtual-Platform
Received: by rich-Parallels-Virtual-Platform (Postfix, from userid 1000)
id 3671B260081; Mon, 1 Sep 2014 10:35:39 -0500 (EST)
Subject: Disk stats for 09/01/2014
To: <rich@rich-Parallels-Virtual-Platform>
X-Mailer: mail (GNU Mailutils 2.1)
Message-Id: <20101209153539.3671B260081@rich-Parallels-Virtual-Platform>
Date: Mon, 1 Sep 2014 10:35:39 -0500 (EST)
From: rich@rich-Parallels-Virtual-Platform (Rich Blum)
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sda1 63315876 2595552 57504044 5% /
none 507052 228 506824 1% /dev
none 512648 192 512456 1% /dev/shm
none 512648 100 512548 1% /var/run
none 512648 0 512648 0% /var/lock
none 4294967296 0 4294967296 0% /media/psf
?

现在你要做的是用cron功能安排每天运行该脚本,这样就可以将磁盘空间报告自动发送到你的收件箱了。系统管理再没比这个更简单的了!

25.4 小结

本章讲解了一些高级功能在脚本中的用法。首先讨论了如何使用MySQL服务器存储应用程序的持久性数据。这只需要为应用程序创建一个数据库和一个唯一的用户账户,然后只给用户赋予该数据库的权限就可以了。你可以创建数据表来存储应用程序数据。shell脚本使用mysql命令行工具作为MySQL服务器的接口,提交SELECT查询,显示检索结果。接着,讨论了如何使用基于文本的浏览器lynx从互联网上的网站中提取数据。lynx工具能够转储Web页面的全部文本, 你可以使用标准的shell编程技巧存储这些数据,并从中查找所需要的内容。最后,介绍了如何使用标准的Mailx程序通过Linux电子邮件服务器发送报表。Mailx程序可以让你轻松地将命令输出发送到任一电子邮件地址。
在接下来的最后一章中,我们会再介绍一些shell脚本的例子,向你展示shell脚本编程的威力。