На Medium очень много пользователей и бесконечное количество постов. Найти интересные аккаунты для подписки более чем трудно.
Интересный для меня пользователь активен и часто пишет ценные комментарии.
Я просмотрел последние посты юзеров, за которыми я слежу, и полистал комментарии: должно быть, те, кто пишут в них что-то полезное, имеют со мной схожие интересы.
Процесс был утомительным. И тогда я вспомнил самый ценный урок, который я усвоил во время своей последней стажировки:
Любая утомительная задача может и должна быть автоматизирована.
Я хотел, автоматизировать следующие вещи:
- Собрать всех пользователей из моих подписок.
- Собрать последние посты каждого.
- Собрать все комментарии к каждому посту.
- Отсеять ответы, которым больше 30 дней.
- Отсеять ответы с минимальным количеством хлопков.
- Сохранить имена оставшихся авторов.
[course id=1052]
Будем пробовать
Сначала я посмотрел API Medium, но решил, что он только ограничивает возможности. Мне было не с чем работать. Я мог получить информацию только о своей учетной записи, а не о других пользователях.
Кроме того, последнее изменение API Medium было более года назад.
Я понял, что мне придется полагаться на HTTP-запросы для получения данных, поэтому я начал ковыряться с помощью Chrome DevTools.
Первой целью было получить перечень моих подписок.
Я открыл DevTools и перешел на вкладку «Network». Я отфильтровал все, кроме XHR, чтобы увидеть, откуда Medium получает список моих подписок. Я нажал кнопку обновления страницы в своем профиле, но не получил ничего интересного.
Но что если я нажму на «Подписки»? Бинго.

