【问题标题】:Parsing unstructured text file with Python使用 Python 解析非结构化文本文件
【发布时间】:2015-08-26 09:31:59
【问题描述】:

我有一个文本文件,其中一些 sn-ps 如下所示:

Page 1 of 515                   
Closing Report for Company Name LLC                 

222 N 9th Street, #100 & 200, Las Vegas, NV, 89101                  

File number:    Jackie Grant    Status: Fell Thru   Primary closing party:  Seller
Acceptance: 01/01/2001  Closing date:   11/11/2011  Property type:  Commercial Lease
MLS number: Sale price: $200,000    Commission: $1,500.00   
Notes:  08/15/2000 02:30PM by Roger Lodge This property is a Commercial Lease handled by etc..  

Seller: Company Name LLC                    
Company name:   Company Name LLC                
Address:    222 N 9th Street, #100 & 200, Las Vegas, NV, 89101              
Home:   Pager:              
Business:   Fax:                
Mobile: Email:              
Buyer: Tomlinson, Ladainian                 
Address:    222 N 9th Street, #100 & 200, Las Vegas, NV, 89101              
Home:   Pager:              
Business:   555-555-5555    Fax:            
Mobile: Email:              
Lessee Agent: Blank, Arthur                 
Company name:   Sprockets Inc.              
Address:    5001 Old Man Dr, North Las Vegas, NV, 89002             
Home:   (575) 222-3455  Pager:          
Business:   Fax:    999-9990            
Mobile: (702) 600-3492  Email:  sprockets@yoohoo.com        
Leasing Agent: Van Uytnyck, Chameleon                   
Company name:   Company Name LLC                
Address:                    
Home:   Pager:              
Business:   Fax:    909-222-2223            
Mobile: 595-595-5959    Email:          

(should be 2 spaces here.. this is not in normal text file)


Printed on Friday, June 12, 2015                    
Account owner: Roger Goodell                    
Page 2 of 515                   
Report for Adrian (Allday) Peterson                     

242 N 9th Street, #100 & 200                    

File number:    Soap    Status: Closed/Paid Primary closing party:  Buyer
Acceptance: 01/10/2010  Closing date:   01/10/2010  Property type:  RRR
MLS number: Sale price: $299,000    Commission: 33.00%  

Seller: SOS, Bank                   
Address:    242 N 9th Street, #100 & 200                
Home:   Pager:              
Business:   Fax:                
Mobile: Email:              
Buyer: Sabel, Aaron                 
Address:                    
Home:   Pager:              
Business:   Fax:                
Mobile: Email:  sia@yoohoo.com          
Escrow Co: Schneider, Patty                 
Company name:   National Football League                
Address:    242 N 9th Street, #100 & 200                
Home:   Pager:              
Business:   800-2009    Fax:    800-1100        
Mobile: Email:              
Buyers Agent: Munchak, Mike                 
Company name:   Commission Group                
Address:                    
Home:   Pager:              
Business:   Fax:                
Mobile: 483374-3892 Email:  donation@yoohoo.net     
Listing Agent: Ricci, Christina                 
Company name:   Other Guys              
Address:                    
Home:   Pager:              
Business:   Fax:                
Mobile: 888-333-3333    Email:  general.adama@cylon.net      

这是我的代码:

import re

file = open('file-path.txt','r')

# if there are more than two consecutive blank lines, then we start a new Entry
entries = []
curr = []
prev_blank = False
for line in file:
    line = line.rstrip('\n').strip()
    if (line == ''):
        if prev_blank == True:
            # end of the entry, create append the entry
            if(len(curr) > 0):
                entries.append(curr)
                print curr
                curr = []
                prev_blank = False
        else:
            prev_blank = True
    else:
        prev_blank = False
        # we need to parse the line
        line_list = line.split()
        str = ''
        start = False
        for item in line_list:
            if re.match('[a-zA-Z\s]+:.*',item):
                if len(str) > 0:
                    curr.append(str)
                str = item
                start = True
            elif start == True:
                str = str + ' ' + item

这是输出:

['number: Jackie Grant', 'Status: Fell Thru Primary closing', 'Acceptance: 01/01/2001 Closing', 'date: 11/11/2011 Property', 'number: Sale', 'price: $200,000', 'Home:', 'Business:', 'Mobile:', 'Home:', 'Business: 555-555-5555', 'Mobile:', 'Home: (575) 222-3455', 'Business:', 'Mobile: (702) 600-3492', 'Home:', 'Business:', 'Mobile: 595-595-5959']

我的问题如下:

  1. 首先,应该有 2 条记录作为输出,而我只输出了一条。
  2. 在文本的顶部块中,我的脚本无法知道以前的值在哪里结束,而新的值从哪里开始:“状态:Fell Thru”应该是一个值,“主要结束方:”,“买方 接受:01/10/2010','截止日期:01/10/2010','物业类型:RRR','MLS编号:','售价:$299,000','佣金:33.00%'应该被抓住。
  3. 一旦正确解析,我需要再次解析以将键与值分开(即“截止日期”:01/10/2010),最好是在字典列表中。

