Содержание


Создание инструмента для анализа профилей Twitter с использованием Node.js, Mongo и D3

Инструмент для создания облака слов на облачной платформе IBM Bluemix

Comments

Работая дизайнером пользовательского интерфейса, я должен знать, чем руководствуются люди, использующие приложения, в создании которых я участвую. Хорошо продуманный пользовательский интерфейс повышает конкурентоспособность продукта.

Я подумал, что сведения об особенностях той или иной пользовательской базы можно получить из социальные сетей. Нужно взять твиты и биографии из профилей пользователей продукта в Twitter и посмотреть, как его последователи описывают себя. Возможно, это поможет понять их мотивацию и использовать результат при создании следующего продукта.

Чем сегодня занимаются люди в социальных сетях? Они создают свой собственный образ и индивидуальность, то есть, по сути, свой бренд. Они общаются с теми, кто желает общаться с ними.

Марк Цукерберг

Изучив несколько профилей, я был впечатлен подробностями биографий, которые выбрал. Однако решая эту задачу вручную, я смог бы обработать лишь малую толику пользователей, чтобы составить общую картину их менталитета. Я чувствовал, что это многообещающая идея, — биографии открыты и позволяют произвести интересный, глубокий анализ аудитории или сравнительный анализ. Тем не менее, инструментов, помогающих заглянуть в зеркало социальных сетей — чтобы понять, какими нас видят другие, оказалось не много.

Хотелось бы получить облако слов на основе биографий тысяч последователей, которое демонстрировало бы, какие слова в них встречаются чаще всего, но я не смог найти бесплатный и доступный инструмент, позволяющий создавать облако слов из биографий.

Идея профильных слов
Идея профильных слов

Я нашел коммерческий инструмент, составляющий список из первой пятерки слов по биографиям моих последователей, но за это нужно было платить, а мне этого не хотелось. К тому же я подозревал, что и в не самых часто употребляемых словах есть своя ценность и что для получения общей картины нужен более широкий спектр данных.

В результате я разработал приложение на облачной платформе IBM Bluemix, помогающее проиллюстрировать эти данные. Ниже приведен пример выходных данных этого приложения. Это облако слов, наиболее часто встречающихся в биографиях из профилей моих последователей. Ту же идею можно использовать и для анализа биографий из профилей пользователей программного продукта и твитов из его учетной записи в Twitter.

Облако слов, наиболее часто встречающихся в биографиях моих последователей в Twitter
Облако слов, наиболее часто встречающихся в биографиях моих последователей в Twitter

Облако слов можно использовать как инструмент:

  • проверки и оттачивания своего собственного образа;
  • сравнения последователей конкурирующих продуктов или персон;
  • оценки новых последователей или людей, которых вы хотели бы видеть в числе своих последователей.

В прекрасной статье, упомянутой выше, Скотт Рич показывает, как создать в Bluemix соответствующую платформу на основе Node, express и Twitter. Платформа моего приложения подобна этой, за исключением того, что я дополнил базовое Node.js-приложение Twitter средствами аутентификации в Twitter, интеграцией с базой данных Mongo и преобразованием результатов в более сложную инфографику.

Я включил обычные шаги по интеграции Mongo с Node и добавил связи с простыми проектами, посвященными обеспечению такой интеграции, но связал все это со своим приложением для изображения облака слов, которое служит примером практической реализации интеграции Mongo и Node.

Запустить приложениеПолучить код

Что нужно для создания подобного приложения

Это сложное приложение. Инструкции этого руководства помогут разработчику среднего уровня узнать, как построить приложение с аутентификацией в Twitter на базе Node и развернуть его в PaaS без большого количества необходимых компонентов и шагов.

Мой подход заключается в том, чтобы создать локальную среду с применением Mongo и Node, а затем использовать функцию автоматического развертывания в DevOps Services для развертывания приложения в Bluemix после внесения изменений в git.

Для локальной разработки я установил серверные модули Node.js и клиентские библиотеки JavaScript.

  • Серверные модули Node.js:
    • Mongodb: для подключения к базе данных mongo;
    • express: помогает организовывать и анализировать HTTP-запросы из конкретных URL-путей;
    • twit: очень удобная библиотека для вызова Twitter API;
    • OAuth: для проверки подлинности в Twitter.
  • Клиентские библиотеки JavaScript:
    • Bootstrap: для программирования некоторых надежных компонентов пользовательского интерфейса HTML;
    • D3: документы, управляемые данными, которые используются при визуализации облака слов.

