【问题标题】:Most straightforward way to cache geocoding data缓存地理编码数据的最直接方法
【发布时间】:2015-02-08 18:41:07
【问题描述】:

我正在使用geopy 获取地址列表的纬度/经度坐标。所有文档都指向通过缓存来限制服务器查询(实际上这里有很多问题),但实际上很少给出实际的解决方案。

最好的方法是什么?

这是针对我正在从事的独立数据处理工作...不涉及应用平台。只是试图减少服务器查询,因为我运行了我以前见过的数据(在我的情况下很可能)。

我的代码如下所示:

from geopy import geocoders
def geocode( address ):
    # address ~= "175 5th Avenue NYC"
    g = geocoders.GoogleV3()
    cache = addressCached( address )

    if ( cache != False ): 
        # We have seen this exact address before,
        # return the saved location
        return cache

    # Otherwise, get a new location from geocoder
    location = g.geocode( address )

    saveToCache( address, location )
    return location

def addressCached( address ):
    # What does this look like?

def saveToCache( address, location ):
    # What does this look like?

【问题讨论】:

    标签: python caching geopy


    【解决方案1】:

    您希望如何实现缓存实际上取决于您的 Python 代码将在什么平台上运行。

    您需要一个相当持久的“缓存”,因为地址的位置不会经常变化:-),所以数据库(在键值模式下)似乎是最好的。

    所以在很多情况下,我会选择sqlite3,这是一个出色的、非常轻量级的 SQL 引擎,它是 Python 标准库的一部分。除非我可能更喜欢一个我无论如何都需要运行的 MySQL 实例,否则一个优点可能是这将允许在不同节点上运行的多个应用程序共享“缓存”——SQL 和非 SQL 的其他 DB 将有利于后者,取决于您的限制和偏好。

    但是,例如,如果我在 Google App Engine 上运行,那么我将使用它包含的数据存储。除非我有特定的理由要在多个不同的应用程序之间共享“缓存”,在这种情况下,我可能会考虑替代方案,例如 google cloud sql 和 google storage,以及另一个由专用“缓存服务器”GAE 应用程序组成的替代方案我自己的服务 RESTful 结果(可能带有端点?)。再次,选择非常非常依赖于您的约束和偏好(延迟、每秒查询数大小等)。

    所以请说明您所在的平台,以及您对数据库“缓存”的其他限制和偏好,然后可以轻松显示非常简单的实现代码。但是在你澄清之前展示六种不同的可能性不会很有成效。

    添加:由于 cmets 建议 sqlite3 可能是可以接受的,并且有一些重要的细节最好在代码中显示(例如,如何将 geopy.location.Location 的实例序列化和反序列化到/从 sqlite3 blob -- 其他底层数据库很可能会出现类似的问题,并且解决方案相似),我认为解决方案示例可能最好在代码中显示。因此,由于“地理缓存”显然最好作为自己的模块实现,我编写了以下简单的geocache.py...:

    import geopy
    import pickle
    import sqlite3
    
    class Cache(object):
        def __init__(self, fn='cache.db'):
           self.conn = conn = sqlite3.connect(fn)
           cur = conn.cursor()
           cur.execute('CREATE TABLE IF NOT EXISTS '
                       'Geo ( '
                       'address STRING PRIMARY KEY, '
                       'location BLOB '
                       ')')
           conn.commit()
    
        def address_cached(self, address):
            cur = self.conn.cursor()
            cur.execute('SELECT location FROM Geo WHERE address=?', (address,))
            res = cur.fetchone()
            if res is None: return False
            return pickle.loads(res[0])
    
        def save_to_cache(self, address, location):
            cur = self.conn.cursor()
            cur.execute('INSERT INTO Geo(address, location) VALUES(?, ?)',
                        (address, sqlite3.Binary(pickle.dumps(location, -1))))
            self.conn.commit()
    
    
    if __name__ == '__main__':
        # run a small test in this case
        import pprint
    
        cache = Cache('test.db')
        address = '1 Murphy St, Sunnyvale, CA'
        location = cache.address_cached(address)
        if location:
            print('was cached: {}\n{}'.format(location, pprint.pformat(location.raw)))
        else:
            print('was not cached, looking up and caching now')
            g = geopy.geocoders.GoogleV3()
            location = g.geocode(address)
            print('found as: {}\n{}'.format(location, pprint.pformat(location.raw)))
            cache.save_to_cache(address, location)
            print('... and now cached.')
    

    我希望这里说明的想法足够清楚 - 每个设计选择都有替代方案,但我尽量保持简单(特别是,我使用了一个简单的 example-cum-mini-test模块直接运行,而不是一套适当的单元测试...)。

    关于序列化到/从 blob 的部分,我选择了带有“最高协议”(-1) 协议的 pickle——cPickle 当然在 Python 2 中也一样好(而且速度更快:-) 但是这些天我尝试编写与 Python 2 或 3 一样好的代码,除非我有特定的理由不这样做:-)。当然,我为测试中使用的 sqlite 数据库使用了不同的文件名test.db,因此您可以毫无疑虑地清除它来测试一些变化,而用于“生产”代码的默认文件名保持不变完整的(它一个相当可疑的设计选择使用相对的文件名 - 意思是“在当前目录中” - 但是决定将此类文件放置在哪里的适当方式非常依赖于平台,而且我不想在这里进入这样的exoterica:-)。

    如果还有其他问题,请提出(也许最好单独提出一个新问题,因为这个答案已经变得如此之大!-)。

    【讨论】:

    • @cslstr,SQLite 对我来说是最自然的解决方案——在开始时用 pickle 加载并在程序结束时转储的内存 dict 可能被认为是“简单的”,但它需要一个无限的,随着时间的推移,内存和启动/关闭时间越来越多,“缓存”越来越大,所以我认为这对于持久程序来说是一个非常糟糕的架构。
    • 这些是我对仅在内存中保留列表/字典的担忧,不确定它们会对事情产生多大影响。 +1 用于调查选项。
    • 我建议先清理一下地址,然后再在缓存中查找它,这样您就不会搜索以不同方式写入的相同地址。这个清理应该非常简单,因为您不想制作相同的不同地址。例如:address.lower(), replace(' ', ' '), replace('\n', ',') 等
    【解决方案2】:

    创建一个存储所有地理编码地址的listdict 怎么样?然后你可以简单地检查一下。

    if address in cached:
        //skip
    

    【讨论】:

    • 然后我会通过...pickle 保存列表?还是类似的?缓存需要通过多次执行来持久化。
    • 泡菜可能是一种方法,但我不得不承认我缺乏与他们合作的经验。因此,我个人会选择JSON,但这不一定是最好的解决方案。
    • @alex-martelli 我同意 SQL DB 是一个很好的解决方案,但首先设置它需要一些时间,(尤其是)如果你从未做过。
    【解决方案3】:

    此缓存从模块加载的那一刻起生效,并且在您使用完此模块后不会保存。您可能希望将其保存到带有 pickle 的文件或数据库中,并在下次加载模块时加载它。

    from geopy import geocoders
    cache = {}
    
    def geocode( address ):
        # address ~= "175 5th Avenue NYC"
        g = geocoders.GoogleV3()
        cache = addressCached( address )
    
        if ( cache != False ): 
            # We have seen this exact address before,
            # return the saved location
            return cache
    
        # Otherwise, get a new location from geocoder
        location = g.geocode( address )
    
        saveToCache( address, location )
        return location
    
    def addressCached( address ):
        global cache
        if address in cache:
            return cache[address]
        return None
    
    def saveToCache( address, location ):
        global cache
        cache[address] = location
    

    【讨论】:

    • 很好,显示代码......现在只需要处理缓存缓存:)
    【解决方案4】:

    这是一个简单的实现,它使用 python shelve 包进行透明和持久的缓存:

    import geopy
    import shelve
    import time
    
    class CachedGeocoder:
        def __init__(self, source = "Nominatim", geocache = "geocache.db"):
            self.geocoder = getattr(geopy.geocoders, source)()
            self.db = shelve.open(geocache, writeback = True)
            self.ts = time.time()+1.1
        def geocode(self, address):
            if not address in self.db:
                time.sleep(max(1 -(time.time() - self.ts), 0))
                self.ts = time.time()
                self.db[address] = self.geocoder.geocode(address)
            return self.db[address]
    
    geocoder = CachedGeocoder()
    print geocoder.geocode("San Francisco, USA")
    

    它存储一个时间戳以确保发出请求的频率不超过每秒一次(这是 Nominatim 的要求)。一个弱点是它不能处理来自 Nominatim 的超时响应。

    【讨论】:

      猜你喜欢
      • 2013-08-17
      • 1970-01-01
      • 2019-09-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多