本文在 github 中的镜像:Python 的 and-or 技巧。
布尔上下文
在 Python 中,可以在布尔上下文中使用几乎所有类型的表达式。通常的数据类型的“空值”都为 False。
-
None
为假值 -
数值
0
,0.0
等为假值 -
空串
""
为假值 -
空列表
[]
为假值 -
空元组
()
为假值 - ...
逻辑演算
在 Python 中,and 和 or 按照下面的规则执行布尔逻辑演算:
对于 and,从左到右运算:
- 如果所有表达式都为真,则 and 返回最后一个表达式。
- 否则,and 返回第一个假值。
对于 or,从左到右运算:
- 如果有一个为真,则 or 立刻返回该值。
- 否则,or 返回最后一个表达式。
- or 找到第一个真值后会忽略计算剩余的表达式。
注意到:返回的并不是布尔值,而是其中某个参与比较的表达式值。
and-or 技巧
在 C 中,表达式 bool ? a : b
表示当 bool 为真时结果为 a,其它值则为 b。在 Python 中可以使用 and-or 实现类似的功能。
((test and [x]) or [y])[0]
在这个 Python 表达式中,如果 test 为真,则返回 x,否则返回 y。
这里使用列表将 x 和 y 括起来是为了防止 x 为空值的情况,比如想要实现:“如果 test 为真,则取 0,如果 test 为假,则取 1”,如果不将 0 括起来则为:
(test and 0) or 1
不论 test 是真是假,都只会返回 1。
行结束符
一个文本文件是由行组成的,本文所说的就是行与行之间用来表示新行(newline)的间隔,一般称作断行符(link break)或者行结束符(end-of-line, EOF)。
由于历史的原因,不同的操作系统用来表示换行的字符不同,这就给跨操作系统编辑文件带来不便。
操作系统中的差异
使用下面的 Unicode 标准定义的符号:
- LF: Line Feed, U+000A, '\n'
- CR: Carriage Return, U+000D, '\r'
不同的操作系统使用的行结束符:
- Windows 使用 CR+LF,也就是 '\r\n'
- Linux/Unix 系列使用 LF,也就是 '\n'
特别的,目前 Mac OS X 是基于 Unix 的,所以行结束符也是 LF。只有 v9 之前 Mac OS 才是用 '\r'。
历史
为什么对“另起一行”的处理有这样的差异,是因为早期的电传打字机(teletype)从左至右打完一行的时候,需要给打印机头重新移回左边界的时间,在一个字符的时间内,不足以让打印机头移动到正确的位置,这样会影响下一个字符的打印。所以就需要在一行结束的时候额外传递一个 CR 字符令装置 carriage 归位。
进一步阅读:Wikipedia: Newline。
对不同换行符的处理
一般操作系统的运行库决定了文本文件的换行格式,在一个平台上使用另一种换行符的文件通常会有问题。大部分编辑器会自动识别换行符类型,并带有换行转换的功能。
比如某些 FTP 软件在进行文本传输的时候会对换行符进行转换(这样修改了原文件)。
Python 使用 "Universal Newline" 处理这个问题。在以文本方式 open() 的时候,会对换行符进行识别并一致处理成 '\n',在文件写入的时候,也只要 write('\n') 即可,Python 会根据操作系统自动处理。有关文档:
- http://docs.python.org/library/stdtypes.html#file.newlines
- http://docs.python.org/library/functions.html#open
本文 github 中的镜像。
Python 允许通过 exec 和 eval 执行以字符串形式表示的代码片段,这体现了动态语言的特性。利用这种特性,可以让代码变得更灵活。不过一直以来,我对这种"动态"的用法不太"适应",因为:
- 让代码引入了某些不安定因素,这些代码片段执行后可能对全局造成影响。尤其是当使用全局名称空间时,它的作用范围难以控制。
- 对执行的效率也有影响。Python 在执行代码之前也是要预编译的,比如 pyc 文件。因此这些字符串形式的代码片段在执行的时候,需要编译的过程,哪怕是使用 compile 编译后重复使用,第一次的编译是难以避免的。
很多人也倾向于避免这种用法,比如:Use of eval in Python?。
不过最近在写网站解析程序的时候,发现为了实现目的,最好的解决方案还是引入这种字符串形式的代码片段。这个问题具体描述是:我需要对一系列类似的网站进行解析。整体的解析过程是相似的,所以我建立了一个框架,使用 XML 保存每个站点各自的解析属性,通常是 XPath 和正则表达式。但是某些内容各个网站的表现方式有很大差别,这些内容想要解析出来只使用 XPath 和正则表达式是不够的。因此对这些例外情形,最好的方式是直接利用代码片段来计算。
此外,现有的项目,比如 Genshi 之类模板引擎也采用了类似的处理手段。
用户使用程序如果能执行自己的代码片段,这往往是有潜在危险的,尤其是在网络服务中,因此我们在使用的字符串代码片段的时候,要严格限制名称空间。对这个问题的讨论,可以参考 Using eval() safely in python,这里做一个简要的总结。
默认的,eval 和 exec 所运行的代码都位于当前的名称空间中,它们也可以接受一个或两个可选字典参数作为代码执行的全局名称空间和局部名称空间。
eval 的用法最严格的限制(注意对 builtins 的处理)是:
eval(user_func,{"__builtins__":None},{"x":x,"sin":sin})
如果希望允许使用某些变量和函数,可以采用下面的方法:
eval(user_func,{"__builtins__":None},{"x":x,"sin":sin})
有关使用 exec 和 eval 的注意事项,文章 Be careful with exec and eval in Python 进行了细致的讨论。首次编译是不可避免的,使用预编译后重复使用可以提高效率。另外使用局部空间因此可以加快变量的查询速度,所以执行会快。
下面还有几个需要注意的地方:
能避免使用的时候,还是应该采用其他方式,比如转换字符串的时候,使用 int() 而不是 eval(),因为如果转换的这个字符串是用户输入的,危险的情况是 eval() 会执行恶意输入的条命令。详细的解释参考:Python之eval()函数的危险。
尽量避免使用 eval 来获取变量名,想要实现动态变量名,使用 globals(),locals() 以及 vars()。
a = 123 s1 = locals()['a'] s2 = vars()['a'] print s1, s2
类似的情况也出现在 JSON 格式的解析上,参见 Running JSON through Python's eval()? 。
ReadTxT 是基于 Google App Engine 编写的阅读网络小说的工具,初衷是为了使阅读网络小说更方便而设计的。它支持常见的网络文学站点,比如:起点中文网、纵横中文网、飞库电子书等。用户可以在登录后统一管理各个站点自己喜欢的书目,并保存阅读书签,实现一站式阅读。主要的特点有:
- 支持多个主流小说网站。
- 自动抓取最新的章节。
- 无广告。
- 支持手机阅读,并针对 iPhone 优化。
- 提供下载和订阅功能。
项目主页:http://code.google.com/p/bookreader/
部署需要对 Google App Engine 有一点点了解,如果只是希望使用,可以直接进入:
- ReadTxT:http://readtxt.appspot.com/
- 更多的介绍可以参见 ReadTxT 的帮助
可以保证上述部署的应用是最新版本,除非调整数据库结构,书签信息不会清空(有的时候清空了数据...请不要怪我)。 需要补充说明的是,本人不负责使用代码或者应用本身所带来的网络小说版权问题。
2009-04-07Up: 更新到 Python 3.1a2,发现似乎问题解决了...
我在使用 urlopen(...).read() 读取某一个论坛网页的时候,发现获取的内容不全,最为奇怪的是,获取的内容是这个网页最后的部分。给我的感觉是分配给字符串的空间不够,导致读取超过长度限制的时候就把前面的"冲掉"了。但是 Python 的字符串是任意长的,不应该出现这个问题。
我用同一个程序尝试读取其他网页,包括相似的论坛都没有问题。同时,如果我设置一个很大的值,比如 read(100000),则也能完整读取这个网页。
后来我仔细查了一下 HTTP Headers,发现该网页 Response Headers 中有如下信息:
- Content-Encoding : gzip
- 没有 Transfer-Encoding : chunked 项
- 没有 Content-Length 域的值
- 有 Connection : Close 项
我不太懂 HTTP 协议是怎样规定的,我猜测因为这个网页的服务器没有告之文件大小,所以 Python 在读取的时候也无法预先得之大小,因此在读取的每次读一个新块就会把前一个覆盖。而当我指定一个读取大小时,Python 就会按照连接是否断开来判断是否读取结束。
以上是我的猜测,因为这个折腾了半天,也没有去查看源代码是如何实现的。希望有经验的朋友能告诉我对不对,多谢!
另外,使用标准库中的 urllib.request 应该能自动处理 gzip。
环境:Python 3.1a1。
使用 IDLE 运行 Python 程序的时候输出是即时的,但是在命令行下调用 .py 文件的时候,Python 对标准输出默认是有缓冲管理的。也就是说,在程序中尽管 print 试图输出好几行,但是在终端上并没有显示。只有当输出内容足够多的时候才会一起显示。
之前在配置 Notepad++ 的插件 NppExec 时就遇到了这个问题(见使用 Notepad++ 编辑运行 Python 程序最后一部分)。
Python 提供一个 -u 参数,使用它可以实现无缓冲的 IO(详细说明可以查看 Python --help)。
Python 3.0 版本的 -u 参数似乎有一个 bug: Issue4705 (Revision 68451)。安装 3.1 应该就没问题了。
不过即使使用了 -u 参数,也是指行缓存为 0,每当遇到一个换行符的时候才会输出。如果想在任意地方刷新缓冲区以达到立即输出的目的,需要在 print() 后使用 sys.stdout.flush()。(不知道还有没有更好的方法?)
这里谈的是标准输出的缓冲,对于其他对象,比如文件,也会涉及到类似的输入输出缓冲的问题。
今天我写了一个处理 Google Reader API 的 Python 模块,当我在其他程序中 import 这个模块的时候,第一次运行(在生成 pyc 文件之前)顺利通过,第二次运行就失败,显示如下错误:
Traceback (most recent call last):
File "test.py", line 1, in <module>
import readerapi
UnicodeDecodeError: 'utf8' codec can't decode bytes in position 36-37: invalid data
但是我尝试在 Notepad++ 中将文件编码进行改变:ANSI、带 BOM 的 UTF-8、不带 BOM 的 UTF-8,始终都是失败。
后来我尝试把文件从桌面(这个路径含有空格和中文)转移到一个简单的路径,运行就没有问题。
折腾了半天未果,怀疑是 3.0.1 有 bug,下载安装 Python 3.1 alpha 1 就好了=.=!
Notepad++ 是一个开源的文本编辑器,功能强大而且使用方便。编辑和调试 Python 程序使用什么编辑器或者 IDE 不同人有不同见解。在不考虑使用调试工具的情况下,Vim 或者 Emacs 用户自然可以无视其他编辑器,不过在 Windows 环境下,使用 Notepad++ 之类的编辑器也是很好的选择。
我是在 PortableApps.com 上下载的 Notepad++ Portable,它的好处是可以在每次关闭程序的时候把所需的配置文件单独保存,省却每台机器上重新配置的烦恼。
Tab长度和空格转换
因为 Python 对缩进要求严格,我们将 Tab 设置成4个空格,在 "设置->首选项->编辑->制表符设置" 中修改。
语法高亮
只要正确设置了扩展名,Notepad++ 就会自动识别语言并进行语法高亮。如果对高亮的颜色或者字体不满意,在 "设置->语言格式设置" 中可以进行修改。
自动完成
Notepad++ 也提供了自动完成和输入提示功能,在 "设置->首选项->备份与自动完成" 中可以设置。Python 的自动完成所需的文件在安装的时候自带了,存放在 "YOUR_NPP_DIR\plugins\APIs"(详见官方FAQ:Auto-completion)。默认的快捷方式是 Ctrl+Space 和 Ctrl+Enter,可能和输入法的快捷键冲突,不过 Notepad++ 的所有快捷键都可以自定义。
运行程序
点击 "运行->运行" (默认快捷键是 F5 ),在弹出的菜单中输入:
cmd /k C:\Python30\python.exe "$(FULL_CURRENT_PATH)" & PAUSE & EXIT
选择 "保存",就可以给这条命令设置一个快捷键并起一个名字,比如叫 "Run Python"。以后运行直接按自定义的快捷键就可以了。注意如果想修改这个快捷键,可以在 "设置->管理快捷键->Run Commands" 中修改。还有几点说明:
- 如果想修改这条命令,目前只能通过修改 shortcuts.xml 文件,这个文件保存在 Notepad++ 的配置文件中,可能在 Notepad++ 的目录,也可能在 Documents and Settings 下的 Application Data 内。
- $(FULL_CURRENT_PATH) 的含义是当前文件的完整路径,这是 Notepad++ 的宏定义,更多的相关宏可以参见官方FAQ:Run external tools。注意要用引号括起来,防止路径中间有空格。
- 直接执行 python.exe 在运行结束后窗口会自动关闭,所以要用 cmd 来执行。(在 Python 2.5 似乎有所不同,但是 3.0 需要这样设置。)
- cmd /k 的含义是执行后面的命令,并且执行完毕后保留窗口。& 是连接多条命令。PAUSE 表示运行结束后暂停,等待一个任意按键。EXIT 表示关闭命令行窗口。如果使用 cmd /c 就可以省掉 EXIT 了。
更多 Plugin
因为 Notepad++ 支持插件扩展,所以可以使用很多有用的插件(下载页面有插件列表)。比如有一个插件:NppExec,可以在 Notepad++ 中增加一个 Console 窗口,使得运行命令和脚本更加方便。可以通过这个插件设置 Python 的运行命令。
不过这个插件有个问题是只有程序运行结束了才会返回输出结果,这样就没法实时观察程序的运行了。不知道有没有好的解决方法?
2009-03-14Up:
关于输出不及时问题,可以通过刷新 stdout 缓冲来解决。
前几天决定尝试下 Python 3.0,安装的版本是 3.0.1。不出意外,原来的代码都不能用了。
我想要修改的是使用 Python 登录网站中的程序,经过把 print 语句改成函数,重新查找标准库的用法,修改字符串的表达方式这些常规的修改,终于不提示语法错误了。
不过我运行程序,却发现没有登录成功。经过调试,发现在发送登录请求的时候,编码出了问题。论坛需要 GBK,而 Python 使用 urlencode 编码得出的是 UTF-8。
在 Python 3 中取消了原来的 unicode 类型,现在的字符串类型为 str,并且存放的就是 unicode 字符串。这样,在 Python 程序内部就不用为各种字符编码困扰了。现在使用字符串,只要保证在与外界交互,即读取(比如赋值、从文件读、从网络读)和输出(比如存储文件、发送信息)的时候使用正确的编码即可。但是这样也就造成原来很多程序都要重新写。
字符串经过具体编码,就不再是 str 了,而是 bytes 类型。对一个 str 可以用个 encode 编码成 bytes 数据,而一个经过编码的 bytes 数据可以通过 decode 解码成 unicode 字符串。在 Python Docs: What's New In Python 3.0 : Text Vs. Data Instead Of Unicode Vs. 8-bit 中有详细说明。
在读取文件的时候,默认是以 text mode 打开的,所以读取的资料直接可以存放到 str 字符串中,同时在 open 的时候可以指定编码系统。而如果文件不是文本,则需要在 open 的时候使用 "b"以 binary mode 打开。
回到之前那个论坛登录程序,我使用标准库自带的 urlencode 将个人用户名和密码等参数进行 URL 编码,而由于 Pyhton 字符串机制发生根本变化,这个函数默认将文字(比如用户名是中文)按照 UTF-8 进行编码。而论坛需要的是 GBK 编码,所以造成无法登录。
通过查看标准库相关的代码:Lib\urllib\parse.py 文件中的 urlencode 函数可以看出,它默认使用的就是 UTF-8,也没有提供可选的编码参数。所以要想实现 GBK 编码,只能放弃使用 urlencode 函数,直接使用 quote_plus 来编码。不过就要手动实现把编码后的参数用"&"连接起来了。
前面讲了用 Python 模拟浏览器进行登陆、抓取页面然后分析等操作。有的时候,我们还需要把筛选出的页面显示出来,最简单的方法自然是调用系统默认的浏览器。
直接使用标准库中的 webbrowser 模块,看下面的例子:
import webbrowser webbrowser.open_new_tab('www.163.com')
之前说过使用 Python 登陆网站,我们已经可以获取任意一个网址的数据了。但是,在实际应用中,代理服务器往往是少不了的。
在 urllib2 模块中,每一个 opener 可以用多个 handler 来增强功能,在前一篇,我们使用的是 Cookie 的支持,我们只要在这里再加上 proxy 支持就可以了。
#!/usr/bin/env python # -*- coding: GB2312 -*- # from urllib import urlencode import cookielib, urllib2 # 准备cookie cj = cookielib.LWPCookieJar() cookie_support = urllib2.HTTPCookieProcessor(cj) # 设置代理服务器 proxy_info = { 'host' : '127.0.0.1' , 'port' : 8118 } proxy_support = urllib2 . ProxyHandler ( { 'http' : \ 'http://%(host)s:%(port)d' % proxy_info } ) # 构造opener opener = urllib2.build_opener(cookie_support, proxy_support) urllib2.install_opener(opener) # 打开网页 page = urllib2.urlopen("http://www.163.com") print page.read(1000) page.close()
对于大部分论坛,我们想要抓取其中的帖子分析,首先需要登录,否则无法查看。
这是因为 HTTP 协议是一个无状态(Stateless)的协议,服务器如何知道当前请求连接的用户是否已经登录了呢?有两种方式:
- 在URI 中显式地使用 Session ID;
- 利用 Cookie,大概过程是登录一个网站后会在本地保留一个 Cookie,当继续浏览这个网站的时候,浏览器会把 Cookie 连同地址请求一起发送过去。
Python 提供了相当丰富的模块,所以对于这种网络操作只要几句话就可以完成。我以登录 QZZN 论坛为例,事实上下面的程序几乎所有的 PHPWind 类型的论坛都是适用的。
# -*- coding: GB2312 -*- from urllib import urlencode import cookielib, urllib2 # cookie cj = cookielib.LWPCookieJar() opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) urllib2.install_opener(opener) # Login user_data = {'pwuser': '你的用户名', 'pwpwd': '你的密码', 'step':'2' } url_data = urlencode(user_data) login_r = opener.open("http://bbs.qzzn.com/login.php", url_data)
一些注释:
- urllib2 显然是比 urllib 高级一点的模块,里面包括了如何使用 Cookies。
- 在 urllib2 中,每个客户端可以用一个 opener 来抽象,每个 opener 又可以增加多个 handler 来增强其功能。
- 在构造 opener 时指定了 HTTPCookieProcessor 做为 handler,因此这个 handler 支持 Cookie。
- 使用 isntall_opener 后,调用 urlopen 时会使用这个 opener。
- 如果不需要保存 Cookie,cj 这个参数可以省略。
- user_data 存放的就是登录所需要的信息,在登录论坛的时候把这个信息传递过去就行了。
- urlencode 功能是把字典 user_data 编码成"?pwuser=username&pwpwd=password"的形式,这样做是为了使程序易读一些。
最后一个问题是,pwuser、pwpwd 这类的名字是从哪儿来的,这就要分析需要登录的网页了。我们知道,一般的登录界面都是一个表单,节选如下:
<form action="login.php?" method="post" name="login" onSubmit="this.submit.disabled = true;">
<input type="hidden" value="" name="forward" />
<input type="hidden" value="http://bbs.qzzn.com/index.php" name="jumpurl" />
<input type="hidden" value="2" name="step" />
...
<td width="20%" onclick="document.login.pwuser.focus();"><input type="radio" name="lgt" value="0" checked />用户名 <input type="radio" name="lgt" value="1" />UID</td>
<td><input class="input" type="text" maxLength="20" name="pwuser" size="40" tabindex="1" /> <a href="reg1ster.php">马上注册</a></td>
<td>密 码</td>
<td><input class="input" type="password" maxLength="20" name="pwpwd" size="40" tabindex="2" /> <a href="sendpwd.php" target="_blank">找回密码</a></td>
...
</form>
从这里可以看出,我们需要输入的用户名密码对应的就是 pwuser 和 pwpwd,而 step 对应的则是登录(这个是尝试出来的)。
注意到,这个论坛表单采用的是 post 方式,如果是 get 方式则本文的方法就需要变动一下,不能直接 open,而是应该首先 Request,然后再 open。更详细的请看手册...
有一篇文章供参考:解决在 Python 中登录网站的问题。
2009-03-09Up:在 Python 3 下可以参考:Python 3.0 中的编码和字符串。