第三章, 邮箱:老当益壮

这一章介绍一些具体的工具和技术来分析邮件--Internet上一类典型的数据,尽管社交网络的好处还需要几年才能显现--来如下的问题:

  • 谁发送的邮件最多
  • 存在一个在一天中的特定时间(或一周中的某一天)发送者最可能得到对问题的回复吗?
  • 两个人之间谁发的信息最多?
  • 线上讨论都涉及哪些话题?
虽然社交媒体网络赢得了上P级的近乎实时的社交数据,但这里仍存在重大的缺点,不像邮件,社交网络数据是由服务提供者来管理的,服务提供者来规定你可以做哪些事不能做哪些事。而邮件数据则是分散的,它以丰富的邮件列表形式分散在网络中。虽然,服务提供者,如Google, Yahoo!会限制你的邮件列表如果你通过这个服务找回它们,但这里有一个不那么困难的办法来挖掘你的数据而有一个较高的成功的可能性:你可以去订阅一个邮件列表,等到邮箱有足够多的邮件就收集到了你要的数据,或是请邮件列表所有者提供给你一个包。另外令人感兴趣的一点就是,不像社交网站,企业可以完全控制他们的邮箱,他们可以做集中的分析,或是辨别某种趋势。

你可能已经猜到,它是不容易的去找真实的数据集来做示范,但是幸运的是,这一章有一些真实的数据:public Enron data set.


mbox: 快速的但糟糕的Unix邮箱

如果你以前没有接触过邮件,它只是一些连续邮件信息的文本文件,可以很容易的通过文本工具来访问。每个信息的起始是以“From_"为开头的一行,它的样式如“From user@example.com Fri Dec 25 00:06:42 2009“,它的时间戳是asctime,一个标准的定长时间戳形式。

在邮箱文件中,信息之间通过两个空行后接一个"From“开头的行(首个邮件信息除外)来区分。示例3-1是一个虚构的邮箱文件的小片段,它包含两个邮件信息。

示例3-1,一个示例邮箱文件的小片段

rom santa@northpole.example.org Fri Dec 25 00:06:42 2009
Message-ID: <16159836.1075855377439@mail.northpole.example.org>
References: <88364590.8837464573838@mail.northpole.example.org>
In-Reply-To: <194756537.0293874783209@mail.northpole.example.org>
Date: Fri, 25 Dec 2001 00:06:42 -0000 (GMT)
From: St. Nick <santa@northpole.example.org>
To: rudolph@northpole.example.org
Subject: RE: FWD: Tonight
Mime-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Sounds good. See you at the usual location.
Thanks,
-S
-----Original Message-----
From:
Rudolph
Sent:
Friday, December 25, 2009 12:04 AM
To: Claus, Santa
Subject:
FWD: Tonight
Santa -
Running a bit late. Will come grab you shortly. Standby.
Rudy
Begin forwarded message:
> Last batch of toys was just loaded onto sleigh.

>
>
>
>
>
>
>
>
>
>
>
>
Please proceed per the norm.
Regards,
Buddy
--
Buddy the Elf
Chief Elf
Workshop Operations
North Pole
buddy.the.elf@northpole.example.org
From buddy.the.elf@northpole.example.org Fri Dec 25 00:03:34 2009
Message-ID: <88364590.8837464573838@mail.northpole.example.org>
Date: Fri, 25 Dec 2001 00:03:34 -0000 (GMT)
From: Buddy <buddy.the.elf@northpole.example.org>
To: workshop@northpole.example.org
Subject: Tonight
Mime-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Last batch of toys was just loaded onto sleigh.
Please proceed per the norm.
Regards,
Buddy
--
Buddy the Elf
Chief Elf
Workshop Operations
North Pole
buddy.the.elf@northpole.example.org

示例3-1中有两条信息,虽然有证据表明这里至少存在另一条信息。按时间排序,第一条信息的作者是一个名叫Buddy的人,这条信息被发送到workshop@northpole.example.org, 以通知玩具已经被装载,另外一条信息是Santa发送给Rudolph的。另外一条没有显示的信息是Rudolph转发Buddy的信息给Santa的,并注明她会迟到。虽然我们能推断出这些信息通过读这信息文本,我们也有很重要的线索通过Message-ID, References, 和In-Reply-To的头信息。这些头信息很直接,使得可以用算法来显示事情的本质。稍后我们将看一个算法用这些字段来串连信息,但是要点是每一条信息有一个唯一的ID,在回复时包含一个对回复信息的引用,且可能包含多个引用在大型的讨论时。

