在pandas读取csv文件之前预处理数据文件

4

我使用SAP输出的数据进行工作,但它既不是CSV格式(因为它不会引用包含其分隔符的字符串),也不是固定宽度格式(因为它具有多字节字符)。它可以说是一种“逐个字符”的“固定宽度”格式。

目前,为了将其读入pandas,我会先读取文件,获取分隔符位置,围绕分隔符切割每一行,然后将其保存到一个适当的CSV文件中,这样我就可以轻松地读取它。

我发现pandas read_csv可以获取文件缓冲区。我该如何直接将我的流传递给它,而不保存CSV文件?我应该生成器吗?我能否在没有给出文件句柄的情况下获得csv.writer.writerow输出?

以下是我的代码:

import pandas as pd

caminho= r'C:\Users\user\Documents\SAP\Tests\\'
arquivo = "ExpComp_01.txt"
tipo_dado = {"KEY_GUID":"object", "DEL_IND":"object", "HDR_GUID":"object", , "PRICE":"object", "LEADTIME":"int16", "MANUFACTURER":"object", "LOAD_TIME":"object", "APPR_TIME":"object", "SEND_TIME":"object", "DESCRIPTION":"object"} 

def desmembra(linha, limites):
    # This functions receives each delimiter's index and cuts around it
    posicao=limites[0]    
    for limite in limites[1:]:
        yield linha[posicao+1:limite]
        posicao=limite

def pre_processa(arquivo):
    import csv
    import os
    # Translates SAP output in standard CSV
    with open(arquivo,"r", encoding="mbcs") as entrada, open(arquivo[:-3] +
    "csv", "w", newline="", encoding="mbcs") as saida:
        escreve=csv.writer(saida,csv.QUOTE_MINIMAL, delimiter=";").writerow
        for line in entrada:
            # Find heading
            if line[0]=="|":
                delimitadores = [x for x, v in enumerate(line) if v == '|']
                if line[-2] != "|": 
                    delimitadores.append(None)
                cabecalho_teste=line[:50]
                escreve([campo.strip() for campo in desmembra(line,delimitadores)])
                break
        for line in entrada:
            if line[0]=="|" and line[:50]!=cabecalho_teste:
                escreve([campo.strip() for campo in desmembra(line, delimitadores)])

pre_processa(caminho+arquivo)       
dados = pd.read_csv(caminho + arquivo[:-3] + "csv", sep=";",
                    header=0, encoding="mbcs", dtype=tipo_dado)

此外,如果您能分享最佳实践: 我有一些奇怪的日期时间字符串,如20.120.813.132.432,我可以成功地使用以下方法进行转换:
dados["SEND_TIME"]=pd.to_datetime(dados["SEND_TIME"], format="%Y%m%d%H%M%S")
dados["SEND_TIME"].replace(regex=False,inplace=True,to_replace=r'.',value=r'')

