Реклама в Ubuntu 12.10 Взгляд изнутри
Много шума от так называемого нововведения в Ubuntu. Многие считают, что показ рекомендуемых товаров в отдельной области оскорбителен, и является веским поводом для смены дистрибутива.
Вот, что скажите тут такого...
Нижней строкой идут рекомендуемые товары, точно также у меня показывается видио с Youtube, приложения c AppStore Ubuntu-software-center и т.п.
Решил я покапаться в исходниках этой линзы и нашел , насколько я понял, не слишком приятный момент. Как мне показалось, просмотрев код
/*
* Copyright (C) 2012 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* Authored by Michal Hruby
*
*/
using GLib;
namespace Unity.ShoppingLens
{
public class ShoppingScope : Unity.Scope
{
private const string OFFERS_BASE_URI = "http://productsearch.ubuntu.com";
private HashTable results_details_map;
private HashTable global_results_details_map;
public ShoppingScope ()
{
Object (dbus_path: "/com/canonical/unity/scope/shopping");
}
protected override void constructed ()
{
/* Listen for filter changes */
filters_changed.connect (() => {
queue_search_changed (SearchType.DEFAULT);
});
active_sources_changed.connect (() => {
queue_search_changed (SearchType.DEFAULT);
});
/* No need to search if only the whitespace changes */
generate_search_key.connect ((lens_search) => {
return lens_search.search_string.strip ();
});
/* Listen for changes to the lens search entry */
search_changed.connect ((search, search_type, cancellable) => {
update_search_async.begin (search, search_type, cancellable);
});
preview_uri.connect ((uri) => {
// FIXME: async?!
return generate_preview_for_uri (uri);
});
// FIXME: the models will be re-created if this will be a remote scope
results_details_map = new HashTable (str_hash, str_equal);
results_model.set_data ("details-map", results_details_map);
global_results_details_map = new HashTable (str_hash,
str_equal);
global_results_model.set_data ("details-map", global_results_details_map);
}
private async void update_search_async (LensSearch search,
SearchType search_type,
Cancellable cancellable)
{
if (search_type == SearchType.GLOBAL)
yield perform_global_search (search, cancellable);
else
yield perform_search (search, cancellable);
search.finished ();
}
private async void perform_global_search (LensSearch search,
Cancellable cancellable)
{
search.results_model.clear ();
string query_string = search.search_string.strip ();
if (query_string == "") return;
var uri = build_search_uri (query_string, SearchType.GLOBAL);
try
{
var parser = yield get_json_reply_async (uri, cancellable);
process_search_reply_json (parser, Category.TREAT_YOURSELF,
search.results_model);
}
catch (Error err)
{
warning ("Error: %s", err.message);
}
}
private async void perform_search (LensSearch search,
Cancellable cancellable)
{
search.results_model.clear ();
string query_string = search.search_string.strip ();
if (query_string == "") return;
// we want prefix matching too
if (!query_string.has_suffix ("*")) query_string += "*";
var uri = build_search_uri (query_string, SearchType.DEFAULT);
try
{
var parser = yield get_json_reply_async (uri, cancellable);
process_search_reply_json (parser, Category.PURCHASE,
search.results_model);
}
catch (Error err)
{
warning ("Error: %s", err.message);
}
}
private Preview? generate_preview_for_uri (string uri)
{
string? details_uri =
global_results_details_map[uri] ?? results_details_map[uri];
if (details_uri == null)
{
return new GenericPreview (Path.get_basename (uri),
"No data available", null);
}
try
{
var parser = get_json_reply (details_uri, null);
var root = parser.get_root ().get_object ();
unowned string title = root.get_string_member ("title");
unowned string description = root.get_string_member ("description_html");
unowned string price = root.get_string_member ("formatted_price");
if (price == null) price = root.get_string_member ("price");
var img_obj = root.get_object_member ("images");
string image_uri = extract_image_uri (img_obj, int.MAX);
Icon? image = null;
if (image_uri != "")
{
image = new FileIcon (File.new_for_uri (image_uri));
}
var preview = new GenericPreview (title, MarkupCleaner.html_to_pango_markup (description), image);
var icon_dir = File.new_for_path (ICON_PATH);
var icon = new FileIcon (icon_dir.get_child ("service-amazon.svg"));
var buy_action = new PreviewAction ("buy", _("Buy"), icon);
if (price != null) buy_action.extra_text = price;
/* Leaving the activation on unity for now */
// buy_action.activated.connect ((uri) => { });
preview.add_action (buy_action);
return preview;
}
catch (Error err)
{
return new GenericPreview (Path.get_basename (uri), err.message, null);
}
}
private string build_search_uri (string query, SearchType search_type)
{
StringBuilder s = new StringBuilder ();
unowned string base_uri = Environment.get_variable ("OFFERS_URI");
if (base_uri == null) base_uri = OFFERS_BASE_URI;
s.append (base_uri);
s.append ("/v1/search?q=");
s.append (Uri.escape_string (query, "", false));
//if (search_type == SearchType.GLOBAL)
// s.append ("&include_only_high_relevance=true");
return s.str;
}
private async Json.Parser get_json_reply_async (string uri,
Cancellable cancellable)
throws Error
{
message ("Sending request: %s", uri);
var file = File.new_for_uri (uri);
var stream = yield file.read_async (Priority.DEFAULT, cancellable);
var parser = new Json.Parser ();
yield parser.load_from_stream_async (stream);
return parser;
}
private Json.Parser get_json_reply (string uri,
Cancellable? cancellable)
throws Error
{
message ("Sending sync request: %s", uri);
var file = File.new_for_uri (uri);
var stream = file.read (cancellable);
var parser = new Json.Parser ();
parser.load_from_stream (stream);
return parser;
}
private HashTable get_image_uri_dict (Json.Object image_obj)
{
var dict = new HashTable (str_hash, str_equal);
foreach (unowned string dimensions in image_obj.get_members ())
{
int width, height;
int res = dimensions.scanf ("%dx%d", out width, out height);
if (res != 2) continue;
dict[dimensions] =
image_obj.get_array_member (dimensions).get_string_element (0);
}
return dict;
}
private List get_sorted_keys_for_dim_dict (HashTable dict)
{
var list = dict.get_keys ();
list.sort ((a_str, b_str) =>
{
int width1, height1, width2, height2;
a_str.scanf ("%dx%d", out width1, out height1);
b_str.scanf ("%dx%d", out width2, out height2);
return width1 * height1 - width2 * height2;
});
return list;
}
/**
* extract_image_uri:
*
* Returns image uri with pixel size (width * height) that is more than
* or equal to the given pixel size.
* In case only images with smaller pixel size are available, returns
* the largest of those.
*/
private string extract_image_uri (Json.Object image_obj,
int pixel_size)
{
var dict = get_image_uri_dict (image_obj);
var keys_list = get_sorted_keys_for_dim_dict (dict);
if (keys_list == null) return "";
// short-circuit evaluation
if (pixel_size == int.MAX) return dict[keys_list.last ().data];
foreach (unowned string dim_string in keys_list)
{
int width, height;
dim_string.scanf ("%dx%d", out width, out height);
if (width * height >= pixel_size)
{
return dict[dim_string];
}
}
return dict[keys_list.last ().data];
}
private void process_search_reply_json (Json.Parser parser,
Category results_category,
Dee.Model results_model)
{
HashTable details_map =
results_model.get_data ("details-map");
details_map.remove_all ();
var root_object = parser.get_root ().get_object ();
foreach (var r in root_object.get_array_member ("results").get_elements ())
{
var result = r.get_object ();
unowned string result_uri = result.get_string_member ("web_purchase_url");
if (result_uri == null || result_uri == "") continue;
var image_obj = result.get_object_member ("images");
string image_uri = extract_image_uri (image_obj, 128*128);
unowned string price = result.get_string_member ("formatted_price");
if (price == null) price = result.get_string_member ("price");
if (image_uri != "")
{
// TODO: what to do if we have price but no icon?
var file = File.new_for_uri (image_uri);
var icon = new AnnotatedIcon (new FileIcon (file));
// FIXME: dash doesn't like empty string as ribbon
icon.ribbon = price == null || price == "" ? " " : price;
// FIXME: the service doesn't expose categories yet
// icon.category = CategoryType.BOOK;
image_uri = icon.to_string ();
}
/* keep the details uri to be able to generate the preview */
details_map[result_uri] = result.get_string_member ("details");
results_model.append (result_uri,
image_uri,
results_category,
"text/html",
result.get_string_member ("title"),
"",
result_uri);
}
}
}
}
все поисковые запросы отсылаются не в Amazone а на сайт каноникал
http://productsearch.ubuntu.com/
содержащий такой вот код
{
"Ubuntu Product Search API": {
"description": "The product search API is used to search
product catalogues to get lists of items
for purchase. It is primarily used by
the product lens in Ubuntu itself.",
"endpoints": {
"/v1/search": {
"parameters": {
"q": "The actual search query.",
"imagesize": "Size of linked cover art images in search results.
Defaults to 50. Chosen from list 33, 50, 52, 75, 100,
175, 180, 182, 200, 350, 500, 800. Some art may not
exist at high image sizes like 800.",
"genres": "Comma-separated list of genres (e.g., “rock,pop”).",
"grouping": "'1', to convert list of results into a dictionary
of specific record types, keyed by kind of record.
Without grouping, [ artist alice, artist bob, album one ].
With grouping, { artist: [alice, bob], album: [ one ] }.
pagesize param and 'total' results are both per-type. ",
"page": "Page of results, for pagination. Defaults to 1.",
"pagesize": "Number of results returned per page. Defaults to 10.",
"decade": "Comma-separated list of decades to filter on for
release date. Style is, e.g., “1980,1990”; allowed
values are 1900-2020.",
"geo_store": "A two-letter country code designating the store to
search. Defaults to the appropriate country for the
requesting IP. Note that setting this will likely not
do what you want; if you get results for a store
inappropriate to your country, then you won't be
able to buy tracks from that store anyway, so you
likely shouldn't use this."
},
"example_output": {
"total": 19,
"pagesize": 10,
"page": 1
"results": [
{
"purchase_url": "u1ms://…/#url varies by source and geo store",
"artist": "The Rolling Stones",
"image": "http://cdn.7static.com/static/img/sleeveart/00/008/134/0000813486_50.jpg",
"title": "Exile On Main Street",
"source": "Ubuntu One Product Store",
"year": "2010",
"type": "album",
"web_purchase_url": ""
},
…
]
}
}
}
}
}
и, полагаю, заменить рекламу Amazone на Ozone или Яндексмаркет, или на любой другой ресурс, не получится. Увы :(
Комментарии
pomodor
2 октября, 2012 - 22:26
Они и не скрывают, что реклама показывается не на прямую, а через сервера Каноникла. Якобы для того, чтобы анонимизировать запросы перед отправкой в Амазон. На самом деле получается, что данные будет собирать и поставщик рекламы, и третья сторона. Причем, у Каноникла данные будут накапливаться в деанонимированном виде. Редиски, в общем.
MrBison
3 октября, 2012 - 06:49
>> Они и не скрывают, что реклама показывается не на прямую, а через сервера Каноникла. Якобы для того, чтобы анонимизировать запросы перед отправкой в Амазон.
Не якобы, а именно для этого. Отдавать тысячи поисковых запросов напрямую в Amazon (а ещё другие поисковики в будущем) -- это была бы уже не "реклама", а предательство.
>> Причем, у Каноникла данные будут накапливаться в деанонимированном виде. Редиски, в общем.
Я что-то в описании протокола (второй блок кода) вообще не вижу каких-либо идентифицирующих пользователя данных, кроме указания страны. Так что данные отправляются скорее всего действительно анонимно.
pomodor
3 октября, 2012 - 14:43
Именно якобы. Истинная цель — самим собирать базу поисковых запросов. Потом на этой базе можно озолотиться, втюхивая уже таргетированную рекламу. Может им даже Амазон понадобился только для того, чтобы оправдать прикарманивание локальных поисковых запросов.
MrBison
3 октября, 2012 - 16:01
Тогда почему в запросах, отправляемых канониклу, нет никаких идентифицирующих данных?
И да, надо перестать думать, будто все корпорации по определению злые.
comrade
3 октября, 2012 - 16:45
Вообще-то так думать вернее
((-;
pomodor
5 октября, 2012 - 18:39
Кому именно не надо так думать? Вам? Не думайте! :)
Во-вторых, корпорации не несут зло просто из озорства. У них вообще нет цели творить добро или зло. Задача корпорации — зарабатывать. А наша задача заключается в том, чтобы не давать корпорациям переступать разумные рамки в этом желании заработать. Появление в Линуксе рекламы — это как раз пример такого выхода за рамки.
А зачем они нужны? Уже по одному IP можно сортировать запросы и потом таргетировать рекламу. Зачем Канониклу знать, что юзера, например, зовут Вася, а не Петя? Достаточно знать, что именно юзер с IP aaa.bbb.ccc.ddd регулярно ищет на своем компе, чтобы определить круг его интересов и подсунуть соответствующую интересам рекламу.
Чингачгук
3 октября, 2012 - 16:14
Да ладно, обычная партнерская программа с целью монетизации разработки. Вполне привычное явление на винде. Сервера каноникал там нужны только для определения объема траффика с целью уточнения размера выплат.
Чингачгук
3 октября, 2012 - 20:44
Интерфейс Юнити уже начал сильно раздражать. Несколько месяцев на нем проработал, но уже как-то приторно. Раздражает медлительность, боковая вертикальная панель, которую нельзя удалить (можно спрятать, но эта функция реализована очень коряво). То есть, Юнити уже надоел. Хотя такого ощущения никогда не возникало ни с Гномом, ни с XFCE, на с LXDE.
Legun
5 октября, 2012 - 08:57
и дико не хватает возможностей старого Gnome, приятно было переносить панели на любую сторону экрана, менять их размер, добавлять виджеты, да и
панель управления(сори) параметры системы, были куда побогаче.Комментировать