I am LAZY bones ? all linux

几个连接数据库用的python模块

工作中,经常会有用python访问各种数据库的需求,比如从oracle读点配置文件或者往mysql写点结果信息之类的。
这里列一下可能用到的各个模块。

sqlite3: 内置模块
用sqlite,有时候确实很方便,我觉得它确实做到了宣称的“零配置”。python自2.5版以来,就内置了对sqlite3的支持,使用也非常简单,按照文档上来:

#打开db文件,获得连接
conn = sqlite3.connect('数据文件名')
#获得游标
c = conn.cursor()
#执行SQL
c.execute('''SQL 片段''')
#如果有对数据的修改操作,那就需要commit一下
conn.commit()
#关闭游标
c.close()
#关闭连接
conn.close()

另外,关于sqlite在C和bash下的用法,可以参考为以前的文章

oracle: cx_Oracle
其实,前面先介绍sqlite3,除了它确实是个小数据库以外,还有一个原因:其他数据库在python下的操作,其实基本上和sqlite3的操作是一样的,也就是说,python其实已经几乎统一了数据库的接口。
打开cx_Oracle的文档页面,你会发现其风格也和python文档很像,因为他们都是用 Sphinx 做的。模块的使用方法就更像了,把上面的代码里,获得连接的那行,换成这样:

conn = cx_Oracle.connect('username/password@TNSname')

就可以了。只要把用户名、密码、TNS组成一个字符串,传进去,就可以得到一个oracle的连接了。

mysql: MySQLdb
和前两个非常类似,连接的时候用以下两个语法之一:

conn = MySQLdb.connect('host', 'username', 'password', 'database')
conn = MySQLdb.connect(host="host", user="username", passwd="password", db="database")

接下来,也把它当成sqlite用就好了。

excel: pyExcelerator
好吧,我承认excel不算数据库,只是写在这里充数而已,哈哈。因为偶尔还是要取下别人发来的excel里的数据的。
其实,用pyExcelerator来读取文件也是很简单的:

sheets=pyExcelerator.parse_xls('xxx.xls')

这样出来以后,sheets就是整个工作薄了,它是工作表组成的list,而一个工作表对应于一个tuple,格式是: ('工作表名', 内容),而内容又是一个dict,key是一个(行数, 列数)的tuple,value才是正在的对应格子的内容。看起来确实比较绕,好在处理excel的应用也不多,将就吧。
另外,其实pyExcelerator还支持写入数据到excel的,如果有把查询结果保存成excel的需求的话,可以试试看,我还是尽量不用这种格式了,哈哈。

其实,文件也可以truncate

现在觉得,时间的流逝速度和年龄确实是成正比的。也就是说,年纪越大,就会觉得空闲时间越来越少了~
因此,本blog都大半个月没更新了,呵呵。也不能老这么沉寂下去,今天来写点东西。

熟悉数据库的朋友们都知道,大多数数据库都有个truncate指令:truncate table xxx可以把xxx表里的所有数据都删掉,但是保留表结构。其实,在有任何数据库之前,UNIX系统里就有了truncate这个命令了,当然后面的*nix里都保留了这个。可以想像,系统里的truncate命令的操作对象肯定是文件,而且此命令不仅能把文件的数据删成0字节,还可以缩减(甚至扩大)文件至指定的大小(通过 -s 选项指定文件大小值),这对于那种日志头部有些不想删除的关键信息,但后面的部分又很多很杂的情况下很有用。对于普通的日志文件,我们要清理的时候通常可以执行 > log 来清除文件的内容(这样,log文件会变成0字节),但是如果清理的同时想保留原始日志的前面4K的信息,不用truncate就会很麻烦了。

truncate的用法还是通过实战来解释吧,如下:

lily@LLY ~$ echo -n 1234567 > txt
lily@LLY ~$ cat txt
1234567lily@LLY ~$ 
lily@LLY ~$ truncate -s 4 txt
lily@LLY ~$ cat txt
1234lily@LLY ~$ 
lily@LLY ~$ ls -l txt
-rw-r--r-- 1 lily lily 4 1024 16:54 txt
lily@LLY ~$ truncate -s 1M txt
lily@LLY ~$ ls -l txt
-rw-r--r-- 1 lily lily 1048576 1024 17:17 txt
lily@LLY ~$ du txt
4	txt
lily@LLY ~$ wc -c txt
1048576 txt

这里还可以看到一个“奇怪”的现象,本来已经缩至4字节的文件,把它扩展成1M以后,ls 和 wc 的结果显示大小确实是1M,但是 du 的结果却发现大小还是4字节。这也是要注意的地方之一,这种文件称为“空洞文件”,也就是说,文件的部分内容并没有实际存在于硬盘上(即没有分配对应的inode),只是“声称”有1M的大小而已。对于不存在于硬盘上的那部分字节,如果去读的话,也是不会报错的,会读到全0的数据。
这也从另一个方面反映出ls等命令默认显示的是文件“声称”的大小,而du (disk use)默认显示的是真正的磁盘占用。这里是我以前的另外一个例子。

又一个简单的轻量级图片查看器──Viewnior

gnome默认的图片查看器是eog(Eye of GNOME,gnome之眼),但是由于这个比较笨重,我不是很喜欢。
之前用的都是 gpicview 这个轻量级的小玩意。因为我对图片查看器的需求只有:能快速打开,能进行旋转操作就可以了,当然,最好旋转完了以后能自动保存,这是因为我的傻瓜相机真的很傻,没有重力感应装置,所以拍出来的相机有很多都需要手工转一下~
但是,不知道从什么时候起,我的gpicview就不能做旋转照片的操作了(记得好像是哪次jpeg库升级了以后才有的,不是很确定),一点旋转按钮,程序就自动退出了,终端运行的话,会丢出一句让人摸不着头脑的提示:Bogus virtual array access。本来以为马上升级一下就可以解决问题的,但是等了很久也没解决,于是去找类似的替代品,就发现了这个:VIEWNIOR
功能上没啥可说的,就是显示和支持旋转,哈哈,主要是依赖少,基本只依赖一个GTK了。依赖少,自然速度也快~
恩,够用就好。

对老域名用PHP写了个301重定向

之前,这里的域名一直都是 li2z.cn ,但是,被CNNIC逼得走投无路以后,我终于把域名换成 luy.li 了。其实准确的说,不是“换”,是加了一个域名,并射成默认了而已,因此用 li2z.cn 还是可以访问的,甚至连301都没有做,之前我的做法只是把两个域名的后台路径指向同一个了而已(题外话:由于两个站的内容完全一致,所以被google惩罚了,li2z.cn 的PR瞬间就变成0了,所以在意PR的站长千万别这么干哦~)。
现在,时间也过去这么久了,大多数的流量已经是新域名下的了,但是老域名的流量也还有不少,于是我就想能不能在不影响访问的同时,统计一下老域名的每次http请求的来路。
方法自然是把所有request用301重定向到 luy.li 的对应地址,然后在日志里记录refer了(不明白301和refer的请自行google基础知识)。
这个用我三脚猫的php都很容易搞定,就几行代码,index.php如下:

<?php
header("HTTP/1.1 301 Moved Permanently");
header("Location: http://luy.li".$_SERVER['REQUEST_URI']);
date_default_timezone_set('Etc/GMT-8');
$msg = date('Y-m-d H:i:s').' '.$_SERVER['HTTP_REFERER'].' '.$_SERVER['REQUEST_URI']."\n";
file_put_contents('log.txt',$msg,FILE_APPEND);
?>

然后,建个 .htaccess 把域名下的所有请求都指向 index.php:

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php [L]
</IfModule>

测试一下,对get请求,可以完整地转到新的域名了:

$ curl -v "http://li2z.cn/abc?xxx=yyy"
* About to connect() to li2z.cn port 80 (#0)
* Trying 66.147.240.158… connected
* Connected to li2z.cn (66.147.240.158) port 80 (#0)
> GET /abc?xxx=yyy HTTP/1.1
> User-Agent: curl/7.21.1 (i686-pc-linux-gnu) libcurl/7.21.1 GnuTLS/2.10.2 zlib/1.2.5
> Host: li2z.cn
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently < Date: Sat, 02 Oct 2010 12:05:53 GMT < Server: Apache < X-Powered-By: PHP/5.2.14 < Location: http://luy.li/abc?xxx=yyy
< Cache-Control: max-age=1 < Expires: Sat, 02 Oct 2010 12:05:54 GMT < Vary: Accept-Encoding < Content-Length: 0 < Content-Type: text/html < * Connection #0 to host li2z.cn left intact * Closing connection #0 然后在后台目录里会生成一个 log.txt ,里面会有所有访问的时间、refer和uri,这样日后就可以方便地查出老域名的所有来路和受访页面了,最重要的是,由于有301,还不会对正常访问造成影响哦~

sudo的配置

大家都知道,root权限是linux系统中的最高权限,有了root权限,就可以对系统做任何操作。
但是,很多情况下,这样一个笼统的root权限并不能很好地满足需求,比如,有时候想让系统的某几个用户有装包的权限(就是执行apt-get或者yum什么的),但是不能随便更改其他系统配置;又比如,想让某个用户有杀死指定另外一个用户的进程的权限(比如www用户什么的),但是也不能随便杀其他用户的进程。
这样一来,便有了细化这个“最高权限”的需求了。于是,权限管理的一大利器——sudo——便应运而生了。
可能是由于sudo的需求本来就比较复杂,看我上面说的例子,用口语表达都比较拗口;也可能是有sudo需要的安全级别比一般的程序要高一些。导致sudo的配置,看上去有点凌乱和摸不着北,所以这里稍微解释一下。
首先,sudo的配置文件是 /etc/sudoers,虽然你也可以手工打开、编辑、保存。但还是建议使用visudo命令来编辑。这是因为:

  • 它能够防止多个用户同时修改它
  • 它能进行有限的语法检查
  • 它能避免因权限位出错而不被sudo认可

然后,打开配置文件以后可以看到有这几部分:

  • Host_Alias、User_Alias和Cmnd_Alias,分别是主机、用户和命令的别名。
  • Defaults一些默认的特性,比如默认要不要重设环境变量,重设哪几个环境变量,默认以谁的身份执行等等。
  • 后面就是重头戏了,类似这样的一行:
    root ALL=(ALL) ALL
    其实这行是 user machine=(users) commands 这样的格式,也就是,允许在machine登录的 user 用户以users的身份来执行commands命令。这里的machine、users呵呵commands就可以用Host_Alias、User_Alias和Cmnd_Alias来代替了。
  • %group ALL=(ALL) ALL
    和上面的一样,只不过这个按组来限制权限。

还是用例子来说话吧,接着上面的两个例子。

例1. 让系统的某几个用户有装包的权限(就是执行apt-get或者yum什么的),但是不能随便更改其他系统配置。
做法就是将这几个用户加入某个特殊的组里(当然,如果你愿意也可以一个个用户分别设置,哈),比如建了yumer组,把用户都加进去了,然后sudo的配置加上:
%yumer ALL=/usr/bin/yum

例2. 让某个用户(dev)有杀死指定另外一个用户的进程的权限(比如www用户什么的),但是也不能随便杀其他用户的进程。
配置如下:
## Processes
Cmnd_Alias PROCESSES = /bin/kill, /usr/bin/kill, /usr/bin/killall, /usr/bin/pkill
Defaults:dev runas_default=www
dev ALL=(www) PROCESSES

先把几个命令alias成一个 PROCESSES,然后指定dev用户,有以www用户的名义执行PROCESSES里的程序的权限。
本来,dev用户必须使用 sudo -u www kill 1111 来杀死www用户的1111号进程的,但是加个-u显然麻烦,所以有了一行: Defaults:dev runas_default=www
这行的意思,是让sudo知道,只要是dev用户执行的,默认就是www的身份,而不是一般的root身份。

这里只是通过两个简单的例子介绍了sudo最常用的功能,其实sudo还有很多其他的有趣功能。比如,sudo还可以实时将非法操作检测出来,以多种方式记录到日志里,不只是本地日志,还可以通过http等方式传到别的机器等。更多功能,当然得参见man页了,哈哈。

python编程细节──遍历dict的两种方法比较

python以其优美的语法和方便的内置数据结构,赢得了不少程序员的亲睐。
其中有个很有用的数据结构,就是字典(dict),使用非常简单。说到遍历一个dict结构,我想大多数人都会想到 for key in dictobj 的方法,确实这个方法在大多数情况下都是适用的。但是并不是完全安全,请看下面这个例子:

#这里初始化一个dict
>>> d = {'a':1, 'b':0, 'c':1, 'd':0}
#本意是遍历dict,发现元素的值是0的话,就删掉
>>> for k in d:
...   if d[k] == 0:
...     del(d[k])
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration
#结果抛出异常了,两个0的元素,也只删掉一个。
>>> d
{'a': 1, 'c': 1, 'd': 0}
 
>>> d = {'a':1, 'b':0, 'c':1, 'd':0}
#d.keys() 是一个下标的数组
>>> d.keys()
['a', 'c', 'b', 'd']
#这样遍历,就没问题了,因为其实其实这里遍历的是d.keys()这个list常量。
>>> for k in d.keys():
...   if d[k] == 0:
...     del(d[k])
... 
>>> d
{'a': 1, 'c': 1}
#结果也是对的
>>>

其实,这个例子是我简化过的,我是在一个多线程的程序里发现这个问题的,所以,我的建议是:遍历dict的时候,养成使用 for k in d.keys() 的习惯。
不过,如果是多线程的话,这样就绝对安全吗?也不见得:当两个线程都取完d.keys()以后,如果两个线程都去删同一个key的话,先删的会成功,后删的那个肯定会报 KeyError ,这个看来只能通过其他方式来保证了。

mysql数据库表名的大小写问题

记一下一个从昨天一直找到今天才找到原因的问题,希望可以帮助到也在苦苦查找原因的后来人。
有个hive开发环境,元数据库用的是mysql。然后从一个mysql迁移到另一个以后,就发生了莫名其妙的错误,表现在:mysqldump 出来,用 mysql < xxx.sql 导入以后,数据库连接正常,写入也有权限,但是对某些表插入数据的时候,莫名地报主键冲突的错误(那表的主键只有一个字段,主键约束也很简单,select 发现没有和要插入的数据重复的)。 究其原因,可以看到mysql里有两张表,表名除大小写以外都一样。。。这样判断主键是否重复的时候,就会把两个表合起来判断了(mysql 5.0.45 有此问题,其他版本未测试)。 后来通过google,发现mysql数据库在windows下,由于windows的文件名是不分大小写的,所以表名默认也不分大小写;但是在linux服务器下,默认是区分大小写的。。。但是这个方式反而容易引发一系列的兼容性问题,比如从一个复制数据到另一个的时候,可能由于表名大小写不同而访问不到。因此mysql也提供了一个关闭区分表名大小写的方法: 修改 mysql 的配置文件 /etc/my.cnf 在[mysqld]那段里添加这么一行: lower_case_table_names=1
然后再重启一下mysqld,就会自动把所有表名转换成小写存储,问题也就迎刃而解了。

关于我的网络

近一个月来,都在断断续续折腾网络,遇到蛮多不爽的,流水帐记录一下。
首先是拉宽带,杭州有电信、网通、联通,由于联通是新入的,所以搞活动比较多,也最便宜,于是优先考虑,结果打电话过去,告诉他地址之后,人家就说“对不起,我们的网络暂时还不到您的小区!”(都怪我住得太偏了,唉。。)
无奈只能问问老牌的电信,一问1M 1年980,2M 1180,但是还需要初装费100块,觉得还是有点小贵,再问网通,人家1M、2M都是980,选1M还可以送个不值钱的蚕丝被,而且不用初装费,然后又听说网通其实现在用户少,平时限速不严,常常1M的带宽下载也能300+KB/s。又贪图了那100块钱的初装费,就装了1M的网通。
话说,网通的上门速度还真不错,当天打电话,就来给安装了,晚上回去一试,速度果然还可以,就开心地以为没啥问题了呢,到后来才发现那句老话──一分钱,一分货──还是有道理,这个网通果然还是存在不少毛病:首先是网络不稳定,你上得正开心呢,冷不丁就给你来次断线,还好我不怎么玩网络游戏,不然就更郁闷了。再者,我之前都是通过ssh隧道翻墙的,但是自打用了网通以后,就很难连上国外主机的22端口(80端口非常正常)了,奇怪的是,我ssh到国内主机很正常,从国内主机ssh到国外主机也很正常,但是就是不能直接ssh出去,于是基本确定是网通封了某些IP的某些端口或者是网通没有国际线路的带宽?于是郁闷地打丫投诉电话,接线员基本不能理解我说的是什么问题,我想他毕竟只是接线员而已,只要能记录问题就行,也就不多说了,说第二天给派人上门维修,于是第二天就上门了一个可爱的网通工作人员,进门我给他开电脑看问题,一看到linux,就直接不看了,打开他自己带的笔记本,插上我的网线,用他自己的账号拨号上网,然后打开网通的主页,点了一个电影开始放起来,一边对我说:“你看,网络一切正常,速度还不错~”。。。我那个无奈啊。。。后来花了很多时间跟他解释什么是 ssh(他居然压根不知道什么是ssh,什么是端口)、上网不只是能看电影能偷菜就行的等等。然后我帮他一字一句地把上门服务地详情录入到网通的系统里。。。
还好,经过我的N次电话之后,现在勉强能ssh出去了,虽然ssh隧道的速度和看电影的速度不是一个级别的,但是好歹能上推了。。。
这里给广大网友提个醒:如果你上网不只是为了看电影的话,一定要慎选网通。不然真是劳民伤财啊。。。打96171都花了我不少钱了。

设置python的stdout为无缓存模式

考虑以下python程序:

#!/usr/bin/env python
 
import sys
 
sys.stdout.write("stdout1 ")
sys.stderr.write("stderr1 ")
sys.stdout.write("stdout2 ")
sys.stderr.write("stderr2 ")

其中的sys.stdout.write也可以换成print。
运行这程序,你觉得会输出什么?试验一下,就会发现,其实输出并不是

stdout1 stderr1  stdout2 stderr2

而是:

stderr1 stderr2 stdout1  stdout2

究其原因,是因为缓存:虽然stderr和stdout默认都是指向屏幕的,但是stderr是无缓存的,程序往stderr输出一个字符,就会在屏幕上显示一个;而stdout是有缓存的,只有遇到换行或者积累到一定的大小,才会显示出来。这就是为什么上面的会显示两个stderr的原因了。
然而,有时候,你可能还是希望stdout的行为和stderr一样,能不能实现呢?当然是可以的,而且对于python,实现起来还特别方便,以下是两个方法:

python -u stderr_stdout.py
PYTHONUNBUFFERED=1 python stderr_stdout.py

第一种方法是给python指定 -u 参数,第二种方法是在python运行时,指定 PYTHONUNBUFFERED 环境变量,这两种方法其实是等效的。
当然,也可以在程序的第一行指定 #!/usr/bin/python -u 然后程序加可执行权限来运行,或者把 export PYTHONUNBUFFERED=1 写到 .bashrc 里去。

gentoo下的pppoe拨号

最近,无线路由坏了,所以只能先用自己的电脑拨adsl了。
其实这本也没什么,我的win7和ubuntu都只要稍微设置一下就OK了。
这里再稍微提一下ubuntu的pppoe设置:记得以前的版本(应该是6.xx的时候吧),NetworkManager是不直接支持pppoe的,还要自己手工设置,然后执行pon/poff来拨号,但是现在进步了,直接在NM里输一下用户名和密码就可以上了。
但是我的gentoo是用wicd来管理网络的,而wicd至今都还不支持pppoe,于是只能用原始的命令行来拨号了。
于是eix一搜,发现有个net-dialup/rp-pppoe,安上,看到有 pppoe-setup、pppoe-start、pppoe-stop。啥都不用说了,先pppoe-setup,再pppoe-start,本以为会很顺利,但是几次尝试都在最后一步出错了,而且提示的错误都没啥价值,不知道从何查起~
正当我无计可施,想妥协安个NetworkManager的时候,忽然灵感一现,发现了可能的错误原因,那就是──内核模块。原来,之前我的gentoo内核基本上也是按需配置的,以前我一直都有路由器拨号,所以没有在内核选项里打开ppp的支持,才导致了这一郁闷的结果,哈哈,既然发现了可能的原因,那就好办了,make menuconfig 里面选上 Device Drivers —>Network device support —>PPP (point-to-point protocol) support 下面的所有项,编译完再重启。再 pppoe-start ,果然看到了 Connected!