Pull to refresh

Использование Product Advertising API от Amazon

Reading time 19 min
Views 20K
Доброго времени суток, хабровчане!

image

В данном посте хочу рассказать о своем первом опыте работы с 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 с определенным тегом и параметрами выдавать пользователю нужную инфу.

Результат


И теперь о самом главном, девайс был куплен и подарен жене. Все довольны!

Ссылки


Tags:
Hubs:
0
Comments 0
Comments Leave a comment

Articles