Техническая колонка:GitHub.com/Делайте это с душой/Особые…
В то же время, вы также можете обратить внимание на мой публичный аккаунт WeChat AlwaysBeta, вас ждет более интересный контент.
В работе часто встречается один тип требований, чтобы найти информацию об атрибуции, соответствующую IP в соответствии с сегментом IP-адреса. Если процесс запроса помещается в реляционную базу данных, это приведет к большому потреблению операций ввода-вывода, и скорость не может быть удовлетворена, что явно нецелесообразно.
Какой лучший способ сделать это? Для этого были предприняты некоторые попытки, которые подробно описаны ниже.
построить индексный файл
Видел один на GitHubip2regionВ этом проекте автор реализует быстрый запрос, создавая файл, содержащий вторичный индекс, и скорость запроса достаточно высока, на уровне миллисекунд. Но если вы хотите обновить сегмент адреса или информацию об атрибуции, не очень удобно каждый раз перегенерировать файл.
Тем не менее, я все же рекомендую всем взглянуть на этот проект, идея индексации все еще стоит изучения. В опенсорсном проекте автора есть только соответствующий код для запроса, а кода для генерации индексного файла нет.Я написал код для генерации индексного файла согласно схематической диаграмме следующим образом:
# -*- coding:utf-8 -*-
import time
import socket
import struct
IP_REGION_FILE = './data/ip_to_region.db'
SUPER_BLOCK_LENGTH = 8
INDEX_BLOCK_LENGTH = 12
HEADER_INDEX_LENGTH = 8192
def generate_db_file():
pointer = SUPER_BLOCK_LENGTH + HEADER_INDEX_LENGTH
region, index = '', ''
# 文件格式
# 1.0.0.0|1.0.0.255|澳大利亚|0|0|0|0
# 1.0.1.0|1.0.3.255|中国|0|福建省|福州市|电信
with open('./ip.merge.txt', 'r') as f:
for line in f.readlines():
item = line.strip().split('|')
print item[0], item[1], item[2], item[3], item[4], item[5], item[6]
start_ip = struct.pack('I', struct.unpack('!L', socket.inet_aton(item[0]))[0])
end_ip = struct.pack('I', struct.unpack('!L', socket.inet_aton(item[1]))[0])
region_item = '|'.join([item[2], item[3], item[4], item[5], item[6]])
region += region_item
ptr = struct.pack('I', int(bin(len(region_item))[2:].zfill(8) + bin(pointer)[2:].zfill(24), 2))
index += start_ip + end_ip + ptr
pointer += len(region_item)
index_start_ptr = pointer
index_end_ptr = pointer + len(index) - 12
super_block = struct.pack('I', index_start_ptr) + struct.pack('I', index_end_ptr)
n = 0
header_index = ''
for index_block in range(pointer, index_end_ptr, 8184):
header_index_block_ip = index[n * 8184:n * 8184 + 4]
header_index_block_ptr = index_block
header_index += header_index_block_ip + struct.pack('I', header_index_block_ptr)
n += 1
header_index += index[len(index) - 12: len(index) - 8] + struct.pack('I', index_end_ptr)
with open(IP_REGION_FILE, 'wb') as f:
f.write(super_block)
f.write(header_index)
f.seek(SUPER_BLOCK_LENGTH + HEADER_INDEX_LENGTH, 0)
f.write(region)
f.write(index)
if __name__ == '__main__':
start_time = time.time()
generate_db_file()
print 'cost time: ', time.time() - start_time
Кэш с Redis
В настоящее время существует два способа кэширования информации об IP и атрибуции:
Первый — преобразовать начальный IP-адрес, конечный IP-адрес и все промежуточные IP-адреса в целые числа, а затем использовать преобразованный IP-адрес в качестве ключа, а информацию об атрибуции — в качестве значения для хранения в Redis в виде строки;
Во-вторых, использовать метод упорядоченного набора и хеширования, сначала добавить начальный и конечный IP-адрес в упорядоченный набор ip2cityid, идентификатор города используется в качестве члена, преобразованный IP-адрес используется в качестве оценки, а затем идентификатор города и добавляется информация об атрибуции.Добавляется в хэш cityid2city, идентификатор города используется в качестве ключа, а информация об атрибуции используется в качестве значения.
Первый метод не будет введен много, он прост и груб, и не рекомендуется. Скорость запроса конечно очень высокая, на уровне миллисекунд, но недостатки тоже очень очевидны.Я тестировал на 1000 кусках данных.Время кеша большое,около 20 минут,и пространство большое,почти 1G.
Второй способ описан ниже, если смотреть непосредственно на код:
# generate_to_redis.py
# -*- coding:utf-8 -*-
import time
import json
from redis import Redis
def ip_to_num(x):
return sum([256 ** j * int(i) for j, i in enumerate(x.split('.')[::-1])])
# 连接 Redis
conn = Redis(host='127.0.0.1', port=6379, db=10)
start_time = time.time()
# 文件格式
# 1.0.0.0|1.0.0.255|澳大利亚|0|0|0|0
# 1.0.1.0|1.0.3.255|中国|0|福建省|福州市|电信
with open('./ip.merge.txt', 'r') as f:
i = 1
for line in f.readlines():
item = line.strip().split('|')
# 将起始 IP 和结束 IP 添加到有序集合 ip2cityid
# 成员分别是城市 ID 和 ID + #, 分值是根据 IP 计算的整数值
conn.zadd('ip2cityid', str(i), ip_to_num(item[0]), str(i) + '#', ip_to_num(item[1]) + 1)
# 将城市信息添加到散列 cityid2city,key 是城市 ID,值是城市信息的 json 序列
conn.hset('cityid2city', str(i), json.dumps([item[2], item[3], item[4], item[5]]))
i += 1
end_time = time.time()
print 'start_time: ' + str(start_time) + ', end_time: ' + str(end_time) + ', cost time: ' + str(end_time - start_time)
# test.py
# -*- coding:utf-8 -*-
import sys
import time
import json
import socket
import struct
from redis import Redis
# 连接 Redis
conn = Redis(host='127.0.0.1', port=6379, db=10)
# 将 IP 转换成整数
ip = struct.unpack("!L", socket.inet_aton(sys.argv[1]))[0]
start_time = time.time()
# 将有序集合从大到小排序,取小于输入 IP 值的第一条数据
cityid = conn.zrevrangebyscore('ip2cityid', ip, 0, start=0, num=1)
# 如果返回 cityid 是空,或者匹配到了 # 号,说明没有找到对应地址段
if not cityid or cityid[0].endswith('#'):
print 'no city info...'
else:
# 根据城市 ID 到散列表取出城市信息
ret = json.loads(conn.hget('cityid2city', cityid[0]))
print ret[0], ret[1], ret[2]
end_time = time.time()
print 'start_time: ' + str(start_time) + ', end_time: ' + str(end_time) + ', cost time: ' + str(end_time - start_time)
# python generate_to_redis.py
start_time: 1554300310.31, end_time: 1554300425.65, cost time: 115.333260059
# python test_2.py 1.0.16.0
日本 0 0
start_time: 1555081532.44, end_time: 1555081532.45, cost time: 0.000912189483643
Тестовых данных около 500 000 штук, время кеша меньше 2 минут, памяти занято 182M, а скорость запроса миллисекундная. Очевидно, этот метод стоит попробовать.
zrevrangebyscore
Временная сложность метода составляет O (log (N) + M),N
мощность отсортированного множества,M
является мощностью набора результатов. Видно, что чем больше значение N, тем медленнее эффективность запроса, и конкретный объем данных может быть эффективно запрошен, что необходимо проверить. Тем не менее, я не думаю, что эта проблема должна беспокоить. Давайте поговорим об этом, когда вы столкнетесь с ней.
выше.
GitHub просит звездочку: мой технический блог