Chronaxis用户手册English
本页目录

chronaxis.sources.raw_csv

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""直接把原始报文CSV/TXT读成带原行定位的CAN记录,不经过ASC中间文件。23CANCSVSpec使用不可变的原始报文列布局并声明时间/进制/通道起点。CANCSVRead只读打开一次,4逐记录经标准库CSV语法解析后提取所需列,严格核时间、ID、帧型、DLC和载荷。5CSV记录可以跨物理行,但整条记录字节数有界;任何错误停止并关闭,不静默跳过。6数字时间必须明确单位,日内时间默认禁止回退。clock-ms-us-v1明确把最后两段7分别解释为毫秒和微秒,例如.151.2为.151002秒;普通clock不自动采用这一规则。8模块不加载DBC/旧reader,不解码或保存整文件,已返回记录的长期保留由消费层计费。9"""1011import codecs12import csv13from dataclasses import dataclass, field14from datetime import timezone, timedelta15from decimal import Decimal, localcontext16from pathlib import Path17import re1819from ..legacy.records import LegacyRawFrame20from ..results.budget import ResourceBudget, _reserve_owned_ram, _release_owned_ram, _cleanup_owner21from ..timebase.conversion import convert_time, checked_add_ns, parse_datetime_ns, require_int64_ns22from .raw_can import RawCANRecord, CANTextError23from .raw_csv_layout import CANCSVLayout, MAX_COLUMNS2425# 同一表记录即使引号未闭合也不能无限累积;预算按整条记录的最坏文本/分列开销预约。26MAX_CSV_RECORD_BYTES = 6553627DAY_NS = 86400 * 10**928FD_LENGTHS = (0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64)293031@dataclass(frozen=True)32class CSVTimeSpec:33    """声明时间文字含义,固定偏移只施加一次,不用电脑日期或时区补值。"""3435    format: str = 'numeric'  # numeric、clock、clock-ms-us-v1或iso8601,必须显式选择。36    unit: str = 's'  # numeric的s/ms/us/ns,其他格式只接受默认s,避免无效配置。37    offset_ns: int = 0  # 数值固定平移;日内时间可据明确原点映射到会话。38    timezone_offset_minutes: int | None = None  # 仅ISO无时区文本的显式补充。39    rollover_min_drop_ns: int | None = None  # 两种日内格式可选:回退至少此纳秒数才按次日解释。4041    def __post_init__(self):42        """校验时间政策。参数: 无。返回: None。异常: 类型、格式、单位、时区或午夜规则非法。43        """44        # 每种写法只采用一种含义;不依文字长度猜秒/毫秒,也不默认跨午夜。45        if self.format not in ('numeric','clock','clock-ms-us-v1','iso8601') or self.unit not in ('s','ms','us','ns'):46            raise ValueError('CSV时间格式或单位不支持')47        # 日内和ISO文字已有单位,不能再额外乘一次numeric单位。48        if self.format != 'numeric' and self.unit != 's':49            raise ValueError('非数值时间不能额外声明数值单位')50        # 偏移必须能进入纳秒时间轴;只有无时区ISO文字可补显式时区。51        require_int64_ns(self.offset_ns)52        zone = self.timezone_offset_minutes53        if zone is not None and (self.format != 'iso8601' or type(zone) is not int or not -1439 <= zone <= 1439):54            raise ValueError('只有ISO时间可声明整数分钟时区偏移')55        # 午夜只适用于日内时钟;阈值为严格正值且小于一天,避免把任意乱序当作换日。56        drop = self.rollover_min_drop_ns57        if drop is not None and (self.format not in ('clock','clock-ms-us-v1') or type(drop) is not int or not 0 < drop < DAY_NS):58            raise ValueError('跨午夜阈值需日内时间格式的0到一天之间整数纳秒')596061@dataclass(frozen=True)62class CANCSVSpec:63    """冻结单文件原始报文来源;布局/时间声明不打开输入或加载DBC。"""6465    path: Path  # CSV/TXT只是文件名,不用于推断格式。66    layout: CANCSVLayout  # 编码、分隔符、列/中文映射及明确固定值。67    time: CSVTimeSpec = field(default_factory=CSVTimeSpec)  # 时间解释单独声明。68    id_base: int | str = 'auto'  # 0x十六进制、0b二进制、无前缀十进制;裸十六进制表可显式选16。69    length_base: int = 10  # DLC/字节数列的明确进制,原始CSV的0x08应设16。70    channel_origin: int = 0  # 原列从0或1开始,统一转换为0基通道。71    payload_style: str = 'spaced'  # spaced逐字节,或contiguous连续偶数位十六进制。7273    def __post_init__(self):74        """核静态来源契约。参数: 无。返回: None。异常: 声明类型或不支持选项。75        """76        if not isinstance(self.path,Path) or type(self.layout) is not CANCSVLayout or type(self.time) is not CSVTimeSpec:77            raise TypeError('CANCSVSpec需要Path、CANCSVLayout及CSVTimeSpec')78        # 逐物理行字节定位要求LF可单字节分隔,其他编码不能假报准确行号。79        if codecs.lookup(self.layout.encoding).name not in ('utf-8','utf-8-sig','ascii','gbk','cp1252','iso8859-1'):80            raise ValueError('原始CAN CSV当前不支持该逐行编码')81        # 进制与通道起点由配置决定,bool不能借Python的整数继承关系混入。82        if not (type(self.id_base) is str and self.id_base == 'auto'83                or type(self.id_base) is int and self.id_base in (10,16)):84            raise ValueError('CSV ID进制需要auto、10或16')85        if type(self.length_base) is not int or self.length_base not in (10,16):86            raise ValueError('CSV长度进制需要10或16')87        if type(self.channel_origin) is not int or self.channel_origin not in (0,1) or self.payload_style not in ('spaced','contiguous'):88            raise ValueError('CSV通道起点或载荷布局错误')899091def _uint(value, base, maximum, label):92    """解析明确进制的非负整数。参数: value文字/int;base进制;maximum上限;label定位。9394    返回: int。异常: ValueError为符号/字符/隐式转换或越界,不允许bool/下划线。95    """96    # 已映射整数直接校范围;文字仅允许声明进制的数字,不隐式接受float或bool。97    if type(value) is int:98        result = value99    elif type(value) is str:100        if base == 'auto':101            prefix = value[:2].lower()102            base = 16 if prefix == '0x' else 2 if prefix == '0b' else 10103            if prefix == '0b':104                value = value[2:]105        text = value[2:] if base == 16 and value[:2].lower() == '0x' else value106        pattern = r'[0-9a-fA-F]+' if base == 16 else r'[01]+' if base == 2 else r'[0-9]+'107        if not re.fullmatch(pattern,text):108            raise ValueError(label+'字符或进制无效')109        result = int(text,base)110    else:111        raise ValueError(label+'需要文字或整数')112    # 不按ID大小改变标准/扩展标志,也不截断超出字段范围的数值。113    if not 0 <= result <= maximum:114        raise ValueError(label+'越界')115    return result116117118def _payload(text, style):119    """严格读取十六进制载荷,不补零/截短。参数: text原文字、style布局。返回: bytes。异常: 格式/长度错误。120    """121    # 连续格式必须完整匹配偶数字符数,不能把缺半字节的文字补成一个字节。122    if style == 'contiguous':123        if len(text) > 128 or not re.fullmatch(r'(?:[0-9a-fA-F]{2})*',text):124            raise ValueError('连续载荷需不超过64字节的偶数位十六进制')125        return bytes.fromhex(text)126    # 每字节允许一或两位hex,内部空白分隔;不吞0x前缀、控制字段或额外坏字节。127    parts = text.split()128    if len(parts) > 64 or any(not re.fullmatch(r'[0-9a-fA-F]{1,2}',value) for value in parts):129        raise ValueError('分隔载荷需不超过64个十六进制字节')130    return bytes(int(value,16) for value in parts)131132133class CSVClock:134    """保存一次来源的时间进度,默认拒绝回退,显式阈值才把大幅日内回退解释为次日。"""135136    def __init__(self, spec):137        """只建立状态。参数: spec为CSVTimeSpec。返回: 时钟。异常: 类型错误。138        """139        if type(spec) is not CSVTimeSpec:140            raise TypeError('CSVClock需要CSVTimeSpec')141        self.spec, self._last_ns, self._last_clock_ns, self._day_offset = spec, None, None, 0142143    def parse(self, value):144        """按声明转换并推进。参数: value映射后的时间文字/int。返回: (int64纳秒,量化bool)。异常: 格式/时区/回退/越界。145        """146        # 失败前只修改局部候选状态,错误时间不能污染下一次显式调用。147        if type(value) not in (str,int) or type(value) is str and len(value) > MAX_CSV_RECORD_BYTES:148            raise ValueError('CSV时间需要有限文字或整数')149        day_offset,last_clock = self._day_offset,self._last_clock_ns150        if self.spec.format == 'numeric':151            result = convert_time(value,self.spec.unit)152            ns, quantized = result.time_ns, result.quantized153        elif self.spec.format == 'iso8601':154            # 仅用声明的时区补充无时区ISO输入,不读取执行电脑的时区。155            zone = self.spec.timezone_offset_minutes156            tz = None if zone is None else timezone(timedelta(minutes=zone))157            result = parse_datetime_ns(value,source_timezone=tz)158            ns, quantized = result.time_ns, result.quantized159        else:160            # 两种日内格式各自严格匹配;不能根据一行文字临时切换时间含义。161            if type(value) is not str:162                raise ValueError('日内时间需要明确时分秒文字')163            grouped = self.spec.format == 'clock-ms-us-v1'164            pattern = (r'([0-9]{2}):([0-9]{2}):([0-9]{2})\.([0-9]{1,3})\.([0-9]{1,3})'165                       if grouped else r'(\d{2}):(\d{2}):(\d{2})(\.\d+)?')166            match = re.fullmatch(pattern,value)167            if match is None:168                expected = 'HH:MM:SS.毫秒.微秒,各末段为0到999' if grouped else 'HH:MM:SS[.小数];多段小数需显式clock-ms-us-v1'169                raise ValueError('日内时间需'+expected)170            hour,minute,second = (int(match[index]) for index in (1,2,3))171            if hour > 23 or minute > 59 or second > 59:172                raise ValueError('日内时分秒越界')173            if grouped:174                # 两段是各自的整数单位,不按字符拼接小数;微秒规则不引入float舍入。175                clock_ns = (hour*3600+minute*60+second)*10**9 + int(match[4])*10**6 + int(match[5])*1000176                quantized = False177            else:178                # 普通小数秒仍沿既有Decimal转纳秒协议,保留更细输入的量化事实。179                with localcontext() as context:180                    context.prec = MAX_CSV_RECORD_BYTES + 32181                    seconds = Decimal(hour*3600+minute*60+second) + Decimal(match[4] or '0')182                    result = convert_time(seconds,'s')183                clock_ns, quantized = result.time_ns, result.quantized184            # 只在明确允许且回退量达到阈值时进入次日;小幅乱序仍是错误。185            if self._last_clock_ns is not None and clock_ns < self._last_clock_ns:186                drop = self.spec.rollover_min_drop_ns187                if drop is None or self._last_clock_ns-clock_ns < drop:188                    raise ValueError('CSV日内时间回退,未满足明确跨午夜规则')189                day_offset = checked_add_ns(day_offset,DAY_NS)190            ns = checked_add_ns(day_offset,clock_ns)191            last_clock = clock_ns192        ns = checked_add_ns(ns,self.spec.offset_ns)193        # 排序在公开纳秒时间轴核对;更细的文本精度由quantized和raw_time明确保留。194        if self._last_ns is not None and ns < self._last_ns:195            raise ValueError('CSV来源时间回退')196        self._last_ns,self._day_offset,self._last_clock_ns = ns,day_offset,last_clock197        return ns,quantized198199200def _csv_frame(values, spec, time_ns, flag_resolver=None):201    """把已明确映射的列转换成原帧,不按载荷/ID大小猜帧型。202203    参数: values规范字段映射、spec来源声明、time_ns精确纳秒;flag_resolver仅补缺少的帧型。204    返回: (LegacyRawFrame, DLC编码或None)。异常: ID/通道/载荷/标志/DLC不一致。205    """206    # 布局已校验身份枚举;ID范围按显式标准/扩展标志决定,通道转为0基。207    fd,extended,frame_type = values.get('is_fd'),values.get('is_extended'),values['frame_kind']208    identifier = _uint(values['frame_id'],spec.id_base,0x1fffffff,'CSV ID')209    channel = _uint(values['channel'],10,65536,'CSV通道') - spec.channel_origin210    if channel < 0:211        raise ValueError('CSV通道低于明确起点')212    if fd is None or extended is None:213        if flag_resolver is None:214            raise ValueError('CSV缺少is_extended或is_fd;请补列映射,或通过带DBC的分析入口自动识别')215        extended, fd = flag_resolver(channel, identifier, extended, fd)216    if type(extended) is not bool or type(fd) is not bool or not extended and identifier > 0x7ff:217        raise ValueError('CSV报文ID与标准/扩展帧声明不符')218    # DLC编码和载荷字节数分别解析,CAN FD的编码9对应12字节,不能混用。219    data = _payload(values['data'],spec.payload_style)220    code = (_uint(values['dlc_code'],spec.length_base,15 if fd else 8,'CSV DLC编码')221            if 'dlc_code' in values else None)222    length = (_uint(values['payload_length'],spec.length_base,64 if fd else 8,'CSV载荷长度')223              if 'payload_length' in values else len(data))224    if len(data) != length:225        raise ValueError('CSV声明载荷长度与实际字节数不一致')226    if frame_type == 'remote':227        # 远程请求的DLC可非零,实际载荷必须为空;FD协议没有远程帧。228        if fd or data or code is None:229            raise ValueError('远程帧需经典CAN、空载荷和明确DLC编码')230        dlc = code231    elif frame_type == 'error':232        # 当前错误帧格式不接受数据载荷;不伪造普通数据帧或用它参与DBC解码。233        if data or code not in (None,0):234            raise ValueError('当前CSV错误帧需空载荷/零长度')235        code,dlc = None,0236    else:237        # 数据帧必须同时满足实际载荷长度和协议DLC;缺编码时才由长度导出。238        expected = FD_LENGTHS[code] if fd and code is not None else code239        if code is not None and expected != length:240            raise ValueError('CSV DLC编码与载荷长度不一致')241        if fd and length not in FD_LENGTHS:242            raise ValueError('CAN FD字节数不能对应合法DLC编码')243        if code is None:244            code = FD_LENGTHS.index(length) if fd else length245        dlc = length246    brs,esi = values.get('bitrate_switch',False),values.get('error_state_indicator',False)247    # FD专用标志不能套在经典帧上;float秒仅为既有原帧模型的兼容表示。248    if not fd and (brs or esi):249        raise ValueError('经典CAN不能携带FD BRS/ESI标志')250    return LegacyRawFrame(timestamp=time_ns/10**9,arbitration_id=identifier,is_extended_id=extended,251                          is_remote_frame=frame_type=='remote',is_error_frame=frame_type=='error',channel=channel,252                          dlc=dlc,data=data,is_fd=fd,is_rx=values.get('is_rx'),bitrate_switch=brs,253                          error_state_indicator=esi),code254255256class _CSVLines:257    """把一次有界表记录的物理行交给标准CSV解析器,位置/字节累计由父reader持有。"""258259    def __init__(self, owner):260        """借用父对象。参数: owner为CANCSVRead。返回: 行迭代器。异常: 无。261        """262        self.owner = owner263264    def __iter__(self):265        """不重新打开文件。参数: 无。返回: self。异常: 无。266        """267        return self268269    def __next__(self):270        """提供一个实际物理行。参数: 无。返回: 含换行str。异常: EOF、定位编码错、超限或取消。271        """272        owner = self.owner273        if owner._check_cancel is not None:274            owner._check_cancel()275        # 配额在CSV读取每条表记录前归零;跨引号的多行仍共用一个65536字节上限。276        remaining = MAX_CSV_RECORD_BYTES-owner._record_bytes277        raw = owner._file.readline(remaining+1)278        if not raw:279            raise StopIteration280        # 实际读到物理行才推进计数;超长记录定位其开始行,坏编码定位当前物理行。281        owner.line_number += 1282        owner._record_bytes += len(raw)283        if owner._record_bytes > MAX_CSV_RECORD_BYTES:284            raise CANTextError(owner.spec.path,owner._record_start,'CSV记录超过65536字节')285        encoding = owner._encoding286        # BOM只在文件第一行解释;后续单元格开头相同字节应保留为实际内容。287        if encoding == 'utf-8-sig' and owner.line_number != 1:288            encoding = 'utf-8'289        try:290            return raw.decode(encoding,errors='strict')291        except UnicodeError as error:292            raise CANTextError(owner.spec.path,owner.line_number,'CSV编码错误') from error293294295class CANCSVRead:296    """拥有一个文件、CSV语法迭代器和时钟,正常/失败/取消均关闭自有资源。"""297298    def __init__(self, spec, budget, *, check_cancel=None, retain_original=True, retain_source_time=True, flag_resolver=None, selected_ids=None):299        """预约并只读打开一次。参数: spec为CANCSVSpec、budget共享预算、check_cancel无参取消回调;selected_ids仅在不需全原帧/来源时间的入口限定ID。300301        返回: reader,不预读数据。异常: 类型/预算/I/O原错;清理失败保raw_csv_cleanup_owner。302        """303        if type(retain_original) is not bool or type(retain_source_time) is not bool:304            raise TypeError('原文/导出时间选择需要bool')305        self._retain_original=retain_original306        self._retain_source_time=retain_source_time307        if flag_resolver is not None and not callable(flag_resolver):308            raise TypeError('flag_resolver需要可调用对象')309        self._flag_resolver = flag_resolver310        if selected_ids is not None and (type(selected_ids) is not tuple or len(selected_ids)>4096311                or any(type(value) is not int or not 0 <= value <= 0x1fffffff for value in selected_ids)):312            raise ValueError('CSV按需ID需要有限合法整数tuple')313        self._selected_ids = None314        # 构造错误在取得文件和预算之前拒绝,不向外暴露半初始化来源。315        if type(spec) is not CANCSVSpec or not isinstance(budget,ResourceBudget):316            raise TypeError('CANCSVRead需要CANCSVSpec和ResourceBudget')317        if check_cancel is not None and not callable(check_cancel):318            raise TypeError('CSV取消检查需要可调用对象')319        # 先登记全部资源归属和进度,构造任意后续步骤失败都可由同一close回收。320        self.spec,self._budget,self._check_cancel = spec,budget,check_cancel321        self._encoding = codecs.lookup(spec.layout.encoding).name322        self._file,self._reader,self._bound,self._clock = None,None,None,None323        self._fixed_cost,self._held_cost,self._closed,self._initialized = 0,0,False,False324        self.line_number,self.count,self._record_bytes,self._record_start = 0,0,0,1325        self._time_position = None326        try:327            # 文本/分列/CSV引号缓存和有界表头一起计费;不加载或预分配整文件。328            _reserve_owned_ram(self,MAX_CSV_RECORD_BYTES*64+MAX_COLUMNS*256+8192+128*len(selected_ids or ()),'_fixed_cost')329            self._selected_ids = None if selected_ids is None else frozenset(selected_ids)330            self._file = spec.path.open('rb')331            self._reader = csv.reader(_CSVLines(self),delimiter=spec.layout.delimiter,strict=True)332            self._clock = CSVClock(spec.time)333        except BaseException as error:334            _cleanup_owner(self,error,'raw_csv_cleanup_owner','原始CSV读取')335            raise336337    @property338    def closed(self):339        """返回真实关闭事实。参数: 无。返回: bool。异常: 无。340        """341        return self._closed342343    def __iter__(self):344        """迭代同次扫描。参数: 无。返回: self。异常: 无,不另开文件。345        """346        return self347348    def _next_row(self):349        """取得一条有界CSV表记录。参数: 无。返回: str列表。异常: EOF/定位语法错/取消。350        """351        # 一条CSV记录可能跨多行,因此在请求首行前记住开始位置并重置整记录配额。352        self._record_bytes,self._record_start = 0,self.line_number+1353        try:354            row = next(self._reader)355        except csv.Error as error:356            # 保留CSV库的具体原因,同时提供文件与记录开始的物理行。357            raise CANTextError(self.spec.path,self._record_start,'CSV语法错误: '+str(error)) from error358        # 即便总字节有限,也限制空列造成的列表和字符串对象数量。359        if len(row) > MAX_COLUMNS:360            raise CANTextError(self.spec.path,self._record_start,'CSV列数超过4096')361        return row362363    def _initialize(self):364        """按声明跳过记录/绑定表头一次。参数: 无。返回: None。异常: 缺表头/缺列或EOF。365        """366        # 跳过的是显式声明的CSV记录数;输入不足是错误,不能当作正常空数据。367        for _ in range(self.spec.layout.skip_rows):368            try:369                self._next_row()370            except StopIteration as error:371                raise CANTextError(self.spec.path,self.line_number+1,'CSV声明跳过记录数超过实际输入') from error372        # 只有声明表头才消费这一条;绑定后所有数据行复用相同列位置。373        if self.spec.layout.has_header:374            try:375                header = self._next_row()376            except StopIteration as error:377                raise CANTextError(self.spec.path,self.line_number+1,'CSV缺少已声明的表头') from error378        else:379            header = None380        try:381            self._bound = self.spec.layout.bind_header(header)382        except (ValueError,TypeError) as error:383            raise CANTextError(self.spec.path,self._record_start,str(error)) from error384        # 时间原文取原始单元格,strip/值映射之后仍保留它的输入证据。385        self._time_position = next(position for name,_,position in self._bound.bindings if name=='timestamp')386        self._initialized = True387388    def __next__(self):389        """返回下一原始CAN记录。参数: 无。返回: RawCANRecord。异常: EOF/定位坏行/预算/取消原错。390        """391        # 已关闭来源不会重新打开或重放已产出的记录。392        if self._closed:393            raise StopIteration394        try:395            if not self._initialized:396                self._initialize()397            while True:398                try:399                    row = self._next_row()400                except StopIteration:401                    self.close()402                    raise403                try:404                    if self._selected_ids is not None:405                        # 仅消息汇总且无需来源时间/原帧时,先看ID;无关行不补造帧型。406                        identity = self._bound.map_row(row, names=('frame_id',))407                        identifier = _uint(identity['frame_id'],self.spec.id_base,0x1fffffff,'CSV ID')408                        if identifier not in self._selected_ids:409                            # 显式跨午夜需所有日内时钟推进;只取这一个必要字段,仍不解析帧型和载荷。410                            if self.spec.time.rollover_min_drop_ns is not None:411                                clock = self._bound.map_row(row, names=('timestamp',))412                                self._clock.parse(clock['timestamp'])413                            self.count += 1414                            continue415                    values = self._bound.map_row(row)416                except (ValueError,TypeError,OverflowError) as error:417                    raise CANTextError(self.spec.path,self._record_start,str(error)) from error418                break419            try:420                time_ns,quantized = self._clock.parse(values['timestamp'])421                frame,code = _csv_frame(values,self.spec,time_ns,self._flag_resolver)422                mapping = self.spec.layout.fields['timestamp']423                raw_time = (str(mapping.fixed) if self._time_position is None else row[self._time_position]) if self._retain_original else None424                # 时钟已完成换日/时区解释,撤去且仅撤去显式分析平移,保精确原坐标供导出。425                source_ns = require_int64_ns(time_ns-self.spec.time.offset_ns) if self._retain_source_time else None426                record = RawCANRecord(frame,time_ns,self.spec.path,self._record_start,self.count,427                                      raw_time,'raw-can-csv-v1',code,quantized,source_ns)428            except (ValueError,TypeError,OverflowError) as error:429                # 数据问题携带原行定位,不吞掉程序错误、取消或资源错误。430                raise CANTextError(self.spec.path,self._record_start,str(error)) from error431            # 序号包括按ID跳过的实际输入行;同刻不同原帧不合并。432            self.count += 1433            return record434        except StopIteration:435            raise436        except BaseException as error:437            # 保留原错,关闭持续失败时把真实reader附回异常,供调用方重试清理。438            _cleanup_owner(self,error,'raw_csv_cleanup_owner','原始CSV读取')439            raise440441    def close(self):442        """关闭文件和解析状态再归还自有预算,可重试。参数: 无。返回: None。异常: 关闭/预算原错。443        """444        # 关闭幂等;句柄关闭成功后才丢弃引用,失败时仍能对同一对象重试。445        if self._closed:446            return447        if self._file is not None:448            self._file.close()449            self._file = None450        # 文件已关闭才释放解析状态与本对象实际持有的预算,不能释放其他来源费用。451        self._reader,self._bound,self._clock = None,None,None452        if self._fixed_cost:453            _release_owned_ram(self,self._fixed_cost,'_fixed_cost')454        self._closed = True455        self._flag_resolver = self._selected_ids = None456457    def __enter__(self):458        """提供受控读取上下文。参数: 无。返回: self。异常: 无。459        """460        return self461462    def __exit__(self, exception_type, error, traceback):463        """保留原错并收尾。参数: exception_type/error/traceback异常信息。返回: False。异常: 无原错时关闭失败。464        """465        # 正常退出直接报告关闭错误;已有业务错误时保原错及必要的恢复owner。466        if error is None:467            self.close()468        else:469            _cleanup_owner(self,error,'raw_csv_cleanup_owner','原始CSV读取')470        return False471
Chronaxis 用户手册 · 离线阅读