当前位置:首页>python>Python学习 | 09 FASTA/FASTQ Parser 与 Generator:逐条处理大型序列文件

Python学习 | 09 FASTA/FASTQ Parser 与 Generator:逐条处理大型序列文件

  • 2026-08-30 20:12:15
Python学习 | 09 FASTA/FASTQ Parser 与 Generator:逐条处理大型序列文件

title: Lesson 09|FASTA / FASTQ Parser 与 Generator:逐条处理大型序列文件jupyter: python3

Lesson 08 我们已经完成了一个非常关键的转换:

磁盘上的文件open逐行读取strip / splitPython object分析

我们处理的文件还是比较简单的 TSV:

seq1    ATGCGTseq2    GGCAATseq3    TTAACC

每一行就是一个完整 record:

seq_id + sequence

所以:

seq_id, seq = line.split(”\t”)

就能完成解析。

但真实的生物信息学文件通常不会这么简单。

例如 FASTA:

>seq1ATGCGTACGT>seq2GGCCAATT>seq3ATATGC

看起来似乎也不复杂。

可是很快就会遇到:

>seq1ATGCGTACGTGGTTAACCCCGG>seq2GGCCAATTATGC

这时候:

一个 sequence record 可以跨很多行。

于是你不能再用:

seq_id, seq = line.split(”\t”)

这种“每一行就是一个 record”的思路。

这一课我们真正要学习的是:

raw lines识别 record boundary积累属于同一个 record 的内容组装一个完整 biological record逐条交给后续分析

这就是:

parser

而进一步,如果文件有几十 GB,我们又不希望:

解析整个文件全部保存到 memory最后才开始分析

更希望:

解析 seq1交给程序解析 seq2交给程序解析 seq3交给程序...

这就会引出这一课最重要的新工具:

yield

以及:

generator

今天我们会完成三个真正的 bioinformatics parser:

FASTA → dictionary parserFASTA → generator parserFASTQ → generator parser

1. FASTA 的真正结构:不要把它理解成“两行一组”

先看最简单的 FASTA:

>seq1ATGCGT>seq2GGGAAA

直觉上可能会觉得:

第 1 行 → header第 2 行 → sequence第 3 行 → header第 4 行 → sequence

于是想写:

header = ...sequence = ...

甚至认为:

FASTA = 每两行一个 record

这是一个非常危险的理解。

因为合法 FASTA 完全可能是:

>seq1ATGCGTACGTAACCGG>seq2GGGAAATT

这里:

seq1

真正的 sequence 是:

ATGCGTACGTAACCGG

也就是说 FASTA 的逻辑不是:

headersequenceheadersequence

而是:

headersequence linesequence linesequence line...下一个 header

所以真正的 record boundary 是:

遇到下一个以 > 开头的 header。


1.1 FASTA record 的心智模型

例如:

>seq1 some descriptionATGCGTAACCGGTTAA

可以理解成:

record├── header│   └── seq1 some description└── sequence    ├── ATGCGT    ├── AACCGG    └── TTAA

Parser 要做的是:

”ATGCGT””AACCGG””TTAA”

组合成:

”ATGCGTAACCGGTTAA”

所以我们需要:

当前 sequence ID+当前 sequence 的所有行

两个“状态”。


2. 第一个 FASTA parser:先返回 dictionary

先暂时不考虑超大文件。

我们希望:

sequences.fa

内容:

>seq1ATGCGTAACCGG>seq2GGGAAA>seq3ATATGCGC

最后得到:

{    ”seq1”: ”ATGCGTAACCGG”,    ”seq2”: ”GGGAAA”,    ”seq3”: ”ATATGCGC”}

2.1 先观察每一行

with open(”sequences.fa”) as f:    for line in f:        line = line.strip()        print(repr(line))

我们可以利用:

line.startswith(”>”)

判断当前是不是 header。

例如:

if line.startswith(”>”):    ...

否则:

else:    ...

就是 sequence line。


3. Parser 为什么需要“记住当前状态”

假设现在程序读到:

>seq1

我们需要记住:

current_id = ”seq1”

接下来看到:

ATGCGT

不能立即结束 record,因为后面可能还有:

AACCGG

所以还需要一个容器:

sequence_parts = []

然后:

sequence_parts.append(”ATGCGT”)sequence_parts.append(”AACCGG”)

得到:

[    ”ATGCGT”,    ”AACCGG”]

等到遇到:

>seq2

我们终于知道:

seq1 已经结束了。

于是:

””.join(sequence_parts)

得到:

”ATGCGTAACCGG”

然后保存:

sequences[current_id] = ””.join(sequence_parts)

再开始新的:

current_id = ”seq2”sequence_parts = []

整个状态变化可以画成:

>seq1current_id = ”seq1”sequence_parts = []ATGCGT[”ATGCGT”]AACCGG[”ATGCGT”, ”AACCGG”]>seq2先保存 seq1current_id = ”seq2”sequence_parts = []

这就是 parser 的核心。


4. 完整的 FASTA dictionary parser

def read_fasta(path):    sequences = {}    currend_id = None    sequences_parts = []    with open(path) as f:        for line in f:            line = line.strip()            if line == ””:                continue            if line.startswith(”>”):                if current_id is not None:                    sequences[current_id] = ””.join(sequences_parts)                currend_id = line[1:]                sequences_parts = []            else:                sequences_parts.append(line)    if current_id is not None:        sequences[current_id] = ””.join(sequences_parts)    return sequences

这一段需要认真理解。

它第一次真正涉及:

parser state


4.1 为什么是 line[1:]

如果:

line = ”>seq1”

那么:

line[0]

是:

>

而:

line[1:]

就是:

seq1

所以:

current_id = line[1:]

把 > 去掉。

如果 header 是:

>seq1 chromosome_1 gene_x

那么:

line[1:]

得到:

seq1 chromosome_1 gene_x

现在我们暂时把整个 header 保存下来。

后面需要时,可以进一步拆:

seq_id = line[1:].split()[0]

得到:

seq1

5. 一个非常容易漏掉的问题:最后一个 record

看看核心部分:

if line.startswith(”>”):    if current_id is not None:        sequences[current_id] = ””.join(sequence_parts)    current_id = line[1:]    sequence_parts = []

你会发现:

当前 record 是在“遇到下一个 header”时才被保存的。

比如:

>seq1AAAA>seq2CCCC>seq3GGGG

当看到:

>seq2

保存:

seq1

看到:

>seq3

保存:

seq2

但是文件结束之后:

seq3

后面没有新的 header。

所以如果我们不额外处理:

if current_id is not None:    sequences[current_id] = ””.join(sequence_parts)

最后一个 sequence 就会消失。

这是 parser 中非常典型的:

end-of-file edge case

以后写 parser 时应该经常问自己:

“最后一个 record 是什么时候被处理的?”

6. 为什么使用 list + "".join(),而不是不断 +=

我们现在用了:

sequence_parts = []sequence_parts.append(line)sequence = ””.join(sequence_parts)

你可能会问:

为什么不直接:

sequence = ””sequence += line

对于很小的 sequence:

sequence += line

当然也能运行。

但 string 是:

immutable

也就是说:

sequence += line

本质上可能需要不断产生新的 string。

因此对于大量 sequence fragments,Python 中更典型的模式是:

parts = []parts.append(...)parts.append(...)parts.append(...)result = ””.join(parts)

类似:

先收集 pieces一次 join

这个模式以后会反复出现。


7. 给 FASTA parser 加验证

真实 parser 不应该默认输入永远正确。

例如错误文件:

ATGCGT>seq1GGGAAA

sequence 出现在任何 header 之前。

或者:

>ATGCGT

header 是空的。

我们可以加强 parser:

def read_fasta(path):    sequences = {}    current_id = None    sequence_parts = []    with open(path) as f:        for line_number, line in enumerate(f, start=1):            line = line.strip()            if line == ””:                continue            if line.startswith(”>”):                if current_id is not None:                    sequences[current_id] = ””.join(sequence_parts)                current_id = line[1:].strip()                if current_id == ””:                    raise ValueError(                        f”Empty FASTA header at line {line_number}”                    )                sequence_parts = []            else:                if current_id is None:                    raise ValueError(                        f”Sequence before header at line {line_number}”                    )                sequence_parts.append(line)    if current_id is not None:        sequences[current_id] = ””.join(sequence_parts)    return sequences

这里 Lesson 07 的:

raise ValueError(...)

开始真正参与 parser 设计。


8. Dictionary parser 有什么问题?

我们的:

read_fasta()

最终返回:

{    ”seq1”: ”...”,    ”seq2”: ”...”,    ”seq3”: ”...”}

对于一个:

10 MB FASTA

可能完全没问题。

可是如果是:

human genome FASTA

甚至更大的 metagenome assembly:

20 GB50 GB100 GB

那么这个设计意味着:

读取 seq1放进 dictionary读取 seq2也放进 dictionary读取 seq3继续放...整个 FASTA全部留在 memory

但假设我们的目的只是:

计算每条 contig 的长度

其实没有必要把所有 sequence 同时留在内存。

我们真正希望:

读到 contig1算长度完成读到 contig2算长度完成

也就是:

一次产生一个 record。

这就是 generator 的工作。


9. yield:第一次认识 generator

先不看 FASTA。

从一个非常小的例子开始。

普通 function:

def get_numbers():    return [123]

调用:

numbers = get_numbers()

此时:

numbers

已经是:

[1, 2, 3]

三个数字全部已经产生。


现在写:

def generate_numbers():    yield 1    yield 2    yield 3

调用:

numbers = generate_numbers()

你可能以为:

numbers

会是:

[1, 2, 3]

但实际上:

type(numbers)

是:

generator

真正使用:

for number in numbers:    print(number)

才得到:

123

10. yield 和 return 的关键区别

普通:

return value

意味着:

把 value 返回function 结束

而:

yield value

意味着:

产生一个 value暂停 function保留当前位置和状态下次继续

例如:

def demo():    print(”A”)    yield 1    print(”B”)    yield 2    print(”C”)

执行:

g = demo()

此时甚至还没有打印:

A

然后:

next(g)

输出:

A

并得到:

1

程序暂停在:

yield 1

第二次:

next(g)

从那里继续:

B

然后产生:

2

第三次:

next(g)

继续:

C

然后 generator 结束。


11. Generator 的真正心智模型

可以把普通 function 想成:

function完成所有工作return 最终结果

