允许字符串中任意位置一个不匹配的字符串搜索

26 投票
14 回答
30671 浏览
提问于 2025-04-15 20:16

我正在处理长度为25的DNA序列(见下面的例子)。我有一个包含230,000个序列的列表,需要在整个基因组中查找每个序列(毒浆虫基因组)。我不确定基因组有多大,但肯定比230,000个序列要长得多。

我需要查找每个25个字符的序列,比如说,(AGCCTCCCATGATTGAACAGATCAT)。

基因组的格式是一个连续的字符串,也就是像这样:(CATGGGAGGCTTGCGGAGCCTGAGGGCGGAGCCTGAGGTGGGAGGCTTGCGGAGTGCGGAGCCTGAGCCTGAGGGCGGAGCCTGAGGTGGGAGGCTT....)

我不在乎它出现的位置或次数,只想知道它是否存在。
我觉得这很简单 --

str.find(AGCCTCCCATGATTGAACAGATCAT)

但我还想找到一个近似匹配,定义为在任何位置有一个错误(不匹配),并记录在序列中的位置。我不太确定该怎么做。我能想到的唯一方法是使用通配符,在每个位置进行搜索,也就是说,要搜索25次。

例如,

AGCCTCCCATGATTGAACAGATCAT    
AGCCTCCCATGATAGAACAGATCAT

一个在第13个位置有不匹配的近似匹配。

速度不是大问题,因为我只需要做3次,虽然如果能快一点那就更好了。

有一些程序可以做到这一点——找到匹配和部分匹配——但我在寻找一种特定类型的部分匹配,这些应用程序无法发现。

这里有一个类似的帖子,关于perl,虽然他们只是比较序列,而不是在一个连续字符串中搜索:

相关帖子

14 个回答

7

我在网上搜索“弓形虫基因组”,想找到一些相关的基因组文件。我找到了一份我认为比较接近的文件,名字叫“TgondiiGenomic_ToxoDB-6.0.fasta”,大小大约是158MB,地址在http://toxodb.org。我用下面的pyparsing表达式提取基因序列,花了不到2分钟:

fname = "TgondiiGenomic_ToxoDB-6.0.fasta"
fastasrc = open(fname).read()   # yes! just read the whole dang 158Mb!

"""
Sample header:
>gb|scf_1104442823584 | organism=Toxoplasma_gondii_VEG | version=2008-07-23 | length=1448
"""
integer = Word(nums).setParseAction(lambda t:int(t[0]))
genebit = Group(">gb|" + Word(printables)("id") + SkipTo("length=") + 
                "length=" + integer("genelen") + LineEnd() + 
                Combine(OneOrMore(Word("ACGTN")),adjacent=False)("gene"))

# read gene data from .fasta file - takes just under a couple of minutes
genedata = OneOrMore(genebit).parseString(fastasrc)

(惊讶!有些基因序列中竟然有一串'N'!这到底是怎么回事?!)

然后我写了一个类,作为pyparsing的Token类的子类,用于进行近似匹配:

class CloseMatch(Token):
    def __init__(self, seq, maxMismatches=1):
        super(CloseMatch,self).__init__()
        self.name = seq
        self.sequence = seq
        self.maxMismatches = maxMismatches
        self.errmsg = "Expected " + self.sequence
        self.mayIndexError = False
        self.mayReturnEmpty = False

    def parseImpl( self, instring, loc, doActions=True ):
        start = loc
        instrlen = len(instring)
        maxloc = start + len(self.sequence)

        if maxloc <= instrlen:
            seq = self.sequence
            seqloc = 0
            mismatches = []
            throwException = False
            done = False
            while loc < maxloc and not done:
                if instring[loc] != seq[seqloc]:
                    mismatches.append(seqloc)
                    if len(mismatches) > self.maxMismatches:
                        throwException = True
                        done = True
                loc += 1
                seqloc += 1
        else:
            throwException = True

        if throwException:
            exc = self.myException
            exc.loc = loc
            exc.pstr = instring
            raise exc

        return loc, (instring[start:loc],mismatches)

每次匹配时,这个类会返回一个元组,里面包含实际匹配的字符串和一个不匹配位置的列表。完全匹配的情况下,第二个值会返回一个空列表。(我很喜欢这个类,觉得可以把它加到下一个pyparsing的版本里。)

接着,我运行了这段代码,搜索从.fasta文件中读取的所有序列中的“最多2个不匹配”的匹配(记得genedata是一个ParseResults组的序列,每个组包含一个id、一个整数长度和一个序列字符串):

searchseq = CloseMatch("ATCATCGAATGGAATCTAATGGAAT", 2)
for g in genedata:
    print "%s (%d)" % (g.id, g.genelen)
    print "-"*24
    for t,startLoc,endLoc in searchseq.scanString(g.gene):
        matched, mismatches = t[0]
        print "MATCH:", searchseq.sequence
        print "FOUND:", matched
        if mismatches:
            print "      ", ''.join(' ' if i not in mismatches else '*' 
                            for i,c in enumerate(searchseq.sequence))
        else:
            print "<exact match>"
        print "at location", startLoc
        print
    print

