Доброго времени суток, хабровчане!

В данном посте хочу рассказать о своем первом опыте работы с Amazon Product Advertising API. Этот API позволяет Вам производить параметризированный поиск товаров и всей информации, которая с ними связана на сайтах амазона. Также есть партнерская программа про помощи, которой можно в дальнейшем монетизироваться (1. Получаете Партнерский ID, 2. Добавляете его в запросы, 3. Profit? ...). Зарегистрировать партнерку можно тут.
Если интересно, читаем дальше.
Начну пост с предыстории, зачем вообще я туда полез? Шел 2012 год, март месяц, самое начало. И вот на горизонте начал виднеться «Международный женский день», в который принято дарить цветы, разного рода подарки, своим матерям, женам, дочерям, девушкам. После нескольких минут размышлений было принято решение о покупке жене второго iPad'а и цветов соответственно. Поскольку цветы в отличие от девайса можно приобрести без проблем, их покупку было решено отложить на тот самый день «Х». В городе, в котором я живу, плохо развит рынок всяких гаджетов, нет здоровой конкуренции (да и нездоровой в принципе тоже), поэтому цены просто запредельные. По счастливой случайности, мой коллега находился в командировке за океаном и я решил купить девайс на Amazon.com. Предложений была масса, цены тоже варьировались в пределах 300$-700$. И как-то, раз зашел на сайт в поисках более выгодного предложения и увидел, что какой-то реселлер выкинул на продажу 8 новых девайсов по смешной цене, пока я пытался сделать чекаут и забивал в форму данные, их уже размели. Немного погуглив на эту тему, было принято решение написать что-нибудь для мониторинга цен по данному устройству.
Для начала, заходим в свой AWS аккаунт, переходим в Security Credentials и там жмем Create a new Access Key. В результате получите следующую картину:

Заветная пара ключей получена, давайте ее протестируем. Вводим наши ключи на форме, в Unsigned URL пишем для нашей конкретной ситуации URL:
Немного поясню, я не регистрировал партнерку поэтому AssociateTag из головы. Operation=ItemLookup (все операции которые поддерживает сервис можо посмотреть тут) возвращает все или некоторые свойства (в зависимости от ResponseGroup) по искомому ItemId (который можно определить по URL).
Жмем Display Signed URL… и вуаля, у нас есть подписанный нашими ключами линк! Перейдя по нему получим:
Из Response видно, какая информация для нас предоставляет ценность (для примера была взята ResponseGroup = Large, поэтому для конкретной проблемы мы получили много лишних данных, лучше подойдет ResponseGroup = Offers).
Все информацию по работе с сервисом можно найти в Developers Guide.
Сервис предоставляет WSDL. Т.е. если мы добавим его как Web Service Reference к нашему решению, в итоге, получим proxy для работы с сервисом. Все бы хорошо, но Request который мы будем посылать на сервис надо же еще и подписать полученным SAK (Secret Access Key).
Процесс аутентификации с нашей выглядит следующим образом:

Со стороны AWS:

Все предельно ясно, но в сервисе отсутствует функционал по подписке риквеста Вашим SAK. Самым простым решением на тот момент казалось просмотреть семплы и найти нужный нам функционал, но не все так просто как кажется. Из всех пересмотренных мной семплов не удалось найти рабочий код для подписи риквеста. Быстро загуглив нашлось решение:
Использовать будем следующим образом:
Сначала я пытался парсить XmlDocument который пришел из респонза (что в корне было не правильно и заняло много времени). Потом я вспомнил про proxy в котором был класс ItemLookupResponse (корневой элемент XML респонза) и десериализацию. И все решилось в разы проще:
В итоге мы получаем объект ItemLookupResponse со всеми нужными нам свойствами. Цены мы можем вытянуть из объекта Item
Все что хотели, мы получили. Сразу оговорюсь запрос по ItemLookup не ограничивается одним &ItemId=B0047DVWLW их может быть больше, в разы. ItemID — это так называемый ASIN (Amazon Standard Identification Number). Еще немного поисследовав ItemLookupResponse в глаза бросилась секция:
Т.е. в XML ответе еще приходит набор похожих девайсов. Поэтому, появилась идея выгрести из базы все нужные девайсы и их похожие продукты через RetrieveItemDetails(string itemID) (пишем рекурсивный метод). Как Вы наверное заметили, под ASIN = B003D8GAA0 прячется совсем не нужный нам скрин протектор (и соответственно фильтруем такие элементы в респонзе).
В качестве оповещения была выбрана почта и твиттер. Устанавливался таймер и через определенный интервал проверял цены на устройства, и если цена отличалась в меньшую сторону с момента последней проверки или минимально заданной на мыло улетало письмо и определенный аккаунт в твиттере постал месадж (через Twitterizer).
Если позволит желание и время, есть идея переписать это все дело в Windows Service который будет по заданным параметрам мониторить нужные покупки. И написать отзывчивого твитбота который будет на mention с определенным тегом и параметрами выдавать пользователю нужную инфу.
И теперь о самом главном, девайс был куплен и подарен жене. Все довольны!