除了使用正则表达式来挑选键,然后抓取随后的文本的 sn-ps 之外,我想不出更好的方法。

完成后,我想要一个带有键的标题行的 csv,我可以将其导入到带有 read_csv 的 pandas 中。我在这个上花了好几个小时..

【问题讨论】:

  • 在冒号上拆分?似乎您的主要问题是键可以是一两个词。也许创建一个包含两个单词键的列表,否则键必须是一个单词。并且关键字之前的任何内容都必须是上一条记录的值。
  • 不理解反对票 - 该问题显示了研究、努力、编码的路径,并且布局非常好.. 只需要帮助前进,仅此而已.. thx
  • 您的循环只显示一个条目而不是两个条目的原因是您构建它的方式。它正在循环并找到与您提供的条件匹配的内容,然后停止。它正在做它应该做的事情。你最好遍历文件并分配每个条目(作为一个整体)然后构造一个列表。
  • 为什么不直接使用正则表达式并完成它?
  • @sln 你让它听起来很简单.. 总共有大约 50 个字段名称,其中许多并没有出现在每条记录上.. 如果我能用正则表达式做到这一点,我会.. smtg 建设性的会很高兴帮助我度过难关.. thx

标签: python regex parsing csv pyparsing


【解决方案1】:

(这不是一个完整的答案,但评论太长了)。

  • 字段名称可以有空格(例如MLS number
  • 每行可以出现几个字段(例如Home: Pager:
  • Notes 字段中包含时间,其中包含 :

这意味着您无法通过正则表达式来识别字段名。它不可能知道“MLS”是前一个数据值的一部分还是后一个字段名的一部分。

一些Home: Pager: 行是指卖方,一些是指买方或承租代理人或租赁代理人。这意味着我在下面采用的幼稚的逐行方法也不起作用。

这是我正在处理的代码,它针对您的测试数据运行,但由于上述原因给出了不正确的输出。这是我采用的方法的参考:

replaces = [
    ('Closing Report for', 'Report_for:')
    ,('Report for', 'Report_for:')
    ,('File number', 'File_number')
    ,('Primary closing party', 'Primary_closing_party')
    ,('MLS number', 'MLS_number')
    ,('Sale Price', 'Sale_Price')
    ,('Account owner', 'Account_owner')
    # ...
    # etc.
]

def fix_linemash(data):
    # splits many fields on one line into several lines

    results = []
    mini_collection = []
    for token in data.split(' '):
        if ':' not in token:
            mini_collection.append(token)
        else:
            results.append(' '.join(mini_collection))
            mini_collection = [token]

    return [line for line in results if line]

def process_record(data):
    # takes a collection of lines
    # fixes them, and builds a record dict
    record = {}

    for old, new in replaces:
        data = data.replace(old, new)

    for line in fix_linemash(data):
        print line
        name, value = line.split(':', 1)
        record[name.strip()] = value.strip()

    return record


records = []
collection = []
blank_flag = False

for line in open('d:/lol.txt'):
    # Read through the file collecting lines and
    # looking for double blank lines
    # every pair of blank lines, process the stored ones and reset

    line = line.strip()
    if line.startswith('Page '): continue
    if line.startswith('Printed on '): continue

    if not line and blank_flag:      # record finished
        records.append( process_record(' '.join(collection)) )
        blank_flag = False
        collection = []

    elif not line:  # maybe end of record?
        blank_flag = True

    else:   # false alarm, record continues
        blank_flag = False
        collection.append(line)

for record in records:
    print record

我现在认为对数据进行一些预处理整理步骤会更好:

  1. 删除“Page n of n”和“Printed on ...”行等类似内容
  2. 识别所有有效的字段名称,然后分解组合的行,这意味着每一行只有一个字段,字段从一行的开头开始。
  3. 运行并仅处理卖方/买方/代理块,用识别前缀替换字段名,例如Email: -> Seller Email:.

然后编写一个记录解析器,这应该很容易 - 检查两个空行,在第一个冒号处拆分行,使用左侧位作为字段名称,右侧位作为值。随心所欲地存储(注意,字典键是无序的)。

【讨论】:

  • 这次尝试给了我新的灵感,让我重新回到这个领域——太棒了。我真的很喜欢这个问题的多次通过方法,因为有许多限制 - 你提到的很多 - 这使得它比规范更加困难。确实很努力..
【解决方案2】:

我想通过点击“Page”这个词来开始新记录会更容易。

只是分享一点我自己的经验——写一个通用的解析器太难了。

鉴于这里的数据,情况并没有那么糟糕。不要使用简单的列表来存储条目,而是使用对象。将所有其他字段作为属性/值添加到对象中。

【讨论】: