将文本格式的数据读入Python Pandas数据框中

3

我在Windows上运行Python 2.7。

我有一个大文本文件(2 GB),其中涉及50万多封电子邮件。该文件没有明确的文件类型,格式如下:

email_message#: 1
email_message_sent: 10/10/1991 02:31:01
From: tomf@abc.com| Tom Foo |abc company|
To: adee@abc.com| Alex Dee |abc company|
To: benfor12@xyz.com| Ben For |xyz company|
email_message#: 2
email_message_sent: 10/12/1991 01:28:12
From: timt@abc.com| Tim Tee |abc company|
To: tomf@abc.com| Tom Foo |abc company|
To: adee@abc.com| Alex Dee |abc company|
To: benfor12@xyz.com| Ben For|xyz company|
email_message#: 3
email_message_sent: 10/13/1991 12:01:16
From: benfor12@xyz.com| Ben For |xyz company|
To: tomfoo@abc.com| Tom Foo |abc company|
To: t212@123.com| Tatiana Xocarsky |numbers firm |
...

如您所见,每个电子邮件都与以下数据相关:

1) 发送时间

2) 发件人电子邮件地址

3) 发件人姓名

4) 发件人所在公司

5) 所有收件人电子邮件地址

6) 每个收件人的姓名

7) 每个收件人所在公司

文本文件中有500K+封电子邮件,每封邮件最多可有16K个收件人。这些电子邮件中关于人名或工作公司的称呼没有规律可循。

我想使用python操作这个大文件,并将其转换为Pandas Dataframe。我希望pandas dataframe的格式类似于下面的excel截图:

Sample Data Structure

编辑

我的解决方案是编写一个“解析器”,它会读取这个文本文件并逐行读入,将每行中的文本分配到 pandas dataframe 的特定列中。

我计划编写类似下面的代码。有人能确认这是执行此操作的正确方法吗?我想确保我没有错过任何内置的 pandas 函数或来自不同 module 的函数。

#connect to object 
data = open('.../Emails', 'r')

#build empty dataframe
import pandas as pd
df = pd.DataFrame()

#function to read lines of the object and put pieces of text into the
# correct column of the dataframe
for line in data:
     n = data.readline()
    if n.startswith("email_message#:"):
        #put a slice of the text into a dataframe
    elif n.startswith("email_message_sent:"):
        #put a slice of the text into a dataframe
    elif n.startswith("From:"):
        #put slices of the text into a dataframe
    elif n.startswith("To:"):
        #put slices of the text into a dataframe

@谁曾经对这个问题进行了负面评价。你能简要解释一下为什么会这样吗?即使是在这个问题上给我指点方向,我也会非常感激。 - BeeGee
抱歉 @BeeGee,我在做的时候分心了。如果能知道你的努力和实际困难的情况会更有帮助。否则唯一可能的答案就是为你编写解析器,我认为这不是重点。除此之外,问题很清晰、写得很好,我愿意提供帮助。 - Stop harming Monica
@Goyo感谢您帮助解答我的问题。我将会编写自己的解析器,并且如果确实没有模块可用的话,我会发布我的解决方案。 - BeeGee
不,如果你正在寻找一个一行代码完成该任务的方法,我认为至少在Python标准库或Pandas中是没有这样的方法的。 - Stop harming Monica
2个回答

1
我不知道最好的做法。你肯定没有忽略一个明显的一行代码,这可能会让你放心。
看起来你当前的解析器(称为my_parse)执行了所有处理。伪代码如下:
finished_df = my_parse(original_text_file)

然而,对于如此大的文件,这有点像使用镊子清理飓风后的残骸。一个两阶段的解决方案可能更快,您可以先将文件粗略地削减到想要的结构,然后使用Pandas系列操作来完善其余部分。继续伪代码,您可以执行以下操作:
rough_df = rough_parse(original_text_file)
finished_df = refine(rough_df)

rough_parse 使用 Python 标准库,而 refine 使用 pandas 系列操作,特别是 Series.str 方法

我建议 rough_parse 的主要目标只是实现一封电子邮件对应一行的结构。因此,您需要查找所有换行符并用某种唯一分隔符替换它们,该分隔符在文件中没有其他位置出现,例如 "$%$%$",除非换行符后面的下一个字符是 "email_message#:"

然后,使用 Series.str 可以很好地处理其余字符串。


1
我忍不住想尝试一下,这是我的方法。
from __future__ import unicode_literals

import io

import pandas as pd
from pandas.compat import string_types


def iter_fields(buf):
    for l in buf:
        yield l.rstrip('\n\r').split(':', 1)


def iter_messages(buf):
    it = iter_fields(buf)
    k, v = next(it)
    while True:
        n = int(v)
        _, v = next(it)
        date = pd.Timestamp(v)
        _, v = next(it)
        from_add, from_name, from_comp = v.split('|')[:-1]
        k, v = next(it)
        to = []
        while k == 'To':
            to_add, to_name, to_comp = v.split('|')[:-1]
            yield (n, date, from_add[1:], from_name[1:-1], from_comp,
                   to_add[1:], to_name[1:-1], to_comp)
            k, v = next(it)

    if not hasattr(filepath_or_buffer, read):
        filepath_or_buffer


def _read_email_headers(buf):
    columns=['email_message#', 'email_message_sent',
             'from_address', 'from_name', 'from_company',
             'to_address', 'to_name', 'to_company']
    return pd.DataFrame(iter_messages(buf), columns=columns)


def read_email_headers(path_or_buf):
    close_buf = False
    if isinstance(path_or_buf, string_types):
        path_or_buf = io.open(path_or_buf)
        close_buf = True
    try:
        return _read_email_headers(path_or_buf)
    finally:
        if close_buf:
            path_or_buf.close

这是如何使用它的:

df = read_email_headers('.../data_file')

只需使用文件路径调用它,即可获得数据框。现在,接下来的内容仅供测试目的。在实际生产中,您不会这样处理实际数据。由于我(或随机的StackOverflow读者)没有您的文件副本,因此必须使用字符串进行模拟:
text = '''email_message#: 1
email_message_sent: 10/10/1991 02:31:01
From: tomf@abc.com| Tom Foo |abc company|
To: adee@abc.com| Alex Dee |abc company|
To: benfor12@xyz.com| Ben For |xyz company|
email_message#: 2
email_message_sent: 10/12/1991 01:28:12
From: timt@abc.com| Tim Tee |abc company|
To: tomf@abc.com| Tom Foo |abc company|
To: adee@abc.com| Alex Dee |abc company|
To: benfor12@xyz.com| Ben For|xyz company|'''

然后我可以创建一个类似文件的对象并将其传递给函数:
df = read_email_headers(io.StringIO(text))
print(df.to_string())

   email_message#  email_message_sent  from_address from_name from_company        to_address   to_name   to_company
0               1 1991-10-10 02:31:01  tomf@abc.com   Tom Foo  abc company      adee@abc.com  Alex Dee  abc company
1               1 1991-10-10 02:31:01  tomf@abc.com   Tom Foo  abc company  benfor12@xyz.com   Ben For  xyz company
2               2 1991-10-12 01:28:12  timt@abc.com   Tim Tee  abc company      tomf@abc.com   Tom Foo  abc company
3               2 1991-10-12 01:28:12  timt@abc.com   Tim Tee  abc company      adee@abc.com  Alex Dee  abc company
4               2 1991-10-12 01:28:12  timt@abc.com   Tim Tee  abc company  benfor12@xyz.com    Ben Fo  xyz company

或者,如果我想要使用实际文件工作:
with io.open('test_file.txt', 'w') as f:
    f.write(text)

df = read_email_headers('test_file.txt')
print(df.to_string())  # Same output as before.

但是,再次强调,您不必这样做才能使用该函数处理您的数据。只需使用文件路径调用即可。

我理解这种想法。虽然这个方法适用于我的示例,所以我投了一票给你,但手动设置文本和列不适合我的解决方案进行大规模应用。 - BeeGee
我担心我的观点没有表达清楚。我编辑了我的答案,以展示您应该如何使用它。当然,您不需要将文件内容复制到字符串中进行测试。在之前的版本中,您必须打开文件并将文件对象传递给函数。现在我编写了一个函数来为您完成这个过程,因此您只需传递文件名即可。 - Stop harming Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接