而 generator:

function做到一部分yield 一个结果暂停          ↑          │调用者再次请求          │继续 ←─────yield 下一个结果

所以 generator 特别适合:

很多数据+一次只需要一个

比如:

FASTA recordsFASTQ readsVCF variantsSAM alignments日志文件大型 TSV

12. 把 FASTA parser 改成 generator

现在我们不再返回:

{    ”seq1”: ”...”,    ”seq2”: ”...”,    ...}

而希望:

for seq_id, seq in read_fasta(”genome.fa”):    print(seq_id, len(seq))

每解析完一个 sequence,就:

yield seq_id, seq

完整代码:

def read_fasta(path):    current_id = None    sequence_parts = []    with open(path) as f:        for line_number, line in enumerate(f, start=1):            line = line.strip()            if line == ””:                continue            if line.startswith(”>”):                if current_id is not None:                    sequence = ””.join(sequence_parts)                    yield current_id, sequence            current_id = line[1:].strip()            if current_id == ””:                raise ValueError(f”Empty FASTA header at line {line_number}”)            sequence_parts = []        else:            if current_id is None:                raise ValueError(f”Sequence before header at line {line_number}”)            sequence_parts.append(line)    if current_id is not None:        sequence = ””.join(sequence_parts)        yield current_id, sequence

这就是我们这一课最重要的一段代码。


13. Generator FASTA parser 究竟发生了什么?

假设:

>seq1AAAA>seq2CCCC>seq3GGGG

运行:

for seq_id, seq in read_fasta(”test.fa”):    print(seq_id, seq)

程序大致经历:

打开文件读 >seq1记录 seq1读 AAAA保存到 sequence_parts读 >seq2seq1 已经完整yield (”seq1”, ”AAAA”)

此时 parser:

暂停。

调用者拿到:

(”seq1”, ”AAAA”)

执行:

print(...)

然后下一轮 for 再向 generator 请求 record。

Parser 从暂停位置继续:

开始 seq2读 CCCC遇到 >seq3yield (”seq2”, ”CCCC”)

如此继续。

所以内存中通常只需要保留:

当前 record

而不是:

所有 records

14. Streaming:为什么 generator 特别适合 genomics

假设一个 FASTA:

50 GB

包含:

2,000,000 contigs

如果:

sequences = read_all_fasta(...)

返回 dictionary:

50 GB 数据大量对象全部留在 RAM

可能根本不可行。

而 generator:

for seq_id, seq in read_fasta(path):    ...

则近似:

磁盘 ↓当前 contig ↓分析 ↓丢掉 ↓下一个 contig

这就是:

streaming

要注意 generator 并不神奇地让 sequence 本身消失。

如果某一条 chromosome 本身:

250 Mb

你仍然需要保存这一条 sequence 的内容。

但是你不再需要同时保存:

所有 chromosome

这已经是巨大的差别。


15. 一个真正实用的 FASTA streaming 分析

假设我们只想统计:

sequence IDlengthGC content

完全不需要把整个 FASTA 保存成 dictionary。

可以:

def gc_content(seq):    if len(seq) == 0:        raise ValueError(”Sequence is empty”)    gc = seq.count(”G”) + seq.count(”C”)    return gc / len(seq)with open(”summary.tsv”, ”w”) as outfile:    outfile.write(”seq_id\tlength\tgc_content\n”)    for seq_id, seq in read_fasta(”genome.fa”):        length = len(seq)        gc = gc_content(seq)        outfile.write(f”{seq_id}\t{length}\t{gc}\n”)

这里的数据流非常漂亮:

genome.fa   ↓read_fasta()   ↓一个 sequence   ↓len / gc_content   ↓写 summary.tsv   ↓丢掉 sequence   ↓下一个

整个分析不需要:

all_sequences = ...

这种变量。

这就是大型 sequencing workflow 非常重要的一种设计方式。


16. Generator 是 iterator,所以可以直接 for

Lesson 05 我们已经用过:

for x in something:

以前的:

something

通常是:

listtupledictstringfile

现在 generator 也可以:

forrecordinread_fasta(path):    ...

因为 generator 是一种:

iterator

你可以暂时这样建立关系:

iterable└── 可以被 for 遍历iterator└── 可以逐个产生下一个值generator└── 一种很方便的 iterator

现阶段不需要深入 Python iterator protocol。

只需要记住:

yield

让 function 变成:

generator function

调用它后得到:

generator object

然后:

for

会不断向它请求下一个结果。


17. Generator 只能走一次

这是一个很重要的特点。

g = read_fasta(”genome.fa”)

第一次:

for seq_id, seq in g:    print(seq_id)

会遍历所有 sequence。

然后再:

for seq_id, seq in g:    print(seq_id)

通常什么都不会输出。

因为 generator 已经:

exhausted

也就是已经走到结尾。

如果想重新读取:

g = read_fasta(”genome.fa”)

重新创建一个 generator。

或者直接:

for seq_id, seq in read_fasta(”genome.fa”):    ...

以后需要第二遍时再次:

for seq_id, seq in read_fasta(”genome.fa”):    ...

18. 不要随手 list(generator),否则可能失去 streaming 优势

你当然可以:

records = list(    read_fasta(”genome.fa”))

得到:

[    (”seq1”, ”...”),    (”seq2”, ”...”),    ...]