В данном посте хочу рассказать о своем первом опыте работы с Amazon Product Advertising API. Этот API позволяет Вам производить параметризированный поиск товаров и всей информации, которая с ними связана на сайтах амазона. Также есть партнерская программа про помощи, которой можно в дальнейшем монетизироваться (1. Получаете Партнерский ID, 2. Добавляете его в запросы, 3. Profit? ...). Зарегистрировать партнерку можно тут.
Если интересно, читаем дальше.
Предыстория
Начну пост с предыстории, зачем вообще я туда полез? Шел 2012 год, март месяц, самое начало. И вот на горизонте начал виднеться «Международный женский день», в который принято дарить цветы, разного рода подарки, своим матерям, женам, дочерям, девушкам. После нескольких минут размышлений было принято решение о покупке жене второго iPad'а и цветов соответственно. Поскольку цветы в отличие от девайса можно приобрести без проблем, их покупку было решено отложить на тот самый день «Х». В городе, в котором я живу, плохо развит рынок всяких гаджетов, нет здоровой конкуренции (да и нездоровой в принципе тоже), поэтому цены просто запредельные. По счастливой случайности, мой коллега находился в командировке за океаном и я решил купить девайс на Amazon.com. Предложений была масса, цены тоже варьировались в пределах 300$-700$. И как-то, раз зашел на сайт в поисках более выгодного предложения и увидел, что какой-то реселлер выкинул на продажу 8 новых девайсов по смешной цене, пока я пытался сделать чекаут и забивал в форму данные, их уже размели. Немного погуглив на эту тему, было принято решение написать что-нибудь для мониторинга цен по данному устройству.
Inception
Для начала, заходим в свой AWS аккаунт, переходим в Security Credentials и там жмем Create a new Access Key. В результате получите следующую картину:

