Chrome Extension Programming: Illustrating a Basic Survival Skill with a Twitter Case Study
Andrej Karpathy рассказывает о навыке быстрого создания расширений для Chrome, позволяющих модифицировать любые веб-сайты по своему вкусу. На примере Twitter он демонстрирует, как за ~100 строк JavaScript и 10 минут работы можно убрать раздражающие элементы интерфейса, скрыть рекламные твиты, настроить автоматическую подгрузку новых записей и добавить подсветку твитов от редко пишущих авторов. Анализ ленты за неделю показал, что всего 30 аккаунтов из 384 подписок формируют 50% всего контента. Расширение использует локальное хранилище Chrome для накопления статистики частоты публикаций и подсвечивает жёлтым редких авторов, а зелёным — избранных VIP-аккаунтов. Кarpathy подчёркивает, что умение инспектировать DOM и писать JavaScript открывает мощные возможности для персонализации любого сайта.
Extension Hacking
Хакинг расширений
I wanted to share a few examples of a powerful skill that I’ve been gradually picking up over the last year. It is simply the ability to quickly hack together custom browser extensions in Chrome and using them to customize my favorite websites. Writing extensions is very fast: you need a short manifest file that contains some boring meta information, a few js/html files with your code in a folder, and then you simply activate the folder as an extension from the Extensions menu with a few clicks. In general, you can do a lot of fancy things with extensions:
Я хотел поделиться несколькими примерами мощного навыка, который постепенно осваивал в течение последнего года. Это просто умение быстро собирать собственные расширения для браузера Chrome и с их помощью настраивать любимые сайты под себя. Писать расширения очень быстро: нужен короткий файл манифеста с некоторой скучной мета-информацией, несколько js/html-файлов с кодом в папке, а затем вы просто активируете эту папку как расширение из меню «Расширения» парой кликов. В целом с расширениями можно делать массу интересного:
добавлять кнопки, пункты контекстного меню модифицировать функциональность адресной строки (Omnibox) создавать специальные веб-страницы расширения для отображения различных данных/настроек расширению доступно локальное (или синхронизируемое) постоянное хранилище данных можно выполнять практически произвольный Javascript над DOM любой веб-страницы
I can’t stress how powerful the last item is. You can run Javascript. On top of any webpage. You can Read the page DOM. You can write to it, automatically, on load of the webpage or even periodically! This gives you complete freedom in modifying any webpage to your tastes: remove annoying content, add new features, log/scrape website data, change the layout, etc. It’s completely crazy!
Невозможно переоценить, насколько мощным является последний пункт. Вы можете запускать Javascript. Поверх любой веб-страницы. Вы можете читать DOM страницы. Вы можете писать в него — автоматически, при загрузке страницы или даже периодически! Это даёт полную свободу в модификации любой веб-страницы на свой вкус: убирать раздражающий контент, добавлять новые функции, логировать/парсить данные сайта, менять вёрстку и так далее. Это абсолютно невероятно!
I’ll walk you through some examples with possible mods of Twitter just to give you a glimpse of how easy and powerful this can be. Twitter is fun and I use it often, but their website is annoying, has some ugly elements, and sometimes lacks certain functionality I would like it to have. A normal person would request features and wait, but with the dark arts of extension hacking we can do much better. Lets get right to it.
Я проведу вас через несколько примеров возможных модификаций Twitter, чтобы дать представление о том, насколько это просто и мощно. Twitter — это весело, и я пользуюсь им часто, но их сайт раздражает, содержит уродливые элементы и порой лишён функциональности, которую я хотел бы видеть. Обычный человек запросил бы фичи и ждал, но с тёмными искусствами хакинга расширений мы можем сделать гораздо лучше. Давайте приступим.
Fixing the Ugly
Исправляем уродливое
A recent change on Twitter added this ugly text, visible by default and always, on every single tweet: 
Недавнее изменение в Twitter добавило этот уродливый текст, видимый по умолчанию и постоянно, под каждым твитом:
I understand it gets people to accidentally click on these more and pads Twitter’s engagement numbers, but it’s useless, ugly, and it just takes up too much space. Lets right click on one of these and choose Inspect Element. This opens up the HTML of the page and here we see the culprit DOM elements:
Я понимаю, что это заставляет людей случайно кликать по этим элементам и накручивает метрики вовлечённости Twitter, но это бесполезно, уродливо и просто занимает слишком много места. Давайте кликнем правой кнопкой по одному из таких элементов и выберем «Просмотреть код». Откроется HTML страницы, и здесь мы видим виновные элементы DOM:
So we have a list <ul></ul> with items <li> one for each of Reply, Retweet, Favorite and More. Inside every of them they have a <a>(anchor that processes the click action), deeper we have a <span> that becomes the icon, and finally followed by the ugly text wrapped in <b>. That looks easy enough, we will find all these elements based on their class attribute, descend down to find the text and get rid of it. So we create a new folder for TwitterClean extension, copy paste some manifest boring code and set it up to load a javascript file anytime twitter loads. For example, right after twitter.com page loads, lets execute:
Итак, у нас есть список с элементами — по одному для Reply, Retweet, Favorite и More. Внутри каждого из них находится (ссылка, обрабатывающая клик), глубже — , который становится иконкой, и наконец уродливый текст, обёрнутый в . Выглядит достаточно просто: мы найдём все эти элементы по атрибуту class, спустимся до текста и избавимся от него. Создаём новую папку для расширения TwitterClean, копируем шаблонный код манифеста и настраиваем его на загрузку JavaScript-файла при каждом открытии Twitter. Например, сразу после загрузки страницы twitter.com выполним:
var clean_twitter = function(){ var ugly = []; ugly.push('.action-reply-container'); ugly.push('.action-rt-container'); ugly.push('.action-del-container'); ugly.push('.action-fav-container'); ugly.push('.more-tweet-actions'); for(var i=0;i<ugly.length;i++) { var u = $(ugly[i]).find('b'); u.text(''); } }
var clean_twitter = function(){ var ugly = []; ugly.push('.action-reply-container'); ugly.push('.action-rt-container'); ugly.push('.action-del-container'); ugly.push('.action-fav-container'); ugly.push('.more-tweet-actions'); for(var i=0;i<ugly.length;i++) { var u = $(ugly[i]).find('b'); u.text(''); } }
Load the Extension, refresh Twitter and poof! All the text is gone and we’re just left with the icons. These suffice. Oh and while we’re at code we run automatically on load of twitter.com, lets slip this one is as well:
Загружаем расширение, обновляем Twitter — и вуаля! Весь текст исчез, остались только иконки. Их вполне достаточно. О, и раз уж мы пишем код, который автоматически выполняется при загрузке twitter.com, давайте подкинем ещё вот это:
$('.promoted-tweet').hide(); // oops!
$('.promoted-tweet').hide(); // упс!
I’ll let you figure out what that single naughty line of code does for you :)
Предоставлю вам самим догадаться, что делает эта одна шаловливая строчка кода :)
Loading new tweets automatically
Автоматическая загрузка новых твитов
Here’s another annoyance: you have your Twitter running on your side monitor and new tweets come in, but Twitter doesn’t load them automatically! It just shows this:
Вот ещё одна раздражающая вещь: у вас открыт Twitter на втором мониторе, приходят новые твиты, но Twitter не загружает их автоматически! Он просто показывает вот это:
That’s the passive aggressive look of Twitter telling you that there are two more tweets to show, but also refusing to actually show them. That would be too useful to their users. Instead, they want you to stop what you’re doing and click the button to load the new tweets. Luckily, you are skilled at extension hacking so you can simply right click the caption, go to Inspect Element, and see that the <div> element that tells you there are more tweets has class “js-new-tweets-bar”. Easy enough:
Это пассивно-агрессивное поведение Twitter: он сообщает, что есть ещё два новых твита, но при этом отказывается их показывать. Это было бы слишком полезно для пользователей. Вместо этого они хотят, чтобы вы бросили свои дела и нажали кнопку для загрузки новых твитов. К счастью, вы владеете искусством хакинга расширений, поэтому можно просто кликнуть правой кнопкой по надписи, выбрать «Просмотреть код» и увидеть, что элемент
var periodic = function() { L = document.getElementsByClassName('js-new-tweets-bar'); if(L.length > 0){ L[0].click(); } } setInterval(periodic, 1000);
var periodic = function() { L = document.getElementsByClassName('js-new-tweets-bar'); if(L.length > 0){ L[0].click(); } } setInterval(periodic, 1000);
When this gets run when twitter.com loads, it sets up the code to look for the annoying bar every second (1000 milliseconds) and then runs its click event handler which loads the new tweets. That’s all it takes, and now your tweets are streaming down automatically whenever they are available without you having to explicitly refresh them all the time. We’ve only written code for 5 minutes and in that time we tweaked the way Twitter looked, removed some “functionality” and added some functionality! We’re on a roll! Let’s do something fancier now.
Когда этот код запускается при загрузке twitter.com, он настраивает проверку раздражающей плашки каждую секунду (1000 миллисекунд) и вызывает её обработчик клика, который подгружает новые твиты. Вот и всё — теперь ваши твиты автоматически подгружаются, как только появляются, без необходимости постоянно обновлять страницу вручную. Мы писали код всего 5 минут, и за это время подправили внешний вид Twitter, убрали кое-какой «функционал» и добавили новый! Мы на ходу! Давайте теперь сделаем что-нибудь поинтереснее.
Highlighting tweets from rare tweeters (wait, or tweepers?)
Подсветка твитов от редко пишущих авторов (или, подождите, твиперов?)
One day I decided to collect tweets on my timeline over a period of a week using Twitter’s REST API and saw that 30 accounts make up 50% of everything I see on Twitter. Since I follow 384 accounts in total, that’s only 7%! Unfortunately, for Twitter every tweet is created equal, which means that this annoying social media guru person who tweets 100 times a day completely drowns tweets coming from your other friends who believe that one should also have something worthy of tweeting too. Okay well it’s not exactly like that but I wished there was a mechanism for highlighting the very infrequent tweeters and seeing that low frequency content. Twitter will never implement this because it makes Zero sense for their revenue model, but luckily, we can hack this together quite easily! First, here’s a function that goes through all tweets on your timeline, looks at who tweeted, and “charges” every unique tweet to the originating user:
Однажды я решил собрать твиты из своей ленты за неделю через REST API Twitter и обнаружил, что 30 аккаунтов формируют 50% всего, что я вижу в Twitter. Учитывая, что я подписан на 384 аккаунта, это всего 7%! К сожалению, для Twitter все твиты равны, а это значит, что какой-нибудь навязчивый гуру соцсетей, который твитит 100 раз в день, полностью заглушает твиты от ваших друзей, которые считают, что перед публикацией неплохо бы иметь что-то стоящее. Ладно, не совсем так, но мне хотелось иметь механизм для подсветки очень редких авторов и возможность видеть их контент. Twitter никогда не реализует это, потому что для их модели монетизации это не имеет никакого смысла, но, к счастью, мы можем легко собрать это сами! Вот функция, которая проходит по всем твитам в вашей ленте, смотрит, кто написал твит, и «начисляет» каждый уникальный твит соответствующему пользователю:
var charge_tweets = function() { // get all tweets in twitter timeline var items = $('.tweet'); for(var i=0;i<items.length;i++) { var it = items[i]; // extract information from tweet HTML var original_user = $(it).attr('data-screen-name'); var retweeter = $(it).attr('data-retweeter'); var tweet_id = $(it).attr('data-tweet-id'); // a bit of logic var charged_user = original_user; if(typeof retweeter !== 'undefined') { charged_user = retweeter; } // charge tweet to the user if(charge.hasOwnProperty(charged_user)) { var L = charge[charged_user]; if($.inArray(tweet_id, L) === -1) { L.push(tweet_id); } } else { charge[charged_user] = [tweet_id]; } } };
var charge_tweets = function() { // получаем все твиты в ленте Twitter var items = $('.tweet'); for(var i=0;i<items.length;i++) { var it = items[i]; // извлекаем информацию из HTML твита var original_user = $(it).attr('data-screen-name'); var retweeter = $(it).attr('data-retweeter'); var tweet_id = $(it).attr('data-tweet-id'); // немного логики var charged_user = original_user; if(typeof retweeter !== 'undefined') { charged_user = retweeter; } // начисляем твит пользователю if(charge.hasOwnProperty(charged_user)) { var L = charge[charged_user]; if($.inArray(tweet_id, L) === -1) { L.push(tweet_id); } } else { charge[charged_user] = [tweet_id]; } } };
Basically, it turns out every tweet has class “tweet”, so it is trivial to iterate over them as seen above. Similarly, by inspecting the way the HTML is laid out, it turns out we can simply scrape the user and the (unique) tweet id and use it to build up a dictionary of user_string -> [tweet id, ...]. Of course, we will have to let this accumulate for a few days before it measures a good tweeting frequency distribution for all people we follow as we visit Twitter again and again always seeing new tweets from more people. But this also means we have to load and save the charge dictionary from Chrome’s local extension storage or otherwise we would lose all our charging work whenever we close the Tab! Easy enough:
По сути, оказывается, что каждый твит имеет класс «tweet», поэтому перебирать их тривиально, как показано выше. Аналогично, изучив структуру HTML, оказывается, что мы можем просто извлечь имя пользователя и (уникальный) id твита и использовать их для построения словаря user_string -> [tweet id, ...]. Конечно, нужно подождать несколько дней, чтобы накопить хорошее распределение частоты публикаций для всех, на кого мы подписаны, — ведь мы заходим в Twitter снова и снова, каждый раз видя новые твиты от разных людей. Но это также означает, что нам нужно сохранять и загружать словарь charge из локального хранилища расширения Chrome, иначе мы потеряем всю накопленную статистику при закрытии вкладки! Это несложно:
var save_charge = function() { chrome.storage.local.set({'charge': charge}); } var load_charge = function() { chrome.storage.local.get('charge', function (result) { if(result.charge) { charge = result.charge; console.log('loaded tweet frequency stats:'); console.log(charge); } else { console.log('no tweet frequency to load'); } }); }
var save_charge = function() { chrome.storage.local.set({'charge': charge}); } var load_charge = function() { chrome.storage.local.get('charge', function (result) { if(result.charge) { charge = result.charge; console.log('loaded tweet frequency stats:'); console.log(charge); } else { console.log('no tweet frequency to load'); } }); }
Now we just make sure to run load_charge() at start up, and save_charge() anytime there are new tweets and our charge dictionary changes. Based on this charge dictionary we can easily find, say, the 50th percentile frequency, and highlight any tweet that comes from a user who tweets less often than 50% of the users we follow:
Теперь нужно просто убедиться, что load_charge() вызывается при запуске, а save_charge() — каждый раз, когда появляются новые твиты и наш словарь charge обновляется. На основе этого словаря charge мы можем легко найти, скажем, 50-й перцентиль частоты и подсветить любой твит от пользователя, который публикует реже, чем 50% людей, на которых мы подписаны:
var display_charges = function() { var items = $('.tweet'); for(var i=0;i<items.length;i++) { var it = items[i]; // ... as above and then: var charged_tweets = charge[charged_user]; var charge_count = charged_tweets.length; // adjust highlight color of the tweet according to rareness if(charge_percentile > 0) { var ratio = charge_count / charge_percentile; var x = Math.floor(Math.min(ratio,1)*255); $(it).css('background-color', 'rgb(255,255,' + x + ')'); } } }
var display_charges = function() { var items = $('.tweet'); for(var i=0;i<items.length;i++) { var it = items[i]; // ... как выше, а затем: var charged_tweets = charge[charged_user]; var charge_count = charged_tweets.length; // подстраиваем цвет подсветки твита в зависимости от редкости if(charge_percentile > 0) { var ratio = charge_count / charge_percentile; var x = Math.floor(Math.min(ratio,1)*255); $(it).css('background-color', 'rgb(255,255,' + x + ')'); } } }
This is just one possibility out of many. Here, ratio will be low for users who rarely tweet, and we’re setting their tweet to be yellow based on their rareness. Very hard to not notice on your timeline! :) And while we’re at it, why not also fit in:
Это лишь одна из множества возможностей. Здесь ratio будет низким для пользователей, которые редко твитят, и мы окрашиваем их твиты в жёлтый цвет в зависимости от редкости. Такое очень трудно не заметить в ленте! :) А раз уж мы тут, почему бы не добавить ещё:
var VIP = ['elonmusk']; if($.inArray(charged_user, VIP) !== -1) { $(it).css('background-color', 'rgb(150,255,150)'); }
var VIP = ['elonmusk']; if($.inArray(charged_user, VIP) !== -1) { $(it).css('background-color', 'rgb(150,255,150)'); }
This way, Elon Musk’s (or your other Twitter favorites) tweets will always glow a vibrant, green color that is hard to notice! Nice. Here’s what we get:
Таким образом, твиты Elon Musk (или других ваших фаворитов в Twitter) будут всегда светиться ярким зелёным цветом, который трудно не заметить! Отлично. Вот что получается:
Just look at that! Mashable and some person who needed every single one of his followers to know “Aarrrgh” look normal, Elon’s tweets are hard to miss green, and someone who doesn’t tweet relatively as often is highlighted a bit as yellow.
Только посмотрите на это! Mashable и некто, кому нужно было донести до каждого подписчика своё «Ааааррргх», выглядят обычно; твиты Elon невозможно пропустить благодаря зелёному цвету, а кто-то, кто публикует относительно редко, подсвечен жёлтым.
Summary
Итоги
It took us ~100 lines and 10 minutes of Javascript (with a bit of practice) and we tweaked Twitter’s look, removed err… undesirable content, made Twitter autorefresh, and added an entirely new feature that highlights infrequent tweepers!
Нам понадобилось ~100 строк и 10 минут JavaScript (с небольшой практикой), и мы подправили внешний вид Twitter, убрали э-э… нежелательный контент, сделали автообновление Twitter и добавили совершенно новую функцию — подсветку редко пишущих твиперов!
Yet we’ve only barely scratched the surface. If you’re comfortable with navigating HTML of pages with Chrome’s awesome inspector and writing HTML/Javascript/CSS, these quick hacks have the potential to significantly improve your online experience by giving you powerful options for customizing your favorite sites. And if you are not comfortable, perhaps it’s time to head over to Chrome Extensions “Getting Started” and write a few hacks :)
И всё же мы лишь едва коснулись поверхности. Если вам комфортно ориентироваться в HTML страниц с помощью отличного инспектора Chrome и писать HTML/Javascript/CSS, эти быстрые хаки способны значительно улучшить ваш онлайн-опыт, давая мощные инструменты для кастомизации любимых сайтов. А если вам пока некомфортно, возможно, пора заглянуть в руководство Chrome Extensions «Getting Started» и написать пару хаков :)
Oh, and if you’d like the full code of the above, you may find it here: LINK (Note it is a bit rough around the edges, but then it is a quick hack after all!). Let me know if you have any issues on @karpathy, and until later!
О, и если вам нужен полный код вышеописанного, вы можете найти его здесь: ССЫЛКА (Учтите, код немного сыроват, но это ведь быстрый хак, в конце концов!). Сообщайте мне о любых проблемах в @karpathy, а пока — до встречи!