Недавно мне понадобилось получить отсортированный по лайкам или репостам список записей со страницы в VK. Готового решения я не нашел, пришлось решать задачу самостоятельно. В итоге получилось простенькое приложение, работающее достаточно быстро.
Требования к приложению заключались в следующем. На вход подается ссылка на страницу и дополнительные параметры. Сперва был только один параметр — ключ сортировки, позже был добавлен параметр, задающий промежуток времени (в днях). Посты, опубликованные раньше этого промежутка, не учитываются. Например, при значении параметра 7 на выходе получим список постов с максимальным количеством лайков (или репостов) за поледнюю неделю.
Для решения задачи использовал Python 3 и модуль Requests. Некоторые детали реализации не описываются, полный код доступен по ссылке, в конце статьи. Итак, приступим.
Для начала нужно распарсить аргументы командной строки. Для этого воспользуемся модулем argparse из стандартной библиотеки:
Для проверки введенного url используется функция url_validator, которая, в случае успеха, возвращает словарь, содержащий тип введенного url и идентификатора страницы:
Например:
Или:
Результат парсинга входных данных, например, для
Выглядит следующим образом:
Для дальнейшей работы с API и формирования ссылок на записи, требуется числовой ID страницы:
Теперь перейдем непосредственно к работе с VK API.
Основной метод для получения записей — wall.get, за один запрос может вернуть максимум 100 постов, что достаточно мало. Для решения проблемы воспользуемся методом execute — универсальным методом, который позволяет запускать последовательность других методов, сохраняя и фильтруя промежуточные результаты. За один запрос execute может выполнить не более 25 обращений к API, из чего следует, что за один запрос к execute мы будем получать не 100 записей, как при обычном wall.get, а 2500. Execute имеет обязательный параметр code — код алгоритма в VKScript, формате, похожем на JavaSсript.
Запросы к API выполняются в цикле:
В примере используется метод execute.getPosts — хранимая процедура, которая создана для удобства в настройках приложения в VK, что позволяет не передавать каждый раз код метода, а обращаться к нему по имени.
Код, приведенный выше, неэффективен, т.к. получает много лишней информации о записях, что существенно увеличивает время получения и дальнейшей обработки. Например, при получении 30.000+ постов с одной страницы, суммарный объем полученного текста может составлять более 50 Мб.
Проведем фильтрацию ответов на стороне сервера, что значительно ускорит работу приложения:
Пример ответа:
Как видим, количество получаемой информации сократилось в разы. Казалось бы, такой результат должен удовлетворить, но, как упоминалось в начале статьи, есть параметр, задающий промежуток времени (в днях). Код выше никак не учитывает его. Например, если требуется получить самые популярные посты за последние два дня, то сначала скачиваются все посты, после чего локально отбираются те, которые опубликованы не ранее 2-ух прошлый дней. Таким образом, потенциальных постов, удовлетворяющих запросу, может быть штук 10, а получено будет несколько тысяч, что неприемлемо.
Передадим дополнительный параметр в метод execute.getPosts — крайнюю допустимую дату публикации в формате Unix Timestamp. Затем сравним последний полученный пост с этой датой. Если пост опубликован раньше, то возвращаем текущий список записей.
Код execute.getPosts получился достаточно неуклюжим. Это связано с ограничением на количество операций за один запрос.
Итоговая функция для получения постов:
Теперь, когда у нас есть нужные посты, остается только отсортировать и вывести их на экран.
Пример работы. Получаем самые популярные посты в сообществе хабра за последний месяц:
Полный код доступен на Github.
Требования к приложению заключались в следующем. На вход подается ссылка на страницу и дополнительные параметры. Сперва был только один параметр — ключ сортировки, позже был добавлен параметр, задающий промежуток времени (в днях). Посты, опубликованные раньше этого промежутка, не учитываются. Например, при значении параметра 7 на выходе получим список постов с максимальным количеством лайков (или репостов) за поледнюю неделю.
Для решения задачи использовал Python 3 и модуль Requests. Некоторые детали реализации не описываются, полный код доступен по ссылке, в конце статьи. Итак, приступим.
Для начала нужно распарсить аргументы командной строки. Для этого воспользуемся модулем argparse из стандартной библиотеки:
parse_args
def parse_args():
""" Parses input arguments """
parser = argparse.ArgumentParser()
parser.add_argument('url',
action='store',
default=None,
help='target page',
type=url_validator)
compar_key = parser.add_mutually_exclusive_group()
compar_key.add_argument('-l',
'--likes',
help='sort posts by likes',
action='store_true',
default=True)
compar_key.add_argument('-r',
'--reposts',
help='sort posts by reposts',
action='store_true')
parser.add_argument('-t',
'--top',
help='number of showing posts.',
default=10,
type=num_validator)
parser.add_argument('-d',
'--days',
type=int,
default=-1,
help='period for post processing.')
return parser.parse_args()
Для проверки введенного url используется функция url_validator, которая, в случае успеха, возвращает словарь, содержащий тип введенного url и идентификатора страницы:
url_validator
def url_validator(arg):
""" Checks correctness of url argument """
arg = arg.lower()
# If url something like https://vk.com/textual_id
matched_txt_id = const.TXT_ID_REGEXP.match(arg)
if matched_txt_id:
url = matched_txt_id.groupdict()
url['type'] = 'symbolic'
return url
# If url something like https://vk.com/id123456
matched_numeric_id = const.NUM_ID_REGEXP.match(arg)
if matched_numeric_id:
return matched_numeric_id.groupdict()
raise argparse.ArgumentTypeError(
const.INVALID_URL.format(url=arg))
Например:
>> url_validator("club123456")
{'type': 'club', 'id': '123456'}
Или:
>> url_validator("https://vk.com/habr")
{'type': 'symbolic', 'id': 'habr'}
Результат парсинга входных данных, например, для
python vktop.py vk.com/habr -t 10 -l -d 10
Выглядит следующим образом:
>> args = parse_args()
Namespace(days=10, likes=True, reposts=False, top=10, url={'type': 'symbolic', 'id': 'habr'})
Для дальнейшей работы с API и формирования ссылок на записи, требуется числовой ID страницы:
get_page_id
def get_page_id(url):
""" Returns page's numeric ID """
if url['type'] not in ['id', 'public', 'event', 'club']:
params = {'screen_name': url['id']}
request = requests.get(const.API_URL + 'utils.resolveScreenName?',
params=params)
response = json.loads(request.text)['response']
if response:
if response['type'] == 'user':
return response['object_id']
else:
# Groups have negative id
return -response['object_id']
else:
raise PageNotAvailable(url['id'] + ' is not available')
if url['type'] == 'id':
return int(url['id'])
else:
return -int(url['id'])
Теперь перейдем непосредственно к работе с VK API.
Основной метод для получения записей — wall.get, за один запрос может вернуть максимум 100 постов, что достаточно мало. Для решения проблемы воспользуемся методом execute — универсальным методом, который позволяет запускать последовательность других методов, сохраняя и фильтруя промежуточные результаты. За один запрос execute может выполнить не более 25 обращений к API, из чего следует, что за один запрос к execute мы будем получать не 100 записей, как при обычном wall.get, а 2500. Execute имеет обязательный параметр code — код алгоритма в VKScript, формате, похожем на JavaSсript.
Запросы к API выполняются в цикле:
recieve_posts
def recieve_posts(page_id):
"""
Returns :received_posts: from :page_id:
"""
params = {'access_token': const.ACCESS_TOKEN,
'id': page_id,
}
received_posts = []
offset = 0
while True:
params['offset'] = offset
response = json.loads(requests.post(
const.API_URL + 'execute.getPosts?', params=params).text)
# Interrupt loop when all posts were received
if not response['response']:
break
received_data = response['response']
for chunk in received_data:
for post in chunk:
received_posts.append(post)
offset += 1
return received_posts
В примере используется метод execute.getPosts — хранимая процедура, которая создана для удобства в настройках приложения в VK, что позволяет не передавать каждый раз код метода, а обращаться к нему по имени.
Тело execute.getPosts
// количество итераций цикла\запросов к API
var ITERS = 25;
// количество сообщений получаемых за один запрос
var COUNT = 100;
// список полученных постов
var posts = [];
var req_params = {
"owner_id" : Args.id, // id страницы
"offset" : 0, // смещение
"count" : COUNT,
"v" : "5.34" // версия API
};
var i = 0;
while(i < ITERS) {
req_params.offset = i*COUNT + ITERS*COUNT*Args.offset;
// Делаем запрос к API
var items = API.wall.get(req_params).items;
// Пустой список означает, что все записи получены
if (items.length == 0) {
return posts;
}
// Добавляем промежуточный результат
// в итоговый список
posts.push(items);
i = i + 1;
}
return posts;
Код, приведенный выше, неэффективен, т.к. получает много лишней информации о записях, что существенно увеличивает время получения и дальнейшей обработки. Например, при получении 30.000+ постов с одной страницы, суммарный объем полученного текста может составлять более 50 Мб.
Проведем фильтрацию ответов на стороне сервера, что значительно ускорит работу приложения:
Измененный execute.getPosts
// количество итераций цикла\запросов к API
var ITERS = 25;
// количество сообщений получаемых за один запрос
var COUNT = 100;
// список полученных постов
var posts = [];
var req_params = {
"owner_id" : Args.id, // id страницы
"offset" : 0, // смещение
"count" : COUNT,
"v" : "5.34" // версия API
};
var i = 0;
while(i < ITERS) {
req_params.offset = i*COUNT + ITERS*COUNT*Args.offset;
// Делаем запрос к API
var items = API.wall.get(req_params).items;
// Пустой список означает, что все записи получены
if (items.length == 0) {
return posts;
}
// Для хранения промежуточных ответов
var tmp = {};
// Фильтр. Получем список из id постов
tmp.ids = items@.id;
// Фильтр. Получем список из дат, публикаций постов
tmp.dates = items@.date;
// Так же, в execute передается доп. параметр Args.compar_key,
// В зависимости от которого, добавляется нужный список.
if (Args.compar_key == "likes") {
tmp.likes = items@.likes@.count;
} else {
tmp.reposts = items@.reposts@.count;
}
posts.push(tmp);
i = i + 1;
}
return posts;
Пример ответа:
response: [{
ids: [677448, 649369, 593585],
dates: [1450981583, 1449937068, 1448431475],
likes: [14957, 14923, 15493]
}, {
ids: [555311, 549734, 549376],
dates: [1447699602, 1447677107, 1447675941],
likes: [12384, 122548, 26709]
}]
Как видим, количество получаемой информации сократилось в разы. Казалось бы, такой результат должен удовлетворить, но, как упоминалось в начале статьи, есть параметр, задающий промежуток времени (в днях). Код выше никак не учитывает его. Например, если требуется получить самые популярные посты за последние два дня, то сначала скачиваются все посты, после чего локально отбираются те, которые опубликованы не ранее 2-ух прошлый дней. Таким образом, потенциальных постов, удовлетворяющих запросу, может быть штук 10, а получено будет несколько тысяч, что неприемлемо.
Передадим дополнительный параметр в метод execute.getPosts — крайнюю допустимую дату публикации в формате Unix Timestamp. Затем сравним последний полученный пост с этой датой. Если пост опубликован раньше, то возвращаем текущий список записей.
Итоговый метод execute.getPosts
// количество итераций цикла\запросов к API
var ITERS = 25;
// количество сообщений получаемых за один запрос
var COUNT = 100;
// список полученных постов
var posts = [];
var req_params = {
"owner_id" : Args.id, // id страницы
"offset" : 0, // смещение
"count" : COUNT,
"v" : "5.34" // версия API
};
var i = 0;
while(i < ITERS) {
req_params.offset = i*COUNT + ITERS*COUNT*Args.offset;
// Делаем запрос к API
var items = API.wall.get(req_params).items;
// Пустой список означает, что все записи получены
if (items.length == 0) {
return posts;
}
// Для хранения промежуточных ответов
var tmp = {};
// Фильтр. Получем список из id постов
tmp.ids = items@.id;
// Фильтр. Получем список из дат, публикаций постов
tmp.dates = items@.date;
// Так же, в execute передается доп. параметр Args.compar_key,
// В зависимости от которого, добавляется нужный список.
if (Args.compar_key == "likes") {
tmp.likes = items@.likes@.count;
} else {
tmp.reposts = items@.reposts@.count;
}
// Args.deadline - крайняя дата публикации.
// Если равна -1, значит нужно получить все посты.
// Иначе смотрим на дату последнего полученного поста,
// Если он опубликован раньше deadline, значит дальнейшие
// Запросы делать не нужно. Вернуть результат.
if (Args.deadline != -1 && tmp.dates[tmp.dates.length - 1] < Args.deadline) {
// Флаг, для остановки локального цикла.
tmp.stop = "True";
posts.push(tmp);
return posts;
} else {
posts.push(tmp);
}
i = i + 1;
}
return posts;
Код execute.getPosts получился достаточно неуклюжим. Это связано с ограничением на количество операций за один запрос.
Итоговая функция для получения постов:
recieve_posts
def recieve_posts(page_id, last_days, reposts):
"""
Returns posts from :page_id: that were posted not earlier
than :last_days: ago
"""
deadline = datetime.now() - timedelta(days=last_days)
unix_stamp = int(deadline.strftime("%s"))
if reposts:
compar_key = const.REPOSTS
else:
compar_key = const.LIKES
params = {'access_token': const.ACCESS_TOKEN,
'id': page_id,
'compar_key': compar_key,
'deadline': unix_stamp if last_days != -1 else last_days
}
received_posts = []
offset = 0
ONGOING = True
while ONGOING:
params['offset'] = offset
response = json.loads(requests.post(
const.API_URL + 'execute.getPostsNew?', params=params).text)
# Interrupt loop when all posts were received
if not response['response']:
break
received_data = response['response']
for chunk in received_data:
chunk_size = len(chunk['ids'])
for i in range(chunk_size):
post = dict()
post['date'] = datetime.fromtimestamp(chunk['dates'][i])
if last_days == -1
or post['date'].year >= deadline.year
and post['date'].month >= deadline.month
and post['date'].day >= deadline.day:
post['id'] = chunk['ids'][i]
post[compar_key] = chunk[compar_key][i]
received_posts.append(post)
if 'stop' in chunk:
ONGOING = False
break
offset += 1
return received_posts
Теперь, когда у нас есть нужные посты, остается только отсортировать и вывести их на экран.
Пример работы. Получаем самые популярные посты в сообществе хабра за последний месяц:
Полный код доступен на Github.