Заветная пара ключей получена, давайте ее протестируем. Вводим наши ключи на форме, в Unsigned URL пишем для нашей конкретной ситуации URL:
http://ecs.amazonaws.com/onca/xml?Service=AWSECommerceService
&Version=2011-08-01
&AssociateTag=520
&Operation=ItemLookup
&ResponseGroup=Large
&ItemId=B0047DVWLW
Немного поясню, я не регистрировал партнерку поэтому AssociateTag из головы. Operation=ItemLookup (все операции которые поддерживает сервис можо посмотреть тут) возвращает все или некоторые свойства (в зависимости от ResponseGroup) по искомому ItemId (который можно определить по URL).
Жмем Display Signed URL… и вуаля, у нас есть подписанный нашими ключами линк! Перейдя по нему получим:
XML Response вида (пришлось немного окоротить, Хабр отказался есть его целиком):
<ItemLookupResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2011-08-01">
<OperationRequest>
<HTTPHeaders>
<Header Name="UserAgent" Value="Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1"/>
</HTTPHeaders>
<RequestId>9645cf90-c1ae-4505-be21-08198e5e8274</RequestId>
<Arguments>
<Argument Name="ItemId" Value="B0047DVWLW"/>
<Argument Name="Operation" Value="ItemLookup"/>
<Argument Name="Service" Value="AWSECommerceService"/>
<Argument Name="AWSAccessKeyId" Value="AKIAILZWKKRUXK7QRNRA"/>
<Argument Name="Timestamp" Value="2012-09-21T10:34:09.000Z"/>
<Argument Name="Signature" Value="w8kK7v5WuPO2lxaUwtnw1fax10SMcqN8Wg8qTqUDeHQ="/>
<Argument Name="ResponseGroup" Value="Large"/>
<Argument Name="AssociateTag" Value="520"/>
<Argument Name="Version" Value="2011-08-01"/>
</Arguments>
<RequestProcessingTime>0.2015160000000000</RequestProcessingTime>
</OperationRequest>
<Items>
<Request>
<IsValid>True</IsValid>
<ItemLookupRequest>
<IdType>ASIN</IdType>
<ItemId>B0047DVWLW</ItemId>
<ResponseGroup>Large</ResponseGroup>
<VariationPage>All</VariationPage>
</ItemLookupRequest>
</Request>
<Item>
<ASIN>B0047DVWLW</ASIN>
<ParentASIN>B004QGY7M6</ParentASIN>
<DetailPageURL>
http://www.amazon.com/Apple-MC979LL-Tablet-White-Generation/dp/B0047DVWLW%3FSubscriptionId%3DAKIAILZWKKRUXK7QRNRA%26tag%3D520%26linkCode%3Dxm2%26camp%3D2025%26creative%3D165953%26creativeASIN%3DB0047DVWLW
</DetailPageURL>
<ItemLinks>
<ItemLink>
<Description>Technical Details</Description>
<URL>
http://www.amazon.com/Apple-MC979LL-Tablet-White-Generation/dp/tech-data/B0047DVWLW%3FSubscriptionId%3DAKIAILZWKKRUXK7QRNRA%26tag%3D520%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3DB0047DVWLW
</URL>
</ItemLink>
<ItemLink>
<Description>Add To Baby Registry</Description>
<URL>
http://www.amazon.com/gp/registry/baby/add-item.html%3Fasin.0%3DB0047DVWLW%26SubscriptionId%3DAKIAILZWKKRUXK7QRNRA%26tag%3D520%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3DB0047DVWLW
</URL>
</ItemLink>
<ItemLink>
<Description>Add To Wedding Registry</Description>
<URL>
http://www.amazon.com/gp/registry/wedding/add-item.html%3Fasin.0%3DB0047DVWLW%26SubscriptionId%3DAKIAILZWKKRUXK7QRNRA%26tag%3D520%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3DB0047DVWLW
</URL>
</ItemLink>
<ItemLink>
<Description>Add To Wishlist</Description>
<URL>
http://www.amazon.com/gp/registry/wishlist/add-item.html%3Fasin.0%3DB0047DVWLW%26SubscriptionId%3DAKIAILZWKKRUXK7QRNRA%26tag%3D520%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3DB0047DVWLW
</URL>
</ItemLink>
<ItemLink>
<Description>Tell A Friend</Description>
<URL>
http://www.amazon.com/gp/pdp/taf/B0047DVWLW%3FSubscriptionId%3DAKIAILZWKKRUXK7QRNRA%26tag%3D520%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3DB0047DVWLW
</URL>
</ItemLink>
<ItemLink>
<Description>All Customer Reviews</Description>
<URL>
http://www.amazon.com/review/product/B0047DVWLW%3FSubscriptionId%3DAKIAILZWKKRUXK7QRNRA%26tag%3D520%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3DB0047DVWLW
</URL>
</ItemLink>
<ItemLink>
<Description>All Offers</Description>
<URL>
http://www.amazon.com/gp/offer-listing/B0047DVWLW%3FSubscriptionId%3DAKIAILZWKKRUXK7QRNRA%26tag%3D520%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3DB0047DVWLW
</URL>
</ItemLink>
</ItemLinks>
<SalesRank>8</SalesRank>
<SmallImage>
<URL>
http://ecx.images-amazon.com/images/I/41Yisrlx%2BFL._SL75_.jpg
</URL>
<Height Units="pixels">63</Height>
<Width Units="pixels">75</Width>
</SmallImage>
<MediumImage>
<URL>
http://ecx.images-amazon.com/images/I/41Yisrlx%2BFL._SL160_.jpg
</URL>
<Height Units="pixels">135</Height>
<Width Units="pixels">160</Width>
</MediumImage>
<LargeImage>
<URL>
http://ecx.images-amazon.com/images/I/41Yisrlx%2BFL.jpg
</URL>
<Height Units="pixels">365</Height>
<Width Units="pixels">434</Width>
</LargeImage>
<ImageSets>
<ImageSet Category="primary">
<SwatchImage>
<URL>
http://ecx.images-amazon.com/images/I/41Yisrlx%2BFL._SL30_.jpg
</URL>
<Height Units="pixels">25</Height>
<Width Units="pixels">30</Width>
</SwatchImage>
<SmallImage>
<URL>
http://ecx.images-amazon.com/images/I/41Yisrlx%2BFL._SL75_.jpg
</URL>
<Height Units="pixels">63</Height>
<Width Units="pixels">75</Width>
</SmallImage>
<ThumbnailImage>
<URL>
http://ecx.images-amazon.com/images/I/41Yisrlx%2BFL._SL75_.jpg
</URL>
<Height Units="pixels">63</Height>
<Width Units="pixels">75</Width>
</ThumbnailImage>
<TinyImage>
<URL>
http://ecx.images-amazon.com/images/I/41Yisrlx%2BFL._SL110_.jpg
</URL>
<Height Units="pixels">93</Height>
<Width Units="pixels">110</Width>
</TinyImage>
<MediumImage>
<URL>
http://ecx.images-amazon.com/images/I/41Yisrlx%2BFL._SL160_.jpg
</URL>
<Height Units="pixels">135</Height>
<Width Units="pixels">160</Width>
</MediumImage>
<LargeImage>
<URL>
http://ecx.images-amazon.com/images/I/41Yisrlx%2BFL.jpg
</URL>
<Height Units="pixels">365</Height>
<Width Units="pixels">434</Width>
</LargeImage>
</ImageSet>
<ImageSet Category="variant">
<SwatchImage>
<URL>
http://ecx.images-amazon.com/images/I/31lmfWbe6-L._SL30_.jpg
</URL>
<Height Units="pixels">15</Height>
<Width Units="pixels">30</Width>
</SwatchImage>
<SmallImage>
<URL>
http://ecx.images-amazon.com/images/I/31lmfWbe6-L._SL75_.jpg
</URL>
<Height Units="pixels">38</Height>
<Width Units="pixels">75</Width>
</SmallImage>
<ThumbnailImage>
<URL>
http://ecx.images-amazon.com/images/I/31lmfWbe6-L._SL75_.jpg
</URL>
<Height Units="pixels">38</Height>
<Width Units="pixels">75</Width>
</ThumbnailImage>
<TinyImage>
<URL>
http://ecx.images-amazon.com/images/I/31lmfWbe6-L._SL110_.jpg
</URL>
<Height Units="pixels">56</Height>
<Width Units="pixels">110</Width>
</TinyImage>
<MediumImage>
<URL>
http://ecx.images-amazon.com/images/I/31lmfWbe6-L._SL160_.jpg
</URL>
<Height Units="pixels">81</Height>
<Width Units="pixels">160</Width>
</MediumImage>
<LargeImage>
<URL>
http://ecx.images-amazon.com/images/I/31lmfWbe6-L.jpg
</URL>
<Height Units="pixels">253</Height>
<Width Units="pixels">500</Width>
</LargeImage>
</ImageSet>
<ImageSet Category="variant">
<SwatchImage>
<URL>
http://ecx.images-amazon.com/images/I/41omkn%2BPH6L._SL30_.jpg
</URL>
<Height Units="pixels">30</Height>
<Width Units="pixels">25</Width>
</SwatchImage>
<SmallImage>
<URL>
http://ecx.images-amazon.com/images/I/41omkn%2BPH6L._SL75_.jpg
</URL>
<Height Units="pixels">75</Height>
<Width Units="pixels">61</Width>
</SmallImage>
<ThumbnailImage>
<URL>
http://ecx.images-amazon.com/images/I/41omkn%2BPH6L._SL75_.jpg
</URL>
<Height Units="pixels">75</Height>
<Width Units="pixels">61</Width>
</ThumbnailImage>
<TinyImage>
<URL>
http://ecx.images-amazon.com/images/I/41omkn%2BPH6L._SL110_.jpg
</URL>
<Height Units="pixels">110</Height>
<Width Units="pixels">90</Width>
</TinyImage>
<MediumImage>
<URL>
http://ecx.images-amazon.com/images/I/41omkn%2BPH6L._SL160_.jpg
</URL>
<Height Units="pixels">160</Height>
<Width Units="pixels">131</Width>
</MediumImage>
<LargeImage>
<URL>
http://ecx.images-amazon.com/images/I/41omkn%2BPH6L.jpg
</URL>
<Height Units="pixels">500</Height>
<Width Units="pixels">409</Width>
</LargeImage>
</ImageSet>
<ImageSet Category="variant">
<SwatchImage>
<URL>
http://ecx.images-amazon.com/images/I/31gqTQEEqAL._SL30_.jpg
</URL>
<Height Units="pixels">30</Height>
<Width Units="pixels">25</Width>
</SwatchImage>
<SmallImage>
<URL>
http://ecx.images-amazon.com/images/I/31gqTQEEqAL._SL75_.jpg
</URL>
<Height Units="pixels">75</Height>
<Width Units="pixels">63</Width>
</SmallImage>
<ThumbnailImage>
<URL>
http://ecx.images-amazon.com/images/I/31gqTQEEqAL._SL75_.jpg
</URL>
<Height Units="pixels">75</Height>
<Width Units="pixels">63</Width>
</ThumbnailImage>
<TinyImage>
<URL>
http://ecx.images-amazon.com/images/I/31gqTQEEqAL._SL110_.jpg
</URL>
<Height Units="pixels">110</Height>
<Width Units="pixels">93</Width>
</TinyImage>
<MediumImage>
<URL>
http://ecx.images-amazon.com/images/I/31gqTQEEqAL._SL160_.jpg
</URL>
<Height Units="pixels">160</Height>
<Width Units="pixels">135</Width>
</MediumImage>
<LargeImage>
<URL>
http://ecx.images-amazon.com/images/I/31gqTQEEqAL.jpg
</URL>
<Height Units="pixels">365</Height>
<Width Units="pixels">309</Width>
</LargeImage>
</ImageSet>
</ImageSets>
<ItemAttributes>
<Binding>Personal Computers</Binding>
<Brand>Apple</Brand>
<CatalogNumberList>
<CatalogNumberListElement>B0047DVWLW</CatalogNumberListElement>
</CatalogNumberList>
<Color>White</Color>
<EAN>0885909471812</EAN>
<EANList>
<EANListElement>0885909471812</EANListElement>
<EANListElement>0811331000009</EANListElement>
</EANList>
<Feature>Designed for Apple's 2nd generation of iPads</Feature>
<Feature>
9.7-inch (diagonal) LED-backlit glossy widescreen Multi-Touch display with IPS technology
</Feature>
<Feature>1 GHz dual-core Apple A5 custom-designed processor</Feature>
<Feature>Forward facing and rear facing cameras</Feature>
<Feature>Apple's iOS 4 and access to Apple's app store</Feature>
<Format>CD-ROM</Format>
<HardwarePlatform>Mac</HardwarePlatform>
<IsAutographed>0</IsAutographed>
<IsEligibleForTradeIn>1</IsEligibleForTradeIn>
<IsMemorabilia>0</IsMemorabilia>
<ItemDimensions>
<Height Units="hundredths-inches">949</Height>
<Length Units="hundredths-inches">35</Length>
<Weight Units="hundredths-pounds">133</Weight>
<Width Units="hundredths-inches">732</Width>
</ItemDimensions>
<Label>Apple Computer</Label>
<LegalDisclaimer>
Item Will Not Be Shipped Until Payment Clears And Funds Are Tranferred To My Bank Account!
</LegalDisclaimer>
<ListPrice>
<Amount>39900</Amount>
<CurrencyCode>USD</CurrencyCode>
<FormattedPrice>$399.00</FormattedPrice>
</ListPrice>
<Manufacturer>Apple Computer</Manufacturer>
<Model>MC979LL/A</Model>
<MPN>MC979LL/A</MPN>
<NumberOfItems>1</NumberOfItems>
<OperatingSystem>Apple iOS 5.0</OperatingSystem>
<PackageDimensions>
<Height Units="hundredths-inches">350</Height>
<Length Units="hundredths-inches">1140</Length>
<Weight Units="hundredths-pounds">133</Weight>
<Width Units="hundredths-inches">960</Width>
</PackageDimensions>
<PackageQuantity>1</PackageQuantity>
<PartNumber>MC979LL/A</PartNumber>
<ProductGroup>Personal Computer</ProductGroup>
<ProductTypeName>TABLET_COMPUTER</ProductTypeName>
<Publisher>Apple Computer</Publisher>
<Size>16GB</Size>
<SKU>118087@634532900684301250</SKU>
<Studio>Apple Computer</Studio>
<Title>
Apple iPad 2 MC979LL/A Tablet (16GB, Wifi, White) 2nd Generation
</Title>
<TradeInValue>
<Amount>33000</Amount>
<CurrencyCode>USD</CurrencyCode>
<FormattedPrice>$330.00</FormattedPrice>
</TradeInValue>
<UPC>811331000009</UPC>
<UPCList>
<UPCListElement>811331000009</UPCListElement>
<UPCListElement>885909471812</UPCListElement>
</UPCList>
</ItemAttributes>
<OfferSummary>
<LowestNewPrice>
<Amount>39900</Amount>
<CurrencyCode>USD</CurrencyCode>
<FormattedPrice>$399.00</FormattedPrice>
</LowestNewPrice>
<LowestUsedPrice>
<Amount>34000</Amount>
<CurrencyCode>USD</CurrencyCode>
<FormattedPrice>$340.00</FormattedPrice>
</LowestUsedPrice>
<LowestRefurbishedPrice>
<Amount>34999</Amount>
<CurrencyCode>USD</CurrencyCode>
<FormattedPrice>$349.99</FormattedPrice>
</LowestRefurbishedPrice>
<TotalNew>85</TotalNew>
<TotalUsed>100</TotalUsed>
<TotalCollectible>0</TotalCollectible>
<TotalRefurbished>17</TotalRefurbished>
</OfferSummary>
<Offers>
<TotalOffers>1</TotalOffers>
<TotalOfferPages>1</TotalOfferPages>
<MoreOffersUrl>
http://www.amazon.com/gp/offer-listing/B0047DVWLW%3FSubscriptionId%3DAKIAILZWKKRUXK7QRNRA%26tag%3D520%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3DB0047DVWLW
</MoreOffersUrl>
<Offer>
<OfferAttributes>
<Condition>New</Condition>
</OfferAttributes>
<OfferListing>
<OfferListingId>
5tIFOSgtOfWUjj1N2%2FBqWUyXOtsYzDcDWOygGn8T3wdoo5gs1FLVQGbaoTAnVlGmTXh1rWsYI57d%2FfNHr%2BWexLUNQrcrhi1RM1OxR%2B65I%2Fs2Ofz0nfJ83bhbwZNUqm75udmgNjgk2t%2F3%2FJhFd5Cc87KIbmpEK7SH
</OfferListingId>
<Price>
<Amount>41400</Amount>
<CurrencyCode>USD</CurrencyCode>
<FormattedPrice>$414.00</FormattedPrice>
</Price>
<AvailabilityAttributes>
<AvailabilityType>futureDate</AvailabilityType>
<MinimumHours>0</MinimumHours>
<MaximumHours>0</MaximumHours>
</AvailabilityAttributes>
<IsEligibleForSuperSaverShipping>1</IsEligibleForSuperSaverShipping>
</OfferListing>
</Offer>
</Offers>
Из Response видно, какая информация для нас предоставляет ценность (для примера была взята ResponseGroup = Large, поэтому для конкретной проблемы мы получили много лишних данных, лучше подойдет ResponseGroup = Offers).
Все информацию по работе с сервисом можно найти в Developers Guide.
Поиск велосипедов
Сервис предоставляет WSDL. Т.е. если мы добавим его как Web Service Reference к нашему решению, в итоге, получим proxy для работы с сервисом. Все бы хорошо, но Request который мы будем посылать на сервис надо же еще и подписать полученным SAK (Secret Access Key).
Процесс аутентификации с нашей выглядит следующим образом:

Со стороны AWS:

Все предельно ясно, но в сервисе отсутствует функционал по подписке риквеста Вашим SAK. Самым простым решением на тот момент казалось просмотреть семплы и найти нужный нам функционал, но не все так просто как кажется. Из всех пересмотренных мной семплов не удалось найти рабочий код для подписи риквеста. Быстро загуглив нашлось решение:
C# решение для подписи Request
class SignRequestHelper
{
private string endPoint;
private string akid;
private byte[] secret;
private HMAC signer;
private const string REQUEST_URI = "/onca/xml";
private const string REQUEST_METHOD = "GET";
/*
* Use this constructor to create the object. The AWS credentials are available on
* http://aws.amazon.com
*
* The destination is the service end-point for your application:
* US: ecs.amazonaws.com
* JP: ecs.amazonaws.jp
* UK: ecs.amazonaws.co.uk
* DE: ecs.amazonaws.de
* FR: ecs.amazonaws.fr
* CA: ecs.amazonaws.ca
*/
public SignRequestHelper(string awsAccessKeyId, string awsSecretKey, string destination)
{
this.endPoint = destination.ToLower();
this.akid = awsAccessKeyId;
this.secret = Encoding.UTF8.GetBytes(awsSecretKey);
this.signer = new HMACSHA256(this.secret);
}
/*
* Sign a request in the form of a Dictionary of name-value pairs.
*
* This method returns a complete URL to use. Modifying the returned URL
* in any way invalidates the signature and Amazon will reject the requests.
*/
public string Sign(IDictionary<string, string> request)
{
// Use a SortedDictionary to get the parameters in natural byte order, as
// required by AWS.
ParamComparer pc = new ParamComparer();
SortedDictionary<string, string> sortedMap = new SortedDictionary<string, string>(request, pc);
// Add the AWSAccessKeyId and Timestamp to the requests.
sortedMap["AWSAccessKeyId"] = this.akid;
sortedMap["Timestamp"] = this.GetTimestamp();
// Get the canonical query string
string canonicalQS = this.ConstructCanonicalQueryString(sortedMap);
// Derive the bytes needs to be signed.
StringBuilder builder = new StringBuilder();
builder.Append(REQUEST_METHOD)
.Append("\n")
.Append(this.endPoint)
.Append("\n")
.Append(REQUEST_URI)
.Append("\n")
.Append(canonicalQS);
string stringToSign = builder.ToString();
byte[] toSign = Encoding.UTF8.GetBytes(stringToSign);
// Compute the signature and convert to Base64.
byte[] sigBytes = signer.ComputeHash(toSign);
string signature = Convert.ToBase64String(sigBytes);
// now construct the complete URL and return to caller.
StringBuilder qsBuilder = new StringBuilder();
qsBuilder.Append("http://")
.Append(this.endPoint)
.Append(REQUEST_URI)
.Append("?")
.Append(canonicalQS)
.Append("&Signature=")
.Append(this.PercentEncodeRfc3986(signature));
return qsBuilder.ToString();
}
/*
* Sign a request in the form of a query string.
*
* This method returns a complete URL to use. Modifying the returned URL
* in any way invalidates the signature and Amazon will reject the requests.
*/
public string Sign(string queryString)
{
IDictionary<string, string> request = this.CreateDictionary(queryString);
return this.Sign(request);
}
/*
* Current time in IS0 8601 format as required by Amazon
*/
private string GetTimestamp()
{
DateTime currentTime = DateTime.UtcNow;
string timestamp = currentTime.ToString("yyyy-MM-ddTHH:mm:ssZ");
return timestamp;
}
/*
* Percent-encode (URL Encode) according to RFC 3986 as required by Amazon.
*
* This is necessary because .NET's HttpUtility.UrlEncode does not encode
* according to the above standard. Also, .NET returns lower-case encoding
* by default and Amazon requires upper-case encoding.
*/
private string PercentEncodeRfc3986(string str)
{
str = HttpUtility.UrlEncode(str, System.Text.Encoding.UTF8);
str = str.Replace("'", "%27").Replace("(", "%28").Replace(")", "%29").Replace("*", "%2A").Replace("!", "%21").Replace("%7e", "~").Replace("+", "%20");
StringBuilder sbuilder = new StringBuilder(str);
for (int i = 0; i < sbuilder.Length; i++)
{
if (sbuilder[i] == '%')
{
if (Char.IsLetter(sbuilder[i + 1]) || Char.IsLetter(sbuilder[i + 2]))
{
sbuilder[i + 1] = Char.ToUpper(sbuilder[i + 1]);
sbuilder[i + 2] = Char.ToUpper(sbuilder[i + 2]);
}
}
}
return sbuilder.ToString();
}
/*
* Convert a query string to corresponding dictionary of name-value pairs.
*/
private IDictionary<string, string> CreateDictionary(string queryString)
{
Dictionary<string, string> map = new Dictionary<string, string>();
string[] requestParams = queryString.Split('&');
for (int i = 0; i < requestParams.Length; i++)
{
if (requestParams[i].Length < 1)
{
continue;
}
char[] sep = { '=' };
string[] param = requestParams[i].Split(sep, 2);
for (int j = 0; j < param.Length; j++)
{
param[j] = HttpUtility.UrlDecode(param[j], System.Text.Encoding.UTF8);
}
switch (param.Length)
{
case 1:
{
if (requestParams[i].Length >= 1)
{
if (requestParams[i].ToCharArray()[0] == '=')
{
map[""] = param[0];
}
else
{
map[param[0]] = "";
}
}
break;
}
case 2:
{
if (!string.IsNullOrEmpty(param[0]))
{
map[param[0]] = param[1];
}
}
break;
}
}
return map;
}
private string ConstructCanonicalQueryString(SortedDictionary<string, string> sortedParamMap)
{
StringBuilder builder = new StringBuilder();
if (sortedParamMap.Count == 0)
{
builder.Append("");
return builder.ToString();
}
foreach (KeyValuePair<string, string> kvp in sortedParamMap)
{
builder.Append(this.PercentEncodeRfc3986(kvp.Key));
builder.Append("=");
builder.Append(this.PercentEncodeRfc3986(kvp.Value));
builder.Append("&");
}
string canonicalString = builder.ToString();
canonicalString = canonicalString.Substring(0, canonicalString.Length - 1);
return canonicalString;
}
}
class ParamComparer : IComparer<string>
{
public int Compare(string p1, string p2)
{
return string.CompareOrdinal(p1, p2);
}
}
Использовать будем следующим образом:
/*...*/
SignRequestHelper signRequestHelper = new SignRequestHelper(Settings.Default.MyAWSKeyID, Settings.Default.MyAWSSecretKey, Settings.Default.Destination);
/*....*/
XmlDocument document = RetrieveXmlResponse(this.signRequestHelper.Sign(requestString));
/*...*/
private static XmlDocument RetrieveXmlResponse(string url)
{
try
{
WebRequest request = HttpWebRequest.Create(url);
WebResponse response = request.GetResponse();
XmlDocument doc = new XmlDocument();
doc.Load(response.GetResponseStream());
return doc;
}
catch (Exception e)
{
Console.WriteLine("Caught Exception: " + e.Message);
Console.WriteLine("Stack Trace: " + e.StackTrace);
}
return null;
}
Сначала я пытался парсить XmlDocument который пришел из респонза (что в корне было не правильно и заняло много времени). Потом я вспомнил про proxy в котором был класс ItemLookupResponse (корневой элемент XML респонза) и десериализацию. И все решилось в разы проще:
public ItemLookupResponse RetrieveItemDetails(string itemID)
{
ItemLookupResponse result = new ItemLookupResponse();
string requestString = String.Format(Settings.Default.RequestFormatString,
Settings.Default.ServiceParameter,
Settings.Default.VersionParameter,
Settings.Default.OperationParameter,
ResponseGroup.Large.ToString(),
itemID,
Settings.Default.AssociateTagParameter);
XmlDocument document = RetrieveXmlResponse(this.signRequestHelper.Sign(requestString));
var ser = new XmlSerializer(typeof(ItemLookupResponse), Settings.Default.NamespaceURI);
try
{
var wrapper = (ItemLookupResponse)ser.Deserialize(new XmlNodeReader(document.DocumentElement));
result = wrapper;
}
catch (Exception ex)
{
Console.WriteLine("Caught Exception: " + ex.Message);
Console.WriteLine("Stack Trace: " + ex.StackTrace);
}
return result;
}
В итоге мы получаем объект ItemLookupResponse со всеми нужными нам свойствами. Цены мы можем вытянуть из объекта Item
Item item = GetItem(result);
/* item.OfferSummary.LowestCollectiblePrice;
item.OfferSummary.LowestNewPrice;
item.OfferSummary.LowestRefurbishedPrice;
item.OfferSummary.LowestUsedPrice;
*/
Все что хотели, мы получили. Сразу оговорюсь запрос по ItemLookup не ограничивается одним &ItemId=B0047DVWLW их может быть больше, в разы. ItemID — это так называемый ASIN (Amazon Standard Identification Number). Еще немного поисследовав ItemLookupResponse в глаза бросилась секция:
<SimilarProducts>
<SimilarProduct>
<ASIN>B0013FRNKG</ASIN>
<Title>
Apple iPad 2 MC769LL/A Tablet (16GB, WiFi, Black) 2nd Generation
</Title>
</SimilarProduct>
<SimilarProduct>
<ASIN>B003D8GAA0</ASIN>
<Title>
3 Pack of Premium Crystal Clear Screen Protectors for Apple iPad
</Title>
</SimilarProduct>
/*..........*/
Т.е. в XML ответе еще приходит набор похожих девайсов. Поэтому, появилась идея выгрести из базы все нужные девайсы и их похожие продукты через RetrieveItemDetails(string itemID) (пишем рекурсивный метод). Как Вы наверное заметили, под ASIN = B003D8GAA0 прячется совсем не нужный нам скрин протектор (и соответственно фильтруем такие элементы в респонзе).
Поиск похожих устройств
private void GetAllSimilarItemDetails(string asin)
{
try
{
ItemLookupResponse lookupResponse = RetrieveItemDetails(asin);
if (!devicesCollection.ContainsKey(asin))
{
devicesCollection.Add(asin, lookupResponse);
}
else
{
return;
}
foreach (var item in lookupResponse.Items)
{
foreach (var internalItem in item.Item)
{
bool isContainNeededNode = false;
if (internalItem.ItemAttributes.Title.ToLower().Contains("device_title"))
{
foreach (var node in internalItem.BrowseNodes.BrowseNode)
{
if (node.Name == "Tablets" || node.Name == "Electronics")
{
isContainNeededNode = true;
}
}
if (isContainNeededNode)
{
foreach (var similarItem in internalItem.SimilarProducts)
{
GetAllSimilarItemDetails(similarItem.ASIN);
}
}
else
{
break;
}
}
}
}
}
catch (Exception ex)
{
throw ex;
}
}
Оповещение
В качестве оповещения была выбрана почта и твиттер. Устанавливался таймер и через определенный интервал проверял цены на устройства, и если цена отличалась в меньшую сторону с момента последней проверки или минимально заданной на мыло улетало письмо и определенный аккаунт в твиттере постал месадж (через Twitterizer).
В перспективе
Если позволит желание и время, есть идея переписать это все дело в Windows Service который будет по заданным параметрам мониторить нужные покупки. И написать отзывчивого твитбота который будет на mention с определенным тегом и параметрами выдавать пользователю нужную инфу.
Результат
И теперь о самом главном, девайс был куплен и подарен жене. Все довольны!