Перейдя по ссылке, я нашел большой ответ. Это был хорошо отформатированный JSON, за исключением строки символов в начале ответа:
])}while(1);</x>
Я написал функцию, чтобы превратить очищенный от лишних символов JSON в словарь Python:
import json
def clean_json_response(response):
return json.loads(response.text.split('])}while(1);</x>')[1])
Я нашел точку входа. Да начнется код.
[read id=797]
Перечень моих подписок
Чтобы запросить эту информацию, мне нужен мой ID.
Ища способ получить ID пользователя, я узнал, что вы можете добавить ?format=json
для большинства URL-адресов Medium, чтобы получить JSON с этой страницы. Я пробовал это на своей странице профиля.
Смотрите, вот и ID:
])}while(1);</x>{"success":true,"payload":{"user":{"userId":"d540942266d0","name":"Radu Raicea","username":"Radu_Raicea",
...
Я написал функцию, чтобы вытащить ID из имени пользователя. Опять же, мне пришлось использовать clean_json_response
для удаления нежелательных символов в начале ответа.
Я также создал константу под названием MEDIUM
, которая содержит базу для всех URL-адресов Medium:
import requests
MEDIUM = 'https://medium.com'
def get_user_id(username):
print('Retrieving user ID...')
url = MEDIUM + '/@' + username + '?format=json'
response = requests.get(url)
response_dict = clean_json_response(response)
return response_dict['payload']['user']['userId']
С ID пользователя я запросил конечную точку /_/api/users/<user_id>/following
и получил имена пользователей из моих подписок.
Когда я сделал это в DevTools, то заметил, что JSON выдал только восемь имен. Странно.
После того, как я нажал «показать больше людей», то увидел и других пользователей. Medium использует разбивку на страницы для списка подписок.

Разбивка на страницы работает путем указания limit
(элементов на страницу) и to
(первый элемент следующей страницы). Я должен был найти способ получить ID следующего элемента.
В конце JSON-ответа от /_/api/users/<user_id>/following
я увидел интересный ключ:
...
"paging":{"path":"/_/api/users/d540942266d0/followers","next":{"limit":8,"to":"49260b62a26c"}}},"v":3,"b":"31039-15ed0e5"}
Начиная отсюда, писать код стало легко.
def get_list_of_followings(user_id):
print('Retrieving users from Followings...')
next_id = False
followings = []
while True:
if next_id:
# If this is not the first page of the followings list
url = MEDIUM + '/_/api/users/' + user_id
+ '/following?limit=8&to=' + next_id
else:
# If this is the first page of the followings list
url = MEDIUM + '/_/api/users/' + user_id + '/following'
response = requests.get(url)
response_dict = clean_json_response(response)
payload = response_dict['payload']
for user in payload['value']:
followings.append(user['username'])
try:
# If the "to" key is missing, we've reached the end
# of the list and an exception is thrown
next_id = payload['paging']['next']['to']
except:
break
return followings
Последние посты каждого пользователя
Как только я получил список пользователей, я захотел найти их последние посты. Я мог бы сделать это с запросом https://medium.com/@<username>/latest?format=json.
Я написал функцию, которая выдает ID всех последних постов пользователей во входном списке:
def get_list_of_latest_posts_ids(usernames):
print('Retrieving the latest posts...')
post_ids = []
for username in usernames:
url = MEDIUM + '/@' + username + '/latest?format=json'
response = requests.get(url)
response_dict = clean_json_response(response)
try:
posts = response_dict['payload']['references']['Post']
except:
posts = []
if posts:
for key in posts.keys():
post_ids.append(posts[key]['id'])
return post_ids
Комментарии к постам
Из списка последних постов я извлек все комментарии, используя https://medium.com/_/api/posts/<post_id>/respon
ses.
Эта функция принимает список ID постов и выдает список комментариев:
def get_post_responses(posts):
print('Retrieving the post responses...')
responses = []
for post in posts:
url = MEDIUM + '/_/api/posts/' + post + '/responses'
response = requests.get(url)
response_dict = clean_json_response(response)
responses += response_dict['payload']['value']
return responses
Сортировка комментариев
Сначала я хотел получить комментарии с минимальным количеством хлопков. Но затем понял, что это не лучший показатель: пользователь может поставить несколько хлопков одной и той же статье.
Вместо этого я отфильтровал комментарии по количеству рекомендаций — этот показатель не учитывает дубликаты.
Я хотел, чтобы минимум был динамическим, поэтому я создал переменную с именем recommend_min
.
Следующая функция принимает комментарий и переменную recommend_min
. Затем она проверяет, соответствует ли комментарий этому минимуму:
def check_if_high_recommends(response, recommend_min):
if response['virtuals']['recommends'] >= recommend_min:
return True
Я все еще хотел проанализировать свежие комментарии. Поэтому я отфильтровал те, которые были старше 30 дней, используя эту функцию:
from datetime import datetime, timedelta
def check_if_recent(response):
limit_date = datetime.now() - timedelta(days=30)
creation_epoch_time = response['createdAt'] / 1000
creation_date = datetime.fromtimestamp(creation_epoch_time)
if creation_date >= limit_date:
return True
Имена пользователей, оставивших коммментарий
Как только у меня были все отфильтрованные комментарии, я собрал ID их авторов, используя следующую функцию:
def get_user_ids_from_responses(responses, recommend_min):
print('Retrieving user IDs from the responses...')
user_ids = []
for response in responses:
recent = check_if_recent(response)
high = check_if_high_recommends(response, recommend_min)
if recent and high:
user_ids.append(response['creatorId'])
return user_ids
ID пользователей бесполезны, когда вы пытаетесь получить доступ к чьему-то профилю. Я сделал следующий запрос функции к конечной точке /_/api/users/<user_id>
, чтобы получить имена пользователей:
def get_usernames(user_ids):
print('Retrieving usernames of interesting users...')
usernames = []
for user_id in user_ids:
url = MEDIUM + '/_/api/users/' + user_id
response = requests.get(url)
response_dict = clean_json_response(response)
payload = response_dict['payload']
usernames.append(payload['value']['username'])
return usernames
Собирая все вместе
После завершения всех функций я создал конвейер, чтобы получить список рекомендуемых пользователей:
def get_interesting_users(username, recommend_min):
print('Looking for interesting users for %s...' % username)
user_id = get_user_id(username)
usernames = get_list_of_followings(user_id)
posts = get_list_of_latest_posts_ids(usernames)
responses = get_post_responses(posts)
users = get_user_ids_from_responses(responses, recommend_min)
return get_usernames(users)
Скрип готов! Чтобы запустить его, вы должны вызвать конвейер:
interesting_users = get_interesting_users('Radu_Raicea', 10)
print(interesting_users)

Наконец, я добавил опцию добавления результатов в CSV с пометкой времени.
import csv
def list_to_csv(interesting_users_list):
with open('recommended_users.csv', 'a') as file:
writer = csv.writer(file)
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
interesting_users_list.insert(0, now)
writer.writerow(interesting_users_list)
interesting_users = get_interesting_users('Radu_Raicea', 10)
list_to_csv(interesting_users)
Исходный код проекта доступен на GitHub.
перевод: Астафьева Наталья
оригинал статьи: How I Used Python to find interesting people to follow on Medium