但是这意味着:

generator所有 recordslist全部进入 memory

所以如果你写 generator 本来是为了处理:

50 GB FASTA

然后马上:

list(...)

那就基本把 streaming 的优势又取消了。

当然对于测试小文件:

records = list(read_fasta(”tiny.fa”))

非常方便。

关键不是:

永远不能 list()

而是要知道:

你是在主动选择把所有结果 materialize 到 memory。


19. FASTQ:record boundary 又不一样了

FASTA 的 record:

>headersequence...

FASTQ 的一个标准 record 通常是四行:

@read1ATGCGTAC+IIIIIIII

分别是:

line 1 → headerline 2 → sequenceline 3 → +line 4 → quality

连续 records:

@read1ATGCGT+IIIIII@read2GGCAAT+HHHHHH

所以 FASTQ parser 的逻辑和 FASTA 不完全相同。

FASTA:

record 长度不固定靠下一个 > 判断边界

FASTQ:

通常一个 record = 4 行

因此它反而更适合用:

readline()

一次读取四行。


20. 一个最基础的 FASTQ parser

def read_fastq(path):    with open(path) as f:        while True:            header = f.readline()            if header == ””:                break            sequence = f.readline()            plus = f.readline()            quality = f.readline()            header = header.strip()            sequence = sequence.strip()            plus = plus.strip()            quality = quality.strip()            yield header, sequence, quality

这里第一次在文件 parser 中真正使用:

while True:

流程是:

读 header如果 EOF → break读 sequence读 +读 qualityyield 一个 read下一轮

21. 为什么用 header == "" 判断 EOF,而不是 strip() == ""

这一点值得注意。

f.readline()

在真正到达文件结尾时返回:

””

所以:

if header == ””:    break

表示:

没有更多数据。

但如果文件中只是一个空行:

”\n”

它并不等于:

””

这两个概念不同:

”\n”→ 文件里真的有一行,只是内容为空””→ EOF,没有任何字符可读

这也是为什么文件处理时:

””

具有特殊意义。


22. FASTQ parser 必须做验证

上面的 parser 太信任输入。

假设文件坏了:

@read1ATGCGT+

quality 行没了。

我们的 parser 可能仍然产生一个奇怪的 record。

所以应该检查:

def read_fastq(path):    with open(path) as f:        record_number = 0        while True:            header = f.readline()            if header == ””:                break            sequence = f.readline()            plus = f.readline()            quality = f.readline()            recored_number += 1            if(sequence == ”” or plus == ”” or quality == ””):                raise ValueError(f”Incomplete FASTQ record {record_number}”)            header = header.strip()            sequence = sequence.strip()            plus = plus.strip()            quality = quality.strip()            if not header.startswith(”@”):                raise ValueError(F”Invalid FASTQ header in recored {record_number}”)            if not plus.startswith(”+”):                raise ValueError(F”Invalid FASTQ separator in recored {record_number}”)            if len(sequence) != len(quality):                raise ValueError(f”Sequence/quality length mismatch in recored {record_number}”)            yield header, sequence, quality

现在 parser 已经相当像真正的数据工具了。


23. 为什么 FASTQ 的 sequence 和 quality 长度必须相等?

例如:

@read1ATGCGA+IIIIII

sequence:

A T G C G A

一共:

6

quality:

I I I I I I

也是:

6

因为每一个 nucleotide 都对应一个 quality score。

所以:

len(sequence) == len(quality)

应该成立。

如果:

ATGCGA

对应:

IIIII

只有 5 个 quality character,就说明 record 有问题。

因此:

if len(sequence) != len(quality):    raise ValueError(...)

是非常自然的 validation。


24. 为什么这里 yield header[1:], sequence, quality

原始 header:

@read1

和 FASTA 类似,我们往往不需要:

@

所以:

header[1:]

得到:

read1

于是:

for read_id, seq, qual in read_fastq(”reads.fastq”):    ...

非常自然。


25. FASTA 和 FASTQ parser 的差别

现在可以比较:

FASTA
FASTQ
header marker
>@
sequence
可以跨多行
常见格式中 record 内 sequence 为一行
quality
没有
record 边界
下一个 >
四行 record
parser 关键状态
当前 header + sequence parts
每次读取四行
常见输出
(seq_id, seq)(read_id, seq, qual)

所以:

Parser 不是背一个固定模板。

真正重要的是:

先理解文件格式找出 record boundary决定程序需要保存什么 state逐条产生 structured record

26. Parser 和 analysis 应该分开

一个非常值得现在建立的软件设计习惯是:

不要写:

def read_fasta_and_calculate_gc_and_filter_and_write(...):    ...

更推荐:

def read_fasta(path):    ...

只负责:

FASTAPython records

然后:

def gc_content(seq):    ...

只负责:

sequenceGC content

最后主程序组合:

for seq_id, seq in read_fasta(path):    gc = gc_content(seq)

为什么?

因为同一个:

read_fasta()

以后可以被不同任务复用。

比如:

for seq_id, seq in read_fasta(path):    print(seq_id, len(seq))

或者:

for seq_id, seq in read_fasta(path):    print(seq_id, gc_content(seq))

或者:

for seq_id, seq in read_fasta(path):    kmers = generate_kmers(seq, 31)

或者:

for seq_id, seq in read_fasta(path):    if len(seq) >= 1000:        ...

Parser 不需要知道:

“用户之后想对 sequence 做什么。”

它只负责:

把格式解析正确。

这叫:

separation of concerns

也是以后写科研软件非常重要的设计原则。


27. 一个完整 pipeline:过滤短 contig 并写 FASTA

假设:

assembly.fa

我们想保留:

长度 >= 1000 bp

的 contig。

现在已经非常简单:

def filter_fasta(input_path, output_path, min_length):    with open(output_path, ”w”) as outfile:        for seq_id, seq in read_fasta(input_path):            if len(seq) < min_length:                continue            outfile.write(f”>{seq_id}\n”)            outfile.write(f”{seq}\n”)

调用:

filter_fasta(    ”assembly.fa”,    ”assembly.filtered.fa”,    1000)

数据流:

assembly.faread_fasta一个 contiglength filter写入 filtered.fa下一个 contig

注意这里甚至不需要:

filtered_sequences = []

因为每一个通过过滤的 record 可以立即写入文件。

这就是:

stream input+stream processing+stream output

28. FASTA sequence 输出时为什么有时要 wrap

我们刚才输出:

outfile.write(f”{seq}\n”)

于是 sequence 全部在一行。

例如:

>chr1ATGCGTACGTACGTACGTACGTACGT...

这在很多情况下是合法的。

但是很多 FASTA 文件会把 sequence 按:

60 bp80 bp

等长度换行。

例如:

>seq1ATGCGTACGTAACCGGTTAAGGCC...

现在我们已经有能力自己完成。

例如每 80 个字符:

for i in range(0, len(seq), 80):    outfile.write(        seq[i:i + 80] + ”\n”    )

这里重新使用了:

range+slicing

以前学到的内容又回来了。


29. 写一个 write_fasta_record()

为了不让写 FASTA 的代码到处重复,可以单独封装:

def write_fasta_record(outfile, seq_id, seq, width=80):    outfile.write(f”>{seq_id}\n”)    for i in range(0, len(seq), width):        outfile.write(seq[i:i + width] + ”\n”)

然后过滤程序变成:

def filter_fasta(input_path, output_path, min_length):    with open(output_path, ”w”) as outfile:        for seq_id, seq in read_fasta(input_path):            if len(seq) < min_length:                continue            write_fasta_record(outfile, seq_id, seq)

结构明显更清楚:

read_fasta→ 负责读filter_fasta→ 负责决定留不留write_fasta_record→ 负责 FASTA 输出格式

30. 不要默认 header 只有一个单词

真实 FASTA header 可能是:

>NC_000001.11 Homo sapiens chromosome 1

如果:

current_id = line[1:]

得到:

NC_000001.11 Homo sapiens chromosome 1

如果只想要 identifier:

current_id = line[1:].split()[0]

得到:

NC_000001.11

这两个设计都可能是正确的。

关键是:

Parser 应该明确自己保留的是完整 header,还是第一个 identifier field。

不要无意识地丢掉 metadata。

例如更清楚的设计可能是:

header = line[1:].strip()seq_id = header.split()[0]

然后决定:

yield seq_id, seq

还是:

yield header, seq

31. 不要在 parser 里过度“清洗”数据

假设 FASTA sequence:

atgcgt

你当然可以:

sequence = sequence.upper()

但这属于一个设计决定。

Parser 是否应该:

原样读取

还是:

自动转 uppercase

没有永远唯一答案。

对于科研程序,更重要的是:

行为应该明确,而不是偷偷修改输入。

例如:

def read_fasta(path, uppercase=False):    ...

然后:

if uppercase:    sequence = sequence.upper()

会比无条件修改更清楚。

现在不必立刻把 parser 设计得这么复杂,但需要开始有这种意识。


32. 一个更完整的 FASTA generator

到目前为止,我们可以写成:

from pathlib import Pathdef read_fasta(path):    path = Path(path)    if not path.exists():        raise FileNotFoundError(f”FASTA file no found: {path}”)    current_id = None    sequence_parts = []    with open(path, ”r”) as f:        for line_number, line in enumerate(f, start=1):            line = line.strip()            if line == ””:                continue            if line.startswith(”>”):                if current_id is not None:                    sequence = ””.join(sequence_parts)                    yield current_id, sequence                current_id = line[1:].strip()                if current_id == ””:                    raise ValueError(f”Empty FASTA header at line {line_number}”)                sequence_parts = []            else:                if current_id is None:                    raise ValueError(f”sequence before FASTA header at line {line_number}”)                sequence_parts.append(line)    if current_id is not None:        sequence = ””.join(sequence_parts)        yield current_id, sequence

调用

for seq_id, seq in read_fasta(”genome.fa”):    print(seq_id, len(seq))

这已经是一个真正有实用价值的 parser。


33. 一个更完整的 FASTQ generator

from pathlib import Pathdef read_fastq(path):    path = Path(path)    if not path.exists():        raise FileNotFoundError(f”FASTQ file no found: {path}”)    with open(path, ”r”) as f:        record_number = 0        while True:            header = f.readline()            if header == ””:                break            sequence = f.readline()            plus = f.readline()            quality = f.readline()            record_number += 1            if(sequence == ”” or plus == ”” or quality == ””):                raise ValueError(f”Incomplete FASTQ record {record_number}”)            header = header.strip()            sequence = sequence.strip()            plus = plus.strip()            quality = quality.strip()            if not header.startswith(”@”):                raise ValueError(f”Invalid FASTQ header in record {record_number}”)            if not plus.startswith(”+”):                raise ValueError(f”Invalid FASTQ separator in record {record_number}”)            read_id = header[1:]            yield read_id, sequence, quality
for read_id, seq, qual in read_fastq( ”reads.fastq” ):    print( read_id, len(seq) )

34. FASTQ quality 现在先当作 string

例如:

IIIIII

目前:

quality

只是:

”IIIIII”

我们暂时不解释:

ASCIIPhred+33quality score conversion

因为那会把这一课的重点从:

parser + generator + streaming

带到 sequencing quality encoding。

现在只需要知道:

每一个 quality character一个 nucleotide

所以:

len(sequence) == len(quality)

是一个重要的格式检查。


35. yield 后面的值也可以是 tuple

我们写:

yield seq_id, sequence

其实等价于产生:

(seq_id, sequence)

所以:

forrecordinread_fasta(path):    print(record)

得到:

('seq1''ATGC...')('seq2''GGAA...')

然后:

for seq_id, sequence in read_fasta(path):

只是再次使用:

tuple unpacking

也就是说:

yield+tuple+for+unpacking

又组合起来了。


36. next():手动看看 generator 怎样工作

假设:

g = read_fasta(”tiny.fa”)

可以:

first = next(g)

得到:

(”seq1”, ”ATGCGT”)

再:

second = next(g)

得到:

(”seq2”, ”GGGAAA”)

如果没有更多结果,再:

next(g)

会出现:

StopIteration

通常你不需要自己处理它。

因为:

for

已经自动帮你处理。

也就是说:

for record in generator:    ...

本质上不断做类似:

nextnextnext...

直到:

StopIteration

37. Generator 让 pipeline 非常自然

有了:

read_fasta()

之后,可以直接:

for seq_id, seq in read_fasta(”assembly.fa”):    if len(seq) < 1000:        continue    gc = gc_content(gc)    print(seq_id, len(seq), gc)

甚至可以把不同处理阶段继续拆开。

例如:

def long_sequences(records, min_length):    for seq_id, seq in records:        if len(seq) >= min_length:            yield seq_id, sqe

于是:

records = read_fasta(”assembly.fa”)records = long_sequences(    records,    min_length=1000)for seq_id, seq in records:    print(seq_id)

数据像水一样流过:

FASTAread_fastalong_sequencesanalysis

这种设计以后在:

genomicsdata engineeringcommand-line pipelines

里都会不断遇到。


38. Generator 并不意味着“代码一定更快”

需要纠正一个容易产生的误解:

generator自动运行更快

它最大的优势通常是:

memory efficiency+lazy evaluation+streaming

例如:

list(range(100000000))

需要把大量 integer 全部放进 memory。

而:

range(100000000)

本身不会生成一个装满一亿个 integer 的 list。

Generator 也类似:

需要一个才产生一个

这叫:

lazy evaluation

它可能改善整体 workflow,但重点不是“神奇加速”,而是:

不提前计算和储存所有结果。


39. R → Python:generator 可以怎样理解?

R 里很多常见 workflow 更偏:

读取完整 objectvector / dataframe操作整个 object

Python generator 的思路更接近:

建立一个数据流调用者需要下一个才产生下一个

所以不要强行寻找一个:

R generator = Python generator

的一对一对应。

真正值得建立的新思维是:

不是所有数据都必须先完整存在内存里,然后程序才能开始分析。

这对 genomics 尤其重要。


40. 本课综合程序:FASTA summary

现在把 Lesson 01–09 串起来。

目标:

input:assembly.faoutput:assembly_summary.tsv

输出:

seq_id    length    gc_contentcontig1   18342     0.4832contig2   9271      0.5011...

代码:

from pathlib import Pathdef gc_content(seq):    if len(seq) == 0:        raise ValueError(”Sequence is Empty”)    gc = seq.count(”C”) + seq.count(”G”)    return gc / len(seq)def read_fasta(path):    path = Path(path)    if not path.exists():        raise FileNotFoundError(f”FASTA file not found: {path}”)    current_id = None    sequence_parts = []    with open(path) as f:        for line_number, line in enumerate(f, start=1):            line = line.strip()            if line == ””:                continue            if line.startswith(”>”):                if current_id is not None:                    yield current_id, ””.join(sequence_parts)                current_id = line[1:].strip()                if current_id == ””:                    raise ValueError(f”Empty FASTA header at line {line_number}”)                sequence_parts = []            else:                if current_id is None:                    raise ValueError(f”Sequence before header at line {line_number}”)                sequence_parts.append(line)    if current_id is not None:        yield(current_id, ””.join(sequence_parts))def summarize_fasta(input_path, output_path):    with open(output_path, ”w”) as outfile:        outfile.write(”seq_id\tlength\tgc_content\n”)        for seq_id, seq in read_fasta(input_path):            outfile.write(                f”{seq_id}\t”                f”{len(seq)}\t”                f”{gc_content(seq)}\n”            )
summarize_fasta( ”genome.fa”, ”genome_summary.tsv” )