我无法为此编写解析器,因为我的日期存储在不同的字符串格式中。在导入期间指定转换器是否会更快,还是在最后由pandas逐列执行转换?我有一个类似的问题,需要将代码99999999添加点99.999.999。我不知道是否应该编写转换器或等到导入后执行df.replace编辑 - 示例数据:
|                        KEY_GUID|DEL_IND|                        HDR_GUID|Prod_CD |DESCRIPTION                      |      PRICE|LEADTIME|MANUFACTURER|          LOAD_TIME|APPR_TIME     |          SEND_TIME|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|000427507E64FB29E2006281548EB186|       |4C1AD7E25DC50D61E10000000A19FF83|75123636|Vneráéíoaeot.sadot.m             |     29,55 |30      |            |20.120.813.132.432 |20120813132929|20.120.505.010.157 |
|000527507E64FB29E2006281548EB186|       |4C1AD7E25DC50D61E10000000A19FF83|75123643|Tnerasodaeot|sadot.m             |    122,91 |30      |            |20.120.813.132.432 |20120813132929|20.120.505.010.141 |
|0005DB50112F9E69E10000000A1D2028|       |384BB350BF56315DE20062700D627978|75123676|Dnerasodáeot.sadot.m             |252.446,99 |3       |POLAND      |20.121.226.175.640 |20121226183608|20.121.222.000.015 |
|000627507E64FB29E2006281548EB186|       |4C1AD7E25DC50D61E10000000A19FF83|75123652|Pner|sodaeot.sadot.m             |    657,49 |30      |            |20.120.813.132.432 |20120813132929|20.120.505.010.128 |
|000727507E64FB29E2006281548EB186|       |4C1AD7E25DC50D61E10000000A19FF83|        |Rnerasodaeot.sadot.m             |    523,63 |30      |            |20.120.813.132.432 |20120813132929|20.120.707.010.119 |
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|                        KEY_GUID|DEL_IND|                        HDR_GUID|Prod_CD |DESCRIPTION                      |      PRICE|LEADTIME|MANUFACTURER|          LOAD_TIME|APPR_TIME     |          SEND_TIME|
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------   |000827507E64FB29E2006281548EB186|       |4C1AD7E25DC50D61E10000000A19FF83|75123603|Inerasodéeot.sadot.m             |  2.073,63 |30      |            |20.120.813.132.432 |20120813132929|20.120.505.010.127 |
|000927507E64FB29E2006281548EB186|       |4C1AD7E25DC50D61E10000000A19FF83|75123662|Ane|asodaeot.sadot.m             |      0,22 |30      |            |20.120.813.132.432 |20120813132929|20.120.505.010.135 |
|000A27507E64FB29E2006281548EB186|       |4C1AD7E25DC50D61E10000000A19FF83|75123626|Pneraíodaeot.sadot.m             |    300,75 |30      |            |20.120.813.132.432 |20120813132929|20.120.505.010.140 |
|000B27507E64FB29E2006281548EB186|       |4C1AD7E25DC50D61E10000000A19FF83|        |Aneraéodaeot.sadot.m             |      1,19 |30      |            |20.120.813.132.432 |20120813132929|20.120.505.010.131 |
|000C27507E64FB29E2006281548EB186|       |4C1AD7E25DC50D61E10000000A19FF83|75123613|Cnerasodaeot.sadot.m             |     30,90 |30      |            |20.120.813.132.432 |20120813132929|20.120.505.010.144 |
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

我需要处理其他表格,这些表格包含其他字段,都是这种一般的形式。我只能相信标题中的分隔符。此外,数据中可能会有重复的标题。它看起来像一个矩阵式的打印输出。


你能发布一些原始数据样本吗?另外,有一个read_fwf函数,但它没有像read_csv那样多的调整选项。 - EdChum
添加示例数据。我读到read_fwf不够快。由于每个周期需要处理数百万条记录,因此我没有尝试。我在处理含有多字节字符的固定宽度时遇到了很多麻烦。我的第一次尝试是使用struct.unpack。这种预处理实际上很快,但我希望避免保存新的桥接文件。 - mvbentes
1个回答

3
如果你想建立一个DataFrame,而不必先写入CSV,则不需要使用pd.read_csv。虽然可以使用io.BytesIO或cString.StringIO将其写入内存文件,但是将可迭代值(如desmembra(line, delimitadores))转换为单个字符串,以便重新解析它与pd.read_csv一起使用是没有意义的。
相反,更直接的方法是使用pd.DataFrame,因为pd.DataFrame可以接受行数据的迭代器。通常,使用纯Python逐个操作值的效率不是最快的。通常,对整列使用Pandas函数更快。因此,我会首先将arquivo解析为字符串的DataFrame,然后使用Pandas函数将列后处理为正确的dtype和值。
import pandas as pd
import os
import csv
import io

caminho = r'C:\Users\u5en\Documents\SAP\Testes\\'
arquivo = os.path.join(caminho, "ExpComp_01.txt")
arquivo_csv = os.path.splitext(arquivo)[0] + '.csv'

def desmembra(linha, limites):
    # This functions receives each delimiter's index and cuts around it
    return [linha[limites[i]+1:limites[i+1]].strip()
            for i in range(len(limites[:-1]))]

def pre_processa(arquivo, enc):
    # Translates SAP output into an iterator of lists of strings
    with io.open(arquivo, "r", encoding=enc) as entrada:
        for line in entrada:
            # Find heading
            if line[0] == "|":
                delimitadores = [x for x, v in enumerate(line) if v == '|']
                if line[-2] != "|": 
                    delimitadores.append(None)
                cabecalho_teste = line[:50]
                yield desmembra(line, delimitadores)
                break
        for line in entrada:
            if line[0] == "|" and line[:50] != cabecalho_teste:
                yield desmembra(line, delimitadores)                

def post_process(dados):
    dados['LEADTIME'] = dados['LEADTIME'].astype('int16')
    for col in ('SEND_TIME', 'LOAD_TIME', 'PRICE'):
        dados[col] = dados[col].str.replace(r'.', '')
    for col in ('SEND_TIME', 'LOAD_TIME', 'APPR_TIME'):
        dados[col] = pd.to_datetime(dados[col], format="%Y%m%d%H%M%S")
    return dados

enc = 'mbcs'  
saida = pre_processa(arquivo, enc)
header = next(saida)
dados = pd.DataFrame(saida, columns=header)
dados = post_process(dados)
print(dados)

产量
                           KEY_GUID DEL_IND                          HDR_GUID  \
0  000427507E64FB29E2006281548EB186          4C1AD7E25DC50D61E10000000A19FF83   
1  000527507E64FB29E2006281548EB186          4C1AD7E25DC50D61E10000000A19FF83   
2  0005DB50112F9E69E10000000A1D2028          384BB350BF56315DE20062700D627978   
3  000627507E64FB29E2006281548EB186          4C1AD7E25DC50D61E10000000A19FF83   
4  000727507E64FB29E2006281548EB186          4C1AD7E25DC50D61E10000000A19FF83   
5  000927507E64FB29E2006281548EB186          4C1AD7E25DC50D61E10000000A19FF83   
6  000A27507E64FB29E2006281548EB186          4C1AD7E25DC50D61E10000000A19FF83   
7  000B27507E64FB29E2006281548EB186          4C1AD7E25DC50D61E10000000A19FF83   
8  000C27507E64FB29E2006281548EB186          4C1AD7E25DC50D61E10000000A19FF83   

    Prod_CD           DESCRIPTION      PRICE  LEADTIME MANUFACTURER  \
0  75123636  Vneráéíoaeot.sadot.m      29,55        30                
1  75123643  Tnerasodaeot|sadot.m     122,91        30                
2  75123676  Dnerasodáeot.sadot.m  252446,99         3       POLAND   
3  75123652  Pner|sodaeot.sadot.m     657,49        30                
4            Rnerasodaeot.sadot.m     523,63        30                
5  75123662  Ane|asodaeot.sadot.m       0,22        30                
6  75123626  Pneraíodaeot.sadot.m     300,75        30                
7            Aneraéodaeot.sadot.m       1,19        30                
8  75123613  Cnerasodaeot.sadot.m      30,90        30                

            LOAD_TIME           APPR_TIME           SEND_TIME  
0 2012-08-13 13:24:32 2012-08-13 13:29:29 2012-05-05 01:01:57  
1 2012-08-13 13:24:32 2012-08-13 13:29:29 2012-05-05 01:01:41  
2 2012-12-26 17:56:40 2012-12-26 18:36:08 2012-12-22 00:00:15  
3 2012-08-13 13:24:32 2012-08-13 13:29:29 2012-05-05 01:01:28  
4 2012-08-13 13:24:32 2012-08-13 13:29:29 2012-07-07 01:01:19  
5 2012-08-13 13:24:32 2012-08-13 13:29:29 2012-05-05 01:01:35  
6 2012-08-13 13:24:32 2012-08-13 13:29:29 2012-05-05 01:01:40  
7 2012-08-13 13:24:32 2012-08-13 13:29:29 2012-05-05 01:01:31  
8 2012-08-13 13:24:32 2012-08-13 13:29:29 2012-05-05 01:01:44  

谢谢你的建议!我刚刚试着用StringIO做了一下,正如你所劝告的那样。 :P 我不会再这么做了。关于在列上应用函数的问题,我和你想的一样,只是考虑到它具有转换器字典功能而重新思考了一下。但实际上没有必要。感谢您的全面改进!你评论的那部分是当行溢出且没有“|”来结束行时使用的。它被截断了,但我现在会编辑它。 - mvbentes
虽然 pd.read_csv 具有潜在有用的 converters 参数,但是对于百万条记录的 CSV 文件中的每个值调用一次 Python 函数可能需要相当长的时间。而使用 astype向量化字符串函数进行后处理也需要一些时间,但很可能会更快。 - unutbu
我真的应该在desmembra中进行编码吗?然后当我使用DataFrame.to_csv时,它将字节字符串repr作为_u'some_string'_写入文件中。我可以将其去掉并在内部使用Unicode,但我想知道这种编码背后是否有什么理由。再次感谢您的帮助! - mvbentes
抱歉,那是我的错误。通常情况下,您应该在内部使用Unicode,并仅在输出时进行编码。感谢您的纠正。 - unutbu

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