ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FASTA与FASTQ格式详解:从Phred质量值到Python处理实战

FASTA与FASTQ格式详解:从Phred质量值到Python处理实战 干生物信息学这一行FASTA和FASTQ两种格式就像厨师的刀工和火候天天都在用但很少有人停下来把细节琢磨透。我见过不少朋友拿到测序数据后对着一串质量字符发懵也见过有同学因为搞错质量编码体系跑完整个数据清洗流程后才发现结果全偏了。这两种看起来只是“文本文件”的格式其实藏着不少设计逻辑和约定俗成的规则。这篇文章我就把FASTA和FASTQ从历史、语法、质量值计算到Python解析、格式转换、常见坑位全部拆开讲一遍力求看完就能直接上手处理自己的数据。1. 两种格式的前世今生1.1 FASTA一台运行了快四十年的老引擎FASTA格式的最初形态可以追溯到1985年David Lipman和William Pearson发表了FASTP程序用于快速的蛋白序列相似性搜索随后衍生出FASTA算法。这个格式从诞生起就很简单用开头的描述行作为序列的唯一标记后面跟着纯字母的序列内容。想想看那时候连个人电脑都还没普及设计者自然优先考虑“人眼可读、程序好解析”所以整个格式就是纯文本没有任何二进制加密也没有复杂的嵌套结构。这种简洁带来的好处在今天依然明显。GenBank、UniProt、Ensembl这些数据库导出的参考序列几乎都用FASTA作为交换格式BLAST、MUSCLE、HMMER等比对工具也都直接吃FASTA。哪怕你的序列只有一行或者长达几百万个碱基的染色体语法规则完全一致。坦率地说FASTA的设计思路和Unix哲学很像每个工具只做一件事文件只是流式的纯文本中转。正因如此它活了快四十年都没有被淘汰。但也有代价。FASTA不携带任何质量信息你只知道这个位置是“A”还是“T”不知道这个碱基到底靠不靠谱。对于早期一代测序那种“测一条序列反复验证”的场景还好到了高通量测序时代每条read都是数百万条并行产出没有质量信息根本没法判断数据可用性。于是FASTQ顺势而生。1.2 FASTQ给序列配上“体检报告”FASTQ格式最早出现在Wellcome Trust Sanger Institute由Jim Mullikin和Richard Durbin等人在2000年前后设计目的是把Phred碱基识别程序输出的质量分数和碱基序列放进同一个文件。这个名字很有意义FAST“快”Q代表Phred质量分数Quality。从诞生之初FASTQ就定位成“FASTA加上质量字符”的扩展格式因此保留了FASTA的部分习惯比如以开头对应FASTA的第三行用作分隔。但和FASTA不同FASTQ固定以四行为一个单位也可理解为四条线第一行是标识符第二行是序列第三行是有的实现会重复标识符第四行是与序列等长的质量字符串。这种“四行一条记录”的结构十分紧凑一条read的所有信息都塞在几行文本里非常符合测序仪批量产出的需求。质量值本身也有讲究。测序仪判断每个碱基的荧光信号后会给出一个错误概率P再转换成Phred质量分数Q-10log10(P)。Q20表示该碱基错误概率为1%Q30为0.1%。我们看到的FASTQ第四行不是什么随机符号而是把Q值加上一个偏移量之后映射成的ASCII字符。以目前最通用的Phred33体系为例ASCII码33对应Q0也就是字符!ASCII码53对应Q20也就是字符5。这套编码把数字评分变成了“肉眼不可读但机器很好解析”的字符流在存储和传输上非常高效。2. 格式语法逐行拆解2.1 FASTA的每一行都有讲究一条标准的FASTA记录长得像这样chr1_12345_67890 sample_description ACGTACGTACGTACGTACGT ACGTACGTACGTACGTACGT第一行以开始后面接着标识符ID和可选的描述。这里的空格其实是个容易踩的坑很多工具会默认把后面的内容按空格拆开第一段作为序列ID其余部分作为描述。比如chr1 abc def有的程序认为ID是chr1有的程序则把chr1 abc def整个当作名称。NCBI的FASTA规范通常使用标识符 描述这种格式所以如果你要保证兼容性最好自己约定一种写法不要随便在ID里塞空格。序列行只能用标准的IUPAC核酸或氨基酸字母核酸包括A、C、G、T、URNA用以及简并碱基R、Y、S、W、K、M、B、D、H、V、N蛋白序列则有20种常见氨基酸字母再加上BAsx、ZGlx、X未知、*终止等。序列行内不能出现数字、空格或特殊符号否则很多比对软件会直接报错。还有两点值得注意。第一FASTA的序列允许换行也就是说一条完整序列可以拆成多行这主要是为了在终端里阅读方便。也因此程序解析时必须等遇到下一个或文件结束才算终止。第二描述行有时候会包含数据库的管道符号如sp|P69905|HBA_HUMAN这种NCBI风格如果只用split()取ID会拿到sp|P69905|HBA_HUMAN整体而不是P69905具体取哪一段取决于你的需求。2.2 FASTQ四行结构的每个细节FASTQ的每条read有且仅有四行EAS139:136:FC706VJ:2:2104:15343:197393 1:Y:18:ATCACG GATTTGGGGTTCAAAGCAGTATCGATCAAATAGTAAATCCATTTGTTCAACTCACAGTTT !*((((***))%%%)(%%%%).1***-*))**55CCFCCCCCCC65逐行拆解第1行以开头紧跟着read的唯一标识。以Illumina平台为例后面的字段通常包含仪器ID、flowcell编号、lane、tile、x坐标、y坐标等信息冒号分隔还可能有1:Y:18:ATCACG这样的过滤标记和index序列。对于双端测序read 1和read 2常通过/1、/2或2:N:0:index来区分。第2行是实际的碱基序列长度就是read length只能用核酸字母。第3行以开头后面的内容理论上可以重复第1行的标识符但大多数工具输出时这里为空。解析时不能因为这一行是空行就跳过它是四行结构中的固定“分界石”。第4行是质量字符串长度必须和第2行相同否则这条record就是损坏的。要特别留意有些非标准FASTQ会在第3行写一些额外注释或者在第1行之后空一行这类文件在严格解析时会被拒收。正常的FASTQ里质量字符不允许出现空格或换行因为Phred33体系里ASCII 32对应的字符就是空格Q-1非法所以看到空格基本可以断定编码或格式出了问题。2.3 Phred质量值从ASCII字符到错误概率质量编码体系是FASTQ最绕、也最容易出错的地方。我们先用一张表说明常见版本编码版本偏移量ASCII范围对应Q值范围典型场景Sanger / Phred333333-730-40Illumina 1.8、NCBI SRA、公共数据库Solexa / Phred646459-104理论59-126-5-40理论早期Solexa 2004-2006Illumina 1.36464-1040-402009年前后旧格式Illumina 1.56467-104实际区间更窄2-40部分旧数据Illumina 1.83333-730-40目前主流如果你拿到一个旧数据不知道它到底用的是哪个编码可以看质量字符的范围。标准Phred33下质量字符一般不会出现小于!ASCII 33的字符Phred64下最小的质量字符通常大于等于ASCII 64附近大量字符在h、i甚至更高。一个很粗糙的经验法则是如果文件里几乎看不到小于5ASCII 53的字符那有可能是Phred64如果能看到大量!、、#这些低ASCII基本就是Phred33。但要注意低质量的Phred64数据也可能出现接近ASCII 64的字符而高质量的Phred33数据可能出现8到~等较高ASCII字符所以单纯看范围判断并不百分百准确。更稳妥的办法是用FastQC跑一遍或者用Seqtk、BioPython等工具读取时人工抽检几条再对照平台说明文档。3. 实操解析、统计与格式转换3.1 Python手写一个FASTA解析器虽然有很多现成库可以调用但自己动手写能更深入理解格式细节。下面这个函数是我经常用的一个简化版核心思路是“逐行扫描、遇到切换当前记录、其余累加到序列缓冲区”def read_fasta(filepath): sequences {} with open(filepath, r) as f: current_id None current_seq [] for line in f: line line.strip() if not line: continue if line.startswith(): if current_id is not None: sequences[current_id] .join(current_seq) header line[1:].strip() # 取第一个空白符前的部分作为ID其余作为描述 parts header.split(None, 1) current_id parts[0] if parts else current_seq [] else: current_seq.append(line.upper()) if current_id is not None: sequences[current_id] .join(current_seq) return sequences这里有两个容易被忽略的细节。第一line.strip()会把Windows下的\r\n里的\r去掉避免污染序列第二如果原始序列里包含小写字母某些软件用大小写区分exon和intron我在读取时统一转大写但如果你想保留原始状态就不要加.upper()。这个按规范实现的解析器能很好地处理NCBI下载的多行FASTA。但它在序列长度非常大的染色体文件上会有内存压力——全部塞进字典后可能占用GB级内存。如果只想逐条处理并统计更好的方式是用生成器def iter_fasta(filepath): with open(filepath, r) as f: current_id None current_seq [] for line in f: line line.strip() if not line: continue if line.startswith(): if current_id is not None: yield current_id, .join(current_seq) current_id line[1:].split()[0] current_seq [] else: current_seq.append(line) if current_id is not None: yield current_id, .join(current_seq)用生成器时每条序列处理完就能释放内存。比如跑BLAST前想先看看每条序列的GC含量这种逐条yield的方式非常顺手for seq_id, seq in iter_fasta(genome.fasta): gc (seq.count(G) seq.count(C)) / len(seq) * 100 print(f{seq_id}: {len(seq)} bp, GC{gc:.2f}%)3.2 Python解析FASTQ并计算质量FASTQ的解析可以比FASTA更无脑因为四行是一个固定单元只要按下标读取就行。我的常用代码def parse_fastq(filepath): with open(filepath, r) as f: while True: ident_line f.readline() if not ident_line: break ident ident_line.strip() seq f.readline().strip() plus_line f.readline().strip() qual f.readline().strip() if not (ident.startswith() and plus_line.startswith()): raise ValueError(f格式错误: {ident}) if len(seq) ! len(qual): raise ValueError(f序列与质量长度不一致: {ident}) yield ident[1:], seq, qual用这个解析器能快速算平均质量import statistics def quality_to_phred(qual_str, offset33): return [ord(c) - offset for c in qual_str] if __name__ __main__: all_q [] read_count 0 for ident, seq, qual in parse_fastq(sample.fastq): q_scores quality_to_phred(qual) all_q.extend(q_scores) read_count 1 print(f共 {read_count} 条reads, 平均Q{statistics.mean(all_q):.2f}) print(fQ30率: {sum(1 for q in all_q if q 30) / len(all_q) * 100:.2f}%)在实际项目中计算每条read的平均质量比算全局平均更常用因为后续过滤往往要按每条read的Q值判断是否保留。你可以顺手在循环里维护一个list最后再统计分布。3.3 FASTA与FASTQ相互转换FASTQ转FASTA只需丢弃质量字符串并调整第一行的为这是最常规的操作。但要注意两点第一转换后的FASTA序列行通常会按60或80字符换行虽然不换行也能用但考虑到下游工具的可读性最好加上换行第二如果你在双端测序数据中做转换read1和read2的标识符顺序要保持一致否则后续比对时很难再配对上。def fastq_to_fasta(input_fastq, output_fasta, width80): with open(input_fastq, r) as fin, open(output_fasta, w) as fout: for ident, seq, qual in parse_fastq(fin): fout.write( ident \n) for i in range(0, len(seq), width): fout.write(seq[i:iwidth] \n)反向转换FASTA转FASTQ就麻烦一些因为FASTA本身不携带质量信息。通常做法是给每条序列填一个统一的占位质量字符串比如用IPhred33下Q40填满序列长度表示“假定所有碱基质量都很好”。这种假数据可以在测试流程时用但正式分析中不要拿它当成真实信号。def fasta_to_pseudo_fastq(input_fasta, output_fastq, q_charI): with open(input_fasta, r) as fin, open(output_fastq, w) as fout: for seq_id, seq in iter_fasta(fin): fout.write( seq_id \n) fout.write(seq \n) fout.write(\n) fout.write(q_char * len(seq) \n)3.4 别重复造轮子几个好用的命令行工具手写解析器适合理解原理和定制需求但日常分析我更推荐用成熟工具省时又不容易出错。查看FASTQ基础统计seqkit stats sample.fastq.gz一行命令就能输出reads数、总碱基数、平均长度、GC含量等。FASTQ转FASTAseqtk seq -A input.fastq output.fasta这几乎是行业标准操作。FASTA转FASTQ伪质量值seqtk seq -F I input.fasta output.fastq。质量值编码检测seqtk seq -h或者跑FastQC看“Quality score ranges”图例基本一眼就能认出是Phred33还是Phred64。子采样seqtk sample input.fastq 10000 subset.fastq从大数据集中抽出一万条reads测试上游流程非常方便。这些工具都默认支持gzip压缩文件输入路径直接带.gz就可以省去了手动解压和重新压缩的麻烦。我自己踩过的坑是有些老版本工具不支持自动识别压缩格式必须显式先gunzip再处理所以遇到报错先查一下工具版本。4. 高频问题与排查技巧实录4.1 质量编码体系判断错误这类问题最常见症状是下游工具比如Fastp、Trimmomatic跑完一看所有read质量都异常低或者异常高甚至报“ASCII offset 33 vs 64”的警告。原因就是你用Phred33的偏移量去解析了一份Phred64的老数据。判断方法我前面介绍过这里再补充一个可以写进脚本里的定量方法def guess_quality_offset(filepath, sample_reads1000): min_ascii 255 max_ascii 0 for i, (ident, seq, qual) in enumerate(parse_fastq(filepath)): if i sample_reads: break ascii_codes [ord(c) for c in qual] min_ascii min(min_ascii, min(ascii_codes)) max_ascii max(max_ascii, max(ascii_codes)) if min_ascii 59: return 33 elif max_ascii 74: return 64 else: return 33 # 默认现代格式这个逻辑的核心是Phred64体系中质量分数最低一般也在0以上对应ASCII 64以上所以如果字符范围大量低于59基本排除Phred64。如果最小字符在59到64之间同时最高字符又很高就要结合测序平台说明再判断。稳妥做法是随机抽取几条read转成Q值后看看分布是否符合预期。4.2 换行符和空行陷阱Windows环境下转过来的FASTA/FASTQ文件经常带\r\n。大多数现代解析器可以容忍\r的存在但旧程序可能会把\r认成序列字符导致序列长度多一截。这种情况在FASTQ里更隐蔽你看到的质量字符串好像和序列一样长但仔细数才发现每行尾部多出一个\r某些严格程序会直接拒绝。解决办法是在解析时用line.rstrip(\n)而不是简单strip()这样能保留行首空格用于检测同时去掉行尾换行。如果整个文件可能是UTF-8带BOM记得用utf-8-sig编码读取否则第一行会莫名多出\ufeff。还有一类情况是文件中间有空行。有些工具误操作会在FASTQ记录之间插入空行这会让四行结构错位解析器报“标识符不以开头”之类的错误。遇到这种文件比较安全的方式是先做一次“空行清理”预处理sed /^$/d broken.fastq clean.fastq但注意如果空行出现在序列或质量字符串中间sed会把序列上下两段拼起来可能会导致不可预知的错误所以清洗前最好抽样看看空行的具体位置。4.3 标识符重复与序列长度不一致有时候FASTQ里会出现两条read的标识符完全一样。单端测序还好双端测序中read1和read2的标识符通常只有后缀不同如/1、/2但某些工具处理后会丢失后缀导致两条记录的ID一模一样。这在后续比对或定量时很难区分解决方法是统一为每条read的ID加上唯一序号counter 0 for ident, seq, qual in parse_fastq(dup.fastq): counter 1 unique_id f{ident}_{counter} # 写入新文件序列长度不一致则分为两种一是同一文件里各条read长度不同这通常是测序仪在poly-A或被truncate后的结果本身不一定错误二是同一条read里序列行和质量串长度不相等那基本就是文件损坏或解析出错。长度不一致时程序应当抛出异常而不是默默截断因为截断会让后续比对结果失真。4.4 大文件处理与性能优化测序数据的单个FASTQ文件动辄几十GB如果直接readlines()读取内存很快就爆。我一直遵守几个原则用流式解析一次只处理一条read或一个固定大小的block不要一次性全读入内存。在Python里处理时逐行循环如果发现程序太慢考虑用mmap或者改用seqkit、seqtk等C语言工具先做过滤再让Python处理下游结果。如果经常对压缩文件做操作直接用gzip.open(filepath, rt)读取.gz文件Python会自动解压不要先解压到磁盘再读那样既占空间又费时间。另外FASTQ文件行数有时很夸张8行、12行甚至更多的情况是因为有些工具的wrap宽度不同。虽然通常四行一条记录但有些软件如BWA mem的某些旧版本会输出跨多行的序列或质量值这并不完全符合规范。遇到这种文件最稳的解析方式是把序列和质量字符串分别累积到空白行为止或者直接改用seqkit这类能自动兼容wrap的工具。5. 写在最后的一些个人体会我刚开始接触NGS数据时也曾经把FASTQ里的质量字符串当成乱码直接整行删掉只保留序列去做比对还奇怪为什么结果那么差。后来补上质量过滤、接头去除这些步骤后有效数据率明显提升才真正明白FASTQ里那串看着像“密码”的字符有多大的信息量。如果让我给新手一个建议那就是拿到任何测序文件后先别急着分析养成用seqkit stats和FastQC看一眼的习惯确认文件完整、质量编码正确、碱基分布正常再开始下游流程。这些前置检查花不了十分钟但能帮你避开后面几十个小时的排查泥潭。格式解析这种基础活表面上没什么技术含量可恰恰是它决定了你后续所有分析能不能站在一个可靠的地基上。希望这篇拆解能帮你少走一些我当年走过的弯路。
返回列表