这些头信息绝对是很重要的,即使是在这个简单的示例中,你已经看到事情是如何变得复杂的当你在解析信息的主体时。Rudolph的信息中引用了转发的信息以>字符开头,但是Santa的回复没有引用任何东西,而是包含一个易于阅读的头部。试图从会话中解析出信息流是一个很麻烦的工作,虽然不是绝对不可能,因为牵涉到模糊性。大部分邮箱客户端包含一个选项,显示扩展的邮件头除了你一般能看到的外,如果你对这一技术感兴趣这是一个方便的方式而不用深入的分析原始邮件数据。示例3-2说明了示例3-1的信息流,图3-1展示了一个示例邮件头被Apple Mail显示的。


图3-1, 大部分的邮箱允许你通过一个选项查看扩展的邮件头

Fri, 25 Dec 2001 00:03:34 -0000 (GMT) - Buddy sends a message to the workshop
Friday, December 25, 2009 12:04 AM
- Rudolph forwards Buddy's message to Santa
with an additional note
Fri, 25 Dec 2001 00:06:42 -0000 (GMT) - Santa replies to Rudolph

对于我们来说,幸运的是这里有许多你可以做的而不用根本上从头开始去实现一个邮箱。另外,如果你要做的只是浏览邮件,你可以将它导到一个邮箱客户端来浏览它,对吧?虽然它是一个死记硬背的操作,但是它是值得花一点时间来检察你的邮箱是否有导入导出mbox邮件格式的文件的功能,以便你能切分邮箱文件。
Python提供了一些基本的工具去解析邮件数据,示例3-3提供了一种基本的技术将邮件数据转化为json数组。

示例3-3,转化邮件数据为方便的json格式(mailboxes__jsonify_mbox.py

# -*- coding: utf-8 -*-

import sys
import mailbox
import email
import quopri
from BeautifulSoup import BeautifulSoup

try:
    import jsonlib2 as json  # much faster then Python 2.6.x's stdlib
except ImportError:
    import json

MBOX = sys.argv[1]

def cleanContent(msg):

    # Decode message from "quoted printable" format

    msg = quopri.decodestring(msg)

    # Strip out HTML tags, if any are present

    soup = BeautifulSoup(msg)
    return ''.join(soup.findAll(text=True))


def jsonifyMessage(msg):
    json_msg = {'parts': []}
    for (k, v) in msg.items():
        json_msg[k] = v.decode('utf-8', 'ignore')

    # The To, CC, and Bcc fields, if present, could have multiple items
    # Note that not all of these fields are necessarily defined

    for k in ['To', 'Cc', 'Bcc']:
        if not json_msg.get(k):
            continue
        json_msg[k] = json_msg[k].replace('\n', '').replace('\t', '').replace('\r'
                , '').replace(' ', '').decode('utf-8', 'ignore').split(',')

    try:
        for part in msg.walk():
            json_part = {}
            if part.get_content_maintype() == 'multipart':
                continue
            json_part['contentType'] = part.get_content_type()
            content = part.get_payload(decode=False).decode('utf-8', 'ignore')
            json_part['content'] = cleanContent(content)

            json_msg['parts'].append(json_part)
    except Exception, e:
        sys.stderr.write('Skipping message - error encountered (%s)' % (str(e), ))
    finally:
        return json_msg

# Note: opening in binary mode is recommended
mbox = mailbox.UnixMailbox(open(MBOX, 'rb'), email.message_from_file)  
json_msgs = []
while 1:
    msg = mbox.next()
    if msg is None:
        break
    json_msgs.append(jsonifyMessage(msg))

print json.dumps(json_msgs, indent=4)


这个简短的脚本是一个相当好的工作从邮件中解析相关的信息并建立轻便json对象。我们有更多可以做到的,但它解决了一些常见的问题,包括一个原始的机制去解码引用的文本,剔除HTML标签。quopri被用来处理引用格式,并且有编码将7位通道转换为8位的内容(见http://en.wikipedia.org/wiki/Quoted-printable)。示例3-4是示例3-3的简单输出。

示例3-4,json格式的简单输出,用示例3-3的脚本从示例3-1的示例邮件产生的。


有了新的技能去解析邮件数据到一个方便的格式,去分析它的动力是很自然的。余下的这一章节将用公开的Enron邮件数据,可以下载mbox格式的文件。enron.mbox.gz文件是mbox结构的数据从Enron原始收件箱文档中导出的,而enron.mbox.json.gz文件是同样的数据转化为json格式用上面示例中的脚本。虽然没有作为一个示例展示,你可以下载脚本mailboxes__convert_enron_inbox_to_mbox.py来执行从Enron的原始数据到mbox格式的数据的转化。


mbox + CouchDB = 轻松的邮件分析

用正确的工具来做这个工作能极大的简单分析的工作量,一个最明显的方法去处理结构化数据可能就是花一点时间来建立先验的schema,将数据导入,当发现我忘记了某些东西时修改这个schema,然后多次重复以上步骤,这肯定不是一个轻松的工作。已知以文件为中心的邮件信息和json数据结构的关系,我们将mbox数据导入到CouchDB中,它是一个面向文件的数据库,且它提供了map/reduce能力,非常容易建立索引,且能执行集中的统计频率来回答这些问题:“多少邮件被发送,诸如此类”,“在某一个日期多个信息被发送?”

另外一个好处用CouchDB来做文件分析任务是它提供了基于REST的接口,你可以把它集成到任何以web为基础的架构中,且它提供了很易于备份的能力,你可以允许别人来复制你的数据库(或是分析)。

余下的章节假定你可以由二进制文件安装CouchDB,或是通过源码编译安装。你可能想看一下CouchOne--CouchDB的创建者和主要贡献者建立的公司,它提供大部分平台的二进制文件下载,增加了对CouchDB一些托管选项可能对你很有用。Cloudant是另外一个线上托管选择你可能想要的。

随着你开始进入状态,你可能认为CouchDB是一个key-value的数据库,key是任意的标识,而value是json格式的文档。图3-2示范了文档的集合,以及一个单独的文档在这个集合中的通过Futon, CouchDB的基于网页的管理接口。你可以进入Futon在你本地通过http://localhost:5984/_utils/。


图3-2, CouchDB的集成视图以及其中一个单独的文档

一旦CouchDB被安装,另外一个管理者的任务你需要做的就是安装python的客户端模块CouchDB用easy_install couchdb.你可以读更多的文档通过pydoc 或是在线文档 http://packages.python.org/CouchDB/。当CouchDB和python的客户端被安装后,我们可以轻松一下来写一些代码将json格式的mbox数据导入到CouchDB中了--但是请随意的花点时间在Futon上如果这是你第一次用CouchDB,如果你觉得耳听为虚,你可以花点时间浏览一下CouchDB的前面几章:The Definitive Guide (O’Reilly), 它可以从网上得到: http://books.couchdb.org/relax.


批量导入文档到CouchDB中

在未压缩的enron.mail文件上跑示例3-3的脚本,将输出重定向到一个文件,得到很大json结构的数据(约200M)。示例3-5示范了怎么将数据导入到的CouchDB用couchdb-python,你应该已经用easy_install couchdb安装了它。它将花几分钟时间如果你正在使用如笔记本这样的设备。虽然看起来有些慢,但你考虑一下json数据结构中包含了40000个对象,它们被写入到CouchDB中,约300个文档每秒,这是相当好的性能了在一般的笔记本上。用性能监视器如top在*nix系统上,或是Windows上的任务管理器,你会发现你的一个CPU正累的要死,它意味正全速运转。在开发时,只用其中一部分数据是一个好主意。你可以很容易的分出一打或差不多的来跑下面的示例。

示例3-5,一个简单的示例示范将数据导入到CouchDB中(mailboxes__load_json_mbox.py)

# -*- coding: utf-8 -*-

import sys
import os
import couchdb
try:
    import jsonlib2 as json
except ImportError:
    import json

JSON_MBOX = sys.argv[1]  # i.e. enron.mbox.json
DB = os.path.basename(JSON_MBOX).split('.')[0]

server = couchdb.Server('http://localhost:5984')
db = server.create(DB)
docs = json.loads(open(JSON_MBOX).read())
db.update(docs, all_or_nothing=True)


随着数据被导入,可能是时候稍微休息一会,但当从Futon查看这些数据时,引起我们思考一些问题,谁正在交流,频率是怎么样,在什么时候。让我们使CouchDB的map/reduce可以工作以便来回其中一些问题。

在map过程中,它获得文档集合,将其中每一个转化为新的key/value, 然而reduce过程,获得文档集合,将它们以某种方式减少。例如,计算平方和:f(x) = x1^2 + x2^2 + ... + xn^2, 可以表示为一个map过程对每个值取平方,而reducer只是简单将mapping的结果想加而减少为一个值。这种编程方式很容易去并行处理问题,当然不是所有问题。用CouchDB来处理问题要假设手边的任务适合这种计算模式。


合理的排序


或许不能立马看出,但你仔细的看一下图3-2,你会发现导入到CouchDB中的文档是按关键字排序的。默认的,关键字是特殊的字段_id由CouchDB自己自动指定的,因此这种排序是没什么用处的。为了以更合理的方式在Futon中浏览数据,以及执行有效的范围查询,我们想要执行一个mapping操作来按别的某种顺序排序。按时间排序看起来是个好主意,且打开了与时间相关的分析的大门,那么让我们从这种开始看看会发生什么吧。但是首先,我们要做一点配置的改变,使得我们能用python写map/reducer程序来执行这个任务。

CouchDB为此做了特别的规划,它是用Erlang写的,一种语言设计来应付高并发和容错。事实上,用来查询和通过map/reducer来转化数据的语言是JavaScript.注意到这,我们当然可以用JavaScript来写map/reducer,且会有一些好处由CouchDB提供的,如_sum, _count, _stat.但是从开发环境中的语法检查和高亮显示得到的好处可能会更多。另外,假设你的couchdb是安装的,一点配置的改变就能使用python的视图服务,以便你可以写CouchDB的代码在python中,更精确的来说,用CouchDB的语言,map/reducer被称作视图函数在一个特殊的文档被称设计文档

简单的将下面一行插入到CouchDB的配置文件local.ini的合适位置,其中的couchpy可执行文件是一个视图服务和couchdb模块一起安装的。执行以上操作后需要重启CouchDB.

[query_servers] python = /path/to/couchpy

如果你想写代码解析各种格式的日期到标准的格式,你也需要安装easy_install dateutil. 它是一个为你节约时间的工具包,因此你不必考虑将会碰到哪种日期格式在大量混乱的邮件文档中。示例3-6示范了用脚本来mapping文档按它们的时间戳,你应该可以用已经导入的数据来跑这个脚本在你为python配置了CouchDB后。示例的输出是满足查询条件的文档列表,为了简洁省略了它们。

示例3-6,一个简单的mapper去map文档按它们的时间戳(mailboxes__map_json_mbox_by_date_time.py

# -*- coding: utf-8 -*-

import sys
import couchdb
from couchdb.design import ViewDefinition
try:
    import jsonlib2 as json
except ImportError:
    import json

DB = sys.argv[1]
START_DATE = sys.argv[2] #YYYY-MM-DD
END_DATE = sys.argv[3]   #YYYY-MM-DD

server = couchdb.Server('http://localhost:5984')
db = server[DB]

def dateTimeToDocMapper(doc):

    # Note that you need to include imports used by your mapper 
    # inside the function definition

    from dateutil.parser import parse
    from datetime import datetime as dt
    if doc.get('Date'):
        # [year, month, day, hour, min, sec]
        _date = list(dt.timetuple(parse(doc['Date']))[:-3])  
        yield (_date, doc)

# Specify an index to back the query. Note that the index won't be 
# created until the first time the query is run

view = ViewDefinition('index', 'by_date_time', dateTimeToDocMapper,
                      language='python')
view.sync(db)

# Now query, by slicing over items sorted by date

start = [int(i) for i in START_DATE.split("-")]
end = [int(i) for i in END_DATE.split("-")]
print 'Finding docs dated from %s-%s-%s to %s-%s-%s' % tuple(start + end)

docs = []
for row in db.view('index/by_date_time', startkey=start, endkey=end):
    docs.append(db.get(row.id))
print json.dumps(docs, indent=4)



理解以上代码最基础的是函数dateTimeToDocMapper,它是一个自定义的generator.它接收一个文档作为参数,返回同样的文档,但该文档以一个方便的日期值作为关键字,它易于维护和排序。注意mapping函数在CouchDB中是无副作用的,不管mapping函数中做了什么处理,也不管它返回什么,都不会改变文档。按CouchDB的话来讲, dateTimeToDocMapper是一个视图名称为by_date_time, 它是 design document的索引的一部分。看一下Futon,你可以将右上角的"all document"改为"design document"来自己确认一下,如果3-3所示,你也可以用pydoc来查看ViewDefinition。

第一次执行以上代码会发大约5分钟来运行mapping过程,你的一个CPU将会满负荷运行在这个过程中的大部分时间。其中80%的时间贡献给了建立索引,而其它的时间用在查询了。然而,索引的建立只需要执行一次,余下的查询是相当高效的,花大约20秒返回了2200个文档在给定的时间范围内,每秒钟约110个文档。

mapping函数的要点是所有的文档按日期来索引,可以在Futon中验证,通过在右上角的组合框中选择by_date_time的索引的视图,如图3-4.可以按某种标准来排序文档不是世界上最有趣的事,但它是必要的一步在某一些集成的函数中,如计算满足某个标准的文档的数量在文集中。下一次示范一些基本的代码来做这类分析。


图3-3,观察design document 在Futon中



图3-4,按时间排序的文档

Map/Reducer--绝佳的频率分析工具

列表的频率分析可能是你第一个要探索的任务当碰到新的数据集的时间,因为它可以让你花较少的时间得到较多的东西。这一节研究一些方法来用CouchDB建立频率索引。


频率按日期/时间范围

我们的by_date_time索引做的很好将文档按时间排序,但它不是很有用在计算文档的频率按年,月,星期等上传的时间。然而我们可以写客户端的代码来计算频率通过调用db.view('index/by_date_time', startkey=start, endkey=end),它将是很有效的让CouchDB为我们做这个工作。所有需要做的就是一点对mapping函数的改变,以及介绍一点reducer函数。mapping函数将简单的返回1对于每个时间戳,reducer函数将计算相同的关键字的数量。示例3-7阐述了这种方法。注意你需要easy_install prettytable,一个包产生漂亮的表格,在跑下面的示例之前。

示例3-7,用一个mapper和一个reducer函数来计算文档的数据按时间(mailboxes__count_json_mbox_by_date_time.py)。

# -*- coding: utf-8 -*-

import sys
import couchdb
from couchdb.design import ViewDefinition
from prettytable import PrettyTable

DB = sys.argv[1]

server = couchdb.Server('http://localhost:5984')
db = server[DB]

def dateTimeCountMapper(doc):
    from dateutil.parser import parse
    from datetime import datetime as dt
    if doc.get('Date'):
        _date = list(dt.timetuple(parse(doc['Date']))[:-3])
        yield (_date, 1)


def summingReducer(keys, values, rereduce):
    return sum(values)


view = ViewDefinition('index', 'doc_count_by_date_time', dateTimeCountMapper,
                      reduce_fun=summingReducer, language='python')
view.sync(db)

# Print out message counts by time slice such that they're
# grouped by year, month, day

fields = ['Date', 'Count']
pt = PrettyTable(fields=fields)
[pt.set_field_align(f, 'l') for f in fields]

for row in db.view('index/doc_count_by_date_time', group_level=3):  
    pt.add_row(['-'.join([str(i) for i in row.key]), row.value])

pt.printt()


总结一下,我们创建了一个视图叫做doc_count_by_date_time存储在索引design document中; 它包含一个mapper函数返回一个日期关键字对每一个文档,一个reducer函数对所有输入计算其和。缺失的一环你正在寻找的可能就是summingReducer是怎么工作的。在上面的例子中,reducer计算所有相同关键字的文档的数目和。CouchDB的reducer函数的工作原理简单的来说就是传入对应的关键字列表和值列表,一个自定义的函数来执行集成操作(如求和,在以上例子中)。rereducer参数是一个装置来使增加map/reducer成为可能通过处理reducer的结果还需要再reduce的情况。这个例子中没有特别需要rereducer的情况,如果需要它可以去读更多的关于rereducer在“关于rereduce的笔记“在128页,或是 网上资料

group_level参数允许我们执行按照时间的各种粒度来进行频率统计,CouchDB用这个参数来切隔关键字为前N个组成部分,自动的执行reduce操作在匹配的key/value对上。在我们例子中,意味着计算匹配前N个部分的关键字的和。如果reducer被传入整个的日期作为关键字,就相当于查询在同一时间发送的邮件的数量,这可能不是很有用。传入关键字的第一部分则计算的是每一年发送的邮件数量,传入的是关键字的前两部分则计算的是每一个月发送的邮件的数量,以此类推。关键字参数group_level是你可以传给db.view函数的,用来控制CouchDB的关键字的哪一部分传给reducer.在执行了这个代码得到如下索引后,你可以在Futon中探索这个功能,如图3-5。


图3-5,你可以探索group_by选项通过Futon

花在dateTimeCountMapper的时间去建立doc_count_by_date_time索引等同于上一个示例花在这上面的时间,它还可以跑一些有用的查询通过改变group_level参数。记住这里还可以有许多的改变,如将关键字变为以毫秒为单位的时间,在某些情况也可能很有用的,你需要在mapper函数中返回毫秒为单位的时间,同时改变start_key和end_key为这样的时间,而不需要reducer函数。

如果你要做一些不依赖于时间单位天的分析该怎么办呢,如统计每天某一小时发送的邮件数。视图分组查询得益于数据库的B树结构,因此你不能直接的对关键中的随意元素进行分组,如python所做的那样,k[4:-2], 或k[4:5].对于这种查询,你可能要去组成一个新的关键字以合适的前缀,如[小时,分钟,秒],在客户端过滤要求遍历整个文档集合而没有因增加了索引对接下来的查询带来好处。


频率按发送者/接收者字段统计

其它的度量,如某个人发送了多少邮件,某一群人直接进行了多少次交流,是非常重要的统计在邮件分析中。示例3-8示范了如何计算两个人的交流次数按邮件中的To_和From_字段,很明显,Cc和Bc字段包含了特殊的值,信赖于你问的问题是什么。这个示例和3-6的示例最大的不同是在mapper函数中返回了key/value对,你会发现示范的样式被很小的修改就能做很多种的集成操作。

示例3-8, mapping和reducing按发送者和接收者(mailboxes__count_json_mbox_by_sender_recipient.py

# -*- coding: utf-8 -*-

import sys
import couchdb
from couchdb.design import ViewDefinition
from prettytable import PrettyTable

DB = sys.argv[1]

server = couchdb.Server('http://localhost:5984')
db = server[DB]

def senderRecipientCountMapper(doc):
    if doc.get('From') and doc.get('To'):
        for recipient in doc['To']:
            yield ([doc['From'], recipient], 1)


def summingReducer(keys, values, rereduce):
    return sum(values)


view = ViewDefinition('index', 'doc_count_by_sender_recipient',
                      senderRecipientCountMapper, reduce_fun=summingReducer,
                      language='python')
view.sync(db)

# print out a nicely formatted table
fields = ['Sender', 'Recipient', 'Count']
pt = PrettyTable(fields=fields)
[pt.set_field_align(f, 'l') for f in fields]

for row in db.view('index/doc_count_by_sender_recipient', group=True):
    pt.add_row([row.key[0], row.key[1], row.value])

pt.printt()


按值排序文档

CouchDB按关键字的持久化排序在很多情况下是很有用的,但这里也有些其它情况你想要按值排序。作为这个想法下的例子,将示例3-7的结果按频率排序,这样就能得到前N个结果。对于小的数据集,较好的方法就是自己去执行一个客户端排序。几乎所有的编程语言都实现了快速排序算法,它在平均情况有很好的表现。

但是对于大的数据集或更加苛刻的情况,你可能想要选择另外一种。一种方法就是转移reduce后的数据(如图3-5),将它们导入到另一个数据库,这样它就能按关键字自动排序了。虽然这种方法有些绕弯子了,但它实现起来不费力,且效率表现得很好当不同的数据库映射到不同的CPU对于大的文档集。示例3-9示范了这种方法。

示例3-9,按关键字排序通过置换的mapping,且导入到另一个数据库

# -*- coding: utf-8 -*-

import sys
import couchdb
from couchdb.design import ViewDefinition
from prettytable import PrettyTable

DB = sys.argv[1]

server = couchdb.Server('http://localhost:5984')
db = server[DB]

# Query out the documents at a given group level of interest
# Group by year, month, day

docs = db.view('index/doc_count_by_date_time', group_level=3)

# Now, load the documents keyed by [year, month, day] into a new database

db_scratch = server.create(DB + '-num-per-day')
db_scratch.update(docs)


def transposeMapper(doc):
    yield (doc['value'], doc['key'])


view = ViewDefinition('index', 'num_per_day', transposeMapper, language='python')
view.sync(db_scratch)

fields = ['Date', 'Count']
pt = PrettyTable(fields=fields)
[pt.set_field_align(f, 'l') for f in fields]

for row in db_scratch.view('index/num_per_day'):
    if row.key > 10:  # display stats where more than 10 messages were sent
        pt.add_row(['-'.join([str(i) for i in row.value]), row.key])

虽然在客户端排序后导入到数据库是较直接的方法对于相对简单的情况,这里存在一个工业强度的解决方案,它基于Lucene能够解决各种类型和数据量的索引。下一节将研究怎样用couchdb-lucene来做基于文本的索引--Lucene的面包和黄油(是一句谚语吧),当然也有其它的可能性,如按值排序文档,索引地理位置信息等等,也在可触及的范围内。


couchdb-lucene全文索引及其它

Lucene是一个java搜索引擎库, 提供高性能全文索引;它最常用的地方是将关键字搜索能力集成到应用中。couchdb-lucene实质上是一个网络服务包装器包括一些lucene的核心功能,使得它可以索引CouchDB的文档。couchdb-lucene在一个单独的虚拟机进程中运行,通过HTTP与CouchDB通信,因此你可以完成在另一台机器上运行如果你有可用的硬件的话。

即使是浮光掠影的介绍一个Lucene和couchdb-lucene也会远离正题,但是我们会做一个简单的示例来示范怎么搭建和跑这些工具,由此你将认识到在手边的任务中建立全文索引是很有必要的。如果全索引不是你现在需要的,那很可以跳过这一节,以后再来看它。作为比较,它当然也是可能的去用一个简单的mapping函数来返回关键字和文档对来执行全文索引,如示例3-10。

示例3-10,一个mapper来对文档分词

def tokenizingMapper(doc):
    tokens = doc.split()
    for token in tokens:
        if isInteresting(token): # Filter out stop words, etc.
             yield token, doc
但是很快你会发现你要做更多的功课关于信息检索的概念如果你想要建立一个好的打分功能来排列文档按相关性或其它超出频率分析的。然而Lucene的好处是很多的,它是一个很好的选择用couchdb-lucene而不是自己写mapping函数来建立全文索引。

二进制快照和安装couchdb-lucene的介绍在http://github.com/rnewson/couchdb-lucene,必要的配置细节在README文件中,但是它很简短,在java已经安装的情况下,执行couchdb-lucene的run脚本,它将建立一个网络服务,要求对CouchDB的配置文件local.ini做一点小的改变,使得couchdb-lucene能与CouchDB通过HTTP通信。关键的配置改变在couchdb-lucene的README文件中,如示例3-11。

示例3-11,配置CouchDB使得couchdb-lucene可用

[couchdb]
os_process_timeout=300000 ; increase the timeout from 5 seconds.
[external]
fti=/path/to/python /path/to/couchdb-lucene/tools/couchdb-external-hook.py
[httpd_db_handlers]
_fti = {couch_httpd_external, handle_external_req, <<"fti">>}

简单来说,我们将超时时间增加到5分钟,定义到一个自定义行为叫做fti(全文索引),它调用couchdb-external-hook.py脚本中的功能当数据库中_fti上下文被调用时。这个脚本同在JVM中跑的lucene通信提供全文索引。细节很有意思,但你不必为它停顿下来除非你想这么做。

一旦couchdb-lucene建立起来且正在运行,请尝试执行示例3-12的脚本,它执行默认的索引对Subject字段和在数据库中的文档的内容。记住,有了对邮件信息的主题和内容基于文本的索引,你可以用一般的Lucene查询语言进行细粒度的搜索,然而,默认的关键字搜索经常是很重要的。

示例3-12,用couchdb-lucene来建立全文索引

# -*- coding: utf-8 -*-

import sys
import httplib
from urllib import quote
import json

DB = sys.argv[1]
QUERY = sys.argv[2]

#  The body of a JavaScript-based design document we'll create

dd = \
    {'fulltext': {'by_subject': {'index': '''function(doc) { 
                            var ret=new Document(); 
                            ret.add(doc.Subject); 
                            return ret 
                        }'''},
     'by_content': {'index': '''function(doc) { 
                            var ret=new Document(); 
                            for (var i=0; i < doc.parts.length; i++) {
                                ret.add(doc.parts[i].content); 
                            }
                            return ret 
                        }'''}}}

#  Create a design document that'll be identified as "_design/lucene"
#  The equivalent of the following in a terminal:
#  $ curl -X PUT http://localhost:5984/DB/_design/lucene -d @dd.json

try:
    conn = httplib.HTTPConnection('localhost', 5984)
    conn.request('PUT', '/%s/_design/lucene' % (DB, ), json.dumps(dd))
    response = conn.getresponse()
finally:
    conn.close()

if response.status != 201:  #  Created
    print 'Unable to create design document: %s %s' % (response.status,
            response.reason)
    sys.exit()

#  Querying the design document is nearly the same as usual except that you reference
#  couchdb-lucene's _fti HTTP handler
#  $ curl http://localhost:5984/DB/_fti/_design/lucene/by_subject?q=QUERY

try:
    conn.request('GET', '/%s/_fti/_design/lucene/by_subject?q=%s' % (DB,
                 quote(QUERY)))
    response = conn.getresponse()
    if response.status == 200:
        response_body = json.loads(response.read())
        print json.dumps(response_body, indent=4)

你也许想要翻一翻涉及到的couchdb-lucene在线文档,要点就是一个特殊格式的“design document"(字面意思是设计文档,但它是couchdb的术语),它包含一个文本格式的字段,其中是索引名称和基于Javascript的索引函数。这个索引函数返回一个Document对象,其中包含了Lucene索引要用到的字段。注意Document对象是被couchdb-lucene定义的而不是CouchDB当建立索引时。用不常用到的字“raptor"来做的示例查询结果如示例3-13.注意你最好用id的值来直接从CouchDB中查询为了进一步的分析。

示例3-13,"raptor"的查询结果在Enron数据集上

/* Sample query results for
http://localhost:5984/enron/_fti/_design/lucene/by_content?q=raptor */
{ "etag" : "11b7c665b2d78be0",
"fetch_duration" : 2,
"limit" : 25,
"q" : "default:raptor",
"rows" : [ { "id" : "3b2c340c28782c8986737c35a355d0eb",
"score" : 1.4469228982925415
},
{ "id" : "3b2c340c28782c8986737c35a3542677",
"score" : 1.3901585340499878
},
{ "id" : "3b2c340c28782c8986737c35a357c6ae",
"score" : 1.375900149345398
},
/* ... output truncated ... */
{ "id" : "2f84530cb39668ab3cdab83302e56d65",
"score" : 0.8107569217681885
}
],
"search_duration" : 0,
"skip" : 0,
"total_rows" : 72
}
完整的结果在参考文档中,但是有令人感兴趣的是'rows'字段,它包含文档的id,可以从CouchDB中由此id得到该文档。在数据流方面,你应该发现你根本没有直接和couchdb-lucene打交道;






















  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值