这段程序已经非常接近一个小型真实 bioinformatics tool。


这里串起了 Lesson 01–09

String

line.strip()line.startswith(”>”)””.join(...)

List

sequence_parts = []sequence_parts.append(line)

Tuple

yield seq_id, seq

Unpacking

for seq_id, seq in read_fasta(...):

Condition

if line.startswith(”>”):

Loop

for line in f:

和:

while True:

continue / break

continuebreak

Function

gc_content()read_fasta()summarize_fasta()

Exception

raise ValueError(...)raise FileNotFoundError(...)

File IO

with open(...)

Path

Path(...)

Generator

yield

这已经不是孤立语法了。

它们开始形成:

真实数据处理程序

Exercises


Exercise A|预测 FASTA parser 的输出

文件:

test.fa

内容:

>seq1ATGCGTAA>seq2CCGG

执行:

for seq_id, seq in read_fasta(”test.fa”):    print(seq_id, seq, len(seq))

问输出是什么?

第一个 record:

ATGCGTAA

需要拼接成:

ATGCGTAA

长度:

8

第二个:

CCGG

长度:

4

因此:

seq1 ATGCGTAA 8seq2 CCGG 4

这里最重要的是:

FASTA 的换行通常不是 sequence 本身的一部分。


Exercise B|找出 parser bug

下面 parser 有问题:

def read_fasta(path):    current_id = None    parts = []    with open(path) as f:        for line in f:            line = line.strip()            if line.startswith(”>”):                if current_id is not None:                    yield current_id, ””.join(parts)                current_id = line[1:]                parts = []            else:                parts.append(line)

对于:

>seq1AAAA>seq2CCCC

它只会产生:

seq1

为什么?

应该怎样修改?

因为 record 是在:

遇到下一个 header

时才:

yield

所以:

seq1

在遇到:

>seq2

时会被产生。

但是:

seq2

后面没有第三个 header。

文件直接结束了。

因此需要在 loop 之后:

if current_id is not None:    yield current_id, ””.join(parts)

完整修复:

def read_fasta(path):    current_id = None    parts = []    with open(path) as f:        for line in f:            line = line.strip()            if line.startswith(”>”):                if current_id is not None:                    yield current_id, ””.join(parts)                current_id = line[1:]                parts = []            else:                parts.append(line)    if current_id is not None:        yield current_id, ””.join(parts)

这是 parser 中非常典型的:

last-record problem

Exercise C|只打印长度超过 1000 bp 的 contig

假设已经有:

read_fasta()

要求:

读取 assembly.fa只输出 length >= 1000 的 sequence ID 和 length

不要把所有 sequence 放入 list。

for seq_id, seq in read_fasta(”assembly.fa”):    if len(seq) > 1000:        print(seq_id, len(seq))

Exercise D|计算 FASTA 中 sequence 的数量

要求写:

def count_sequences(path):    ...

利用:

read_fasta()

不要再重新解析一次 FASTA 格式。

答案

def count_sequences(path):    count = 0    for seq_id, seq in read_fasta(path):        count += 1    return count

调用:

n = count_sequences(”genome.fa”)

也可以:

def count_sequences(path):    return sum(        1        for _ in read_fasta(path)    )

不过第二种涉及 generator expression。

现在能看懂即可,不要求优先这样写。

第一种更适合当前阶段。


Exercise E|找到最长 contig

要求:

def longest_sequence(path):    ...

返回:

(seq_id, sequence)

例如:

>seq1AAAA>seq2CCCCCCCC>seq3GGG

返回:

(”seq2”, ”CCCCCCCC”)

不能把整个文件先变成 dictionary。

答案

def longest_sequence(path):    longest_id = None    longest_seq = ””    for seq_id, seq in read_fasta(path):        if len(seq) > len(longest_seq):            longest_id = seq_id            longest_seq = seq    if longest_id is None:        raise ValueError(            ”FASTA file contains no sequences”        )    return longest_id, longest_seq

这里虽然整个 FASTA 可以很大,但程序只保留:

当前 sequence+目前发现的最长 sequence

而不是所有 sequence。

这就是 streaming algorithm 的一个典型例子。


Exercise F|写一个 FASTA filter

要求:

def filter_fasta(    input_path,    output_path,    min_length):    ...

只保留:

len(seq) >= min_length

的 sequence。

输出要求 sequence 每行最多:

60 bp
def filter_fasta(input_path, output_path, min_length, width = 60):    with open(output_path, ”w”) as outfile:        for seq_id, seq in read_fasta(input_path):            if len(seq) >= min_length:                outfile.write(f”>{seq_id}\n”)                for i in range(0len(seq), width):                    outfile.write(seq[i:i + width] + ”\n”)
def write_fasta_record(outfile, seq_id, seq, width=60):    outfile.write(f”>{seq_id}\n”)    for i in range(0len(seq), width):        outfile.write(seq[i:i + width] + ”\n”)def filter_fasta(input_path, output_path, min_length):    with open(output_path, ”w”) as outfile:        for seq_id, seq in read_fasta(input_path):            if len(seq) < min_length:                continue            write_fasta_record(outfile,seq_id,seq)

Exercise G|判断下面代码是否仍然是 streaming

代码 1:

for seq_id, seq in read_fasta(    ”genome.fa”):    print(seq_id)

代码 2:

records = list(    read_fasta(”genome.fa”))for seq_id, seq in records:    print(seq_id)

哪一个保持了 generator 的主要 memory 优势?

代码 1:

for seq_id, seq in read_fasta(...):

保持 streaming。

大致:

产生一个处理一个继续下一个

代码 2:

records = list(read_fasta(...))

会首先把 generator 中的所有结果收集成:

list

因此:

所有 sequence同时进入 memory

所以对于大型 FASTA:

list(read_fasta(...))

通常会失去 generator 最重要的 memory advantage。

但对于小测试文件:

list(...)

仍然非常方便。


Exercise H|FASTQ parser

假设:

reads.fastq

内容:

@read1ATGCGT+IIIIII@read2GGCA+HHHH

要求:

for read_id, seq, qual in read_fastq(...):    ...

依次产生:

(”read1”, ”ATGCGT”, ”IIIIII”)(”read2”, ”GGCA”, ”HHHH”)

并检查:

header 以 @ 开头第三行以 + 开头sequence 与 quality 长度一致
def read_fastq(path):    path = Path(path)    with open(path) as f:        record_number = 0        while True:            header = f.readline()            if header == ””:                break            sequence = f.readline()            plus = f.readline()            quality = f.readline()            record_number += 1            if (sequence == ”” or plus == ”” or quality == ””):                raise ValueError(f”Incomplete FASTQ record {record_number}”)            header = header.strip()            sequence = sequence.strip()            plus = plus.strip()            quality = quality.strip()            if not header.startswith(”@”):                raise ValueError(f”Invalid header in FASTQ record {record_number}”)            if not plus.startswith(”+”):                raise ValueError(f”Invalid separator in FASTQ record {record_number}”)            if len(sequence) != len(quality):                raise ValueError(f”Sequence/quality length mismatch in FASTQ record {record_number}”)            yield (header[1:], sequence, quality)
for read_id, seq, qual in read_fastq(”reads.fastq”):    print(read_id, seq, qual)

Exercise I|综合题:统计 FASTQ reads

写:

def summarize_fastq(    input_path,    output_path):    ...

输入:

@read1ATGCGT+IIIIII@read2GGGG+HHHH@read3ATATAT+JJJJJJ

输出:

read_id	length	gc_contentread1	6	0.5read2	4	1.0read3	6	0.0

要求:

复用 read_fastq()复用 gc_content()逐条读取不要把全部 reads 放入 list
def summarize_fasta(input_path, output_path):    with open(output_path, ”w”) as outfile:        outfile.write(f”read_id\tlength\tgc_content\n”)        for read_id, seq, qual in read_fastq(input_path):            length = len(seq)            gc = gc_content(seq)            outfile.write(f”{read_id}\t{length}\t{gc}\n”)

Exercise J|本课最重要的思考题

下面两种程序设计:

sequences = read_all_fasta(”assembly.fa”)for seq_id, seq in sequences.items():    ...
for seq_id, seq in read_fasta(    ”assembly.fa”):    ...

什么时候设计 1 可能更合适?

什么时候设计 2 更合适?

如果:

文件很小+后续需要反复随机访问 sequence

例如:

sequences[”seq37”]

或者同一批 sequences 要被重复使用很多次,那么 dictionary:

{    seq_id: sequence}

可能非常方便。

因为数据已经全部存在 memory。


如果:

文件很大+通常每条 sequence 只处理一次

例如:

计算长度计算 GC过滤统计写新文件

那么 generator:

for seq_id, seq in read_fasta(...):

通常更适合。

所以并不是:

generator 永远比 dictionary 好

真正的判断标准是:

数据规模+访问方式+后续任务+内存需求

这其实已经开始进入:

程序设计与算法选择

而不仅仅是 Python syntax。


# Lesson 09 最重要的几个心智模型## ① Parser 的核心不是 `split()`Lesson 08 的 TSV:```text一行 = 一个 record

所以:

split()

就够了。

但 FASTA 告诉我们:

record 可以跨很多行

真正的 parser 必须:

识别 record boundary+保存 state+组装完整 record

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-09-01 03:30:44 HTTP/2.0 GET : https://f.mffb.com.cn/a/512366.html
  2. 运行时间 : 0.192523s [ 吞吐率:5.19req/s ] 内存消耗:4,843.56kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=d3505c1aa7be31482f8e35a0b2db1000
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.001257s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001788s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000729s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000633s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001269s ]
  6. SELECT * FROM `set` [ RunTime:0.000567s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001507s ]
  8. SELECT * FROM `article` WHERE `id` = 512366 LIMIT 1 [ RunTime:0.002852s ]
  9. UPDATE `article` SET `lasttime` = 1788204644 WHERE `id` = 512366 [ RunTime:0.005648s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.000586s ]
  11. SELECT * FROM `article` WHERE `id` < 512366 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001233s ]
  12. SELECT * FROM `article` WHERE `id` > 512366 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001098s ]
  13. SELECT * FROM `article` WHERE `id` < 512366 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.002191s ]
  14. SELECT * FROM `article` WHERE `id` < 512366 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.002527s ]
  15. SELECT * FROM `article` WHERE `id` < 512366 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.004032s ]
0.196268s