我随机从某个基因片段中选了一个搜索序列,以确保能找到一个完全匹配的,同时也好奇有多少个1个和2个元素的不匹配。

这段代码运行花了一点时间。经过45分钟,我得到了这个输出,列出了每个id和基因长度,以及找到的任何部分匹配:

scf_1104442825154 (964)
------------------------

scf_1104442822828 (942)
------------------------

scf_1104442824510 (987)
------------------------

scf_1104442823180 (1065)
------------------------
...

我有点沮丧,因为一直没有看到匹配,直到:

scf_1104442823952 (1188)
------------------------
MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCGAACGGAATCGAATGGAAT
                *      *        
at location 33

MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCGAATGGAATCGAATGGAAT
                       *        
at location 175

MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCGAATGGAATCGAATGGAAT
                       *        
at location 474

MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCGAATGGAATCGAATGGAAT
                       *        
at location 617

MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCGAATGGAATCGAATAGAAT
                       *   *    
at location 718

MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCGAATGGATTCGAATGGAAT
                    *  *        
at location 896

MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCGAATGGAATCGAATGGTAT
                       *     *  
at location 945

最后我找到了我的完全匹配在:

scf_1104442823584 (1448)
------------------------
MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCGAATGGACTCGAATGGAAT
                    *  *        
at location 177

MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCGAATGGAATCAAATGGAAT
                       *        
at location 203

MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCAAATGGAATCGAATGGAAT
             *         *        
at location 350

MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCGAATGGAATCGAATGGAAA
                       *       *
at location 523

MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCAAATGGAATCGAATGGAAT
             *         *        
at location 822

MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCATCGAATGGAATCTAATGGAAT
<exact match>
at location 848

MATCH: ATCATCGAATGGAATCTAATGGAAT
FOUND: ATCGTCGAATGGAGTCTAATGGAAT
          *         *           
at location 969

虽然这没有创造任何速度记录,但我完成了任务,还找到了几个2个不匹配的情况,可能会有用。

为了比较,这里有一个基于正则表达式的版本,只找到1个不匹配的匹配:

import re
seqStr = "ATCATCGAATGGAATCTAATGGAAT"
searchSeqREStr = seqStr + '|' + \
    '|'.join(seqStr[:i]+"[ACTGN]".replace(c,'') +seqStr[i+1:] 
             for i,c in enumerate(seqStr))

searchSeqRE = re.compile(searchSeqREStr)

for g in genedata:
    print "%s (%d)" % (g.id, g.genelen)
    print "-"*24
    for match in searchSeqRE.finditer(g.gene):
        print "MATCH:", seqStr
        print "FOUND:", match.group(0)
        print "at location", match.start()
        print
    print

(起初,我试着直接搜索原始的FASTA文件,但对比pyparsing版本,发现匹配的数量少得多,感到困惑。后来我意识到,有些匹配可能跨越了换行符,因为fasta文件的输出在n个字符处换行。)

所以在第一次用pyparsing提取基因序列以便匹配之后,这个基于正则表达式的搜索器又花了大约1分半钟扫描所有未换行的序列,找到了与pyparsing解决方案相同的所有1个不匹配的条目。

28

Python的regex库支持模糊正则表达式匹配。它比TRE有一个好处,就是可以在文本中找到所有符合正则表达式的匹配项(还支持重叠匹配)。

import regex
m=regex.findall("AA", "CAG")
>>> []
m=regex.findall("(AA){e<=1}", "CAAG") # means allow up to 1 error
m
>>> ['CA', 'AG']
29

在继续阅读之前,你有没有查看过biopython

看起来你想找到大约匹配的字符串,允许一个字符的替换错误,而不允许插入或删除错误,也就是汉明距离为1。

如果你有一个计算汉明距离的匹配函数(可以参考Ignacio提供的链接),你可以像这样使用它来搜索第一个匹配:

any(Hamming_distance(genome[x:x+25], sequence) == 1 for x in xrange(len(genome)))

不过这样做会比较慢,因为(1)汉明距离函数在第二个替换错误后仍会继续计算;(2)在失败后,它只会向前移动一个字符,而不是根据之前的结果跳过一些字符(就像Boyer-Moore搜索那样)。

你可以用这样的函数来解决(1)的问题:

def Hamming_check_0_or_1(genome, posn, sequence):
    errors = 0
    for i in xrange(25):
        if genome[posn+i] != sequence[i]:
            errors += 1
            if errors >= 2:
                return errors
    return errors 

注意:这故意不是Python风格的,而是C风格,因为你需要用C(可能通过Cython)来获得合理的速度。

Navarro和Raffinot做了一些关于位并行近似Levenshtein搜索的跳过方法的工作(可以在网上搜索“Navarro Raffinot nrgrep”),这可以适应汉明搜索。需要注意的是,位并行方法在查询字符串的长度和字母表大小上有一些限制,但你的情况是25和4,所以没有问题。更新:对于字母表大小为4的情况,跳过可能帮助不大。

当你在网上搜索汉明距离搜索时,你会发现很多关于在硬件中实现它的内容,而软件方面的资料不多。这是一个很大的提示,说明你可能需要在C或其他编译语言中实现你想出的算法。

更新:位并行方法的工作代码

我还提供了一种简单的方法来帮助检查正确性,并且我打包了保罗的re代码的一个变体以进行比较。注意,使用re.finditer()会返回不重叠的结果,这可能导致距离为1的匹配覆盖精确匹配;请查看我的最后一个测试案例。

位并行方法具有以下特点:保证线性行为O(N),其中N是文本长度。注意,简单方法是O(NM),正则表达式方法也是如此(M是模式长度)。Boyer-Moore风格的方法在最坏情况下是O(NM),预期是O(N)。此外,当输入需要缓冲时,位并行方法也很容易使用:它可以一次处理一个字节或一个兆字节;没有前瞻,也没有缓冲边界的问题。最大的优势是:当用C编写时,这种简单的每字节输入代码速度非常快。

缺点是:模式长度实际上受限于快速寄存器中的位数,例如32或64。在这种情况下,模式长度是25;没有问题。它使用额外的内存(S_table),与模式中不同字符的数量成正比。在这种情况下,“字母表大小”只有4;没有问题。

详细信息请参考这份技术报告。那里的算法是用于近似搜索的Levenshtein距离。要转换为使用汉明距离,我只是简单地去掉了处理插入和删除的第2.1条中的部分内容。你会注意到很多提到“R”的地方,带有“d”的上标。“d”表示距离。我们只需要0和1。这些“R”变成了下面代码中的R0和R1变量。

# coding: ascii

from collections import defaultdict
import re

_DEBUG = 0


# "Fast Text Searching with Errors" by Sun Wu and Udi Manber
# TR 91-11, Dept of Computer Science, University of Arizona, June 1991.
# http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.20.8854

def WM_approx_Ham1_search(pattern, text):
    """Generate (Hamming_dist, start_offset)
    for matches with distance 0 or 1"""
    m = len(pattern)
    S_table = defaultdict(int)
    for i, c in enumerate(pattern):
        S_table[c] |= 1 << i
    R0 = 0
    R1 = 0
    mask = 1 << (m - 1)
    for j, c in enumerate(text):
        S = S_table[c]
        shR0 = (R0 << 1) | 1
        R0 = shR0 & S
        R1 = ((R1 << 1) | 1) & S | shR0
        if _DEBUG:
            print "j= %2d msk=%s S=%s R0=%s R1=%s" \
                % tuple([j] + map(bitstr, [mask, S, R0, R1]))
        if R0 & mask: # exact match
            yield 0, j - m + 1
        elif R1 & mask: # match with one substitution
            yield 1, j - m + 1

if _DEBUG:

    def bitstr(num, mlen=8):
       wstr = ""
       for i in xrange(mlen):
          if num & 1:
             wstr = "1" + wstr
          else:
             wstr = "0" + wstr
          num >>= 1
       return wstr

def Ham_dist(s1, s2):
    """Calculate Hamming distance between 2 sequences."""
    assert len(s1) == len(s2)
    return sum(c1 != c2 for c1, c2 in zip(s1, s2))

def long_check(pattern, text):
    """Naively and understandably generate (Hamming_dist, start_offset)
    for matches with distance 0 or 1"""
    m = len(pattern)
    for i in xrange(len(text) - m + 1):
        d = Ham_dist(pattern, text[i:i+m])
        if d < 2:
            yield d, i

def Paul_McGuire_regex(pattern, text):
    searchSeqREStr = (
        '('
        + pattern
        + ')|('
        + ')|('.join(
            pattern[:i]
            + "[ACTGN]".replace(c,'')
            + pattern[i+1:]
            for i,c in enumerate(pattern)
            )
        + ')'
        )
    searchSeqRE = re.compile(searchSeqREStr)
    for match in searchSeqRE.finditer(text):
        locn = match.start()
        dist = int(bool(match.lastindex - 1))
        yield dist, locn


if __name__ == "__main__":

    genome1 = "TTTACGTAAACTAAACTGTAA"
    #         01234567890123456789012345
    #                   1         2

    tests = [
        (genome1, "ACGT ATGT ACTA ATCG TTTT ATTA TTTA"),
        ("T" * 10, "TTTT"),
        ("ACGTCGTAAAA", "TCGT"), # partial match can shadow an exact match
        ]

    nfailed = 0
    for genome, patterns in tests:
        print "genome:", genome
        for pattern in patterns.split():
            print pattern
            a1 = list(WM_approx_Ham1_search(pattern, genome))
            a2 = list(long_check(pattern, genome))
            a3 = list(Paul_McGuire_regex(pattern, genome))
            print a1
            print a2
            print a3
            print a1 == a2, a2 == a3
            nfailed += (a1 != a2 or a2 != a3)
    print "***", nfailed

撰写回答