В эти выходные на конференции я обнаружил, что у New York Times довольно глубокий API для разработчиков . Он охватывает как данные их газет, так и правительственную и региональную информацию. Я немного поэкспериментировал с этим и нашел, что его очень легко использовать через JavaScript (я не думаю, что они это документируют, но они используют CORS, так что вы можете пропустить JSONP), и решил, что постараюсь провести небольшой эксперимент. Что если бы мы могли использовать API для отображения количества случаев, когда ключевое слово появлялось в заголовках с течением времени?
Я начал с просмотра документации API для поиска статей и обнаружил, что они позволяют сузить ваш поисковый запрос до определенной части статьи. Я начал с простого примера, который просто искал во всей базе данных термин в заголовке.
$.get("http://api.nytimes.com/svc/search/v2/articlesearch.json", { "api-key":API, sort:"oldest", fq:"headline:(\""+term+"\")", fl:"headline,snippet,multimedia,pub_date"}, function(res) { console.dir(res.response); }, "JSON");
Довольно просто. Я также ограничил результат обратно. В настоящее время я не отображаю никаких фактических данных результатов, просто собираю данные, но я сосредоточился на нескольких ключевых элементах для результата, который, как я думал, я мог бы добавить позже. Важной вещью является объект ответа. Он содержит мета-ключ с общим количеством результатов.
Хорошо, это на полпути. Следующим шагом было ограничение результатов одним календарным годом. Опять же, API делает это чертовски простым. Я написал функцию, которая принимала год и поисковый запрос и выполняла поиск.
function fetchForYear(year, term) { //YYYYMMDD var startYearStr = year + "0101"; var endYearStr = year + "1231"; console.log('doing year '+year); return $.get("http://api.nytimes.com/svc/search/v2/articlesearch.json", { "api-key":API, sort:"oldest", begin_date:startYearStr, end_date:endYearStr, fq:"headline:(\""+term+"\")", fl:"headline,snippet,multimedia,pub_date"}, function(res) { //Ok, currently assume a good response //todo - check the response //console.dir(res.response); totalDone++; }, "JSON"); }
Обратите внимание, что я возвращаю обещание, сгенерированное обработчиком Ajax jQuery. Зачем? Ну, мне нужно выполнить большое количество вызовов Ajax. Чтобы справиться со знанием того, когда все они сделаны, я могу объединить их в массив и ждать, пока все они закончат. Обещания серьезно подходят для подобных вещей.
function search() { var term = $search.val(); if(term === '') return; totalDone = 0; console.log("Searching for "+term); var currentYear = START_YEAR; var promises = []; //Gather up all the promises... for(var i=START_YEAR; i<=END_YEAR; i++) { promises.push(fetchForYear(i,term)); } //And when done, lets graph this! $.when.apply($, promises).done(function() { console.log('DONE'); console.dir(arguments); }); }
Код выше работал отлично. Я проверил его на множестве звонков, и он правильно обработал, дожидаясь их завершения и предоставив мне легкий доступ ко всем результатам. Тогда я столкнулся с проблемой. Я изменил свои данные, чтобы перейти от 10 звонков к 100, а затем включил ограничение скорости API NYT. По какой-то глупой причине (и, честно говоря, это действительно глупо), Нью-Йорк Таймс отказывается сообщать вам, каковы их пределы скорости, пока вы не зарегистрируетесь и не создадите приложение. Теперь вы можете зарегистрироваться бесплатно, поэтому ничего не стоит, но для этого нет веских причин!Если вы предоставляете API, будьте предельно откровенны с вашими ограничениями. Хорошо, напыщенная речь сделана. Таким образом, их предел составляет 10 в секунду. Это бросило меня в тупик. Мне известен код, который будет ограничивать количество вызовов за период времени, но эти методы убивают (насколько мне известно) «лишние» вызовы. Я не смог найти библиотеку, которая говорила: «Если я дам вам X в период времени Y, а предел будет немного меньше, просто сдвиньте остаток до Y». Последователь в Твиттере рекомендовал просто создать рекурсивную функцию и добавить тайм-аут, так что я выбрал этот подход.
Я построил функцию с именем processSets, которая в основном смотрит на мои желаемые данные как набор … хороших наборов. Учитывая, что мне нужно более 100 лет данных, и учитывая, что NYT требует не более 10 обращений в секунду, я подумал, что делать 10 попаданий одновременно, а затем рекурсивное обращение к себе по таймауту может сработать. Я получил это работает, хотя мне пришлось использовать несколько глобальных переменных, и я чувствую, что это немного грязно. Я должен действительно абстрагировать это в модуле. На данный момент, хотя, это работает. Вот мое текущее решение.
/* Given an array of data, I process X items async at a time. When done, I see if I need to do more, and if so, I call it in Y miliseconds. The idea being I do chunks of aysnc requests with a 'pad' between them to slow down the requests. */ var globalData; var searchTerm; var currentYear = START_YEAR; var PER_SET = 10; function processSets() { var promises = []; for(var i=0;i<PER_SET;i++) { var yearToGet = currentYear + i; if(yearToGet <= END_YEAR) { promises.push(fetchForYear(yearToGet,searchTerm)); } } $.when.apply($, promises).done(function() { console.log('DONE with Set '+promises.length); //update progress var percentage = Math.floor(totalDone/(END_YEAR-START_YEAR)*100); $progress.text("Working on data, "+percentage +"% done."); //massage into something simpler // handle cases where promises array is 1 if(promises.length === 1) { var toAddRaw = arguments[0]; globalData.push({ year:currentYear, results:toAddRaw.response.meta.hits }); } else { for(var i=0,len=arguments.length;i<len;i++) { var toAddRaw = arguments[i][0]; var year = currentYear+i; globalData.push({ year:year, results:toAddRaw.response.meta.hits }); } } currentYear += PER_SET; //Am I done yet? if(currentYear <= END_YEAR) { setTimeout(processSets, 900); } else { $progress.text(""); render(globalData); } }); } function search() { var term = $search.val(); if(term === '') return; totalDone = 0; console.log("Searching for "+term); globalData = []; searchTerm = term; $progress.text("Beginning work..."); processSets(); }
Надеюсь, в этом есть какой-то смысл. Опять же, я уверен, что некоторые из моих читателей разорвут мне новый рассказ о том, как я это сделал, но это версия 0, так что будьте добры. ? Финальная часть головоломки представляла результат. Я начал пытаться использовать Chart.js , но мне не удалось заставить его визуализировать ось гистограммы / линейной диаграммы с меньшим числом тиков, чем в моем наборе данных. Затем я переключился на Highcharts . Я смог найти демо довольно чертовски близко к тому, что я хотел, но было немного трудно понять некоторые из их документации. На самом деле, у меня ушло почти столько же времени, чтобы правильно составить график, как и до того, как я преодолел проблему «10 в секунду». Highcharts очень сложен, или я очень устал, но я смог заставить его работать. Один оченьклассная часть их документов в том, что у них есть ссылки на JSFiddle. На самом деле, именно так я наконец-то решил свою проблему. Я использовал одну из их скрипок, модифицировал ее и понял, что мне нужно сделать. Конечный результат впечатляет, я думаю. Помните, что я не дизайнер, поэтому, наверное, это можно сделать лучше, но мне действительно нравится внешний вид.
Во-первых, пример поиска в интернете. Нажмите для увеличения изображения.
А вот и поиск войны. Опять же, нажмите для увеличения.
Простите за опечатку в заголовке — очевидно, мои навыки правописания лишь немного хуже, чем моя способность хорошо одеваться. Итак — хотите поиграть с этим? Я прикрепил почтовый индекс моего кода к этой записи в блоге, за исключением моего ключа API. Я также собираюсь дать ссылку на демоверсию, но учтите, что у меня разрешено посещать только 10 тысяч обращений к API в день. Мой средний пост в блоге получает около 1000 просмотров, и если вы все попробуете это, он быстро истечет. Я, конечно, не виню Нью-Йорк Таймс за это, но имейте это в виду, если вы попробуете это. У меня нет хороших отчетов об ошибках (то есть «любая»), поэтому проверьте консоль. Потому что я предполагаю , что я буду превышу предел, вот видео его в действии.