При нажатии приведенной выше кнопки Получить код загружается файл package.json.

{
  "name": "ProfileWords",
  "version": "v0.0.1",
  "description": "A tool for analyzing Twitter accounts, using follower's bios",
  "dependencies": {
    "mongodb": "*",
    "express": "*",
    "twit": "~1.1.11",
    "cookie-parser": "*",
    "express-session": "~1.0.3",
    "errorhandler": "*",
    "morgan": "*",
    "request":"*",
    "multiparty":"*",
    "oauth": "*"
  },
  "engines": {
    "node": ">=0.10.0"
  },
  "repository": {}
}

Этот файл также содержит несколько пакетов, необходимых для работы Mongo и OAauth.

Весь этот код разработан с помощью технологий простого стека MEAN и предназначен для работы в Bluemix. Я привожу описание настройки службы MongoLabs в Bluemix.

Чтобы построить свое визуальное mashup-приложение для Twitter, я поступил следующим образом.

  1. Выполнил инструкции из статьи Скотта [Рича].
  2. Локально установил Mongo.
  3. Создал приложение для Twitter, описанное на сайте для разработчиков Twitter.
  4. Настроил конфигурацию для проверки подлинности в Twitter.
  5. Создал простое Mongo-приложение.
  6. Добавил базовую аутентификацию в Twitter, завершив тем самым создание платформы.
  7. Начал писать код для вызова API Twitter и записи в базу данных.
  8. Добавил библиотеку визуализации (D3).

Шаг 1. Хранение конфигурации в среде исполнения

Одно из 12 условий разработки современного программного обеспечения — хранение конфигурации в среде исполнения. Этот метод помогает надежно хранить ключи API (и другие данные).

Так как в этом проекте используется API Twitter, нужно создать новое приложение для Twitter, в котором будет работать код. Для этого предоставляются некоторые ключи Twitter, которые должны быть известны приложению app.js; в противном случае Twitter будет отклонять вызовы API, исходящие от приложения. Подробнее см. в файле README.MD моего приложения.

В моем файле app.js есть следующие строки кода, которые создают объект потребительского ключа аутентификации.

var consumer = new oauth.OAuth(
        "https://twitter.com/oauth/request_token", "https://twitter.com/oauth/access_token",
        process.env.TWITTER_CONSUMER_KEY, 
        process.env.TWITTER_CONSUMER_SECRET, 
        "1.0A", _callBackPage, "HMAC-SHA1");

        app.use(errorHandler({ dumpExceptions: true, showStack: true }));
        app.use(logger());
        app.use(cookieParser());
        app.use(session({ secret: "very secret" })
);

В этом листинге есть строковые значения process.env.TWITTER_CONSUMER_KEY и process.env.TWITTER_CONSUMER_SECRET, предоставленные Twitter для моего приложения. Создайте новое приложение для Twitter и перейдите на вкладку API keys своего приложения, как показано на следующем рисунке. Определите ключи для своей среды. Мое приложение называется ProfileWords. У вашего будет свое имя.

Пользователям Mac: чтобы определить ключи для своей среды, выполните следующие действия.

  1. Откройте окно терминала.
  2. Выполните следующую команду:
    sudo vi /etc/launchd.conf
  3. Отредактируйте файл launchd.conf и добавьте следующие строки:
    	setenv TWITTER_CONSUMER_KEY YOUR-STRING
    	setenv TWITTER_CONSUMER_SECRET YOUR-STRING
    	setenv TWITTER_ENDPOINT YOUR-ENDPOINT

Щелкните на вкладке Settings своего приложения. Укажите имя, описание, URL веб-сайта и URL обратного вызова. Установите флажок Allow this application to be used to Sign in with Twitter.

Страница параметров приложения для Twitter с URL обратного вызова
Страница параметров приложения для Twitter с URL обратного вызова

Перезагрузите систему. Теперь при запуске Node-приложения оно получит доступ к вашим строковым переменным.

Для пользователей Windows: выполните следующую команду из оболочки:

SET TWITTER_CONSUMER_KEY=YOUR-STRING

Шаг 2. Копирование проекта из DevOps Services

  1. Войдите в Bluemix и добавьте экземпляр службы MongoLab. Оставив экземпляр непривязанным, задайте имя (например, MongoLab_profilesExample) и запишите его. Служба Bluemix MongoLabs
    Служба Bluemix MongoLabs
  2. Щелкните на профиле проекта, нажмите кнопку Edit code, затем нажмите кнопку Fork, чтобы дать скопированному проекту имя. DevOps Services скопирует структуру проекта.
  3. Измените файл manifest.yml, связав его с базой данных и указав уникальное имя своего приложения, как показано в следующем листинге.
    	applications:
    	- services:
    	  - MongoLab-profilesExample
    	  disk_quota: 1024M
    	  host: myprofilesapp
    	  name: myprofilesapp
    	  command: node app.js
    	  path: .
    	  domain: mybluemix.net
    	  instances: 1
    	  memory: 512M
  4. Прежде чем развернуть приложение в Bluemix и запустить его, необходимо установить некоторые переменные среды.
  5. Перейдите в раздел Runtime своего нового приложения. В этом разделе Bluemix предоставляет удобный пользовательский интерфейс для настройки переменных среды и метод защиты конфиденциальности ключей, а также позволяет безопасно исполнять код, не раскрывая своих ключей.

    Перейдите в раздел Environment variables и добавьте ключи во вкладке User-Defined, как показано на следующем рисунке.

    Вкладка Runtime приложения в Bluemix
    Вкладка Runtime приложения в Bluemix
  6. Перезапустите приложение из Bluemix. Оно должно быть готово к работе.

Шаг 3. Настройка аутентификации и задание предельной пропускной способности

В Twitter предусмотрена предельная пропускная способность API. Этот предел ограничивает количество запросов, которые пользователь может подавать в течение определенного периода времени для предотвращения неограниченной нагрузки на серверы. Twitter советует пользователям предусматривать в своих приложениях собственное распределение ресурсов, вместо того, чтобы запускать приложение из одной учетной (например, разработчика).

Когда пользователь моего приложения ProfileWords вводит имя последователя, оно обращается к списку последователей. Этот запрос может возвращать до 1000 учетных записей. Ограничение Twitter по пропускной способности позволяет делать 15 таких запросов в течение 15 минут. (Когда занимаешься изучением профилей, это ограничение кажется тесным, но c'est la vie.)

Первый шаг заключается в том, чтобы аутентифицировать пользователя, поэтому можно вызвать API и работать в рамках персонального предела пропускной способности этого пользователя.

Изучение способа проверки подлинности в Twitter с помощью Node заняло у меня несколько часов. Хотя это обычная задача (открытую аутентификацию используют многие службы), технология Node меняется так быстро, что те примеры, которые я нашел, с последней версией модуля OAuth не работали.

Поэтому я разработал отдельный проект, чтобы показать, как использовать OAuth для подключения к Twitter. Файл README в этом проекте содержит инструкции по клонированию этого приложения с вашей собственной информацией разработчика Twitter.

Общий процесс показан в следующем примере кода:

  • когда пользователь вводит URL-адрес приложения, ему предлагается войти через Twitter;
  • когда пользователь нажимает кнопку Login, он перенаправляется на страницу аутентификации Twitter;
  • после того как пользователь выполняет вход в Twitter, он возвращается на заданную страницу обратного вызова (в данном случае это страница облака слов). URL обратного вызова запрограммирован в файле server.js подпроекта.
var express = require('express');
var util = require('util');
var oauth = require('oauth');
var http = require('http');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var errorHandler = require('errorhandler');
var logger = require('morgan');
var app = express();
var server = http.createServer(app);

// Адрес для получения учетных данных: https://dev.twitter.com/apps

var _twitterConsumerKey = "YOUR TWITTER CONSUMER KEY";
var _twitterConsumerSecret = " YOUR TWITTER CONSUMER SECRET";

var consumer = new oauth.OAuth(
    "https://twitter.com/oauth/request_token", 
    "https://twitter.com/oauth/access_token",
    _twitterConsumerKey, _twitterConsumerSecret, "1.0A", "http://127.0.0.1:8080/sessions/callback", "HMAC-SHA1");

app.use(errorHandler({ dumpExceptions: true, showStack: true }));
app.use(logger());
app.use(cookieParser());
app.use(session({ secret: "very secret" }));

app.use(function(req, res, next){
  var err = req.session.error,
      msg = req.session.success;
      delete req.session.error;
      delete req.session.success;
  res.locals.message = '';
  if (err) res.locals.message = '<p class="msg error">' + err + '</p>';
  if (msg) res.locals.message = '<p class="msg success">' + msg + '</p>';
  next();
});

app.get('/sessions/connect', function(req, res){
  consumer.getOAuthRequestToken(
      function(error, oauthToken, oauthTokenSecret, results){
        if (error) {
            res.send("Error getting OAuth request token : " + 
                     util.inspect(error), 500);
    } else {
      req.session.oauthRequestToken = oauthToken;
      req.session.oauthRequestTokenSecret = oauthTokenSecret;
      res.redirect("https://twitter.com/oauth/authorize?oauth_token="+req.session.oauthRequestToken);
      console.log( 'get sessions connect' );
    }
  });
});

app.get('/sessions/callback', function(req, res){
  util.puts(">>"+req.session.oauthRequestToken);
  util.puts(">>"+req.session.oauthRequestTokenSecret);
  util.puts(">>"+req.query.oauth_verifier);
  consumer.getOAuthAccessToken( req.session.oauthRequestToken, req.session.oauthRequestTokenSecret, req.query.oauth_verifier, 
    function(error, oauthAccessToken, oauthAccessTokenSecret, results) {
    if (error) {
    res.send("Error getting OAuth access token : " + util.inspect(error) 
            + "["+oauthAccessToken+"]"+ "[" +oauthAccessTokenSecret 
            + "]" + "[" + util.inspect(results)+"]", 500);
    } else {
      req.session.oauthAccessToken = oauthAccessToken;
      req.session.oauthAccessTokenSecret = oauthAccessTokenSecret;

      console.log( 'get sessions callback' );
      res.redirect('/home');
    }
  });
});

var tAPI = "https://api.twitter.com/1.1/account/verify_credentials.json";

app.get('/home', function(req, res){
    consumer.get( tAPI, req.session.oauthAccessToken, 
                 req.session.oauthAccessTokenSecret, 
                 function (error, data, response) {
      if (error) {
          console.log( 'error\n' );
          console.log( error );
          res.redirect('/sessions/connect');
      } else {
          var parsedData = JSON.parse(data);
          console.log( parsedData );
          res.send('You are signed in: ' + parsedData.screen_name);
      }
    });
});

app.get('*', function(req, res){
    res.redirect('/home');
});

app.listen(8080);

Файл README проекта содержит инструкции по использованию этой структуры кода для аутентификации в Twitter. Путь /sessions/connect приводит пользователя к диалоговому окну входа на его безопасном веб-сайте, а затем возвращает на путь /sessions/callback. Важно войти под своими собственными учетными данными Twitter, как показано.

В проекте ProfileWords я использовал тот же подход к аутентификации и реализовал его в файле app.js.

После аутентификации пользователя в Twitter можно получить больше информации о человеке. Доступна самая разная информация, включая имя пользователя, его местоположение, а также набор цветов в дизайне страницы пользователя в Twitter.

Особенно важная информация – секретный ключ пользователя, который понадобится ему при последующих запросах к Twitter. Этот ключ фигурирует в следующем листинге. Я храню эти данные в базе данных Mongo для последующего применения от имени пользователя.

 app.get('/profilewords.html', function(req, res){

        var twitterVerification = "https://api.twitter.com/1.1/account/verify_credentials.json";
        var token = req.session.oauthAccessToken;
        var secret = req.session.oauthAccessTokenSecret;

        consumer.get( twitterVerification, token, secret, function (error, data, response) {
            if( error ){
                console.log( 'Twitter verification error\n' );
                console.log( error );
                res.redirect('/sessions/connect');

            } else {

                var parsedData = JSON.parse(data);

                var person = ( {'name':parsedData.screen_name,
                                'oauth_access_token': req.session.oauthAccessToken,
                                'oauth_access_token_secret': req.session.oauthAccessTokenSecret } );

                var collection = followersDatabase.collection( 'tokens' );

                collection.remove( { 'name':parsedData.screen_name }, errorHandler );

                collection.insert( person, {safe:true}, errorHandler );

                res.sendfile('profilewords.html');
            }
        });
    });

Шаг 4. Подключение к Mongo

При отправке запросов к API Twitter код для сервера Node должен иметь доступ к секретному ключу пользователя Twitter. В запросе к Twitter указываются как открытый, так и секретный ключи.

Я использую открытый ключ в качестве дескриптора базы данных для поиска секретного ключа. Нужно, чтобы приложение ProfileWords могло поддерживать несколько пользователей одновременно, оно должно принимать и составлять запросы от имени нескольких пользователей, у каждого из которых свои учетные данные Twitter.

При аутентификации пользователя Twitter выдает ему секретный ключ. Этот ключ хранится в базе данных. Затем, когда пользователь отправляет запрос, приложение разыскивает сохраненный секретный ключ по открытому ключу, который хранится в веб-браузере и отправляется вместе с запросом.

Вот последовательность действий.

Последовательность действий по аутентификации в Twitter с применением Node
Последовательность действий по аутентификации в Twitter с применением Node

Mongo работает с пулом соединений. При запуске приложение Node создает соединение:

mongo = {   "hostname":"127.0.0.1",
                "port":27017,
   	            "username":"",
                "password":"",
                "name":"",
                "db":"db",
                "url":"mongodb://127.0.0.1:27017/db" };

path = mongo.url;

MongoClient.connect( path, function(err, followersDatabase) {

Обратите внимание, что все, что делает приложение Node, оно делает в рамках этого соединения, и я могу сослаться на базу данных, просто прибегнув к выражению followersDatabase.

Во многих примерах применения Mongo всякий раз, когда делается запрос, создается новое соединение. Я понял, что при развертывании в среде исполнения PaaS этот метод быстро дает осечку. В документации Mongo (откуда я узнал о MongoClient.connect) советуют работать в сеансе соединения, что я и делаю.

Я специально создал простой проект соединения с базой данных, чтобы продемонстрировать некоторые основы Mongo, но вы можете понять, как я использую ее, и из файла app.js.

Например, следующий код ищет секретный маркер, чтобы можно было направить запрос к Twitter.

app.param('id', function(req, res, next, id){    

        res.setHeader( 'Content-Type', 'application/json' );

        if( req.headers['oauth_token'] ){

            var token = req.headers['oauth_token'];
            var cloudType = req.headers['cloudtype'];

            var collection = followersDatabase.collection( 'tokens' );

            collection.findOne( { 'oauth_access_token': token }, function( err, item ) {                 

                var config = {
                    consumer_key:         'YOUR CONSUMER KEY',
                    consumer_secret:      'YOUR CONSUMER SECRET',
                    access_token_secret: item.oauth_access_token_secret,
                    access_token: item.oauth_access_token
                }

                console.log( 'account: ' + item.name );

                retrieveProfiles( config, res, id, cloudType );   

                var requests = followersDatabase.collection( 'requests' );

                var date = new Date();

                var request = ( { 'twitterId':id, 'timeStamp': date, 'account': item.name } );

                requests.insert( request, {safe:true}, errorHandler );

            });
        }

        next();
    });

    app.get( "/words/:id", function(req, res ){ } );

В базе данных можно хранить самые разнообразные аналитические данные. Например, можно подсчитывать все запросы пользователя или записывать выбор цвета темы для облака слов. В текущей версии приложения база данных используется для управления доступом к маркерам пользователей.

На этом этапе мы продолжили изучение Node и express, освоив работу с Mongo и аутентификацией. Те же ингредиенты можно использовать и в других mashup-приложениях для социальных сетей. Эти строительные блоки позволяют пойти довольно далеко.

Приобретение этих знаний заняло у меня какое-то время, но процесс — решение задач и обучение — стал привычнее. Полезные пакеты Node обеспечивают хороший фундамент.

Шаг 5. Визуализация результатов с помощью D3

За несколько часов я пришел к жестко запрограммированному приложению с извлечением данных аутентификации для Twitter. Сценарий JavaScript для облака слов я нашел довольно быстро – и увидел свою основную идею в действии.

Но дальше дело застопорилось. Мне не терпелось развивать свое приложение, но для представления данных разными способами, такими как графики или круговые диаграммы, требовалась универсальная библиотека визуализации. Немного поискав, я обнаружил D3.

Джейсон Дэвис дополнил JavaScript-библиотеку D3 расширением для облака слов. Он предлагает отличный пример и описание. После многих попыток я понял, как подставлять различные цвета слов в облаке. (Я искал красивые сочетания цветов в облаках слов, но готовые примеры найти трудно, поэтому я решил поэкспериментировать и попробовать составить привлекательные комбинации сам).

Я составил набор тем и написал небольшую процедуру для случайного выбора цвета из темы при каждой прорисовке слова. Экспериментальным путем я обнаружил, что в теме должно быть не более пяти цветов, и включил небольшой компонент пользовательского интерфейса для выбора темы. Я хотел получить возможность предложить, например, тему простой газетной бумаги, чтобы облако можно было скачать для включения в документ. Так получилась однотонная тема, но можно использовать и другие цвета. Мне нравится оранжевый цвет, как показано выше.

Что касается данных, то я написал код Node.js на сервере для сбора биографий и определения наиболее часто встречающихся слов. Поэкспериментировав, я решил, что 70 слов выглядят привлекательно с точки зрения сбалансированности распределения данных и не слишком перегруженно. Сервер возвращает данные, и облако слов воспроизводится.

function draw(data, bounds) {
  statusText.style("display", "none");
  scale = bounds ? Math.min(
      w / Math.abs(bounds[1].x - w / 2),
      w / Math.abs(bounds[0].x - w / 2),
      h / Math.abs(bounds[1].y - h / 2),
      h / Math.abs(bounds[0].y - h / 2)) / 2 : 1;
  words = data;
  var text = visualisation.selectAll("text")
      .data(words, function(d) { return d.text.toLowerCase(); });
  text.transition()
      .duration(1000)
      .attr("transform", function(d) { return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")"; })
      .style("font-size", function(d) { return d.size + "px"; });
  text.enter().append("text")
      .attr("text-anchor", "middle")
      .attr("transform", function(d) { return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")"; })
      .style("font-size", function(d) { return d.size + "px"; })
      .on("click", function(d) {
        load(d.text);
      })
      .style("opacity", 1e-6)
    .transition()
      .duration(1000)
      .style("opacity", 1);
  text.style("font-family", function(d) { return d.font; })
      .style("fill", customFill)
      .text(function(d) { return d.text; });
  var exitGroup = background.append("g")
      .attr("transform", visualisation.attr("transform"));
  var exitGroupNode = exitGroup.node();
  text.exit().each(function() {
    exitGroupNode.appendChild(this);
  });
  exitGroup.transition()
      .duration(1000)
      .style("opacity", 1e-6)
      .remove();
  visualisation.transition()
      .delay(1000)
      .duration(750)
      .attr("transform", "translate(" + [w >> 1, h >> 1] + ")scale(" + scale + ")");
}

Заключение

Мой код включает в себя множество деталей. Целью было написать реальное приложение, которое могут использовать обычные люди. Поэтому я обрабатываю ошибки; управляю маркерами авторизации; разрешаю включать просмотр списка пользователей, за которыми следит человек, а также их последователей, твиты и «избранное»; реагирую, если страница загружается на телефон; и реализую многие другие функции.

Это руководство посвящено нескольким важным строительным блокам (указатели для базы данных, аутентификация и визуализация) и обеспечивает читателю фундамент для создания подобных приложений. Остальные детали те, кто ими интересуется, могут найти в коде.

В другой моей статье, Что говорят в twitter о кандидатах на выборах, я использую свой инструмент для изучения профилей кандидатов, участвовавших в недавних выборах в провинции Онтарио.

Образ личности в социальных сетях – чрезвычайно увлекательный предмет. Я планирую встроить в эту экспериментальную программу много других вещей и со временем наращивать ее.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура, Облачные вычисления
ArticleID=1011283
ArticleTitle=Создание инструмента для анализа профилей Twitter с использованием Node.js, Mongo и D3
publish-date=07162015