Время прочтения: 7 мин.

Довольно часто о проблемах в той или иной сфере работы организации можно узнать на основании отзывов как клиентов и пользователей, так и самих сотрудников. Когда все сайты с отзывами и рекомендациями уже проверены, но информации все равно критически не хватает, возникает вопрос: «Что дальше? Какие еще источники информации использовать?». Решение есть – использовать информацию из популярных сообществ в социальных сетях! Сегодня более подробно остановимся на разработке приложения, позволяющего производить сбор по информации о постах и комментариях к ним в такой популярной социальной сети как «Вконтакте».

Для начала определимся со структурой, предоставляемой сообществами информации. Каждое сообщество имеет свой уникальный id (Как узнать id сообщества можно почитать в официальном FAQ: https://vk.com/faq18062.) Для каждого размещенного поста установлен свой id, также, как и в случае с комментариями к ним. Тип связи между таблицами будет «один ко многим». В данном типе связей несколько строк из дочерней таблицы (в нашем случае комментарии к публикациям) зависят от одной строки в родительской таблице (публикация). Представим структуру будущих таблиц с необходимыми нам данными в виде UML диаграммы:

Где:

postsInfo – таблица, хранящая информацию о публикациях сообществ

postID – id публикации в сообществе

groupID – id сообщества

postDate – дата размещения публикации

postText – текст публикации

postStatus – статус обработки публикации

commentsInfo – таблица, хранящая информацию о комментариях к публикациям сообществ

commentID – id комментариях к публикации

commentatorID – id пользователя, оставившего комментарий

postDate – дата размещения комментария

commentText – текст комментария к публикации

Многих может заинтересовать вопрос: «Для чего предназначено поле postStatus?». Данное поле было добавлено в ходе программной реализации, с целью отслеживания статуса обработки информации по публикациям, а также во избежание потери информации при возникновении исключений (при возникновении ошибки необходимо только перезапустить код и он продолжит обработку данных с момента «падения»)

После определения схемы хранения данных, приступим к программной реализации.

Импортируем необходимые для работы библиотеки:

import requests
from datetime import datetime as dt
import pyodbc
import os
import time
import re

Для отправки http-запросов к vk api будет использоваться библиотека requests, а запись полученных результатов будет осуществляться в базу данных посредством библиотеки pyodbc.

После импорта необходимых библиотек реализуем класс для обработки и записи получаемой информации в базу данных:

class DbClient:
    def __init__(self,connectionString):
        self.connectionString = connectionString
    def createDb(self,conStrForCreateDb):
        try:
          # проверка на наличие директории для хранения базы данных
            if not os.path.exists("C:\Databases"):
                os.mkdir("C:\Databases") # создание директории в случае ее отсутствия
            #создание подключения к БД
            connection = pyodbc.connect(conStrForCreateDb,autocommit=True)
            createDbRequest = """CREATE DATABASE YOUR_DATABASE_NAME ON PRIMARY (NAME=N' YOUR_DATABASE_NAME ',FILENAME=N'C:\\Databases\\ YOUR_DATABASE_NAME.mdf') LOG ON (NAME=N' YOUR_DATABASE_NAME _log',FILENAME=N'C:\\Databases\\ YOUR_DATABASE_NAME _log.ldf')"""
            # создание объекта- курсора, посредством которого будут производиться запросы к таблицам
            dbCursor = connection.cursor()
            # выполнение скрипта на создание БД
            dbCursor.execute(createDbRequest)
            connection.commit()
            connection.close()
            print("Database creation completed...")
        except Exception as error:
            print("Method failed with error:" + str(error))
    def createTables(self):
        createTablesRequests = ["""CREATE TABLE postsInfo(groupID int,postID int,postDate datetime,postText nvarchar(max),postStatus bit)""",
        """CREATE TABLE commentsInfo(commentId int,commentatorId int,postID int,postDate datetime,commentText nvarchar(MAX),userStatus bit,textStatus bit)"""]
        # итеративное прохождение списка sql-запросов и их выполнение для создания необходимых таблиц
        for request in createTablesRequests:
            connection = pyodbc.connect(self.connectionString)
            dbCursor = connection.cursor()
            dbCursor.execute(request)
            connection.commit()
            connection.close()
        print("Creation of work tables is completed...")
    def insertDataInPostInfo(self,postID,postDate,postText,groupID):
    	try:
    		sqlString = """IF NOT EXISTS(SELECT * FROM postsInfo WHERE postID=?) INSERT INTO postsInfo (groupID,postID,postDate,postText,postStatus) VALUES (?,?,?,?,0)"""
    		connection = pyodbc.connect(self.connectionString)
    		dbCursor = connection.cursor()
    		dbCursor.execute(sqlString,postID,groupID,postID,postDate,re.sub('\n',' ',postText))
    		connection.commit()
    		connection.close()
    	except Exception as error:
    		print(error)
    def insertDataInCommentsInfo(self,commentId,commentatorId,postID,postDate,commentText):
    	try:
    		sqlString = """IF NOT EXISTS(SELECT * FROM commentsInfo WHERE commentId=?)
    		INSERT INTO commentsInfo (commentId,commentatorId,postID,postDate,commentText,userStatus,textStatus) VALUES(?,?,?,?,?,0,0)"""
    		connection = pyodbc.connect(connectionString)
    		dbCursor = connection.cursor()
    		dbCursor.execute(sqlString, commentId,commentId,commentatorId,postID,postDate,re.sub('\n',' ',commentText))
    		connection.commit()
    		connection.close()
    	except Exception as error:
    		print("Error in insertDataInCommentsInfo " +str(error))
    def updatePostInfo(self, postID):
        # запрос на изменение статуса публикации после выгрузки комментариев к нему 
        sqlString = """update postsInfo set postStatus=1 where postID=?"""
        connection = pyodbc.connect(connectionString)
        dbCursor = connection.cursor()
        dbCursor.execute(sqlString,postID)
        connection.commit()
        connection.close()

При инициализации объекта класса DbClient в конструктор будет передана строка подключения к нашей базе. Данный класс содержит следующие методы:

createDb – служит для создания базы данных

createTables – создаёт необходимые для записи таблицы

insertDataInPostInfo – производит запись информации о публикации

insertDataInCommentsInfo – производит запись комментариев к публикации и информации о них

updatePostInfo – обновляет статус обработки данных публикации

Когда вопрос с записью и хранением данных закрыт, можно приступить к непосредственной разработке методов по их сбору.

class Vk:
    def __init__(self,token,connectionString):
        self.token = token
        self.connectionString = connectionString
        self.db =  DbClient(connectionString) #создание экземпляра класса для работы с БД
    def searchPosts(self,groupId):
        offset= 0    # начальный индекс поиска публикаций
        count= 95   #шаг продвижения индекса поиска публикаций
        try:
            for _ in range(0,4):
                #формирование списка параметров запроса к api
                params = {'owner_id':groupId,'offset':offset,'count':count,'filter':'all','extended':1,'access_token':self.token,'v':5.103}
                #отправка запроса с заданными параметрами
                r = requests.get('https://api.vk.com/method/wall.get',params)
                for j in range(0,count):
                    #запись получаемых данных в таблицу
                    self.db.insertDataInPostInfo(r.json()['response']['items'][j]['id'], dt.fromtimestamp(r.json()['response']['items'][j]['date']).strftime('%Y%m%d %H:%M:%S'),r.json()['response']['items'][j]['text'],groupId)
                offset += count   # наращивание шага продвижения по публикациям
                # принудительная «остановка» работы программы, для соблюдения требований api по количеству запрсов
                time.sleep(2)
        except Exception as error:
            print(error)
    def searchComments(self,groupId):
        groupId = int(groupId)
        # поиск публикаций, к которым еще не произведен поиск комментариев
        sqlString = """select postID from postsInfo where groupID=? and postStatus=0"""
        connection = pyodbc.connect(self.connectionString)
        dbCursor = connection.cursor()
        dbCursor.execute(sqlString,groupId)
        for row in dbCursor:
            self.searchPostComments(groupId,row.postID,self.token)
            self.db.updatePostInfo(row.postID)
        connection.commit()
        connection.close()
        time.sleep(2)
    def searchPostComments(self,groupId,postID,userToken):
        params = {'owner_id':groupId,'post_id':postID,'need_likes':0,'offset':0,'count':95,'preview_lenght':0,'extended':1,'access_token':userToken,'v':5.103}
        r2 = requests.get('https://api.vk.com/method/wall.getComments',params)
        commentsCount = int(r2.json()['response']['count'])
        for i in range(0,commentsCount):
            try:
                countThreads = int(r2.json()['response']['items'][i]['thread']['count'])
                commentId = int(r2.json()['response']['items'][i]['id'])
                if countThreads !=0:
                    self.db.insertDataInCommentsInfo(int(r2.json()['response']['items'][i]['id']),
                                        int(r2.json()['response']['items'][i]['from_id']),
                                        int(postID),
                                        dt.fromtimestamp(r2.json()['response']['items'][i]['date']).strftime('%Y%m%d %H:%M:%S'),
                                        str(r2.json()['response']['items'][i]['text']))
                    self.writeThreadsComments(groupId,postID,commentId,userToken)
                else:
                    self.db.insertDataInCommentsInfo(int(r2.json()['response']['items'][i]['id']),
                                        int(r2.json()['response']['items'][i]['from_id']),
                                        int(postID),
                                        dt.fromtimestamp(r2.json()['response']['items'][i]['date']).strftime('%Y%m%d %H:%M:%S'),
                                        str(r2.json()['response']['items'][i]['text']))
            except Exception as error:
                print("Error in searchComments"+str(error))
                continue
        time.sleep(2)
    def writeThreadsComments(self,groupId,postId,commentId,userToken):
        params = {'owner_id':groupId,'post_id':postId,'comment_id':commentId,'need_likes':0,'offset':0,'count':20,'preview_lenght':0,'extended':1,'access_token':userToken,'v':5.103}
        r = requests.get('https://api.vk.com/method/wall.getComments',params)
        commentsCount = int(r.json()['response']['count'])
        for i in range(0,commentsCount):
            try:
                self.db.insertDataInCommentsInfo(int(r.json()['response']['items'][i]['id']),int(r.json()['response']['items'][i]['from_id']),int(postId),dt.fromtimestamp(r.json()['response']['items'][i]['date']).strftime('%Y%m%d %H:%M:%S'),str(r.json()['response']['items'][i]['text']))
            except Exception as error:
                print("Error in writeThreadsComments."+str(error))
                continue
        time.sleep(2)

searchPosts — производит поиск публикаций для заданного сообщества

searchComments – производит поиск еще необработанных публикаций, для которых необходимо произвести поиск комментариев

searchPostComments, writeThreadsComments – методы для поиска и записи комментариев к публикациям.

   Для более удобной манипуляции с разработанными методами создадим небольшое консольное меню, реализовав класс Main

class Main:
    def __init__(self,groupId,token,conStrForCreate,connectionString):
        self.id = groupId
        self.token = token
        self.conStrForCreate = conStrForCreate
        self.connectionString = connectionString
        self.db1 = DbClient(conStrForCreate)
        self.db2 = DbClient(connectionString)
        self.vk = Vk(token,connectionString)
    def menu(self):
        p = ['1-Создать базу данных для работы','2-Создать необходимые таблицы',
        '3-Произвести поиск постов (searchPosts)','4-Поиск комментариев к постам']
        print('Выберите необходимое действие:\n{0}\n{1}\n{2}\n{3}\n{4}'.format(p[0],p[1],p[2],p[3]))
        choice = str(input())
        if choice=="createDb" or choice=="1":
            self.db1.createDb(self.conStrForCreate)
        if choice=="createTables" or choice=="2":
            self.db2.createTables()
        if choice=="searchPosts" or choice=="3":
            self.vk.searchPosts(self.id)
        if choice=="searchComments" or choice=="4":
            self.vk.searchComments(self.id)
После создания всех необходимых 
if __name__=="__main__":
    token = 'YOUR_VK_API_TOKEN'
    conStrForCreate = "Driver={SQL Server Native Client 11.0};Server=DESKTOP-R9VI2A2\SQLEXPRESS;Database=master;Trusted_Connection=yes;"#строка подключения для создания новой БД для работы
    connectionString = "Driver={SQL Server Native Client 11.0};Server=DESKTOP-P\SQLEXPRESS;Database=YOUR_BD_NAME;Trusted_Connection=yes;"#строка подключения к используемой БД
    m = Main(id,token,conStrForCreate,connectionString)
    while(True):
        m.menu()

В результате запуска получаем окно:

Результатом запуска программы являются созданные таблицы со списком публикаций и комментариев к ним: