如何在Python中的实例属性中优化数万个子串搜索?

时间:2014-03-06 16:01:16

标签: python regex optimization

我正在尝试编写一个自动填充用户输入的程序,该程序可能是以下之一:机场的三个字母IATA代码,一个城市的名字,某个给定语言中的城市名称,机场名称,国家/地区名称,一个州的名字。

机场数据全部位于Airport类的实例中,该类具有.match()方法,确定是否有任何相关属性以用户输入开头。

以下是所有相关代码:

class Location(object):
    def __init__(self, code, location_type):
        self.code = code  # Country/city/state/airport codes. Format varies.
        self.type = location_type
        self.name = self.get_name()  # String containing name of location.
        if self.type == 'city':
            self.localizations = self.get_localizations()
            # The above is a dictionary, keys are locales (ex. 'fr-FR'), values
            # are the translated city names in the specified locale.

    def match(self, pattern, match_code=False, locales=[]):
        if match_code:  # Fires if we only match for the 3 letter IATA code 
            return pattern.match(self.code)

        if not self.name:  # Some instances don't have names
            return None

        if locales and self.localizations:  # Fires if there's languages given
            for locale in locales:
                match = pattern.match(self.localizations.get(locale, ''))
                if match:
                    return locale
            return None

        return pattern.match(self.name)


class Airport(Location):
    def __init__(self, airport, city=None, state=None, country=None):
        self.code = airport
        self.type = 'airport'
        self.name = self.get_name()
        self.city = Location(city, 'city')
        self.state = Location(state, 'state')
        self.country = Location(country, 'country')

matches = []
pattern = re.compile('^' + keyword, re.I)  # Keyword is the user's input
for airport in airports:  # airports is a list of Airport instances
    if airport.match(pattern, match_code=True):
        matches.append(airport.create_match('airport', 100))
    elif (airport.city.match(pattern)
          or airport.city.match(pattern, locales=locales)):
        if airport.city.match(pattern):
            matches.append(airport.create_match('locality', 70))
        else:
            locale = airport.city.match(pattern, locales=locales)
            matches.append(airport.create_match('localised_locality', 70,
                                                locale=locale))
    elif airport.match(pattern):
        matches.append(airport.create_match('airport', 50))
    elif airport.country.match(pattern):
        matches.append(airport.create_match('country', 30))
    elif airport.state.match(pattern):
        matches.append(airport.create_match('state', 30))

根据我的测试,Airport.match()方法几乎一直占用。目前有9451个Airport个实例,我的电脑上搜索大约需要50毫秒。

我的程序是在启动时创建所有这些实例的程序,从XML文件加载它们,因此我可以根据需要修改源数据。

2 个答案:

答案 0 :(得分:1)

我认为你正在倒退。那是什么意思?好吧,在我看来,你要匹配的东西列表是静态的(相对),而你的用户将一次输入一个字符的数据。您应该做的是将您可能自动完成的所有内容放入已排序的数组中,然后每次用户键入另一个字符时,找到数组中与用户输入的前缀匹配的第一项。

你可以通过记住你到达的最后一个地方进行优化,例如如果用户键入'S',当你得到下一个字符时,你开始搜索数组中的第一个'S';如果他们输入'SF',那么你从'SF'开始搜索,依此类推。

更新

以下是基于您上面所写内容的示例:

import bisect

# Construct the search array
search_array = [(l.code.lower(), l) for l in locations] + [(l.name.lower(), l) for l in locations] + [(a.city.lower(), a) for a in airports] + [(a.state.lower(), a) for a in airports] + [(a.country.lower(), a) for a in airports]
search_array.sort()

# Now, assume the user enters 'S'; we do
new_entry = bisect.bisect_left(search_array, ('S'.lower(), None))

if new_entry < len(search_array):
  found = search_array[new_entry]
  if found[0].startswith('S'.lower()):
    entry = new_entry
    autocompletion = found[0]

# Let's say they now enter 'F'; we do
new_entry = bisect.bisect_left(search_array, ('SF'.lower(), None), entry)

if new_entry < len(search_array):
  found = search_array[new_entry]
  if found[0].startswith('SF'.lower()):
    entry = new_entry
    autocompletion = found[0]

等等。显然,这只是Stack Overflow中的一个例子。

答案 1 :(得分:-1)

我想出了一个解决方案,但由于它很难看而且最终只使用了一点点,因此以下内容仅供参考:

初始化数据时,为每个实例分配一个名为Location.match_name的附加属性。这只是self.name.lower(),但是通过预先分配它,我们消除了使用不区分大小写的正则表达式匹配的需要,甚至在我们匹配时将self.name转换为小写。相反,我们会将keyword.lower()Location.match_name[:len(keyword)]进行比较。 (不使用str.startswith(),因为它由于某种原因而具有较差的性能。)

然而,如果我没记错的话,这只能带来大约5%或10%的加速,而且我决定不牺牲代码的可读性和初始化时间。