From 729eb85d879c4f7b1ac78c0f6e62135da180ee5e Mon Sep 17 00:00:00 2001 From: Agustin Benassi Date: Tue, 4 Nov 2014 00:14:16 -0300 Subject: [PATCH 001/106] Rework search_images method completely to make it work again. Add utils module with methods used by the new search_images method. Remove BeatifulSoup module and replace it with requirements.txt with modules and versions needed to use Google-Search-API package after these changes. --- BeautifulSoup.py | 2014 ---------------------------------------------- google.py | 298 ++++--- requirements.txt | 3 + utils.py | 59 ++ 4 files changed, 255 insertions(+), 2119 deletions(-) delete mode 100644 BeautifulSoup.py create mode 100644 requirements.txt create mode 100644 utils.py diff --git a/BeautifulSoup.py b/BeautifulSoup.py deleted file mode 100644 index 4b17b85..0000000 --- a/BeautifulSoup.py +++ /dev/null @@ -1,2014 +0,0 @@ -"""Beautiful Soup -Elixir and Tonic -"The Screen-Scraper's Friend" -http://www.crummy.com/software/BeautifulSoup/ - -Beautiful Soup parses a (possibly invalid) XML or HTML document into a -tree representation. It provides methods and Pythonic idioms that make -it easy to navigate, search, and modify the tree. - -A well-formed XML/HTML document yields a well-formed data -structure. An ill-formed XML/HTML document yields a correspondingly -ill-formed data structure. If your document is only locally -well-formed, you can use this library to find and process the -well-formed part of it. - -Beautiful Soup works with Python 2.2 and up. It has no external -dependencies, but you'll have more success at converting data to UTF-8 -if you also install these three packages: - -* chardet, for auto-detecting character encodings - http://chardet.feedparser.org/ -* cjkcodecs and iconv_codec, which add more encodings to the ones supported - by stock Python. - http://cjkpython.i18n.org/ - -Beautiful Soup defines classes for two main parsing strategies: - - * BeautifulStoneSoup, for parsing XML, SGML, or your domain-specific - language that kind of looks like XML. - - * BeautifulSoup, for parsing run-of-the-mill HTML code, be it valid - or invalid. This class has web browser-like heuristics for - obtaining a sensible parse tree in the face of common HTML errors. - -Beautiful Soup also defines a class (UnicodeDammit) for autodetecting -the encoding of an HTML or XML document, and converting it to -Unicode. Much of this code is taken from Mark Pilgrim's Universal Feed Parser. - -For more than you ever wanted to know about Beautiful Soup, see the -documentation: -http://www.crummy.com/software/BeautifulSoup/documentation.html - -Here, have some legalese: - -Copyright (c) 2004-2010, Leonard Richardson - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of the the Beautiful Soup Consortium and All - Night Kosher Bakery nor the names of its contributors may be - used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT. - -""" -from __future__ import generators - -__author__ = "Leonard Richardson (leonardr@segfault.org)" -__version__ = "3.2.0" -__copyright__ = "Copyright (c) 2004-2010 Leonard Richardson" -__license__ = "New-style BSD" - -from sgmllib import SGMLParser, SGMLParseError -import codecs -import markupbase -import types -import re -import sgmllib -try: - from htmlentitydefs import name2codepoint -except ImportError: - name2codepoint = {} -try: - set -except NameError: - from sets import Set as set - -#These hacks make Beautiful Soup able to parse XML with namespaces -sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*') -markupbase._declname_match = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*\s*').match - -DEFAULT_OUTPUT_ENCODING = "utf-8" - -def _match_css_class(str): - """Build a RE to match the given CSS class.""" - return re.compile(r"(^|.*\s)%s($|\s)" % str) - -# First, the classes that represent markup elements. - -class PageElement(object): - """Contains the navigational information for some part of the page - (either a tag or a piece of text)""" - - def setup(self, parent=None, previous=None): - """Sets up the initial relations between this element and - other elements.""" - self.parent = parent - self.previous = previous - self.next = None - self.previousSibling = None - self.nextSibling = None - if self.parent and self.parent.contents: - self.previousSibling = self.parent.contents[-1] - self.previousSibling.nextSibling = self - - def replaceWith(self, replaceWith): - oldParent = self.parent - myIndex = self.parent.index(self) - if hasattr(replaceWith, "parent")\ - and replaceWith.parent is self.parent: - # We're replacing this element with one of its siblings. - index = replaceWith.parent.index(replaceWith) - if index and index < myIndex: - # Furthermore, it comes before this element. That - # means that when we extract it, the index of this - # element will change. - myIndex = myIndex - 1 - self.extract() - oldParent.insert(myIndex, replaceWith) - - def replaceWithChildren(self): - myParent = self.parent - myIndex = self.parent.index(self) - self.extract() - reversedChildren = list(self.contents) - reversedChildren.reverse() - for child in reversedChildren: - myParent.insert(myIndex, child) - - def extract(self): - """Destructively rips this element out of the tree.""" - if self.parent: - try: - del self.parent.contents[self.parent.index(self)] - except ValueError: - pass - - #Find the two elements that would be next to each other if - #this element (and any children) hadn't been parsed. Connect - #the two. - lastChild = self._lastRecursiveChild() - nextElement = lastChild.next - - if self.previous: - self.previous.next = nextElement - if nextElement: - nextElement.previous = self.previous - self.previous = None - lastChild.next = None - - self.parent = None - if self.previousSibling: - self.previousSibling.nextSibling = self.nextSibling - if self.nextSibling: - self.nextSibling.previousSibling = self.previousSibling - self.previousSibling = self.nextSibling = None - return self - - def _lastRecursiveChild(self): - "Finds the last element beneath this object to be parsed." - lastChild = self - while hasattr(lastChild, 'contents') and lastChild.contents: - lastChild = lastChild.contents[-1] - return lastChild - - def insert(self, position, newChild): - if isinstance(newChild, basestring) \ - and not isinstance(newChild, NavigableString): - newChild = NavigableString(newChild) - - position = min(position, len(self.contents)) - if hasattr(newChild, 'parent') and newChild.parent is not None: - # We're 'inserting' an element that's already one - # of this object's children. - if newChild.parent is self: - index = self.index(newChild) - if index > position: - # Furthermore we're moving it further down the - # list of this object's children. That means that - # when we extract this element, our target index - # will jump down one. - position = position - 1 - newChild.extract() - - newChild.parent = self - previousChild = None - if position == 0: - newChild.previousSibling = None - newChild.previous = self - else: - previousChild = self.contents[position-1] - newChild.previousSibling = previousChild - newChild.previousSibling.nextSibling = newChild - newChild.previous = previousChild._lastRecursiveChild() - if newChild.previous: - newChild.previous.next = newChild - - newChildsLastElement = newChild._lastRecursiveChild() - - if position >= len(self.contents): - newChild.nextSibling = None - - parent = self - parentsNextSibling = None - while not parentsNextSibling: - parentsNextSibling = parent.nextSibling - parent = parent.parent - if not parent: # This is the last element in the document. - break - if parentsNextSibling: - newChildsLastElement.next = parentsNextSibling - else: - newChildsLastElement.next = None - else: - nextChild = self.contents[position] - newChild.nextSibling = nextChild - if newChild.nextSibling: - newChild.nextSibling.previousSibling = newChild - newChildsLastElement.next = nextChild - - if newChildsLastElement.next: - newChildsLastElement.next.previous = newChildsLastElement - self.contents.insert(position, newChild) - - def append(self, tag): - """Appends the given tag to the contents of this tag.""" - self.insert(len(self.contents), tag) - - def findNext(self, name=None, attrs={}, text=None, **kwargs): - """Returns the first item that matches the given criteria and - appears after this Tag in the document.""" - return self._findOne(self.findAllNext, name, attrs, text, **kwargs) - - def findAllNext(self, name=None, attrs={}, text=None, limit=None, - **kwargs): - """Returns all items that match the given criteria and appear - after this Tag in the document.""" - return self._findAll(name, attrs, text, limit, self.nextGenerator, - **kwargs) - - def findNextSibling(self, name=None, attrs={}, text=None, **kwargs): - """Returns the closest sibling to this Tag that matches the - given criteria and appears after this Tag in the document.""" - return self._findOne(self.findNextSiblings, name, attrs, text, - **kwargs) - - def findNextSiblings(self, name=None, attrs={}, text=None, limit=None, - **kwargs): - """Returns the siblings of this Tag that match the given - criteria and appear after this Tag in the document.""" - return self._findAll(name, attrs, text, limit, - self.nextSiblingGenerator, **kwargs) - fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x - - def findPrevious(self, name=None, attrs={}, text=None, **kwargs): - """Returns the first item that matches the given criteria and - appears before this Tag in the document.""" - return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs) - - def findAllPrevious(self, name=None, attrs={}, text=None, limit=None, - **kwargs): - """Returns all items that match the given criteria and appear - before this Tag in the document.""" - return self._findAll(name, attrs, text, limit, self.previousGenerator, - **kwargs) - fetchPrevious = findAllPrevious # Compatibility with pre-3.x - - def findPreviousSibling(self, name=None, attrs={}, text=None, **kwargs): - """Returns the closest sibling to this Tag that matches the - given criteria and appears before this Tag in the document.""" - return self._findOne(self.findPreviousSiblings, name, attrs, text, - **kwargs) - - def findPreviousSiblings(self, name=None, attrs={}, text=None, - limit=None, **kwargs): - """Returns the siblings of this Tag that match the given - criteria and appear before this Tag in the document.""" - return self._findAll(name, attrs, text, limit, - self.previousSiblingGenerator, **kwargs) - fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x - - def findParent(self, name=None, attrs={}, **kwargs): - """Returns the closest parent of this Tag that matches the given - criteria.""" - # NOTE: We can't use _findOne because findParents takes a different - # set of arguments. - r = None - l = self.findParents(name, attrs, 1) - if l: - r = l[0] - return r - - def findParents(self, name=None, attrs={}, limit=None, **kwargs): - """Returns the parents of this Tag that match the given - criteria.""" - - return self._findAll(name, attrs, None, limit, self.parentGenerator, - **kwargs) - fetchParents = findParents # Compatibility with pre-3.x - - #These methods do the real heavy lifting. - - def _findOne(self, method, name, attrs, text, **kwargs): - r = None - l = method(name, attrs, text, 1, **kwargs) - if l: - r = l[0] - return r - - def _findAll(self, name, attrs, text, limit, generator, **kwargs): - "Iterates over a generator looking for things that match." - - if isinstance(name, SoupStrainer): - strainer = name - # (Possibly) special case some findAll*(...) searches - elif text is None and not limit and not attrs and not kwargs: - # findAll*(True) - if name is True: - return [element for element in generator() - if isinstance(element, Tag)] - # findAll*('tag-name') - elif isinstance(name, basestring): - return [element for element in generator() - if isinstance(element, Tag) and - element.name == name] - else: - strainer = SoupStrainer(name, attrs, text, **kwargs) - # Build a SoupStrainer - else: - strainer = SoupStrainer(name, attrs, text, **kwargs) - results = ResultSet(strainer) - g = generator() - while True: - try: - i = g.next() - except StopIteration: - break - if i: - found = strainer.search(i) - if found: - results.append(found) - if limit and len(results) >= limit: - break - return results - - #These Generators can be used to navigate starting from both - #NavigableStrings and Tags. - def nextGenerator(self): - i = self - while i is not None: - i = i.next - yield i - - def nextSiblingGenerator(self): - i = self - while i is not None: - i = i.nextSibling - yield i - - def previousGenerator(self): - i = self - while i is not None: - i = i.previous - yield i - - def previousSiblingGenerator(self): - i = self - while i is not None: - i = i.previousSibling - yield i - - def parentGenerator(self): - i = self - while i is not None: - i = i.parent - yield i - - # Utility methods - def substituteEncoding(self, str, encoding=None): - encoding = encoding or "utf-8" - return str.replace("%SOUP-ENCODING%", encoding) - - def toEncoding(self, s, encoding=None): - """Encodes an object to a string in some encoding, or to Unicode. - .""" - if isinstance(s, unicode): - if encoding: - s = s.encode(encoding) - elif isinstance(s, str): - if encoding: - s = s.encode(encoding) - else: - s = unicode(s) - else: - if encoding: - s = self.toEncoding(str(s), encoding) - else: - s = unicode(s) - return s - -class NavigableString(unicode, PageElement): - - def __new__(cls, value): - """Create a new NavigableString. - - When unpickling a NavigableString, this method is called with - the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be - passed in to the superclass's __new__ or the superclass won't know - how to handle non-ASCII characters. - """ - if isinstance(value, unicode): - return unicode.__new__(cls, value) - return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING) - - def __getnewargs__(self): - return (NavigableString.__str__(self),) - - def __getattr__(self, attr): - """text.string gives you text. This is for backwards - compatibility for Navigable*String, but for CData* it lets you - get the string without the CData wrapper.""" - if attr == 'string': - return self - else: - raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr) - - def __unicode__(self): - return str(self).decode(DEFAULT_OUTPUT_ENCODING) - - def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): - if encoding: - return self.encode(encoding) - else: - return self - -class CData(NavigableString): - - def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): - return "" % NavigableString.__str__(self, encoding) - -class ProcessingInstruction(NavigableString): - def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): - output = self - if "%SOUP-ENCODING%" in output: - output = self.substituteEncoding(output, encoding) - return "" % self.toEncoding(output, encoding) - -class Comment(NavigableString): - def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): - return "" % NavigableString.__str__(self, encoding) - -class Declaration(NavigableString): - def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): - return "" % NavigableString.__str__(self, encoding) - -class Tag(PageElement): - - """Represents a found HTML tag with its attributes and contents.""" - - def _invert(h): - "Cheap function to invert a hash." - i = {} - for k,v in h.items(): - i[v] = k - return i - - XML_ENTITIES_TO_SPECIAL_CHARS = { "apos" : "'", - "quot" : '"', - "amp" : "&", - "lt" : "<", - "gt" : ">" } - - XML_SPECIAL_CHARS_TO_ENTITIES = _invert(XML_ENTITIES_TO_SPECIAL_CHARS) - - def _convertEntities(self, match): - """Used in a call to re.sub to replace HTML, XML, and numeric - entities with the appropriate Unicode characters. If HTML - entities are being converted, any unrecognized entities are - escaped.""" - x = match.group(1) - if self.convertHTMLEntities and x in name2codepoint: - return unichr(name2codepoint[x]) - elif x in self.XML_ENTITIES_TO_SPECIAL_CHARS: - if self.convertXMLEntities: - return self.XML_ENTITIES_TO_SPECIAL_CHARS[x] - else: - return u'&%s;' % x - elif len(x) > 0 and x[0] == '#': - # Handle numeric entities - if len(x) > 1 and x[1] == 'x': - return unichr(int(x[2:], 16)) - else: - return unichr(int(x[1:])) - - elif self.escapeUnrecognizedEntities: - return u'&%s;' % x - else: - return u'&%s;' % x - - def __init__(self, parser, name, attrs=None, parent=None, - previous=None): - "Basic constructor." - - # We don't actually store the parser object: that lets extracted - # chunks be garbage-collected - self.parserClass = parser.__class__ - self.isSelfClosing = parser.isSelfClosingTag(name) - self.name = name - if attrs is None: - attrs = [] - elif isinstance(attrs, dict): - attrs = attrs.items() - self.attrs = attrs - self.contents = [] - self.setup(parent, previous) - self.hidden = False - self.containsSubstitutions = False - self.convertHTMLEntities = parser.convertHTMLEntities - self.convertXMLEntities = parser.convertXMLEntities - self.escapeUnrecognizedEntities = parser.escapeUnrecognizedEntities - - # Convert any HTML, XML, or numeric entities in the attribute values. - convert = lambda(k, val): (k, - re.sub("&(#\d+|#x[0-9a-fA-F]+|\w+);", - self._convertEntities, - val)) - self.attrs = map(convert, self.attrs) - - def getString(self): - if (len(self.contents) == 1 - and isinstance(self.contents[0], NavigableString)): - return self.contents[0] - - def setString(self, string): - """Replace the contents of the tag with a string""" - self.clear() - self.append(string) - - string = property(getString, setString) - - def getText(self, separator=u""): - if not len(self.contents): - return u"" - stopNode = self._lastRecursiveChild().next - strings = [] - current = self.contents[0] - while current is not stopNode: - if isinstance(current, NavigableString): - strings.append(current.strip()) - current = current.next - return separator.join(strings) - - text = property(getText) - - def get(self, key, default=None): - """Returns the value of the 'key' attribute for the tag, or - the value given for 'default' if it doesn't have that - attribute.""" - return self._getAttrMap().get(key, default) - - def clear(self): - """Extract all children.""" - for child in self.contents[:]: - child.extract() - - def index(self, element): - for i, child in enumerate(self.contents): - if child is element: - return i - raise ValueError("Tag.index: element not in tag") - - def has_key(self, key): - return self._getAttrMap().has_key(key) - - def __getitem__(self, key): - """tag[key] returns the value of the 'key' attribute for the tag, - and throws an exception if it's not there.""" - return self._getAttrMap()[key] - - def __iter__(self): - "Iterating over a tag iterates over its contents." - return iter(self.contents) - - def __len__(self): - "The length of a tag is the length of its list of contents." - return len(self.contents) - - def __contains__(self, x): - return x in self.contents - - def __nonzero__(self): - "A tag is non-None even if it has no contents." - return True - - def __setitem__(self, key, value): - """Setting tag[key] sets the value of the 'key' attribute for the - tag.""" - self._getAttrMap() - self.attrMap[key] = value - found = False - for i in range(0, len(self.attrs)): - if self.attrs[i][0] == key: - self.attrs[i] = (key, value) - found = True - if not found: - self.attrs.append((key, value)) - self._getAttrMap()[key] = value - - def __delitem__(self, key): - "Deleting tag[key] deletes all 'key' attributes for the tag." - for item in self.attrs: - if item[0] == key: - self.attrs.remove(item) - #We don't break because bad HTML can define the same - #attribute multiple times. - self._getAttrMap() - if self.attrMap.has_key(key): - del self.attrMap[key] - - def __call__(self, *args, **kwargs): - """Calling a tag like a function is the same as calling its - findAll() method. Eg. tag('a') returns a list of all the A tags - found within this tag.""" - return apply(self.findAll, args, kwargs) - - def __getattr__(self, tag): - #print "Getattr %s.%s" % (self.__class__, tag) - if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3: - return self.find(tag[:-3]) - elif tag.find('__') != 0: - return self.find(tag) - raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, tag) - - def __eq__(self, other): - """Returns true iff this tag has the same name, the same attributes, - and the same contents (recursively) as the given tag. - - NOTE: right now this will return false if two tags have the - same attributes in a different order. Should this be fixed?""" - if other is self: - return True - if not hasattr(other, 'name') or not hasattr(other, 'attrs') or not hasattr(other, 'contents') or self.name != other.name or self.attrs != other.attrs or len(self) != len(other): - return False - for i in range(0, len(self.contents)): - if self.contents[i] != other.contents[i]: - return False - return True - - def __ne__(self, other): - """Returns true iff this tag is not identical to the other tag, - as defined in __eq__.""" - return not self == other - - def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING): - """Renders this tag as a string.""" - return self.__str__(encoding) - - def __unicode__(self): - return self.__str__(None) - - BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|" - + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)" - + ")") - - def _sub_entity(self, x): - """Used with a regular expression to substitute the - appropriate XML entity for an XML special character.""" - return "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";" - - def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING, - prettyPrint=False, indentLevel=0): - """Returns a string or Unicode representation of this tag and - its contents. To get Unicode, pass None for encoding. - - NOTE: since Python's HTML parser consumes whitespace, this - method is not certain to reproduce the whitespace present in - the original string.""" - - encodedName = self.toEncoding(self.name, encoding) - - attrs = [] - if self.attrs: - for key, val in self.attrs: - fmt = '%s="%s"' - if isinstance(val, basestring): - if self.containsSubstitutions and '%SOUP-ENCODING%' in val: - val = self.substituteEncoding(val, encoding) - - # The attribute value either: - # - # * Contains no embedded double quotes or single quotes. - # No problem: we enclose it in double quotes. - # * Contains embedded single quotes. No problem: - # double quotes work here too. - # * Contains embedded double quotes. No problem: - # we enclose it in single quotes. - # * Embeds both single _and_ double quotes. This - # can't happen naturally, but it can happen if - # you modify an attribute value after parsing - # the document. Now we have a bit of a - # problem. We solve it by enclosing the - # attribute in single quotes, and escaping any - # embedded single quotes to XML entities. - if '"' in val: - fmt = "%s='%s'" - if "'" in val: - # TODO: replace with apos when - # appropriate. - val = val.replace("'", "&squot;") - - # Now we're okay w/r/t quotes. But the attribute - # value might also contain angle brackets, or - # ampersands that aren't part of entities. We need - # to escape those to XML entities too. - val = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, val) - - attrs.append(fmt % (self.toEncoding(key, encoding), - self.toEncoding(val, encoding))) - close = '' - closeTag = '' - if self.isSelfClosing: - close = ' /' - else: - closeTag = '' % encodedName - - indentTag, indentContents = 0, 0 - if prettyPrint: - indentTag = indentLevel - space = (' ' * (indentTag-1)) - indentContents = indentTag + 1 - contents = self.renderContents(encoding, prettyPrint, indentContents) - if self.hidden: - s = contents - else: - s = [] - attributeString = '' - if attrs: - attributeString = ' ' + ' '.join(attrs) - if prettyPrint: - s.append(space) - s.append('<%s%s%s>' % (encodedName, attributeString, close)) - if prettyPrint: - s.append("\n") - s.append(contents) - if prettyPrint and contents and contents[-1] != "\n": - s.append("\n") - if prettyPrint and closeTag: - s.append(space) - s.append(closeTag) - if prettyPrint and closeTag and self.nextSibling: - s.append("\n") - s = ''.join(s) - return s - - def decompose(self): - """Recursively destroys the contents of this tree.""" - self.extract() - if len(self.contents) == 0: - return - current = self.contents[0] - while current is not None: - next = current.next - if isinstance(current, Tag): - del current.contents[:] - current.parent = None - current.previous = None - current.previousSibling = None - current.next = None - current.nextSibling = None - current = next - - def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING): - return self.__str__(encoding, True) - - def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING, - prettyPrint=False, indentLevel=0): - """Renders the contents of this tag as a string in the given - encoding. If encoding is None, returns a Unicode string..""" - s=[] - for c in self: - text = None - if isinstance(c, NavigableString): - text = c.__str__(encoding) - elif isinstance(c, Tag): - s.append(c.__str__(encoding, prettyPrint, indentLevel)) - if text and prettyPrint: - text = text.strip() - if text: - if prettyPrint: - s.append(" " * (indentLevel-1)) - s.append(text) - if prettyPrint: - s.append("\n") - return ''.join(s) - - #Soup methods - - def find(self, name=None, attrs={}, recursive=True, text=None, - **kwargs): - """Return only the first child of this Tag matching the given - criteria.""" - r = None - l = self.findAll(name, attrs, recursive, text, 1, **kwargs) - if l: - r = l[0] - return r - findChild = find - - def findAll(self, name=None, attrs={}, recursive=True, text=None, - limit=None, **kwargs): - """Extracts a list of Tag objects that match the given - criteria. You can specify the name of the Tag and any - attributes you want the Tag to have. - - The value of a key-value pair in the 'attrs' map can be a - string, a list of strings, a regular expression object, or a - callable that takes a string and returns whether or not the - string matches for some custom definition of 'matches'. The - same is true of the tag name.""" - generator = self.recursiveChildGenerator - if not recursive: - generator = self.childGenerator - return self._findAll(name, attrs, text, limit, generator, **kwargs) - findChildren = findAll - - # Pre-3.x compatibility methods - first = find - fetch = findAll - - def fetchText(self, text=None, recursive=True, limit=None): - return self.findAll(text=text, recursive=recursive, limit=limit) - - def firstText(self, text=None, recursive=True): - return self.find(text=text, recursive=recursive) - - #Private methods - - def _getAttrMap(self): - """Initializes a map representation of this tag's attributes, - if not already initialized.""" - if not getattr(self, 'attrMap'): - self.attrMap = {} - for (key, value) in self.attrs: - self.attrMap[key] = value - return self.attrMap - - #Generator methods - def childGenerator(self): - # Just use the iterator from the contents - return iter(self.contents) - - def recursiveChildGenerator(self): - if not len(self.contents): - raise StopIteration - stopNode = self._lastRecursiveChild().next - current = self.contents[0] - while current is not stopNode: - yield current - current = current.next - - -# Next, a couple classes to represent queries and their results. -class SoupStrainer: - """Encapsulates a number of ways of matching a markup element (tag or - text).""" - - def __init__(self, name=None, attrs={}, text=None, **kwargs): - self.name = name - if isinstance(attrs, basestring): - kwargs['class'] = _match_css_class(attrs) - attrs = None - if kwargs: - if attrs: - attrs = attrs.copy() - attrs.update(kwargs) - else: - attrs = kwargs - self.attrs = attrs - self.text = text - - def __str__(self): - if self.text: - return self.text - else: - return "%s|%s" % (self.name, self.attrs) - - def searchTag(self, markupName=None, markupAttrs={}): - found = None - markup = None - if isinstance(markupName, Tag): - markup = markupName - markupAttrs = markup - callFunctionWithTagData = callable(self.name) \ - and not isinstance(markupName, Tag) - - if (not self.name) \ - or callFunctionWithTagData \ - or (markup and self._matches(markup, self.name)) \ - or (not markup and self._matches(markupName, self.name)): - if callFunctionWithTagData: - match = self.name(markupName, markupAttrs) - else: - match = True - markupAttrMap = None - for attr, matchAgainst in self.attrs.items(): - if not markupAttrMap: - if hasattr(markupAttrs, 'get'): - markupAttrMap = markupAttrs - else: - markupAttrMap = {} - for k,v in markupAttrs: - markupAttrMap[k] = v - attrValue = markupAttrMap.get(attr) - if not self._matches(attrValue, matchAgainst): - match = False - break - if match: - if markup: - found = markup - else: - found = markupName - return found - - def search(self, markup): - #print 'looking for %s in %s' % (self, markup) - found = None - # If given a list of items, scan it for a text element that - # matches. - if hasattr(markup, "__iter__") \ - and not isinstance(markup, Tag): - for element in markup: - if isinstance(element, NavigableString) \ - and self.search(element): - found = element - break - # If it's a Tag, make sure its name or attributes match. - # Don't bother with Tags if we're searching for text. - elif isinstance(markup, Tag): - if not self.text: - found = self.searchTag(markup) - # If it's text, make sure the text matches. - elif isinstance(markup, NavigableString) or \ - isinstance(markup, basestring): - if self._matches(markup, self.text): - found = markup - else: - raise Exception, "I don't know how to match against a %s" \ - % markup.__class__ - return found - - def _matches(self, markup, matchAgainst): - #print "Matching %s against %s" % (markup, matchAgainst) - result = False - if matchAgainst is True: - result = markup is not None - elif callable(matchAgainst): - result = matchAgainst(markup) - else: - #Custom match methods take the tag as an argument, but all - #other ways of matching match the tag name as a string. - if isinstance(markup, Tag): - markup = markup.name - if markup and not isinstance(markup, basestring): - markup = unicode(markup) - #Now we know that chunk is either a string, or None. - if hasattr(matchAgainst, 'match'): - # It's a regexp object. - result = markup and matchAgainst.search(markup) - elif hasattr(matchAgainst, '__iter__'): # list-like - result = markup in matchAgainst - elif hasattr(matchAgainst, 'items'): - result = markup.has_key(matchAgainst) - elif matchAgainst and isinstance(markup, basestring): - if isinstance(markup, unicode): - matchAgainst = unicode(matchAgainst) - else: - matchAgainst = str(matchAgainst) - - if not result: - result = matchAgainst == markup - return result - -class ResultSet(list): - """A ResultSet is just a list that keeps track of the SoupStrainer - that created it.""" - def __init__(self, source): - list.__init__([]) - self.source = source - -# Now, some helper functions. - -def buildTagMap(default, *args): - """Turns a list of maps, lists, or scalars into a single map. - Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and - NESTING_RESET_TAGS maps out of lists and partial maps.""" - built = {} - for portion in args: - if hasattr(portion, 'items'): - #It's a map. Merge it. - for k,v in portion.items(): - built[k] = v - elif hasattr(portion, '__iter__'): # is a list - #It's a list. Map each item to the default. - for k in portion: - built[k] = default - else: - #It's a scalar. Map it to the default. - built[portion] = default - return built - -# Now, the parser classes. - -class BeautifulStoneSoup(Tag, SGMLParser): - - """This class contains the basic parser and search code. It defines - a parser that knows nothing about tag behavior except for the - following: - - You can't close a tag without closing all the tags it encloses. - That is, "" actually means - "". - - [Another possible explanation is "", but since - this class defines no SELF_CLOSING_TAGS, it will never use that - explanation.] - - This class is useful for parsing XML or made-up markup languages, - or when BeautifulSoup makes an assumption counter to what you were - expecting.""" - - SELF_CLOSING_TAGS = {} - NESTABLE_TAGS = {} - RESET_NESTING_TAGS = {} - QUOTE_TAGS = {} - PRESERVE_WHITESPACE_TAGS = [] - - MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'), - lambda x: x.group(1) + ' />'), - (re.compile(']*)>'), - lambda x: '') - ] - - ROOT_TAG_NAME = u'[document]' - - HTML_ENTITIES = "html" - XML_ENTITIES = "xml" - XHTML_ENTITIES = "xhtml" - # TODO: This only exists for backwards-compatibility - ALL_ENTITIES = XHTML_ENTITIES - - # Used when determining whether a text node is all whitespace and - # can be replaced with a single space. A text node that contains - # fancy Unicode spaces (usually non-breaking) should be left - # alone. - STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, } - - def __init__(self, markup="", parseOnlyThese=None, fromEncoding=None, - markupMassage=True, smartQuotesTo=XML_ENTITIES, - convertEntities=None, selfClosingTags=None, isHTML=False): - """The Soup object is initialized as the 'root tag', and the - provided markup (which can be a string or a file-like object) - is fed into the underlying parser. - - sgmllib will process most bad HTML, and the BeautifulSoup - class has some tricks for dealing with some HTML that kills - sgmllib, but Beautiful Soup can nonetheless choke or lose data - if your data uses self-closing tags or declarations - incorrectly. - - By default, Beautiful Soup uses regexes to sanitize input, - avoiding the vast majority of these problems. If the problems - don't apply to you, pass in False for markupMassage, and - you'll get better performance. - - The default parser massage techniques fix the two most common - instances of invalid HTML that choke sgmllib: - -
(No space between name of closing tag and tag close) - (Extraneous whitespace in declaration) - - You can pass in a custom list of (RE object, replace method) - tuples to get Beautiful Soup to scrub your input the way you - want.""" - - self.parseOnlyThese = parseOnlyThese - self.fromEncoding = fromEncoding - self.smartQuotesTo = smartQuotesTo - self.convertEntities = convertEntities - # Set the rules for how we'll deal with the entities we - # encounter - if self.convertEntities: - # It doesn't make sense to convert encoded characters to - # entities even while you're converting entities to Unicode. - # Just convert it all to Unicode. - self.smartQuotesTo = None - if convertEntities == self.HTML_ENTITIES: - self.convertXMLEntities = False - self.convertHTMLEntities = True - self.escapeUnrecognizedEntities = True - elif convertEntities == self.XHTML_ENTITIES: - self.convertXMLEntities = True - self.convertHTMLEntities = True - self.escapeUnrecognizedEntities = False - elif convertEntities == self.XML_ENTITIES: - self.convertXMLEntities = True - self.convertHTMLEntities = False - self.escapeUnrecognizedEntities = False - else: - self.convertXMLEntities = False - self.convertHTMLEntities = False - self.escapeUnrecognizedEntities = False - - self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags) - SGMLParser.__init__(self) - - if hasattr(markup, 'read'): # It's a file-type object. - markup = markup.read() - self.markup = markup - self.markupMassage = markupMassage - try: - self._feed(isHTML=isHTML) - except StopParsing: - pass - self.markup = None # The markup can now be GCed - - def convert_charref(self, name): - """This method fixes a bug in Python's SGMLParser.""" - try: - n = int(name) - except ValueError: - return - if not 0 <= n <= 127 : # ASCII ends at 127, not 255 - return - return self.convert_codepoint(n) - - def _feed(self, inDocumentEncoding=None, isHTML=False): - # Convert the document to Unicode. - markup = self.markup - if isinstance(markup, unicode): - if not hasattr(self, 'originalEncoding'): - self.originalEncoding = None - else: - dammit = UnicodeDammit\ - (markup, [self.fromEncoding, inDocumentEncoding], - smartQuotesTo=self.smartQuotesTo, isHTML=isHTML) - markup = dammit.unicode - self.originalEncoding = dammit.originalEncoding - self.declaredHTMLEncoding = dammit.declaredHTMLEncoding - if markup: - if self.markupMassage: - if not hasattr(self.markupMassage, "__iter__"): - self.markupMassage = self.MARKUP_MASSAGE - for fix, m in self.markupMassage: - markup = fix.sub(m, markup) - # TODO: We get rid of markupMassage so that the - # soup object can be deepcopied later on. Some - # Python installations can't copy regexes. If anyone - # was relying on the existence of markupMassage, this - # might cause problems. - del(self.markupMassage) - self.reset() - - SGMLParser.feed(self, markup) - # Close out any unfinished strings and close all the open tags. - self.endData() - while self.currentTag.name != self.ROOT_TAG_NAME: - self.popTag() - - def __getattr__(self, methodName): - """This method routes method call requests to either the SGMLParser - superclass or the Tag superclass, depending on the method name.""" - #print "__getattr__ called on %s.%s" % (self.__class__, methodName) - - if methodName.startswith('start_') or methodName.startswith('end_') \ - or methodName.startswith('do_'): - return SGMLParser.__getattr__(self, methodName) - elif not methodName.startswith('__'): - return Tag.__getattr__(self, methodName) - else: - raise AttributeError - - def isSelfClosingTag(self, name): - """Returns true iff the given string is the name of a - self-closing tag according to this parser.""" - return self.SELF_CLOSING_TAGS.has_key(name) \ - or self.instanceSelfClosingTags.has_key(name) - - def reset(self): - Tag.__init__(self, self, self.ROOT_TAG_NAME) - self.hidden = 1 - SGMLParser.reset(self) - self.currentData = [] - self.currentTag = None - self.tagStack = [] - self.quoteStack = [] - self.pushTag(self) - - def popTag(self): - tag = self.tagStack.pop() - - #print "Pop", tag.name - if self.tagStack: - self.currentTag = self.tagStack[-1] - return self.currentTag - - def pushTag(self, tag): - #print "Push", tag.name - if self.currentTag: - self.currentTag.contents.append(tag) - self.tagStack.append(tag) - self.currentTag = self.tagStack[-1] - - def endData(self, containerClass=NavigableString): - if self.currentData: - currentData = u''.join(self.currentData) - if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and - not set([tag.name for tag in self.tagStack]).intersection( - self.PRESERVE_WHITESPACE_TAGS)): - if '\n' in currentData: - currentData = '\n' - else: - currentData = ' ' - self.currentData = [] - if self.parseOnlyThese and len(self.tagStack) <= 1 and \ - (not self.parseOnlyThese.text or \ - not self.parseOnlyThese.search(currentData)): - return - o = containerClass(currentData) - o.setup(self.currentTag, self.previous) - if self.previous: - self.previous.next = o - self.previous = o - self.currentTag.contents.append(o) - - - def _popToTag(self, name, inclusivePop=True): - """Pops the tag stack up to and including the most recent - instance of the given tag. If inclusivePop is false, pops the tag - stack up to but *not* including the most recent instqance of - the given tag.""" - #print "Popping to %s" % name - if name == self.ROOT_TAG_NAME: - return - - numPops = 0 - mostRecentTag = None - for i in range(len(self.tagStack)-1, 0, -1): - if name == self.tagStack[i].name: - numPops = len(self.tagStack)-i - break - if not inclusivePop: - numPops = numPops - 1 - - for i in range(0, numPops): - mostRecentTag = self.popTag() - return mostRecentTag - - def _smartPop(self, name): - - """We need to pop up to the previous tag of this type, unless - one of this tag's nesting reset triggers comes between this - tag and the previous tag of this type, OR unless this tag is a - generic nesting trigger and another generic nesting trigger - comes between this tag and the previous tag of this type. - - Examples: -

FooBar *

* should pop to 'p', not 'b'. -

FooBar *

* should pop to 'table', not 'p'. -

Foo

Bar *

* should pop to 'tr', not 'p'. - -

    • *
    • * should pop to 'ul', not the first 'li'. -
  • ** should pop to 'table', not the first 'tr' - tag should - implicitly close the previous tag within the same
    ** should pop to 'tr', not the first 'td' - """ - - nestingResetTriggers = self.NESTABLE_TAGS.get(name) - isNestable = nestingResetTriggers != None - isResetNesting = self.RESET_NESTING_TAGS.has_key(name) - popTo = None - inclusive = True - for i in range(len(self.tagStack)-1, 0, -1): - p = self.tagStack[i] - if (not p or p.name == name) and not isNestable: - #Non-nestable tags get popped to the top or to their - #last occurance. - popTo = name - break - if (nestingResetTriggers is not None - and p.name in nestingResetTriggers) \ - or (nestingResetTriggers is None and isResetNesting - and self.RESET_NESTING_TAGS.has_key(p.name)): - - #If we encounter one of the nesting reset triggers - #peculiar to this tag, or we encounter another tag - #that causes nesting to reset, pop up to but not - #including that tag. - popTo = p.name - inclusive = False - break - p = p.parent - if popTo: - self._popToTag(popTo, inclusive) - - def unknown_starttag(self, name, attrs, selfClosing=0): - #print "Start tag %s: %s" % (name, attrs) - if self.quoteStack: - #This is not a real tag. - #print "<%s> is not real!" % name - attrs = ''.join([' %s="%s"' % (x, y) for x, y in attrs]) - self.handle_data('<%s%s>' % (name, attrs)) - return - self.endData() - - if not self.isSelfClosingTag(name) and not selfClosing: - self._smartPop(name) - - if self.parseOnlyThese and len(self.tagStack) <= 1 \ - and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)): - return - - tag = Tag(self, name, attrs, self.currentTag, self.previous) - if self.previous: - self.previous.next = tag - self.previous = tag - self.pushTag(tag) - if selfClosing or self.isSelfClosingTag(name): - self.popTag() - if name in self.QUOTE_TAGS: - #print "Beginning quote (%s)" % name - self.quoteStack.append(name) - self.literal = 1 - return tag - - def unknown_endtag(self, name): - #print "End tag %s" % name - if self.quoteStack and self.quoteStack[-1] != name: - #This is not a real end tag. - #print " is not real!" % name - self.handle_data('' % name) - return - self.endData() - self._popToTag(name) - if self.quoteStack and self.quoteStack[-1] == name: - self.quoteStack.pop() - self.literal = (len(self.quoteStack) > 0) - - def handle_data(self, data): - self.currentData.append(data) - - def _toStringSubclass(self, text, subclass): - """Adds a certain piece of text to the tree as a NavigableString - subclass.""" - self.endData() - self.handle_data(text) - self.endData(subclass) - - def handle_pi(self, text): - """Handle a processing instruction as a ProcessingInstruction - object, possibly one with a %SOUP-ENCODING% slot into which an - encoding will be plugged later.""" - if text[:3] == "xml": - text = u"xml version='1.0' encoding='%SOUP-ENCODING%'" - self._toStringSubclass(text, ProcessingInstruction) - - def handle_comment(self, text): - "Handle comments as Comment objects." - self._toStringSubclass(text, Comment) - - def handle_charref(self, ref): - "Handle character references as data." - if self.convertEntities: - data = unichr(int(ref)) - else: - data = '&#%s;' % ref - self.handle_data(data) - - def handle_entityref(self, ref): - """Handle entity references as data, possibly converting known - HTML and/or XML entity references to the corresponding Unicode - characters.""" - data = None - if self.convertHTMLEntities: - try: - data = unichr(name2codepoint[ref]) - except KeyError: - pass - - if not data and self.convertXMLEntities: - data = self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref) - - if not data and self.convertHTMLEntities and \ - not self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref): - # TODO: We've got a problem here. We're told this is - # an entity reference, but it's not an XML entity - # reference or an HTML entity reference. Nonetheless, - # the logical thing to do is to pass it through as an - # unrecognized entity reference. - # - # Except: when the input is "&carol;" this function - # will be called with input "carol". When the input is - # "AT&T", this function will be called with input - # "T". We have no way of knowing whether a semicolon - # was present originally, so we don't know whether - # this is an unknown entity or just a misplaced - # ampersand. - # - # The more common case is a misplaced ampersand, so I - # escape the ampersand and omit the trailing semicolon. - data = "&%s" % ref - if not data: - # This case is different from the one above, because we - # haven't already gone through a supposedly comprehensive - # mapping of entities to Unicode characters. We might not - # have gone through any mapping at all. So the chances are - # very high that this is a real entity, and not a - # misplaced ampersand. - data = "&%s;" % ref - self.handle_data(data) - - def handle_decl(self, data): - "Handle DOCTYPEs and the like as Declaration objects." - self._toStringSubclass(data, Declaration) - - def parse_declaration(self, i): - """Treat a bogus SGML declaration as raw data. Treat a CDATA - declaration as a CData object.""" - j = None - if self.rawdata[i:i+9] == '', i) - if k == -1: - k = len(self.rawdata) - data = self.rawdata[i+9:k] - j = k+3 - self._toStringSubclass(data, CData) - else: - try: - j = SGMLParser.parse_declaration(self, i) - except SGMLParseError: - toHandle = self.rawdata[i:] - self.handle_data(toHandle) - j = i + len(toHandle) - return j - -class BeautifulSoup(BeautifulStoneSoup): - - """This parser knows the following facts about HTML: - - * Some tags have no closing tag and should be interpreted as being - closed as soon as they are encountered. - - * The text inside some tags (ie. 'script') may contain tags which - are not really part of the document and which should be parsed - as text, not tags. If you want to parse the text as tags, you can - always fetch it and parse it explicitly. - - * Tag nesting rules: - - Most tags can't be nested at all. For instance, the occurance of - a

    tag should implicitly close the previous

    tag. - -

    Para1

    Para2 - should be transformed into: -

    Para1

    Para2 - - Some tags can be nested arbitrarily. For instance, the occurance - of a

    tag should _not_ implicitly close the previous -
    tag. - - Alice said:
    Bob said:
    Blah - should NOT be transformed into: - Alice said:
    Bob said:
    Blah - - Some tags can be nested, but the nesting is reset by the - interposition of other tags. For instance, a
    , - but not close a tag in another table. - -
    BlahBlah - should be transformed into: -
    BlahBlah - but, - Blah
    Blah - should NOT be transformed into - Blah
    Blah - - Differing assumptions about tag nesting rules are a major source - of problems with the BeautifulSoup class. If BeautifulSoup is not - treating as nestable a tag your page author treats as nestable, - try ICantBelieveItsBeautifulSoup, MinimalSoup, or - BeautifulStoneSoup before writing your own subclass.""" - - def __init__(self, *args, **kwargs): - if not kwargs.has_key('smartQuotesTo'): - kwargs['smartQuotesTo'] = self.HTML_ENTITIES - kwargs['isHTML'] = True - BeautifulStoneSoup.__init__(self, *args, **kwargs) - - SELF_CLOSING_TAGS = buildTagMap(None, - ('br' , 'hr', 'input', 'img', 'meta', - 'spacer', 'link', 'frame', 'base', 'col')) - - PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea']) - - QUOTE_TAGS = {'script' : None, 'textarea' : None} - - #According to the HTML standard, each of these inline tags can - #contain another tag of the same type. Furthermore, it's common - #to actually use these tags this way. - NESTABLE_INLINE_TAGS = ('span', 'font', 'q', 'object', 'bdo', 'sub', 'sup', - 'center') - - #According to the HTML standard, these block tags can contain - #another tag of the same type. Furthermore, it's common - #to actually use these tags this way. - NESTABLE_BLOCK_TAGS = ('blockquote', 'div', 'fieldset', 'ins', 'del') - - #Lists can contain other lists, but there are restrictions. - NESTABLE_LIST_TAGS = { 'ol' : [], - 'ul' : [], - 'li' : ['ul', 'ol'], - 'dl' : [], - 'dd' : ['dl'], - 'dt' : ['dl'] } - - #Tables can contain other tables, but there are restrictions. - NESTABLE_TABLE_TAGS = {'table' : [], - 'tr' : ['table', 'tbody', 'tfoot', 'thead'], - 'td' : ['tr'], - 'th' : ['tr'], - 'thead' : ['table'], - 'tbody' : ['table'], - 'tfoot' : ['table'], - } - - NON_NESTABLE_BLOCK_TAGS = ('address', 'form', 'p', 'pre') - - #If one of these tags is encountered, all tags up to the next tag of - #this type are popped. - RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript', - NON_NESTABLE_BLOCK_TAGS, - NESTABLE_LIST_TAGS, - NESTABLE_TABLE_TAGS) - - NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS, - NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS) - - # Used to detect the charset in a META tag; see start_meta - CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M) - - def start_meta(self, attrs): - """Beautiful Soup can detect a charset included in a META tag, - try to convert the document to that charset, and re-parse the - document from the beginning.""" - httpEquiv = None - contentType = None - contentTypeIndex = None - tagNeedsEncodingSubstitution = False - - for i in range(0, len(attrs)): - key, value = attrs[i] - key = key.lower() - if key == 'http-equiv': - httpEquiv = value - elif key == 'content': - contentType = value - contentTypeIndex = i - - if httpEquiv and contentType: # It's an interesting meta tag. - match = self.CHARSET_RE.search(contentType) - if match: - if (self.declaredHTMLEncoding is not None or - self.originalEncoding == self.fromEncoding): - # An HTML encoding was sniffed while converting - # the document to Unicode, or an HTML encoding was - # sniffed during a previous pass through the - # document, or an encoding was specified - # explicitly and it worked. Rewrite the meta tag. - def rewrite(match): - return match.group(1) + "%SOUP-ENCODING%" - newAttr = self.CHARSET_RE.sub(rewrite, contentType) - attrs[contentTypeIndex] = (attrs[contentTypeIndex][0], - newAttr) - tagNeedsEncodingSubstitution = True - else: - # This is our first pass through the document. - # Go through it again with the encoding information. - newCharset = match.group(3) - if newCharset and newCharset != self.originalEncoding: - self.declaredHTMLEncoding = newCharset - self._feed(self.declaredHTMLEncoding) - raise StopParsing - pass - tag = self.unknown_starttag("meta", attrs) - if tag and tagNeedsEncodingSubstitution: - tag.containsSubstitutions = True - -class StopParsing(Exception): - pass - -class ICantBelieveItsBeautifulSoup(BeautifulSoup): - - """The BeautifulSoup class is oriented towards skipping over - common HTML errors like unclosed tags. However, sometimes it makes - errors of its own. For instance, consider this fragment: - - FooBar - - This is perfectly valid (if bizarre) HTML. However, the - BeautifulSoup class will implicitly close the first b tag when it - encounters the second 'b'. It will think the author wrote - "FooBar", and didn't close the first 'b' tag, because - there's no real-world reason to bold something that's already - bold. When it encounters '' it will close two more 'b' - tags, for a grand total of three tags closed instead of two. This - can throw off the rest of your document structure. The same is - true of a number of other tags, listed below. - - It's much more common for someone to forget to close a 'b' tag - than to actually use nested 'b' tags, and the BeautifulSoup class - handles the common case. This class handles the not-co-common - case: where you can't believe someone wrote what they did, but - it's valid HTML and BeautifulSoup screwed up by assuming it - wouldn't be.""" - - I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \ - ('em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong', - 'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b', - 'big') - - I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ('noscript',) - - NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS, - I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS, - I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS) - -class MinimalSoup(BeautifulSoup): - """The MinimalSoup class is for parsing HTML that contains - pathologically bad markup. It makes no assumptions about tag - nesting, but it does know which tags are self-closing, that - ", html) @@ -180,69 +205,101 @@ def search_images_old(query, image_options = None, pages = 1): res.page = i res.index = j toks = token.split(",") - + # should be 32 or 33, but seems to change, so just make sure no exceptions # will be thrown by the indexing if (len(toks) > 22): for t in range(len(toks)): - toks[t] = toks[t].replace('\\x3cb\\x3e','').replace('\\x3c/b\\x3e','').replace('\\x3d','=').replace('\\x26','&') - match = re.search("imgurl=(?P[^&]+)&imgrefurl", toks[0]) + toks[t] = toks[t].replace('\\x3cb\\x3e', '').replace( + '\\x3c/b\\x3e', '').replace('\\x3d', '=').replace('\\x26', '&') + match = re.search( + "imgurl=(?P[^&]+)&imgrefurl", toks[0]) if match: res.link = match.group("link") res.name = toks[6].replace('"', '') res.thumb = toks[21].replace('"', '') res.format = toks[10].replace('"', '') res.domain = toks[11].replace('"', '') - match = re.search("(?P[0-9]+) × (?P[0-9]+) - (?P[^ ]+)", toks[9].replace('"', '')) + match = re.search( + "(?P[0-9]+) × (?P[0-9]+) - (?P[^ ]+)", toks[9].replace('"', '')) if match: res.width = match.group("width") res.height = match.group("height") - res.filesize = match.group("size") + res.filesize = match.group("size") results.append(res) j = j + 1 return results - + @staticmethod - def search_images(query, image_options = None, pages = 1): + def search_images(query, image_options=None, pages=1): + results = [] + for i in range(pages): - url = get_image_search_url(query, image_options, i) - html = get_html(url) + + url = get_image_search_url(query, image_options, i + 1) + html = get_html_from_dynamic_site(url) + if html: if Google.DEBUG_MODE: - write_html_to_file(html, "images_{0}_{1}.html".format(query.replace(" ", "_"), i)) + write_html_to_file( + html, "images_{0}_{1}.html".format(query.replace(" ", "_"), i)) + + # parse html into bs soup = BeautifulSoup(html) + + # find all divs containing an image + div_container = soup.find("div", {"id": "rg_s"}) + divs = div_container.find_all("div", {"class": "rg_di"}) j = 0 - tds = soup.findAll("td") - for td in tds: - a = td.find("a") - if a and a["href"].find("imgurl") != -1: - res = ImageResult() - res.page = i - res.index = j - tokens = a["href"].split("&") - match = re.search("imgurl=(?P[^&]+)", tokens[0]) - if match: - res.link = match.group("link") - res.format = res.link[res.link.rfind(".") + 1:] - img = td.find("img") - if img: - res.thumb = img["src"] - res.thumb_width = img["width"] - res.thumb_height = img["height"] - match = re.search("(?P[0-9]+) × (?P[0-9]+) - (?P[^&]+)", td.text) - if match: - res.width = match.group("width") - res.name = td.text[:td.text.find(res.width)] - res.height = match.group("height") - res.filesize = match.group("size") - cite = td.find("cite") - if cite: - res.domain = cite["title"] - results.append(res) - j = j + 1 + for div in divs: + + # try: + res = ImageResult() + + # store indexing paramethers + res.page = i + res.index = j + + # get url of image and its paramethers + a = div.find("a") + if a: + google_middle_link = a["href"] + url_parsed = urlparse.urlparse(google_middle_link) + qry_parsed = urlparse.parse_qs(url_parsed.query) + res.link = qry_parsed["imgurl"][0] + res.format = res.link[res.link.rfind(".") + 1:] + res.width = qry_parsed["w"][0] + res.height = qry_parsed["h"][0] + res.site = qry_parsed["imgrefurl"][0] + res.domain = urlparse.urlparse(res.site).netloc + + # get url of thumb and its size paramethers + img = a.find_all("img") + if img: + + # get url trying "src" and "data-src" keys + try: + res.thumb = img[0]["src"] + except: + res.thumb = img[0]["data-src"] + + try: + img_style = img[0]["style"].split(";") + img_style_dict = {i.split(":")[0]: i.split(":")[1] + for i in img_style} + res.thumb_width = img_style_dict["width"] + res.thumb_height = img_style_dict["height"] + except: + exc_type, exc_value, exc_traceback = sys.exc_info() + print exc_type, exc_value, "index=", res.index + + results.append(res) + + j = j + 1 + return results - + @staticmethod def shopping(query, pages=1): results = [] @@ -251,46 +308,48 @@ def shopping(query, pages=1): html = get_html(url) if html: if Google.DEBUG_MODE: - write_html_to_file(html, "shopping_{0}_{1}.html".format(query.replace(" ", "_"), i)) + write_html_to_file( + html, "shopping_{0}_{1}.html".format(query.replace(" ", "_"), i)) j = 0 soup = BeautifulSoup(html) - + products = soup.findAll("li", "g") for prod in products: res = ShoppingResult() - + divs = prod.findAll("div") for div in divs: - match = re.search("from (?P[0-9]+) stores", div.text.strip()) + match = re.search( + "from (?P[0-9]+) stores", div.text.strip()) if match: res.store_count = match.group("count") break - + h3 = prod.find("h3", "r") if h3: a = h3.find("a") if a: res.compare_url = a["href"] res.name = h3.text.strip() - + psliimg = prod.find("div", "psliimg") if psliimg: img = psliimg.find("img") if img: res.thumb = img["src"] - + f = prod.find("div", "f") if f: res.subtext = f.text.strip() - + price = prod.find("div", "psliprice") if price: res.min_price = price.text.strip() - + results.append(res) j = j + 1 return results - + """ Converts one currency to another. [amount] from_curreny = [return_value] to_currency @@ -300,17 +359,19 @@ def convert_currency(amount, from_currency, to_currency): if from_currency == to_currency: return 1.0 conn = httplib.HTTPSConnection("www.google.com") - req_url = "/ig/calculator?hl=en&q={0}{1}=?{2}".format(amount, from_currency.replace(" ", "%20"), to_currency.replace(" ", "%20")) - headers = { "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101" } + req_url = "/ig/calculator?hl=en&q={0}{1}=?{2}".format( + amount, from_currency.replace(" ", "%20"), to_currency.replace(" ", "%20")) + headers = { + "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101"} conn.request("GET", req_url, "", headers) response = conn.getresponse() rval = response.read().decode("utf-8").replace(u"\xa0", "") conn.close() rhs = rval.split(",")[1].strip() - s = rhs[rhs.find('"')+1:] + s = rhs[rhs.find('"') + 1:] rate = s[:s.find(" ")] return float(rate) - + """ Gets the exchange rate of one currency to another. 1 from_curreny = [return_value] to_currency @@ -318,7 +379,7 @@ def convert_currency(amount, from_currency, to_currency): @staticmethod def exchange_rate(from_currency, to_currency): return Google.convert_currency(1, from_currency, to_currency) - + """ Attempts to use google calculator to calculate the result of expr """ @@ -326,7 +387,8 @@ def exchange_rate(from_currency, to_currency): def calculate(expr): conn = httplib.HTTPSConnection("www.google.com") req_url = "/ig/calculator?hl=en&q={0}".format(expr.replace(" ", "%20")) - headers = { "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101" } + headers = { + "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101"} conn.request("GET", req_url, "", headers) response = conn.getresponse() j = response.read().decode("utf-8").replace(u"\xa0", "") @@ -337,24 +399,30 @@ def calculate(expr): j = re.sub(r":\s*'(\w)'\s*([,}])", r':"\1"\2', j) js = json.loads(j) return parse_calc_result(js["lhs"] + " = " + js["rhs"]) - + + def normalize_query(query): return query.strip().replace(":", "%3A").replace("+", "%2B").replace("&", "%26").replace(" ", "+") - -def get_search_url(query, page = 0, per_page = 10): - # note: num per page might not be supported by google anymore (because of google instant) + + +def get_search_url(query, page=0, per_page=10): + # note: num per page might not be supported by google anymore (because of + # google instant) return "http://www.google.com/search?hl=en&q=%s&start=%i&num=%i" % (normalize_query(query), page * per_page, per_page) + def get_shopping_url(query, page=0, per_page=10): return "http://www.google.com/search?hl=en&q={0}&tbm=shop&start={1}&num={2}".format(normalize_query(query), page * per_page, per_page) - + + class ImageType: NONE = None FACE = "face" PHOTO = "photo" CLIPART = "clipart" LINE_DRAWING = "lineart" - + + class SizeCategory: NONE = None ICON = "i" @@ -363,10 +431,11 @@ class SizeCategory: SMALL = "s" LARGER_THAN = "lt" EXACTLY = "ex" - + + class LargerThan: NONE = None - QSVGA = "qsvga" # 400 x 300 + QSVGA = "qsvga" # 400 x 300 VGA = "vga" # 640 x 480 SVGA = "svga" # 800 x 600 XGA = "xga" # 1024 x 768 @@ -381,27 +450,41 @@ class LargerThan: MP_40 = "40mp" # 40 MP (7216 x 5412) MP_70 = "70mp" # 70 MP (9600 x 7200) + class ColorType: NONE = None COLOR = "color" BLACK_WHITE = "gray" SPECIFIC = "specific" - + + def get_image_search_url(query, image_options=None, page=0, per_page=20): - query = query.strip().replace(":", "%3A").replace("+", "%2B").replace("&", "%26").replace(" ", "+") - url = "http://images.google.com/images?q=%s&sa=N&start=%i&ndsp=%i&sout=1" % (query, page * per_page, per_page) + query = query.strip().replace(":", "%3A").replace( + "+", "%2B").replace("&", "%26").replace(" ", "+") + + # url = "http://images.google.com/images?q=%s&sa=N&start=%i&ndsp=%i&sout=1" % ( + # query, page * per_page, per_page) + # TRYING NEW URL + url = "https://www.google.com.ar/search?q={}".format(query) + \ + "&es_sm=122&source=lnms" + \ + "&tbm=isch&sa=X&ei=DDdUVL-fE4SpNq-ngPgK&ved=0CAgQ_AUoAQ" + \ + "&biw=1024&bih=719&dpr=1.25" + if image_options: tbs = image_options.get_tbs() if tbs: url = url + tbs + return url - + + def add_to_tbs(tbs, name, value): if tbs: return "%s,%s:%s" % (tbs, name, value) else: - return "&tbs=%s:%s" % (name, value) - + return "&tbs=%s:%s" % (name, value) + + def parse_calc_result(string): result = CalculatorResult() result.fullstring = string @@ -423,71 +506,76 @@ def parse_calc_result(string): result.unit = token return result return None - + + def is_number(s): try: float(s) return True except ValueError: return False - + + def get_html(url): try: request = urllib2.Request(url) - request.add_header("User-Agent", "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101") + request.add_header( + "User-Agent", "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101") html = urllib2.urlopen(request).read() return html except: print "Error accessing:", url - return None + return None + def write_html_to_file(html, filename): of = open(filename, "w") of.write(html) of.flush() of.close() - + + def test(): search = Google.search("github") - if search is None or len(search) == 0: + if search is None or len(search) == 0: print "ERROR: No Search Results!" - else: + else: print "PASSED: {0} Search Results".format(len(search)) - + shop = Google.shopping("Disgaea 4") - if shop is None or len(shop) == 0: + if shop is None or len(shop) == 0: print "ERROR: No Shopping Results!" - else: + else: print "PASSED: {0} Shopping Results".format(len(shop)) - + options = ImageOptions() options.image_type = ImageType.CLIPART options.larger_than = LargerThan.MP_4 options.color = "green" images = Google.search_images("banana", options) - if images is None or len(images) == 0: + if images is None or len(images) == 0: print "ERROR: No Image Results!" else: print "PASSED: {0} Image Results".format(len(images)) - + calc = Google.calculate("157.3kg in grams") if calc is not None and int(calc.value) == 157300: print "PASSED: Calculator passed" else: print "ERROR: Calculator failed!" - + euros = Google.convert_currency(5.0, "USD", "EUR") if euros is not None and euros > 0.0: print "PASSED: Currency convert passed" else: print "ERROR: Currency convert failed!" - + + def main(): if len(sys.argv) > 1 and sys.argv[1] == "--debug": Google.DEBUG_MODE = True print "DEBUG_MODE ENABLED" test() - + if __name__ == "__main__": main() - \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..838616b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Google-Search-API +BeautifulSoup>=3.2.1, <4.0.0 +selenium>=2.44.0,<3.0.0 \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..329a984 --- /dev/null +++ b/utils.py @@ -0,0 +1,59 @@ +import time +from selenium import webdriver +from bs4 import BeautifulSoup + + +def get_browser_with_url(url, timeout=120, driver="firefox"): + """Returns an open browser with a given url.""" + + # choose a browser + if driver == "firefox": + browser = webdriver.Firefox() + elif driver == "ie": + browser = webdriver.Ie() + elif driver == "chrome": + browser = webdriver.Chrome() + else: + print "Driver choosen is not recognized" + + # set maximum load time + browser.set_page_load_timeout(timeout) + + # open a browser with given url + browser.get(url) + + time.sleep(5) + + return browser + + +def get_html_from_dynamic_site(url, timeout=120, driver="firefox", attempts=10): + """Returns html from a dynamic site, opening it in a browser.""" + + RV = "" + + # try several attempts + for i in xrange(attempts): + try: + # load browser + browser = get_browser_with_url(url, timeout, driver) + + # get html + time.sleep(10) + content = browser.page_source + + # try again if there is no content + if not content: + browser.quit() + raise Exception("No content!") + + # if there is content gets out + browser.quit() + RV = content + break + + except: + print "\nTry ", i, " of ", attempts, "\n" + time.sleep(5) + + return RV From 53d5d87d5fe895a38cdc3189dbe8287ccad31c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 17 Feb 2015 19:36:39 -0500 Subject: [PATCH 002/106] Update readmy, structure like a package and add a setup.py --- README.md | 27 + __init__.py => google_search_api/__init__.py | 0 google_search_api/google.py | 581 +++++++++++++++++++ utils.py => google_search_api/utils.py | 0 google_search_api/utils.pyc | Bin 0 -> 1478 bytes requirements.txt | 1 - setup.py | 6 + 7 files changed, 614 insertions(+), 1 deletion(-) rename __init__.py => google_search_api/__init__.py (100%) create mode 100644 google_search_api/google.py rename utils.py => google_search_api/utils.py (100%) create mode 100644 google_search_api/utils.pyc create mode 100644 setup.py diff --git a/README.md b/README.md index 51f1187..9bfa0c1 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,37 @@ Google Search API ===== +*The original package was developed by https://github.com/BirdAPI This is a forked package that I have re-factored and I mean to maintain in the foreseeable future* + Google Search API is a python based library for searching various functionalities of google. It uses screen scraping to retrieve the results, and thus is unreliable if the way google's web pages are returned change in the future. *Disclaimer: This software uses screen scraping to retreive search results from google.com, and therefore this software may stop working at any given time. Use this software at your own risk. I assume no responsibility for how this software API is used by others.* +Installation +------------ + +The repo is structured like a package, so it can be installed from pip using +github clone url. From command line type: + +``` +pip install git+https://github.com/abenassi/Google-Search-API.git +``` + +To upgrade the package if you have already installed it: + +``` +pip install git+https://github.com/abenassi/Google-Search-API.git --upgrade +``` + +You could also just download or clone the repo and import the package from +Google-Search-API folder. + +```python +import os +os.chdir("C:\Path_where_repo_is") +import google_search_api +``` + ## Google Web Search You can search google web in the following way: diff --git a/__init__.py b/google_search_api/__init__.py similarity index 100% rename from __init__.py rename to google_search_api/__init__.py diff --git a/google_search_api/google.py b/google_search_api/google.py new file mode 100644 index 0000000..51db91c --- /dev/null +++ b/google_search_api/google.py @@ -0,0 +1,581 @@ +from bs4 import BeautifulSoup +from pprint import pprint +import os +import threading +import httplib +import urllib +import urllib2 +import sys +import re +from utils import get_html_from_dynamic_site +import urlparse + +try: + import json +except ImportError: + import simplejson as json + +__author__ = "Anthony Casagrande " +__version__ = "0.9" + +""" +Represents a standard google search result +""" + + +class GoogleResult: + + def __init__(self): + self.name = None + self.link = None + self.description = None + self.thumb = None + self.cached = None + self.page = None + self.index = None + +""" +Represents a result returned from google calculator +""" + + +class CalculatorResult: + + def __init__(self): + self.value = None + self.unit = None + self.expr = None + self.result = None + self.fullstring = None + + +class ShoppingResult: + + def __init__(self): + self.name = None + self.link = None + self.thumb = None + self.subtext = None + self.description = None + self.compare_url = None + self.store_count = None + self.min_price = None + +""" +Represents a google image search result +""" + + +class ImageResult: + + def __init__(self): + self.name = None + self.link = None + self.thumb = None + self.thumb_width = None + self.thumb_height = None + self.width = None + self.height = None + self.filesize = None + self.format = None + self.domain = None + self.page = None + self.index = None + self.site = None + + def __repr__(self): + string = "ImageResult(" + \ + "index={}, page={}, ".format(self.index, self.page) + \ + "domain={}, link={})".format(self.domain, self.link) + return string + + +class ImageOptions: + + def __init__(self): + self.image_type = None + self.size_category = None + self.larger_than = None + self.exact_width = None + self.exact_height = None + self.color_type = None + self.color = None + + def get_tbs(self): + tbs = None + if self.image_type: + # clipart + tbs = add_to_tbs(tbs, "itp", self.image_type) + if self.size_category and not (self.larger_than or (self.exact_width and self.exact_height)): + # i = icon, l = large, m = medium, lt = larger than, ex = exact + tbs = add_to_tbs(tbs, "isz", self.size_category) + if self.larger_than: + # qsvga,4mp + tbs = add_to_tbs(tbs, "isz", SizeCategory.LARGER_THAN) + tbs = add_to_tbs(tbs, "islt", self.larger_than) + if self.exact_width and self.exact_height: + tbs = add_to_tbs(tbs, "isz", SizeCategory.EXACTLY) + tbs = add_to_tbs(tbs, "iszw", self.exact_width) + tbs = add_to_tbs(tbs, "iszh", self.exact_height) + if self.color_type and not self.color: + # color = color, gray = black and white, specific = user defined + tbs = add_to_tbs(tbs, "ic", self.color_type) + if self.color: + tbs = add_to_tbs(tbs, "ic", ColorType.SPECIFIC) + tbs = add_to_tbs(tbs, "isc", self.color) + return tbs + +""" +Defines the public static api methods +""" + + +class Google: + DEBUG_MODE = False + + """ + Returns a list of GoogleResult + """ + @staticmethod + def search(query, pages=1): + results = [] + for i in range(pages): + url = get_search_url(query, i) + html = get_html(url) + if html: + if Google.DEBUG_MODE: + write_html_to_file( + html, "{0}_{1}.html".format(query.replace(" ", "_"), i)) + soup = BeautifulSoup(html) + lis = soup.findAll("li", attrs={"class": "g"}) + j = 0 + for li in lis: + res = GoogleResult() + res.page = i + res.index = j + a = li.find("a") + res.name = a.text.strip() + res.link = a["href"] + if res.link.startswith("/search?"): + # this is not an external link, so skip it + continue + sdiv = li.find("div", attrs={"class": "s"}) + if sdiv: + res.description = sdiv.text.strip() + results.append(res) + j = j + 1 + return results + + """ + OLD WAY OF DOING THIS. Attempts to use google calculator to calculate the result of expr + """ + @staticmethod + def calculate_old(expr): + url = get_search_url(expr) + html = get_html(url) + if html: + soup = BeautifulSoup(html) + topstuff = soup.find("div", id="topstuff") + if topstuff: + a = topstuff.find("a") + if a and a["href"].find("calculator") != -1: + h2 = topstuff.find("h2") + if h2: + return parse_calc_result(h2.text) + return None + + @staticmethod + def search_images_old(query, image_options=None, pages=1): + results = [] + for i in range(pages): + url = get_image_search_url(query, image_options, i) + html = get_html(url) + if html: + if Google.DEBUG_MODE: + write_html_to_file( + html, "images_{0}_{1}.html".format(query.replace(" ", "_"), i)) + j = 0 + soup = BeautifulSoup(html) + match = re.search("dyn.setResults\((.+)\);", html) + if match: + init = unicode(match.group(1), errors="ignore") + tokens = init.split('],[') + for token in tokens: + res = ImageResult() + res.page = i + res.index = j + toks = token.split(",") + + # should be 32 or 33, but seems to change, so just make sure no exceptions + # will be thrown by the indexing + if (len(toks) > 22): + for t in range(len(toks)): + toks[t] = toks[t].replace('\\x3cb\\x3e', '').replace( + '\\x3c/b\\x3e', '').replace('\\x3d', '=').replace('\\x26', '&') + match = re.search( + "imgurl=(?P[^&]+)&imgrefurl", toks[0]) + if match: + res.link = match.group("link") + res.name = toks[6].replace('"', '') + res.thumb = toks[21].replace('"', '') + res.format = toks[10].replace('"', '') + res.domain = toks[11].replace('"', '') + match = re.search( + "(?P[0-9]+) × (?P[0-9]+) - (?P[^ ]+)", toks[9].replace('"', '')) + if match: + res.width = match.group("width") + res.height = match.group("height") + res.filesize = match.group("size") + results.append(res) + j = j + 1 + return results + + @staticmethod + def search_images(query, image_options=None, pages=1): + + results = [] + + for i in range(pages): + + url = get_image_search_url(query, image_options, i + 1) + html = get_html_from_dynamic_site(url) + + if html: + if Google.DEBUG_MODE: + write_html_to_file( + html, "images_{0}_{1}.html".format(query.replace(" ", "_"), i)) + + # parse html into bs + soup = BeautifulSoup(html) + + # find all divs containing an image + div_container = soup.find("div", {"id": "rg_s"}) + divs = div_container.find_all("div", {"class": "rg_di"}) + j = 0 + for div in divs: + + # try: + res = ImageResult() + + # store indexing paramethers + res.page = i + res.index = j + + # get url of image and its paramethers + a = div.find("a") + if a: + google_middle_link = a["href"] + url_parsed = urlparse.urlparse(google_middle_link) + qry_parsed = urlparse.parse_qs(url_parsed.query) + res.link = qry_parsed["imgurl"][0] + res.format = res.link[res.link.rfind(".") + 1:] + res.width = qry_parsed["w"][0] + res.height = qry_parsed["h"][0] + res.site = qry_parsed["imgrefurl"][0] + res.domain = urlparse.urlparse(res.site).netloc + + # get url of thumb and its size paramethers + img = a.find_all("img") + if img: + + # get url trying "src" and "data-src" keys + try: + res.thumb = img[0]["src"] + except: + res.thumb = img[0]["data-src"] + + try: + img_style = img[0]["style"].split(";") + img_style_dict = {i.split(":")[0]: i.split(":")[1] + for i in img_style} + res.thumb_width = img_style_dict["width"] + res.thumb_height = img_style_dict["height"] + except: + exc_type, exc_value, exc_traceback = sys.exc_info() + print exc_type, exc_value, "index=", res.index + + results.append(res) + + j = j + 1 + + return results + + @staticmethod + def shopping(query, pages=1): + results = [] + for i in range(pages): + url = get_shopping_url(query, i) + html = get_html(url) + if html: + if Google.DEBUG_MODE: + write_html_to_file( + html, "shopping_{0}_{1}.html".format(query.replace(" ", "_"), i)) + j = 0 + soup = BeautifulSoup(html) + + products = soup.findAll("li", "g") + for prod in products: + res = ShoppingResult() + + divs = prod.findAll("div") + for div in divs: + match = re.search( + "from (?P[0-9]+) stores", div.text.strip()) + if match: + res.store_count = match.group("count") + break + + h3 = prod.find("h3", "r") + if h3: + a = h3.find("a") + if a: + res.compare_url = a["href"] + res.name = h3.text.strip() + + psliimg = prod.find("div", "psliimg") + if psliimg: + img = psliimg.find("img") + if img: + res.thumb = img["src"] + + f = prod.find("div", "f") + if f: + res.subtext = f.text.strip() + + price = prod.find("div", "psliprice") + if price: + res.min_price = price.text.strip() + + results.append(res) + j = j + 1 + return results + + """ + Converts one currency to another. + [amount] from_curreny = [return_value] to_currency + """ + @staticmethod + def convert_currency(amount, from_currency, to_currency): + if from_currency == to_currency: + return 1.0 + conn = httplib.HTTPSConnection("www.google.com") + req_url = "/ig/calculator?hl=en&q={0}{1}=?{2}".format( + amount, from_currency.replace(" ", "%20"), to_currency.replace(" ", "%20")) + headers = { + "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101"} + conn.request("GET", req_url, "", headers) + response = conn.getresponse() + rval = response.read().decode("utf-8").replace(u"\xa0", "") + conn.close() + rhs = rval.split(",")[1].strip() + s = rhs[rhs.find('"') + 1:] + rate = s[:s.find(" ")] + return float(rate) + + """ + Gets the exchange rate of one currency to another. + 1 from_curreny = [return_value] to_currency + """ + @staticmethod + def exchange_rate(from_currency, to_currency): + return Google.convert_currency(1, from_currency, to_currency) + + """ + Attempts to use google calculator to calculate the result of expr + """ + @staticmethod + def calculate(expr): + conn = httplib.HTTPSConnection("www.google.com") + req_url = "/ig/calculator?hl=en&q={0}".format(expr.replace(" ", "%20")) + headers = { + "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101"} + conn.request("GET", req_url, "", headers) + response = conn.getresponse() + j = response.read().decode("utf-8").replace(u"\xa0", "") + conn.close() + j = re.sub(r"{\s*'?(\w)", r'{"\1', j) + j = re.sub(r",\s*'?(\w)", r',"\1', j) + j = re.sub(r"(\w)'?\s*:", r'\1":', j) + j = re.sub(r":\s*'(\w)'\s*([,}])", r':"\1"\2', j) + js = json.loads(j) + return parse_calc_result(js["lhs"] + " = " + js["rhs"]) + + +def normalize_query(query): + return query.strip().replace(":", "%3A").replace("+", "%2B").replace("&", "%26").replace(" ", "+") + + +def get_search_url(query, page=0, per_page=10): + # note: num per page might not be supported by google anymore (because of + # google instant) + return "http://www.google.com/search?hl=en&q=%s&start=%i&num=%i" % (normalize_query(query), page * per_page, per_page) + + +def get_shopping_url(query, page=0, per_page=10): + return "http://www.google.com/search?hl=en&q={0}&tbm=shop&start={1}&num={2}".format(normalize_query(query), page * per_page, per_page) + + +class ImageType: + NONE = None + FACE = "face" + PHOTO = "photo" + CLIPART = "clipart" + LINE_DRAWING = "lineart" + + +class SizeCategory: + NONE = None + ICON = "i" + LARGE = "l" + MEDIUM = "m" + SMALL = "s" + LARGER_THAN = "lt" + EXACTLY = "ex" + + +class LargerThan: + NONE = None + QSVGA = "qsvga" # 400 x 300 + VGA = "vga" # 640 x 480 + SVGA = "svga" # 800 x 600 + XGA = "xga" # 1024 x 768 + MP_2 = "2mp" # 2 MP (1600 x 1200) + MP_4 = "4mp" # 4 MP (2272 x 1704) + MP_6 = "6mp" # 6 MP (2816 x 2112) + MP_8 = "8mp" # 8 MP (3264 x 2448) + MP_10 = "10mp" # 10 MP (3648 x 2736) + MP_12 = "12mp" # 12 MP (4096 x 3072) + MP_15 = "15mp" # 15 MP (4480 x 3360) + MP_20 = "20mp" # 20 MP (5120 x 3840) + MP_40 = "40mp" # 40 MP (7216 x 5412) + MP_70 = "70mp" # 70 MP (9600 x 7200) + + +class ColorType: + NONE = None + COLOR = "color" + BLACK_WHITE = "gray" + SPECIFIC = "specific" + + +def get_image_search_url(query, image_options=None, page=0, per_page=20): + query = query.strip().replace(":", "%3A").replace( + "+", "%2B").replace("&", "%26").replace(" ", "+") + + # url = "http://images.google.com/images?q=%s&sa=N&start=%i&ndsp=%i&sout=1" % ( + # query, page * per_page, per_page) + # TRYING NEW URL + url = "https://www.google.com.ar/search?q={}".format(query) + \ + "&es_sm=122&source=lnms" + \ + "&tbm=isch&sa=X&ei=DDdUVL-fE4SpNq-ngPgK&ved=0CAgQ_AUoAQ" + \ + "&biw=1024&bih=719&dpr=1.25" + + if image_options: + tbs = image_options.get_tbs() + if tbs: + url = url + tbs + + return url + + +def add_to_tbs(tbs, name, value): + if tbs: + return "%s,%s:%s" % (tbs, name, value) + else: + return "&tbs=%s:%s" % (name, value) + + +def parse_calc_result(string): + result = CalculatorResult() + result.fullstring = string + string = string.strip().replace(u"\xa0", " ") + if string.find("=") != -1: + result.expr = string[:string.rfind("=")].strip() + string = string[string.rfind("=") + 2:] + result.result = string + tokens = string.split(" ") + if len(tokens) > 0: + result.value = "" + for token in tokens: + if is_number(token): + result.value = result.value + token + else: + if result.unit: + result.unit = result.unit + " " + token + else: + result.unit = token + return result + return None + + +def is_number(s): + try: + float(s) + return True + except ValueError: + return False + + +def get_html(url): + try: + request = urllib2.Request(url) + request.add_header( + "User-Agent", "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101") + html = urllib2.urlopen(request).read() + return html + except: + print "Error accessing:", url + return None + + +def write_html_to_file(html, filename): + of = open(filename, "w") + of.write(html) + of.flush() + of.close() + + +def test(): + search = Google.search("github") + if search is None or len(search) == 0: + print "ERROR: No Search Results!" + else: + print "PASSED: {0} Search Results".format(len(search)) + + shop = Google.shopping("Disgaea 4") + if shop is None or len(shop) == 0: + print "ERROR: No Shopping Results!" + else: + print "PASSED: {0} Shopping Results".format(len(shop)) + + options = ImageOptions() + options.image_type = ImageType.CLIPART + options.larger_than = LargerThan.MP_4 + options.color = "green" + images = Google.search_images("banana", options) + if images is None or len(images) == 0: + print "ERROR: No Image Results!" + else: + print "PASSED: {0} Image Results".format(len(images)) + + calc = Google.calculate("157.3kg in grams") + if calc is not None and int(calc.value) == 157300: + print "PASSED: Calculator passed" + else: + print "ERROR: Calculator failed!" + + euros = Google.convert_currency(5.0, "USD", "EUR") + if euros is not None and euros > 0.0: + print "PASSED: Currency convert passed" + else: + print "ERROR: Currency convert failed!" + + +def main(): + if len(sys.argv) > 1 and sys.argv[1] == "--debug": + Google.DEBUG_MODE = True + print "DEBUG_MODE ENABLED" + test() + +if __name__ == "__main__": + main() diff --git a/utils.py b/google_search_api/utils.py similarity index 100% rename from utils.py rename to google_search_api/utils.py diff --git a/google_search_api/utils.pyc b/google_search_api/utils.pyc new file mode 100644 index 0000000000000000000000000000000000000000..831a00ff57e6ade7c66ee0b985b4076ba01df246 GIT binary patch literal 1478 zcmb_b-EJF26h5=Fwo?<6h!TWALc^VrXs#+QT7;kpxV@>;O`#|GfR#C!nQ!*|eCIpz&(ZLY!{49hwEM;Q|2dX>2227k*F2 z0$b1L8+~0g<_Fz$S0h|K(pl@xtSv8XyE1EjIe?j&M$hazm)LWn5!M-&`w3xAteuzo-lfi(d~xrKizibwy>zFmTmLq`M^fp|bIQ!6 z$Rbeu4#$ZP#gW*ps3%8q6pbP?1d?;w%nKKChUI>Pfi|`=t$*b%qmO9k`FFq2wnw*9 z@`9U+CPPE7ByIcLlcl5rM{YYh9n*F|n*q6;_=v6(3AC&Tu6`C&!@)lQ9wOMqibWvrnHuPhu2a+qNiiAj4Vb4^<+k=<|3k;544*f*$}vsK&XnUx9CmWzDGNee zH(5Q`$uZ6e`$^c3XA>0UY}@2IXy#>We3;`e*STJKW9v?ohx`=}bMhvC%2j3F>uTj) z&}Z@uXh)XG5yKpKmt#;+F#lOb@4+j}h#5?o!F0!8%KUu*G>jU_zPu}B^p+fnJF+i^ z;z$lfU-Tj=PQ*zx=IjCup$w->D7vk>xs$-I-Tj1%>GWM&diQVPc|jKd literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index 838616b..28da76d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -Google-Search-API BeautifulSoup>=3.2.1, <4.0.0 selenium>=2.44.0,<3.0.0 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..75fa4e6 --- /dev/null +++ b/setup.py @@ -0,0 +1,6 @@ +from distutils.core import setup + +setup(name='google_search_api', + version='0.1', + packages=['google_search_api'], + ) \ No newline at end of file From f0b94e8a1c19e232edf6435481b47c1294673e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 17 Feb 2015 19:44:33 -0500 Subject: [PATCH 003/106] Modify __init__ so directly import Google class. --- google.py | 581 ---------------------------------- google_search_api/__init__.py | 3 +- 2 files changed, 1 insertion(+), 583 deletions(-) delete mode 100644 google.py diff --git a/google.py b/google.py deleted file mode 100644 index 51db91c..0000000 --- a/google.py +++ /dev/null @@ -1,581 +0,0 @@ -from bs4 import BeautifulSoup -from pprint import pprint -import os -import threading -import httplib -import urllib -import urllib2 -import sys -import re -from utils import get_html_from_dynamic_site -import urlparse - -try: - import json -except ImportError: - import simplejson as json - -__author__ = "Anthony Casagrande " -__version__ = "0.9" - -""" -Represents a standard google search result -""" - - -class GoogleResult: - - def __init__(self): - self.name = None - self.link = None - self.description = None - self.thumb = None - self.cached = None - self.page = None - self.index = None - -""" -Represents a result returned from google calculator -""" - - -class CalculatorResult: - - def __init__(self): - self.value = None - self.unit = None - self.expr = None - self.result = None - self.fullstring = None - - -class ShoppingResult: - - def __init__(self): - self.name = None - self.link = None - self.thumb = None - self.subtext = None - self.description = None - self.compare_url = None - self.store_count = None - self.min_price = None - -""" -Represents a google image search result -""" - - -class ImageResult: - - def __init__(self): - self.name = None - self.link = None - self.thumb = None - self.thumb_width = None - self.thumb_height = None - self.width = None - self.height = None - self.filesize = None - self.format = None - self.domain = None - self.page = None - self.index = None - self.site = None - - def __repr__(self): - string = "ImageResult(" + \ - "index={}, page={}, ".format(self.index, self.page) + \ - "domain={}, link={})".format(self.domain, self.link) - return string - - -class ImageOptions: - - def __init__(self): - self.image_type = None - self.size_category = None - self.larger_than = None - self.exact_width = None - self.exact_height = None - self.color_type = None - self.color = None - - def get_tbs(self): - tbs = None - if self.image_type: - # clipart - tbs = add_to_tbs(tbs, "itp", self.image_type) - if self.size_category and not (self.larger_than or (self.exact_width and self.exact_height)): - # i = icon, l = large, m = medium, lt = larger than, ex = exact - tbs = add_to_tbs(tbs, "isz", self.size_category) - if self.larger_than: - # qsvga,4mp - tbs = add_to_tbs(tbs, "isz", SizeCategory.LARGER_THAN) - tbs = add_to_tbs(tbs, "islt", self.larger_than) - if self.exact_width and self.exact_height: - tbs = add_to_tbs(tbs, "isz", SizeCategory.EXACTLY) - tbs = add_to_tbs(tbs, "iszw", self.exact_width) - tbs = add_to_tbs(tbs, "iszh", self.exact_height) - if self.color_type and not self.color: - # color = color, gray = black and white, specific = user defined - tbs = add_to_tbs(tbs, "ic", self.color_type) - if self.color: - tbs = add_to_tbs(tbs, "ic", ColorType.SPECIFIC) - tbs = add_to_tbs(tbs, "isc", self.color) - return tbs - -""" -Defines the public static api methods -""" - - -class Google: - DEBUG_MODE = False - - """ - Returns a list of GoogleResult - """ - @staticmethod - def search(query, pages=1): - results = [] - for i in range(pages): - url = get_search_url(query, i) - html = get_html(url) - if html: - if Google.DEBUG_MODE: - write_html_to_file( - html, "{0}_{1}.html".format(query.replace(" ", "_"), i)) - soup = BeautifulSoup(html) - lis = soup.findAll("li", attrs={"class": "g"}) - j = 0 - for li in lis: - res = GoogleResult() - res.page = i - res.index = j - a = li.find("a") - res.name = a.text.strip() - res.link = a["href"] - if res.link.startswith("/search?"): - # this is not an external link, so skip it - continue - sdiv = li.find("div", attrs={"class": "s"}) - if sdiv: - res.description = sdiv.text.strip() - results.append(res) - j = j + 1 - return results - - """ - OLD WAY OF DOING THIS. Attempts to use google calculator to calculate the result of expr - """ - @staticmethod - def calculate_old(expr): - url = get_search_url(expr) - html = get_html(url) - if html: - soup = BeautifulSoup(html) - topstuff = soup.find("div", id="topstuff") - if topstuff: - a = topstuff.find("a") - if a and a["href"].find("calculator") != -1: - h2 = topstuff.find("h2") - if h2: - return parse_calc_result(h2.text) - return None - - @staticmethod - def search_images_old(query, image_options=None, pages=1): - results = [] - for i in range(pages): - url = get_image_search_url(query, image_options, i) - html = get_html(url) - if html: - if Google.DEBUG_MODE: - write_html_to_file( - html, "images_{0}_{1}.html".format(query.replace(" ", "_"), i)) - j = 0 - soup = BeautifulSoup(html) - match = re.search("dyn.setResults\((.+)\);", html) - if match: - init = unicode(match.group(1), errors="ignore") - tokens = init.split('],[') - for token in tokens: - res = ImageResult() - res.page = i - res.index = j - toks = token.split(",") - - # should be 32 or 33, but seems to change, so just make sure no exceptions - # will be thrown by the indexing - if (len(toks) > 22): - for t in range(len(toks)): - toks[t] = toks[t].replace('\\x3cb\\x3e', '').replace( - '\\x3c/b\\x3e', '').replace('\\x3d', '=').replace('\\x26', '&') - match = re.search( - "imgurl=(?P[^&]+)&imgrefurl", toks[0]) - if match: - res.link = match.group("link") - res.name = toks[6].replace('"', '') - res.thumb = toks[21].replace('"', '') - res.format = toks[10].replace('"', '') - res.domain = toks[11].replace('"', '') - match = re.search( - "(?P[0-9]+) × (?P[0-9]+) - (?P[^ ]+)", toks[9].replace('"', '')) - if match: - res.width = match.group("width") - res.height = match.group("height") - res.filesize = match.group("size") - results.append(res) - j = j + 1 - return results - - @staticmethod - def search_images(query, image_options=None, pages=1): - - results = [] - - for i in range(pages): - - url = get_image_search_url(query, image_options, i + 1) - html = get_html_from_dynamic_site(url) - - if html: - if Google.DEBUG_MODE: - write_html_to_file( - html, "images_{0}_{1}.html".format(query.replace(" ", "_"), i)) - - # parse html into bs - soup = BeautifulSoup(html) - - # find all divs containing an image - div_container = soup.find("div", {"id": "rg_s"}) - divs = div_container.find_all("div", {"class": "rg_di"}) - j = 0 - for div in divs: - - # try: - res = ImageResult() - - # store indexing paramethers - res.page = i - res.index = j - - # get url of image and its paramethers - a = div.find("a") - if a: - google_middle_link = a["href"] - url_parsed = urlparse.urlparse(google_middle_link) - qry_parsed = urlparse.parse_qs(url_parsed.query) - res.link = qry_parsed["imgurl"][0] - res.format = res.link[res.link.rfind(".") + 1:] - res.width = qry_parsed["w"][0] - res.height = qry_parsed["h"][0] - res.site = qry_parsed["imgrefurl"][0] - res.domain = urlparse.urlparse(res.site).netloc - - # get url of thumb and its size paramethers - img = a.find_all("img") - if img: - - # get url trying "src" and "data-src" keys - try: - res.thumb = img[0]["src"] - except: - res.thumb = img[0]["data-src"] - - try: - img_style = img[0]["style"].split(";") - img_style_dict = {i.split(":")[0]: i.split(":")[1] - for i in img_style} - res.thumb_width = img_style_dict["width"] - res.thumb_height = img_style_dict["height"] - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - print exc_type, exc_value, "index=", res.index - - results.append(res) - - j = j + 1 - - return results - - @staticmethod - def shopping(query, pages=1): - results = [] - for i in range(pages): - url = get_shopping_url(query, i) - html = get_html(url) - if html: - if Google.DEBUG_MODE: - write_html_to_file( - html, "shopping_{0}_{1}.html".format(query.replace(" ", "_"), i)) - j = 0 - soup = BeautifulSoup(html) - - products = soup.findAll("li", "g") - for prod in products: - res = ShoppingResult() - - divs = prod.findAll("div") - for div in divs: - match = re.search( - "from (?P[0-9]+) stores", div.text.strip()) - if match: - res.store_count = match.group("count") - break - - h3 = prod.find("h3", "r") - if h3: - a = h3.find("a") - if a: - res.compare_url = a["href"] - res.name = h3.text.strip() - - psliimg = prod.find("div", "psliimg") - if psliimg: - img = psliimg.find("img") - if img: - res.thumb = img["src"] - - f = prod.find("div", "f") - if f: - res.subtext = f.text.strip() - - price = prod.find("div", "psliprice") - if price: - res.min_price = price.text.strip() - - results.append(res) - j = j + 1 - return results - - """ - Converts one currency to another. - [amount] from_curreny = [return_value] to_currency - """ - @staticmethod - def convert_currency(amount, from_currency, to_currency): - if from_currency == to_currency: - return 1.0 - conn = httplib.HTTPSConnection("www.google.com") - req_url = "/ig/calculator?hl=en&q={0}{1}=?{2}".format( - amount, from_currency.replace(" ", "%20"), to_currency.replace(" ", "%20")) - headers = { - "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101"} - conn.request("GET", req_url, "", headers) - response = conn.getresponse() - rval = response.read().decode("utf-8").replace(u"\xa0", "") - conn.close() - rhs = rval.split(",")[1].strip() - s = rhs[rhs.find('"') + 1:] - rate = s[:s.find(" ")] - return float(rate) - - """ - Gets the exchange rate of one currency to another. - 1 from_curreny = [return_value] to_currency - """ - @staticmethod - def exchange_rate(from_currency, to_currency): - return Google.convert_currency(1, from_currency, to_currency) - - """ - Attempts to use google calculator to calculate the result of expr - """ - @staticmethod - def calculate(expr): - conn = httplib.HTTPSConnection("www.google.com") - req_url = "/ig/calculator?hl=en&q={0}".format(expr.replace(" ", "%20")) - headers = { - "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101"} - conn.request("GET", req_url, "", headers) - response = conn.getresponse() - j = response.read().decode("utf-8").replace(u"\xa0", "") - conn.close() - j = re.sub(r"{\s*'?(\w)", r'{"\1', j) - j = re.sub(r",\s*'?(\w)", r',"\1', j) - j = re.sub(r"(\w)'?\s*:", r'\1":', j) - j = re.sub(r":\s*'(\w)'\s*([,}])", r':"\1"\2', j) - js = json.loads(j) - return parse_calc_result(js["lhs"] + " = " + js["rhs"]) - - -def normalize_query(query): - return query.strip().replace(":", "%3A").replace("+", "%2B").replace("&", "%26").replace(" ", "+") - - -def get_search_url(query, page=0, per_page=10): - # note: num per page might not be supported by google anymore (because of - # google instant) - return "http://www.google.com/search?hl=en&q=%s&start=%i&num=%i" % (normalize_query(query), page * per_page, per_page) - - -def get_shopping_url(query, page=0, per_page=10): - return "http://www.google.com/search?hl=en&q={0}&tbm=shop&start={1}&num={2}".format(normalize_query(query), page * per_page, per_page) - - -class ImageType: - NONE = None - FACE = "face" - PHOTO = "photo" - CLIPART = "clipart" - LINE_DRAWING = "lineart" - - -class SizeCategory: - NONE = None - ICON = "i" - LARGE = "l" - MEDIUM = "m" - SMALL = "s" - LARGER_THAN = "lt" - EXACTLY = "ex" - - -class LargerThan: - NONE = None - QSVGA = "qsvga" # 400 x 300 - VGA = "vga" # 640 x 480 - SVGA = "svga" # 800 x 600 - XGA = "xga" # 1024 x 768 - MP_2 = "2mp" # 2 MP (1600 x 1200) - MP_4 = "4mp" # 4 MP (2272 x 1704) - MP_6 = "6mp" # 6 MP (2816 x 2112) - MP_8 = "8mp" # 8 MP (3264 x 2448) - MP_10 = "10mp" # 10 MP (3648 x 2736) - MP_12 = "12mp" # 12 MP (4096 x 3072) - MP_15 = "15mp" # 15 MP (4480 x 3360) - MP_20 = "20mp" # 20 MP (5120 x 3840) - MP_40 = "40mp" # 40 MP (7216 x 5412) - MP_70 = "70mp" # 70 MP (9600 x 7200) - - -class ColorType: - NONE = None - COLOR = "color" - BLACK_WHITE = "gray" - SPECIFIC = "specific" - - -def get_image_search_url(query, image_options=None, page=0, per_page=20): - query = query.strip().replace(":", "%3A").replace( - "+", "%2B").replace("&", "%26").replace(" ", "+") - - # url = "http://images.google.com/images?q=%s&sa=N&start=%i&ndsp=%i&sout=1" % ( - # query, page * per_page, per_page) - # TRYING NEW URL - url = "https://www.google.com.ar/search?q={}".format(query) + \ - "&es_sm=122&source=lnms" + \ - "&tbm=isch&sa=X&ei=DDdUVL-fE4SpNq-ngPgK&ved=0CAgQ_AUoAQ" + \ - "&biw=1024&bih=719&dpr=1.25" - - if image_options: - tbs = image_options.get_tbs() - if tbs: - url = url + tbs - - return url - - -def add_to_tbs(tbs, name, value): - if tbs: - return "%s,%s:%s" % (tbs, name, value) - else: - return "&tbs=%s:%s" % (name, value) - - -def parse_calc_result(string): - result = CalculatorResult() - result.fullstring = string - string = string.strip().replace(u"\xa0", " ") - if string.find("=") != -1: - result.expr = string[:string.rfind("=")].strip() - string = string[string.rfind("=") + 2:] - result.result = string - tokens = string.split(" ") - if len(tokens) > 0: - result.value = "" - for token in tokens: - if is_number(token): - result.value = result.value + token - else: - if result.unit: - result.unit = result.unit + " " + token - else: - result.unit = token - return result - return None - - -def is_number(s): - try: - float(s) - return True - except ValueError: - return False - - -def get_html(url): - try: - request = urllib2.Request(url) - request.add_header( - "User-Agent", "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101") - html = urllib2.urlopen(request).read() - return html - except: - print "Error accessing:", url - return None - - -def write_html_to_file(html, filename): - of = open(filename, "w") - of.write(html) - of.flush() - of.close() - - -def test(): - search = Google.search("github") - if search is None or len(search) == 0: - print "ERROR: No Search Results!" - else: - print "PASSED: {0} Search Results".format(len(search)) - - shop = Google.shopping("Disgaea 4") - if shop is None or len(shop) == 0: - print "ERROR: No Shopping Results!" - else: - print "PASSED: {0} Shopping Results".format(len(shop)) - - options = ImageOptions() - options.image_type = ImageType.CLIPART - options.larger_than = LargerThan.MP_4 - options.color = "green" - images = Google.search_images("banana", options) - if images is None or len(images) == 0: - print "ERROR: No Image Results!" - else: - print "PASSED: {0} Image Results".format(len(images)) - - calc = Google.calculate("157.3kg in grams") - if calc is not None and int(calc.value) == 157300: - print "PASSED: Calculator passed" - else: - print "ERROR: Calculator failed!" - - euros = Google.convert_currency(5.0, "USD", "EUR") - if euros is not None and euros > 0.0: - print "PASSED: Currency convert passed" - else: - print "ERROR: Currency convert failed!" - - -def main(): - if len(sys.argv) > 1 and sys.argv[1] == "--debug": - Google.DEBUG_MODE = True - print "DEBUG_MODE ENABLED" - test() - -if __name__ == "__main__": - main() diff --git a/google_search_api/__init__.py b/google_search_api/__init__.py index 8d5063a..d89ecaa 100644 --- a/google_search_api/__init__.py +++ b/google_search_api/__init__.py @@ -1,2 +1 @@ -#!/usr/bin/python - +from google import Google From eee64024eb6914f81c20cc285fcc58de32001b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 17 Feb 2015 23:30:42 -0500 Subject: [PATCH 004/106] Add download methods for images. Taking format of images needs to be fixed. --- google_search_api/__init__.py | 2 +- google_search_api/google.py | 82 ++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/google_search_api/__init__.py b/google_search_api/__init__.py index d89ecaa..10a986e 100644 --- a/google_search_api/__init__.py +++ b/google_search_api/__init__.py @@ -1 +1 @@ -from google import Google +from google import * diff --git a/google_search_api/google.py b/google_search_api/google.py index 51db91c..8388457 100644 --- a/google_search_api/google.py +++ b/google_search_api/google.py @@ -9,6 +9,8 @@ import re from utils import get_html_from_dynamic_site import urlparse +import shutil +import requests try: import json @@ -66,8 +68,22 @@ def __init__(self): """ +def download_images(image_results): + """Download a list of images. + + Args: + images_list: A list of ImageResult instances + """ + + for image_result in image_results: + image_result.download() + + class ImageResult: + ROOT_FILENAME = "img" + DEFAULT_FORMAT = "jpg" + def __init__(self): self.name = None self.link = None @@ -89,6 +105,68 @@ def __repr__(self): "domain={}, link={})".format(self.domain, self.link) return string + def download(self, path="download"): + """Download an image to a given path.""" + + self._create_path(path) + + response = requests.get(self.link, stream=True) + + path_filename = self._get_path_filename(path) + with open(path_filename, 'wb') as output_file: + shutil.copyfileobj(response.raw, output_file) + + del response + + def _get_path_filename(self, path): + """Build the filename to download. + + Checks that filename is not already in path. Otherwise looks for + another name. + + >>> ir = ImageResult() + >>> ir._get_path_filename("test") + 'test\\\img3.jpg' + >>> ir.name = "pirulo" + >>> ir.format = "jpg" + >>> ir._get_path_filename("test") + 'test\\\pirulo.jpg' + """ + + path_filename = None + + # preserve the original name + if self.name and self.format: + original_filename = self.name + "." + self.format + path_filename = os.path.join(path, original_filename) + + # create a default name if there is no original name + if not path_filename or os.path.isfile(path_filename): + + # take the format of the file, or use default + if self.format: + file_format = self.format + else: + file_format = self.DEFAULT_FORMAT + + # create root of file, until reaching a non existent one + i = 1 + default_filename = self.ROOT_FILENAME + str(i) + "." + file_format + path_filename = os.path.join(path, default_filename) + while os.path.isfile(path_filename): + i += 1 + default_filename = self.ROOT_FILENAME + str(i) + "." + \ + file_format + path_filename = os.path.join(path, default_filename) + + return path_filename + + def _create_path(self, path): + """Create a path, if it doesn't exists.""" + + if not os.path.isdir(path): + os.mkdir(path) + class ImageOptions: @@ -578,4 +656,6 @@ def main(): test() if __name__ == "__main__": - main() + # main() + import doctest + doctest.testmod() From 0ecdf43f7af8fe125834bf754ee694611eb2ebe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Wed, 18 Feb 2015 13:31:38 -0500 Subject: [PATCH 005/106] Extend download_images capabilties and change setup.py to setuptools. --- google_search_api/google.py | 76 +++++++++++++++++++++++++++++++------ setup.py | 2 +- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/google_search_api/google.py b/google_search_api/google.py index 8388457..9df7d70 100644 --- a/google_search_api/google.py +++ b/google_search_api/google.py @@ -68,15 +68,24 @@ def __init__(self): """ -def download_images(image_results): +def download_images(image_results, path=None): """Download a list of images. Args: images_list: A list of ImageResult instances """ + total_images = len(image_results) + i = 1 for image_result in image_results: - image_result.download() + progress = "".join(["Downloading image ", str(i), + " (", str(total_images), ")"]) + print progress + if path: + image_result.download(path) + else: + image_result.download() + i += 1 class ImageResult: @@ -110,13 +119,18 @@ def download(self, path="download"): self._create_path(path) - response = requests.get(self.link, stream=True) + try: + response = requests.get(self.link, stream=True) - path_filename = self._get_path_filename(path) - with open(path_filename, 'wb') as output_file: - shutil.copyfileobj(response.raw, output_file) + path_filename = self._get_path_filename(path) + with open(path_filename, 'wb') as output_file: + shutil.copyfileobj(response.raw, output_file) - del response + del response + + except Exception as inst: + print self.link, "has failed:" + print inst def _get_path_filename(self, path): """Build the filename to download. @@ -210,6 +224,10 @@ def get_tbs(self): class Google: DEBUG_MODE = False + IMAGE_FORMATS = ["bmp", "gif", "jpg", "png", "psd", "pspimage", "thm", + "tif", "yuv", "ai", "drw", "eps", "ps", "svg", "tiff", + "jpeg", "jif", "jfif", "jp2", "jpx", "j2k", "j2c", "fpx", + "pcd", "png", "pdf"] """ Returns a list of GoogleResult @@ -310,6 +328,17 @@ def search_images_old(query, image_options=None, pages=1): @staticmethod def search_images(query, image_options=None, pages=1): + """Search images in google. + + # >>> results = Google.search_images("banana") + # 'style' index= 97 + # 'style' index= 98 + # 'style' index= 99 + # >>> len(results) + # 100 + # >>> isinstance(results[0], ImageResult) + # True + """ results = [] @@ -319,9 +348,6 @@ def search_images(query, image_options=None, pages=1): html = get_html_from_dynamic_site(url) if html: - if Google.DEBUG_MODE: - write_html_to_file( - html, "images_{0}_{1}.html".format(query.replace(" ", "_"), i)) # parse html into bs soup = BeautifulSoup(html) @@ -346,7 +372,7 @@ def search_images(query, image_options=None, pages=1): url_parsed = urlparse.urlparse(google_middle_link) qry_parsed = urlparse.parse_qs(url_parsed.query) res.link = qry_parsed["imgurl"][0] - res.format = res.link[res.link.rfind(".") + 1:] + res.format = Google._parse_image_format(res.link) res.width = qry_parsed["w"][0] res.height = qry_parsed["h"][0] res.site = qry_parsed["imgrefurl"][0] @@ -378,6 +404,34 @@ def search_images(query, image_options=None, pages=1): return results + @staticmethod + def _parse_image_format(image_link): + """Parse an image format from a download link. + + Args: + image_link: link to download an image. + + >>> link = "http://blogs.elpais.com/.a/6a00d8341bfb1653ef01a73dbb4a78970d-pi" + >>> Google._parse_image_format(link) + + >>> link = "http://minionslovebananas.com/images/gallery/preview/Chiquita-DM2-minion-banana-3.jpg%3Fw%3D300%26h%3D429" + >>> Google._parse_image_format(link) + 'jpg' + + """ + parsed_format = image_link[image_link.rfind(".") + 1:] + + if parsed_format not in Google.IMAGE_FORMATS: + for image_format in Google.IMAGE_FORMATS: + if image_format in parsed_format: + parsed_format = image_format + break + + if parsed_format not in Google.IMAGE_FORMATS: + parsed_format = None + + return parsed_format + @staticmethod def shopping(query, pages=1): results = [] diff --git a/setup.py b/setup.py index 75fa4e6..87ed6c2 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from distutils.core import setup +from setuptools import setup setup(name='google_search_api', version='0.1', From 6404851157df54829444ca710e031db977cae09a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Fri, 20 Feb 2015 21:49:10 -0500 Subject: [PATCH 006/106] Package was refactored to deploy in PIP. --- .travis.yml | 20 +++++++++++++ README.md | 4 +-- google/__init__.py | 1 + {google_search_api => google}/google.py | 36 ++++++++++++++++-------- google/tests/test_google.py | 37 +++++++++++++++++++++++++ {google_search_api => google}/utils.py | 0 google_search_api/__init__.py | 1 - requirements.txt | 2 +- setup.cfg | 8 ++++++ setup.py | 18 +++++++++--- 10 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 .travis.yml create mode 100644 google/__init__.py rename {google_search_api => google}/google.py (95%) create mode 100644 google/tests/test_google.py rename {google_search_api => google}/utils.py (100%) delete mode 100644 google_search_api/__init__.py create mode 100644 setup.cfg diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..42409b9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: python +sudo: false +python: + - '2.6' + - '2.7' + - '3.3' + - '3.4' + - pypy +deploy: + provider: pypi + user: beni + password: + secure: ib4iaqycgcF3ijSC... +install: + - pip install -r requirements.txt + - python setup.py install + - pip install coveralls +script: + - nosetests +after_success: coveralls \ No newline at end of file diff --git a/README.md b/README.md index 9bfa0c1..7a85e4f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ Google Search API ===== -*The original package was developed by https://github.com/BirdAPI This is a forked package that I have re-factored and I mean to maintain in the foreseeable future* +*The original package was developed by Anthony Casagrande and can be downloaded at https://github.com/BirdAPI This is a forked package that I will continue maintaining in the foreseeable future* Google Search API is a python based library for searching various functionalities of google. It uses screen scraping to retrieve the results, and thus is unreliable if the way google's web pages are returned change in the future. -*Disclaimer: This software uses screen scraping to retreive search results from google.com, and therefore this software may stop working at any given time. Use this software at your own risk. I assume no responsibility for how this software API is used by others.* +*Disclaimer: This software uses screen scraping to retrieve search results from google.com, and therefore this software may stop working at any given time. Use this software at your own risk. I assume no responsibility for how this software API is used by others.* Installation ------------ diff --git a/google/__init__.py b/google/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/google/__init__.py @@ -0,0 +1 @@ + diff --git a/google_search_api/google.py b/google/google.py similarity index 95% rename from google_search_api/google.py rename to google/google.py index 9df7d70..cd63bfc 100644 --- a/google_search_api/google.py +++ b/google/google.py @@ -17,8 +17,8 @@ except ImportError: import simplejson as json -__author__ = "Anthony Casagrande " -__version__ = "0.9" +__author__ = "Anthony Casagrande , Agustin Benassi " +__version__ = "1.0.0" """ Represents a standard google search result @@ -36,6 +36,11 @@ def __init__(self): self.page = None self.index = None + def __repr__(self): + list_google = ["Name: ", self.name, + "\nLink: ", self.link] + return "".join(list_google) + """ Represents a result returned from google calculator """ @@ -327,7 +332,7 @@ def search_images_old(query, image_options=None, pages=1): return results @staticmethod - def search_images(query, image_options=None, pages=1): + def search_images(query, image_options=None, images=50): """Search images in google. # >>> results = Google.search_images("banana") @@ -340,11 +345,13 @@ def search_images(query, image_options=None, pages=1): # True """ - results = [] - - for i in range(pages): + results = set() + curr_img = 0 + page = 0 + while curr_img < images: - url = get_image_search_url(query, image_options, i + 1) + page += 1 + url = get_image_search_url(query, image_options, page) html = get_html_from_dynamic_site(url) if html: @@ -362,7 +369,7 @@ def search_images(query, image_options=None, pages=1): res = ImageResult() # store indexing paramethers - res.page = i + res.page = page res.index = j # get url of image and its paramethers @@ -398,11 +405,18 @@ def search_images(query, image_options=None, pages=1): exc_type, exc_value, exc_traceback = sys.exc_info() print exc_type, exc_value, "index=", res.index - results.append(res) + prev_num_results = len(results) + results.add(res) + curr_num_results = len(results) - j = j + 1 + # increment image counter only if new image was added + images_added = curr_num_results - prev_num_results + curr_img += images_added + if curr_img >= images: + break - return results + j = j + 1 + return set(results) @staticmethod def _parse_image_format(image_link): diff --git a/google/tests/test_google.py b/google/tests/test_google.py new file mode 100644 index 0000000..bf34c9c --- /dev/null +++ b/google/tests/test_google.py @@ -0,0 +1,37 @@ +import unittest +import nose +from google import google + + +class GoogleTest(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_search_images(self): + """Test method to search images.""" + + res = google.Google.search_images("apple", images=10) + self.assertEqual(len(res), 10) + + def test_convert_currency(self): + pass + + def test_exchange_rate(self): + pass + + def test_calculate(self): + pass + + def test_search(self): + pass + + def test_shopping(self): + pass + + +if __name__ == '__main__': + nose.main() diff --git a/google_search_api/utils.py b/google/utils.py similarity index 100% rename from google_search_api/utils.py rename to google/utils.py diff --git a/google_search_api/__init__.py b/google_search_api/__init__.py deleted file mode 100644 index 10a986e..0000000 --- a/google_search_api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from google import * diff --git a/requirements.txt b/requirements.txt index 28da76d..93bd5cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ BeautifulSoup>=3.2.1, <4.0.0 -selenium>=2.44.0,<3.0.0 \ No newline at end of file +selenium>=2.44.0,<3.0.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..07c9070 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,8 @@ +[nosetests] +verbosity=1 +detailed-errors=1 +with-coverage=1 +cover-package=google +debug=nose.loader +pdb=1 +pdb-failures=1 \ No newline at end of file diff --git a/setup.py b/setup.py index 87ed6c2..9658f59 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,16 @@ from setuptools import setup -setup(name='google_search_api', - version='0.1', - packages=['google_search_api'], - ) \ No newline at end of file +setup(name='Google-Search-API', + version='1.0.0', + url='https://github.com/abenassi/Google-Search-API', + description='Search in google', + author='Anthony Casagrande, Agustin Benassi', + author_email='birdapi@gmail.com, agusbenassi@gmail.com', + license='MIT', + + packages=['google'], + + setup_requires=['nose>=1.0'], + test_suite='nose.collector', + tests_require=['nose', 'nose-cov'] + ) From e793fc2693a094d8b87e521bc57f572205eda24b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Mon, 23 Feb 2015 12:13:10 -0500 Subject: [PATCH 007/106] Completing comments. --- .travis.yml | 2 ++ google/google.py | 63 ++++++++++++++++++++++++------------------------ 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/.travis.yml b/.travis.yml index 42409b9..b7d6257 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,8 @@ deploy: user: beni password: secure: ib4iaqycgcF3ijSC... + on: + tags: true install: - pip install -r requirements.txt - python setup.py install diff --git a/google/google.py b/google/google.py index cd63bfc..7fd2538 100644 --- a/google/google.py +++ b/google/google.py @@ -20,13 +20,34 @@ __author__ = "Anthony Casagrande , Agustin Benassi " __version__ = "1.0.0" -""" -Represents a standard google search result -""" +# GLOBAL PUBLIC METHODS +def download_images(image_results, path=None): + """Download a list of images. + + Args: + images_list: a list of ImageResult instances + path: path to store downloaded images. + """ + total_images = len(image_results) + i = 1 + for image_result in image_results: + progress = "".join(["Downloading image ", str(i), + " (", str(total_images), ")"]) + print progress + if path: + image_result.download(path) + else: + image_result.download() + i += 1 + + +# RESULT CLASSES class GoogleResult: + """Represents a google search result.""" + def __init__(self): self.name = None self.link = None @@ -41,13 +62,11 @@ def __repr__(self): "\nLink: ", self.link] return "".join(list_google) -""" -Represents a result returned from google calculator -""" - class CalculatorResult: + """Represents a result returned from google calculator.""" + def __init__(self): self.value = None self.unit = None @@ -58,6 +77,8 @@ def __init__(self): class ShoppingResult: + """Represents a shopping result.""" + def __init__(self): self.name = None self.link = None @@ -68,33 +89,11 @@ def __init__(self): self.store_count = None self.min_price = None -""" -Represents a google image search result -""" - - -def download_images(image_results, path=None): - """Download a list of images. - - Args: - images_list: A list of ImageResult instances - """ - - total_images = len(image_results) - i = 1 - for image_result in image_results: - progress = "".join(["Downloading image ", str(i), - " (", str(total_images), ")"]) - print progress - if path: - image_result.download(path) - else: - image_result.download() - i += 1 - class ImageResult: + """Represents a google image search result.""" + ROOT_FILENAME = "img" DEFAULT_FORMAT = "jpg" @@ -609,7 +608,7 @@ def get_image_search_url(query, image_options=None, page=0, per_page=20): "+", "%2B").replace("&", "%26").replace(" ", "+") # url = "http://images.google.com/images?q=%s&sa=N&start=%i&ndsp=%i&sout=1" % ( - # query, page * per_page, per_page) + # query, page * per_page, per_page) # TRYING NEW URL url = "https://www.google.com.ar/search?q={}".format(query) + \ "&es_sm=122&source=lnms" + \ From 09960246085e3b21de6fc5c7d06c2ac5a7aa2bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Mon, 23 Feb 2015 12:17:19 -0500 Subject: [PATCH 008/106] Skip UI browser test. --- google/tests/test_google.py | 1 + 1 file changed, 1 insertion(+) diff --git a/google/tests/test_google.py b/google/tests/test_google.py index bf34c9c..5d6ccd1 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -11,6 +11,7 @@ def setUp(self): def tearDown(self): pass + @unittest.skip("skip") def test_search_images(self): """Test method to search images.""" From b8a96a34c481545b55c6acdcd89e9c3b137308b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Mon, 23 Feb 2015 16:17:32 -0500 Subject: [PATCH 009/106] Starting to refactor main module. --- .gitignore | 3 + .travis.yml | 2 - README.md | 27 +-- TODO.md | 15 ++ google/google.py | 338 +----------------------------------- google/images.py | 329 +++++++++++++++++++++++++++++++++++ google/tests/test_google.py | 2 +- google_search_api/utils.pyc | Bin 1478 -> 0 bytes 8 files changed, 371 insertions(+), 345 deletions(-) create mode 100644 .gitignore create mode 100644 TODO.md create mode 100644 google/images.py delete mode 100644 google_search_api/utils.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0670f65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +*.egg +*.egg-info \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index b7d6257..29202ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ sudo: false python: - '2.6' - '2.7' - - '3.3' - - '3.4' - pypy deploy: provider: pypi diff --git a/README.md b/README.md index 7a85e4f..8135d81 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ GoogleResult: self.page # What page this result was on (When searching more than one page) self.index # What index on this page it was on ``` - + ## Google Calculator Attempts to search google calculator for the result of an expression. Returns a `CalculatorResult` if successful or `None` if it fails. @@ -68,7 +68,7 @@ Google.calculate("157.3kg in grams") 'value': u'157300'} ``` - + ```python Google.calculate("cos(25 pi) / 17.4") ``` @@ -80,20 +80,21 @@ Google.calculate("cos(25 pi) / 17.4") 'unit': None, 'value': u'-0.0574712644'} ``` - + ## Google Image Search Searches google images for a list of images. Image searches can be filtered to produce better results. Perform a google image search on "banana" and filter it: ```python -options = ImageOptions() -options.image_type = ImageType.CLIPART -options.larger_than = LargerThan.MP_4 +from google import google, images +options = images.ImageOptions() +options.image_type = images.ImageType.CLIPART +options.larger_than = images.LargerThan.MP_4 options.color = "green" -results = Google.search_images("banana", options) +results = google.Google.search_images("banana", options) ``` - + Sample Result: ```python @@ -108,10 +109,10 @@ Sample Result: 'thumb': u'http://t3.gstatic.com/images?q=tbn:ANd9GcRzvAUW0en9eZTag3giWelcQ_xbrnBMXVChb3RU3v4HtEgxN3RMS0bSdidf', 'width': u'3104'} ``` - + Filter options: -```python +```python ImageOptions: image_type # face, body, clipart, line drawing size_category # large, small, icon @@ -121,7 +122,7 @@ ImageOptions: color_type # color, b&w, specific color # blue, green, red ``` - + Enums of values that can be used to filter image searches: ```python @@ -131,7 +132,7 @@ class ImageType: PHOTO = "photo" CLIPART = "clipart" LINE_DRAWING = "lineart" - + class SizeCategory: NONE = None ICON = "i" @@ -140,7 +141,7 @@ class SizeCategory: SMALL = "s" LARGER_THAN = "lt" EXACTLY = "ex" - + class LargerThan: NONE = None QSVGA = "qsvga" # 400 x 300 diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..6f48a6d --- /dev/null +++ b/TODO.md @@ -0,0 +1,15 @@ +Todo list for Google-Search-API +==== + +## Next tasks + +1. Mock html test for search images +2. Refactor and split main modules in separated ones +3. Write tests for future methods development + +## Stuff to check out + +* vcrpy to record web requests and make mocks +* SauceLabs to do UI tests +* Sphinx para generar la documentación - estilo readthedocs +* Cookiecutter-pythonpackage \ No newline at end of file diff --git a/google/google.py b/google/google.py index 7fd2538..6d6117b 100644 --- a/google/google.py +++ b/google/google.py @@ -1,48 +1,20 @@ from bs4 import BeautifulSoup -from pprint import pprint -import os -import threading import httplib -import urllib import urllib2 import sys import re -from utils import get_html_from_dynamic_site -import urlparse -import shutil -import requests +import images try: import json except ImportError: import simplejson as json -__author__ = "Anthony Casagrande , Agustin Benassi " +__author__ = "Anthony Casagrande , " + \ + "Agustin Benassi " __version__ = "1.0.0" -# GLOBAL PUBLIC METHODS -def download_images(image_results, path=None): - """Download a list of images. - - Args: - images_list: a list of ImageResult instances - path: path to store downloaded images. - """ - - total_images = len(image_results) - i = 1 - for image_result in image_results: - progress = "".join(["Downloading image ", str(i), - " (", str(total_images), ")"]) - print progress - if path: - image_result.download(path) - else: - image_result.download() - i += 1 - - # RESULT CLASSES class GoogleResult: @@ -90,143 +62,10 @@ def __init__(self): self.min_price = None -class ImageResult: - - """Represents a google image search result.""" - - ROOT_FILENAME = "img" - DEFAULT_FORMAT = "jpg" - - def __init__(self): - self.name = None - self.link = None - self.thumb = None - self.thumb_width = None - self.thumb_height = None - self.width = None - self.height = None - self.filesize = None - self.format = None - self.domain = None - self.page = None - self.index = None - self.site = None - - def __repr__(self): - string = "ImageResult(" + \ - "index={}, page={}, ".format(self.index, self.page) + \ - "domain={}, link={})".format(self.domain, self.link) - return string - - def download(self, path="download"): - """Download an image to a given path.""" - - self._create_path(path) - - try: - response = requests.get(self.link, stream=True) - - path_filename = self._get_path_filename(path) - with open(path_filename, 'wb') as output_file: - shutil.copyfileobj(response.raw, output_file) - - del response - - except Exception as inst: - print self.link, "has failed:" - print inst - - def _get_path_filename(self, path): - """Build the filename to download. - - Checks that filename is not already in path. Otherwise looks for - another name. - - >>> ir = ImageResult() - >>> ir._get_path_filename("test") - 'test\\\img3.jpg' - >>> ir.name = "pirulo" - >>> ir.format = "jpg" - >>> ir._get_path_filename("test") - 'test\\\pirulo.jpg' - """ - - path_filename = None - - # preserve the original name - if self.name and self.format: - original_filename = self.name + "." + self.format - path_filename = os.path.join(path, original_filename) - - # create a default name if there is no original name - if not path_filename or os.path.isfile(path_filename): - - # take the format of the file, or use default - if self.format: - file_format = self.format - else: - file_format = self.DEFAULT_FORMAT - - # create root of file, until reaching a non existent one - i = 1 - default_filename = self.ROOT_FILENAME + str(i) + "." + file_format - path_filename = os.path.join(path, default_filename) - while os.path.isfile(path_filename): - i += 1 - default_filename = self.ROOT_FILENAME + str(i) + "." + \ - file_format - path_filename = os.path.join(path, default_filename) - - return path_filename - - def _create_path(self, path): - """Create a path, if it doesn't exists.""" - - if not os.path.isdir(path): - os.mkdir(path) - - -class ImageOptions: - - def __init__(self): - self.image_type = None - self.size_category = None - self.larger_than = None - self.exact_width = None - self.exact_height = None - self.color_type = None - self.color = None - - def get_tbs(self): - tbs = None - if self.image_type: - # clipart - tbs = add_to_tbs(tbs, "itp", self.image_type) - if self.size_category and not (self.larger_than or (self.exact_width and self.exact_height)): - # i = icon, l = large, m = medium, lt = larger than, ex = exact - tbs = add_to_tbs(tbs, "isz", self.size_category) - if self.larger_than: - # qsvga,4mp - tbs = add_to_tbs(tbs, "isz", SizeCategory.LARGER_THAN) - tbs = add_to_tbs(tbs, "islt", self.larger_than) - if self.exact_width and self.exact_height: - tbs = add_to_tbs(tbs, "isz", SizeCategory.EXACTLY) - tbs = add_to_tbs(tbs, "iszw", self.exact_width) - tbs = add_to_tbs(tbs, "iszh", self.exact_height) - if self.color_type and not self.color: - # color = color, gray = black and white, specific = user defined - tbs = add_to_tbs(tbs, "ic", self.color_type) - if self.color: - tbs = add_to_tbs(tbs, "ic", ColorType.SPECIFIC) - tbs = add_to_tbs(tbs, "isc", self.color) - return tbs - -""" -Defines the public static api methods -""" +class Google: + """Defines the public static api methods.""" -class Google: DEBUG_MODE = False IMAGE_FORMATS = ["bmp", "gif", "jpg", "png", "psd", "pspimage", "thm", "tif", "yuv", "ai", "drw", "eps", "ps", "svg", "tiff", @@ -286,49 +125,9 @@ def calculate_old(expr): @staticmethod def search_images_old(query, image_options=None, pages=1): - results = [] - for i in range(pages): - url = get_image_search_url(query, image_options, i) - html = get_html(url) - if html: - if Google.DEBUG_MODE: - write_html_to_file( - html, "images_{0}_{1}.html".format(query.replace(" ", "_"), i)) - j = 0 - soup = BeautifulSoup(html) - match = re.search("dyn.setResults\((.+)\);", html) - if match: - init = unicode(match.group(1), errors="ignore") - tokens = init.split('],[') - for token in tokens: - res = ImageResult() - res.page = i - res.index = j - toks = token.split(",") - - # should be 32 or 33, but seems to change, so just make sure no exceptions - # will be thrown by the indexing - if (len(toks) > 22): - for t in range(len(toks)): - toks[t] = toks[t].replace('\\x3cb\\x3e', '').replace( - '\\x3c/b\\x3e', '').replace('\\x3d', '=').replace('\\x26', '&') - match = re.search( - "imgurl=(?P[^&]+)&imgrefurl", toks[0]) - if match: - res.link = match.group("link") - res.name = toks[6].replace('"', '') - res.thumb = toks[21].replace('"', '') - res.format = toks[10].replace('"', '') - res.domain = toks[11].replace('"', '') - match = re.search( - "(?P[0-9]+) × (?P[0-9]+) - (?P[^ ]+)", toks[9].replace('"', '')) - if match: - res.width = match.group("width") - res.height = match.group("height") - res.filesize = match.group("size") - results.append(res) - j = j + 1 - return results + """Old method to search images in google.""" + + images.search_old(query, image_options, pages) @staticmethod def search_images(query, image_options=None, images=50): @@ -344,106 +143,7 @@ def search_images(query, image_options=None, images=50): # True """ - results = set() - curr_img = 0 - page = 0 - while curr_img < images: - - page += 1 - url = get_image_search_url(query, image_options, page) - html = get_html_from_dynamic_site(url) - - if html: - - # parse html into bs - soup = BeautifulSoup(html) - - # find all divs containing an image - div_container = soup.find("div", {"id": "rg_s"}) - divs = div_container.find_all("div", {"class": "rg_di"}) - j = 0 - for div in divs: - - # try: - res = ImageResult() - - # store indexing paramethers - res.page = page - res.index = j - - # get url of image and its paramethers - a = div.find("a") - if a: - google_middle_link = a["href"] - url_parsed = urlparse.urlparse(google_middle_link) - qry_parsed = urlparse.parse_qs(url_parsed.query) - res.link = qry_parsed["imgurl"][0] - res.format = Google._parse_image_format(res.link) - res.width = qry_parsed["w"][0] - res.height = qry_parsed["h"][0] - res.site = qry_parsed["imgrefurl"][0] - res.domain = urlparse.urlparse(res.site).netloc - - # get url of thumb and its size paramethers - img = a.find_all("img") - if img: - - # get url trying "src" and "data-src" keys - try: - res.thumb = img[0]["src"] - except: - res.thumb = img[0]["data-src"] - - try: - img_style = img[0]["style"].split(";") - img_style_dict = {i.split(":")[0]: i.split(":")[1] - for i in img_style} - res.thumb_width = img_style_dict["width"] - res.thumb_height = img_style_dict["height"] - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - print exc_type, exc_value, "index=", res.index - - prev_num_results = len(results) - results.add(res) - curr_num_results = len(results) - - # increment image counter only if new image was added - images_added = curr_num_results - prev_num_results - curr_img += images_added - if curr_img >= images: - break - - j = j + 1 - return set(results) - - @staticmethod - def _parse_image_format(image_link): - """Parse an image format from a download link. - - Args: - image_link: link to download an image. - - >>> link = "http://blogs.elpais.com/.a/6a00d8341bfb1653ef01a73dbb4a78970d-pi" - >>> Google._parse_image_format(link) - - >>> link = "http://minionslovebananas.com/images/gallery/preview/Chiquita-DM2-minion-banana-3.jpg%3Fw%3D300%26h%3D429" - >>> Google._parse_image_format(link) - 'jpg' - - """ - parsed_format = image_link[image_link.rfind(".") + 1:] - - if parsed_format not in Google.IMAGE_FORMATS: - for image_format in Google.IMAGE_FORMATS: - if image_format in parsed_format: - parsed_format = image_format - break - - if parsed_format not in Google.IMAGE_FORMATS: - parsed_format = None - - return parsed_format + images.search(query, image_options, images) @staticmethod def shopping(query, pages=1): @@ -603,26 +303,6 @@ class ColorType: SPECIFIC = "specific" -def get_image_search_url(query, image_options=None, page=0, per_page=20): - query = query.strip().replace(":", "%3A").replace( - "+", "%2B").replace("&", "%26").replace(" ", "+") - - # url = "http://images.google.com/images?q=%s&sa=N&start=%i&ndsp=%i&sout=1" % ( - # query, page * per_page, per_page) - # TRYING NEW URL - url = "https://www.google.com.ar/search?q={}".format(query) + \ - "&es_sm=122&source=lnms" + \ - "&tbm=isch&sa=X&ei=DDdUVL-fE4SpNq-ngPgK&ved=0CAgQ_AUoAQ" + \ - "&biw=1024&bih=719&dpr=1.25" - - if image_options: - tbs = image_options.get_tbs() - if tbs: - url = url + tbs - - return url - - def add_to_tbs(tbs, name, value): if tbs: return "%s,%s:%s" % (tbs, name, value) diff --git a/google/images.py b/google/images.py new file mode 100644 index 0000000..001d53d --- /dev/null +++ b/google/images.py @@ -0,0 +1,329 @@ +from utils import get_html_from_dynamic_site + +class ImageOptions: + + """Allows passing options to filter a google images search.""" + + def __init__(self): + self.image_type = None + self.size_category = None + self.larger_than = None + self.exact_width = None + self.exact_height = None + self.color_type = None + self.color = None + + def get_tbs(self): + tbs = None + if self.image_type: + # clipart + tbs = add_to_tbs(tbs, "itp", self.image_type) + if self.size_category and not (self.larger_than or (self.exact_width and self.exact_height)): + # i = icon, l = large, m = medium, lt = larger than, ex = exact + tbs = add_to_tbs(tbs, "isz", self.size_category) + if self.larger_than: + # qsvga,4mp + tbs = add_to_tbs(tbs, "isz", SizeCategory.LARGER_THAN) + tbs = add_to_tbs(tbs, "islt", self.larger_than) + if self.exact_width and self.exact_height: + tbs = add_to_tbs(tbs, "isz", SizeCategory.EXACTLY) + tbs = add_to_tbs(tbs, "iszw", self.exact_width) + tbs = add_to_tbs(tbs, "iszh", self.exact_height) + if self.color_type and not self.color: + # color = color, gray = black and white, specific = user defined + tbs = add_to_tbs(tbs, "ic", self.color_type) + if self.color: + tbs = add_to_tbs(tbs, "ic", ColorType.SPECIFIC) + tbs = add_to_tbs(tbs, "isc", self.color) + return tbs + + +class ImageResult: + + """Represents a google image search result.""" + + ROOT_FILENAME = "img" + DEFAULT_FORMAT = "jpg" + + def __init__(self): + self.name = None + self.link = None + self.thumb = None + self.thumb_width = None + self.thumb_height = None + self.width = None + self.height = None + self.filesize = None + self.format = None + self.domain = None + self.page = None + self.index = None + self.site = None + + def __repr__(self): + string = "ImageResult(" + \ + "index={}, page={}, ".format(self.index, self.page) + \ + "domain={}, link={})".format(self.domain, self.link) + return string + + def download(self, path="download"): + """Download an image to a given path.""" + + self._create_path(path) + + try: + response = requests.get(self.link, stream=True) + + path_filename = self._get_path_filename(path) + with open(path_filename, 'wb') as output_file: + shutil.copyfileobj(response.raw, output_file) + + del response + + except Exception as inst: + print self.link, "has failed:" + print inst + + def _get_path_filename(self, path): + """Build the filename to download. + + Checks that filename is not already in path. Otherwise looks for + another name. + + >>> ir = ImageResult() + >>> ir._get_path_filename("test") + 'test\\\img3.jpg' + >>> ir.name = "pirulo" + >>> ir.format = "jpg" + >>> ir._get_path_filename("test") + 'test\\\pirulo.jpg' + """ + + path_filename = None + + # preserve the original name + if self.name and self.format: + original_filename = self.name + "." + self.format + path_filename = os.path.join(path, original_filename) + + # create a default name if there is no original name + if not path_filename or os.path.isfile(path_filename): + + # take the format of the file, or use default + if self.format: + file_format = self.format + else: + file_format = self.DEFAULT_FORMAT + + # create root of file, until reaching a non existent one + i = 1 + default_filename = self.ROOT_FILENAME + str(i) + "." + file_format + path_filename = os.path.join(path, default_filename) + while os.path.isfile(path_filename): + i += 1 + default_filename = self.ROOT_FILENAME + str(i) + "." + \ + file_format + path_filename = os.path.join(path, default_filename) + + return path_filename + + def _create_path(self, path): + """Create a path, if it doesn't exists.""" + + if not os.path.isdir(path): + os.mkdir(path) + + +# PRIVATE METHODS +def _parse_image_format(image_link): + """Parse an image format from a download link. + + Args: + image_link: link to download an image. + + >>> link = "http://blogs.elpais.com/.a/6a00d8341bfb1653ef01a73dbb4a78970d-pi" + >>> Google._parse_image_format(link) + + >>> link = "http://minionslovebananas.com/images/gallery/preview/Chiquita-DM2-minion-banana-3.jpg%3Fw%3D300%26h%3D429" + >>> Google._parse_image_format(link) + 'jpg' + + """ + parsed_format = image_link[image_link.rfind(".") + 1:] + + if parsed_format not in Google.IMAGE_FORMATS: + for image_format in Google.IMAGE_FORMATS: + if image_format in parsed_format: + parsed_format = image_format + break + + if parsed_format not in Google.IMAGE_FORMATS: + parsed_format = None + + return parsed_format + + +def _get_image_search_url(query, image_options=None, page=0, per_page=20): + query = query.strip().replace(":", "%3A").replace( + "+", "%2B").replace("&", "%26").replace(" ", "+") + + # url = "http://images.google.com/images?q=%s&sa=N&start=%i&ndsp=%i&sout=1" % ( + # query, page * per_page, per_page) + # TRYING NEW URL + url = "https://www.google.com.ar/search?q={}".format(query) + \ + "&es_sm=122&source=lnms" + \ + "&tbm=isch&sa=X&ei=DDdUVL-fE4SpNq-ngPgK&ved=0CAgQ_AUoAQ" + \ + "&biw=1024&bih=719&dpr=1.25" + + if image_options: + tbs = image_options.get_tbs() + if tbs: + url = url + tbs + + return url + + +# PUBLIC METHODS +def search_old(query, image_options=None, pages=1): + results = [] + for i in range(pages): + url = get_image_search_url(query, image_options, i) + html = get_html(url) + if html: + if Google.DEBUG_MODE: + write_html_to_file( + html, "images_{0}_{1}.html".format(query.replace(" ", "_"), i)) + j = 0 + soup = BeautifulSoup(html) + match = re.search("dyn.setResults\((.+)\);", html) + if match: + init = unicode(match.group(1), errors="ignore") + tokens = init.split('],[') + for token in tokens: + res = ImageResult() + res.page = i + res.index = j + toks = token.split(",") + + # should be 32 or 33, but seems to change, so just make sure no exceptions + # will be thrown by the indexing + if (len(toks) > 22): + for t in range(len(toks)): + toks[t] = toks[t].replace('\\x3cb\\x3e', '').replace( + '\\x3c/b\\x3e', '').replace('\\x3d', '=').replace('\\x26', '&') + match = re.search( + "imgurl=(?P[^&]+)&imgrefurl", toks[0]) + if match: + res.link = match.group("link") + res.name = toks[6].replace('"', '') + res.thumb = toks[21].replace('"', '') + res.format = toks[10].replace('"', '') + res.domain = toks[11].replace('"', '') + match = re.search( + "(?P[0-9]+) × (?P[0-9]+) - (?P[^ ]+)", toks[9].replace('"', '')) + if match: + res.width = match.group("width") + res.height = match.group("height") + res.filesize = match.group("size") + results.append(res) + j = j + 1 + return results + + +def search(query, image_options=None, images=50): + """Main method to search images in google.""" + + results = set() + curr_img = 0 + page = 0 + while curr_img < images: + + page += 1 + url = _get_image_search_url(query, image_options, page) + html = get_html_from_dynamic_site(url) + + if html: + + # parse html into bs + soup = BeautifulSoup(html) + + # find all divs containing an image + div_container = soup.find("div", {"id": "rg_s"}) + divs = div_container.find_all("div", {"class": "rg_di"}) + j = 0 + for div in divs: + + # try: + res = ImageResult() + + # store indexing paramethers + res.page = page + res.index = j + + # get url of image and its paramethers + a = div.find("a") + if a: + google_middle_link = a["href"] + url_parsed = urlparse.urlparse(google_middle_link) + qry_parsed = urlparse.parse_qs(url_parsed.query) + res.link = qry_parsed["imgurl"][0] + res.format = Google._parse_image_format(res.link) + res.width = qry_parsed["w"][0] + res.height = qry_parsed["h"][0] + res.site = qry_parsed["imgrefurl"][0] + res.domain = urlparse.urlparse(res.site).netloc + + # get url of thumb and its size paramethers + img = a.find_all("img") + if img: + + # get url trying "src" and "data-src" keys + try: + res.thumb = img[0]["src"] + except: + res.thumb = img[0]["data-src"] + + try: + img_style = img[0]["style"].split(";") + img_style_dict = {i.split(":")[0]: i.split(":")[1] + for i in img_style} + res.thumb_width = img_style_dict["width"] + res.thumb_height = img_style_dict["height"] + except: + exc_type, exc_value, exc_traceback = sys.exc_info() + print exc_type, exc_value, "index=", res.index + + prev_num_results = len(results) + results.add(res) + curr_num_results = len(results) + + # increment image counter only if new image was added + images_added = curr_num_results - prev_num_results + curr_img += images_added + if curr_img >= images: + break + + j = j + 1 + + return set(results) + + +def download(image_results, path=None): + """Download a list of images. + + Args: + images_list: a list of ImageResult instances + path: path to store downloaded images. + """ + + total_images = len(image_results) + i = 1 + for image_result in image_results: + progress = "".join(["Downloading image ", str(i), + " (", str(total_images), ")"]) + print progress + if path: + image_result.download(path) + else: + image_result.download() + i += 1 diff --git a/google/tests/test_google.py b/google/tests/test_google.py index 5d6ccd1..b26cb15 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -11,7 +11,7 @@ def setUp(self): def tearDown(self): pass - @unittest.skip("skip") + # @unittest.skip("skip") def test_search_images(self): """Test method to search images.""" diff --git a/google_search_api/utils.pyc b/google_search_api/utils.pyc deleted file mode 100644 index 831a00ff57e6ade7c66ee0b985b4076ba01df246..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1478 zcmb_b-EJF26h5=Fwo?<6h!TWALc^VrXs#+QT7;kpxV@>;O`#|GfR#C!nQ!*|eCIpz&(ZLY!{49hwEM;Q|2dX>2227k*F2 z0$b1L8+~0g<_Fz$S0h|K(pl@xtSv8XyE1EjIe?j&M$hazm)LWn5!M-&`w3xAteuzo-lfi(d~xrKizibwy>zFmTmLq`M^fp|bIQ!6 z$Rbeu4#$ZP#gW*ps3%8q6pbP?1d?;w%nKKChUI>Pfi|`=t$*b%qmO9k`FFq2wnw*9 z@`9U+CPPE7ByIcLlcl5rM{YYh9n*F|n*q6;_=v6(3AC&Tu6`C&!@)lQ9wOMqibWvrnHuPhu2a+qNiiAj4Vb4^<+k=<|3k;544*f*$}vsK&XnUx9CmWzDGNee zH(5Q`$uZ6e`$^c3XA>0UY}@2IXy#>We3;`e*STJKW9v?ohx`=}bMhvC%2j3F>uTj) z&}Z@uXh)XG5yKpKmt#;+F#lOb@4+j}h#5?o!F0!8%KUu*G>jU_zPu}B^p+fnJF+i^ z;z$lfU-Tj=PQ*zx=IjCup$w->D7vk>xs$-I-Tj1%>GWM&diQVPc|jKd From 8cc4e374c9346bfffa3fff9f79824a3056f27d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Mon, 23 Feb 2015 16:38:49 -0500 Subject: [PATCH 010/106] Refactoring of search images is now passing test. --- TODO.md | 5 +- google/google.py | 11 +- google/images.py | 245 ++++++++++--------- google/tests/test_google.py | 2 +- google/tests/test_search_images.html | 337 +++++++++++++++++++++++++++ 5 files changed, 477 insertions(+), 123 deletions(-) create mode 100644 google/tests/test_search_images.html diff --git a/TODO.md b/TODO.md index 6f48a6d..b1065b6 100644 --- a/TODO.md +++ b/TODO.md @@ -6,10 +6,13 @@ Todo list for Google-Search-API 1. Mock html test for search images 2. Refactor and split main modules in separated ones 3. Write tests for future methods development +4. Be able to manage both Chrome and Firefox ## Stuff to check out * vcrpy to record web requests and make mocks * SauceLabs to do UI tests * Sphinx para generar la documentación - estilo readthedocs -* Cookiecutter-pythonpackage \ No newline at end of file +* Cookiecutter-pythonpackage +* PhantomJS to use browsers without opening (works with selenium) +* Understand UTF-8 \ No newline at end of file diff --git a/google/google.py b/google/google.py index 6d6117b..366e63f 100644 --- a/google/google.py +++ b/google/google.py @@ -67,10 +67,7 @@ class Google: """Defines the public static api methods.""" DEBUG_MODE = False - IMAGE_FORMATS = ["bmp", "gif", "jpg", "png", "psd", "pspimage", "thm", - "tif", "yuv", "ai", "drw", "eps", "ps", "svg", "tiff", - "jpeg", "jif", "jfif", "jp2", "jpx", "j2k", "j2c", "fpx", - "pcd", "png", "pdf"] + """ Returns a list of GoogleResult @@ -127,10 +124,10 @@ def calculate_old(expr): def search_images_old(query, image_options=None, pages=1): """Old method to search images in google.""" - images.search_old(query, image_options, pages) + return images.search_old(query, image_options, pages) @staticmethod - def search_images(query, image_options=None, images=50): + def search_images(query, image_options=None, num_images=50): """Search images in google. # >>> results = Google.search_images("banana") @@ -143,7 +140,7 @@ def search_images(query, image_options=None, images=50): # True """ - images.search(query, image_options, images) + return images.search(query, image_options, num_images) @staticmethod def shopping(query, pages=1): diff --git a/google/images.py b/google/images.py index 001d53d..6cef35c 100644 --- a/google/images.py +++ b/google/images.py @@ -1,4 +1,20 @@ from utils import get_html_from_dynamic_site +from bs4 import BeautifulSoup +import urlparse + + +def write_html_to_file(html, filename): + of = open(filename, "w") + of.write(html.encode("utf-8")) + # of.flush() + of.close() + + + +IMAGE_FORMATS = ["bmp", "gif", "jpg", "png", "psd", "pspimage", "thm", + "tif", "yuv", "ai", "drw", "eps", "ps", "svg", "tiff", + "jpeg", "jif", "jfif", "jp2", "jpx", "j2k", "j2c", "fpx", + "pcd", "png", "pdf"] class ImageOptions: @@ -151,13 +167,13 @@ def _parse_image_format(image_link): """ parsed_format = image_link[image_link.rfind(".") + 1:] - if parsed_format not in Google.IMAGE_FORMATS: - for image_format in Google.IMAGE_FORMATS: + if parsed_format not in IMAGE_FORMATS: + for image_format in IMAGE_FORMATS: if image_format in parsed_format: parsed_format = image_format break - if parsed_format not in Google.IMAGE_FORMATS: + if parsed_format not in IMAGE_FORMATS: parsed_format = None return parsed_format @@ -186,47 +202,47 @@ def _get_image_search_url(query, image_options=None, page=0, per_page=20): # PUBLIC METHODS def search_old(query, image_options=None, pages=1): results = [] - for i in range(pages): - url = get_image_search_url(query, image_options, i) - html = get_html(url) - if html: - if Google.DEBUG_MODE: - write_html_to_file( - html, "images_{0}_{1}.html".format(query.replace(" ", "_"), i)) - j = 0 - soup = BeautifulSoup(html) - match = re.search("dyn.setResults\((.+)\);", html) - if match: - init = unicode(match.group(1), errors="ignore") - tokens = init.split('],[') - for token in tokens: - res = ImageResult() - res.page = i - res.index = j - toks = token.split(",") - - # should be 32 or 33, but seems to change, so just make sure no exceptions - # will be thrown by the indexing - if (len(toks) > 22): - for t in range(len(toks)): - toks[t] = toks[t].replace('\\x3cb\\x3e', '').replace( - '\\x3c/b\\x3e', '').replace('\\x3d', '=').replace('\\x26', '&') - match = re.search( - "imgurl=(?P[^&]+)&imgrefurl", toks[0]) - if match: - res.link = match.group("link") - res.name = toks[6].replace('"', '') - res.thumb = toks[21].replace('"', '') - res.format = toks[10].replace('"', '') - res.domain = toks[11].replace('"', '') - match = re.search( - "(?P[0-9]+) × (?P[0-9]+) - (?P[^ ]+)", toks[9].replace('"', '')) - if match: - res.width = match.group("width") - res.height = match.group("height") - res.filesize = match.group("size") - results.append(res) - j = j + 1 + for i in range(pages): + url = get_image_search_url(query, image_options, i) + html = get_html(url) + if html: + if Google.DEBUG_MODE: + write_html_to_file( + html, "images_{0}_{1}.html".format(query.replace(" ", "_"), i)) + j = 0 + soup = BeautifulSoup(html) + match = re.search("dyn.setResults\((.+)\);", html) + if match: + init = unicode(match.group(1), errors="ignore") + tokens = init.split('],[') + for token in tokens: + res = ImageResult() + res.page = i + res.index = j + toks = token.split(",") + + # should be 32 or 33, but seems to change, so just make sure no exceptions + # will be thrown by the indexing + if (len(toks) > 22): + for t in range(len(toks)): + toks[t] = toks[t].replace('\\x3cb\\x3e', '').replace( + '\\x3c/b\\x3e', '').replace('\\x3d', '=').replace('\\x26', '&') + match = re.search( + "imgurl=(?P[^&]+)&imgrefurl", toks[0]) + if match: + res.link = match.group("link") + res.name = toks[6].replace('"', '') + res.thumb = toks[21].replace('"', '') + res.format = toks[10].replace('"', '') + res.domain = toks[11].replace('"', '') + match = re.search( + "(?P[0-9]+) × (?P[0-9]+) - (?P[^ ]+)", toks[9].replace('"', '')) + if match: + res.width = match.group("width") + res.height = match.group("height") + res.filesize = match.group("size") + results.append(res) + j = j + 1 return results @@ -234,76 +250,77 @@ def search(query, image_options=None, images=50): """Main method to search images in google.""" results = set() - curr_img = 0 - page = 0 - while curr_img < images: - - page += 1 - url = _get_image_search_url(query, image_options, page) - html = get_html_from_dynamic_site(url) - - if html: - - # parse html into bs - soup = BeautifulSoup(html) - - # find all divs containing an image - div_container = soup.find("div", {"id": "rg_s"}) - divs = div_container.find_all("div", {"class": "rg_di"}) - j = 0 - for div in divs: - - # try: - res = ImageResult() - - # store indexing paramethers - res.page = page - res.index = j - - # get url of image and its paramethers - a = div.find("a") - if a: - google_middle_link = a["href"] - url_parsed = urlparse.urlparse(google_middle_link) - qry_parsed = urlparse.parse_qs(url_parsed.query) - res.link = qry_parsed["imgurl"][0] - res.format = Google._parse_image_format(res.link) - res.width = qry_parsed["w"][0] - res.height = qry_parsed["h"][0] - res.site = qry_parsed["imgrefurl"][0] - res.domain = urlparse.urlparse(res.site).netloc - - # get url of thumb and its size paramethers - img = a.find_all("img") - if img: - - # get url trying "src" and "data-src" keys - try: - res.thumb = img[0]["src"] - except: - res.thumb = img[0]["data-src"] - - try: - img_style = img[0]["style"].split(";") - img_style_dict = {i.split(":")[0]: i.split(":")[1] - for i in img_style} - res.thumb_width = img_style_dict["width"] - res.thumb_height = img_style_dict["height"] - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - print exc_type, exc_value, "index=", res.index - - prev_num_results = len(results) - results.add(res) - curr_num_results = len(results) - - # increment image counter only if new image was added - images_added = curr_num_results - prev_num_results - curr_img += images_added - if curr_img >= images: - break - - j = j + 1 + curr_img = 0 + page = 0 + while curr_img < images: + + page += 1 + url = _get_image_search_url(query, image_options, page) + html = get_html_from_dynamic_site(url) + write_html_to_file(html, "test_search_images.html") + + if html: + + # parse html into bs + soup = BeautifulSoup(html) + + # find all divs containing an image + div_container = soup.find("div", {"id": "rg_s"}) + divs = div_container.find_all("div", {"class": "rg_di"}) + j = 0 + for div in divs: + + # try: + res = ImageResult() + + # store indexing paramethers + res.page = page + res.index = j + + # get url of image and its paramethers + a = div.find("a") + if a: + google_middle_link = a["href"] + url_parsed = urlparse.urlparse(google_middle_link) + qry_parsed = urlparse.parse_qs(url_parsed.query) + res.link = qry_parsed["imgurl"][0] + res.format = _parse_image_format(res.link) + res.width = qry_parsed["w"][0] + res.height = qry_parsed["h"][0] + res.site = qry_parsed["imgrefurl"][0] + res.domain = urlparse.urlparse(res.site).netloc + + # get url of thumb and its size paramethers + img = a.find_all("img") + if img: + + # get url trying "src" and "data-src" keys + try: + res.thumb = img[0]["src"] + except: + res.thumb = img[0]["data-src"] + + try: + img_style = img[0]["style"].split(";") + img_style_dict = {i.split(":")[0]: i.split(":")[1] + for i in img_style} + res.thumb_width = img_style_dict["width"] + res.thumb_height = img_style_dict["height"] + except: + exc_type, exc_value, exc_traceback = sys.exc_info() + print exc_type, exc_value, "index=", res.index + + prev_num_results = len(results) + results.add(res) + curr_num_results = len(results) + + # increment image counter only if new image was added + images_added = curr_num_results - prev_num_results + curr_img += images_added + if curr_img >= images: + break + + j = j + 1 return set(results) diff --git a/google/tests/test_google.py b/google/tests/test_google.py index b26cb15..11e3e11 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -15,7 +15,7 @@ def tearDown(self): def test_search_images(self): """Test method to search images.""" - res = google.Google.search_images("apple", images=10) + res = google.Google.search_images("apple", num_images=10) self.assertEqual(len(res), 10) def test_convert_currency(self): diff --git a/google/tests/test_search_images.html b/google/tests/test_search_images.html new file mode 100644 index 0000000..6c17b4a --- /dev/null +++ b/google/tests/test_search_images.html @@ -0,0 +1,337 @@ + +apple - Buscar con Google
    Usuario lector de pantalla, clic aquí para desact. Google Instant.
    Visitar páginaVer imagen
    Ver PDF

    Imágenes relacionadas:
    Ver más
    Las imágenes pueden estar sujetas a derechos de autor.Enviar comentarios
    \ No newline at end of file From 8789b18be1d980391169a690a9ab1297f6f79ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Mon, 23 Feb 2015 17:54:07 -0500 Subject: [PATCH 011/106] Replace test call to method that open a browser with a mock method that loads a test html file. --- google/images.py | 2 +- google/tests/test_google.py | 8 +++++--- google/utils.py | 2 +- requirements.txt | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/google/images.py b/google/images.py index 6cef35c..0b16645 100644 --- a/google/images.py +++ b/google/images.py @@ -257,7 +257,7 @@ def search(query, image_options=None, images=50): page += 1 url = _get_image_search_url(query, image_options, page) html = get_html_from_dynamic_site(url) - write_html_to_file(html, "test_search_images.html") + # write_html_to_file(html, "test_search_images.html") if html: diff --git a/google/tests/test_google.py b/google/tests/test_google.py index 11e3e11..bcbd6ba 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -1,15 +1,17 @@ import unittest import nose from google import google +from mock import Mock class GoogleTest(unittest.TestCase): def setUp(self): - pass - def tearDown(self): - pass + # replace method to get html with a test html file + f = open('test_search_images.html', 'r') + google.images.get_html_from_dynamic_site = \ + Mock(return_value=f.read().decode('utf8')) # @unittest.skip("skip") def test_search_images(self): diff --git a/google/utils.py b/google/utils.py index 329a984..cabebdc 100644 --- a/google/utils.py +++ b/google/utils.py @@ -39,7 +39,7 @@ def get_html_from_dynamic_site(url, timeout=120, driver="firefox", attempts=10): browser = get_browser_with_url(url, timeout, driver) # get html - time.sleep(10) + time.sleep(5) content = browser.page_source # try again if there is no content diff --git a/requirements.txt b/requirements.txt index 93bd5cb..bf02217 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -BeautifulSoup>=3.2.1, <4.0.0 +beautifulsoup4 selenium>=2.44.0,<3.0.0 From 276704a1970db486956e152e6ed422d999ebbbb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Mon, 23 Feb 2015 18:08:40 -0500 Subject: [PATCH 012/106] Write tests for all the search methods. --- google/google.py | 3 +++ google/tests/test_google.py | 31 ++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/google/google.py b/google/google.py index 366e63f..83506e1 100644 --- a/google/google.py +++ b/google/google.py @@ -61,6 +61,9 @@ def __init__(self): self.store_count = None self.min_price = None + def __repr__(self): + return self.name + class Google: diff --git a/google/tests/test_google.py b/google/tests/test_google.py index bcbd6ba..e3a5954 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -13,27 +13,44 @@ def setUp(self): google.images.get_html_from_dynamic_site = \ Mock(return_value=f.read().decode('utf8')) - # @unittest.skip("skip") def test_search_images(self): """Test method to search images.""" res = google.Google.search_images("apple", num_images=10) self.assertEqual(len(res), 10) + # @unittest.skip("skip") + def test_exchange_rate(self): + """Test method to get an exchange rate in google.""" + + usd_to_eur = google.Google.convert_currency("USD", "EUR") + self.assertGreater(usd_to_eur, 0.0) + + # @unittest.skip("skip") def test_convert_currency(self): - pass + """Test method to convert currency in google.""" - def test_exchange_rate(self): - pass + euros = google.Google.convert_currency(5.0, "USD", "EUR") + self.assertGreater(euros, 0.0) + # @unittest.skip("skip") def test_calculate(self): - pass + """Test method to calculate in google.""" + + calc = google.Google.calculate("157.3kg in grams") + self.assertEqual(calc.value, 157300) def test_search(self): - pass + """Test method to search in google.""" + + search = google.Google.search("github") + self.assertNotEqual(len(search), 0) def test_shopping(self): - pass + """Test method for google shopping.""" + + shop = google.Google.shopping("Disgaea 4") + self.assertNotEqual(len(shop), 0) if __name__ == '__main__': From b71a89ee0f5602f11fa0e6d501f2b6f83f995b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 24 Feb 2015 11:11:01 -0500 Subject: [PATCH 013/106] Update todo file with new tasks and switch to marked/unmarked tasks list style. --- TODO.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/TODO.md b/TODO.md index b1065b6..006c880 100644 --- a/TODO.md +++ b/TODO.md @@ -3,16 +3,17 @@ Todo list for Google-Search-API ## Next tasks -1. Mock html test for search images -2. Refactor and split main modules in separated ones -3. Write tests for future methods development -4. Be able to manage both Chrome and Firefox +- [x] Mock html test for search images +- [ ] Refactor and split main modules in separated ones +- [ ] Write tests for future methods development (they should failed and be skipped up to the proper method development moment) +- [ ] Be able to manage both Chrome and Firefox, and maybe Ie too, depending in what browser the user has +- [ ] Write a full suite of docs test and unit tests -## Stuff to check out +## Stuff to check out later on -* vcrpy to record web requests and make mocks +* vcrpy to record web requests and make more automated mocks * SauceLabs to do UI tests -* Sphinx para generar la documentación - estilo readthedocs -* Cookiecutter-pythonpackage -* PhantomJS to use browsers without opening (works with selenium) -* Understand UTF-8 \ No newline at end of file +* Sphinx to generate documentation - readthedocs style +* Cookiecutter-pythonpackage to make an instant and complete python package structure +* PhantomJS to use browsers without actually opening them (works with selenium) +* Understand UTF-8 encoding in the context of getting html, writing it into a file and getting it back \ No newline at end of file From 81075ca926128f5dfddea2b4a494b3e7c54ae119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 24 Feb 2015 12:22:45 -0500 Subject: [PATCH 014/106] Udpate TODO file. Change private method name in images.py. Test and repair convert curreny method. --- TODO.md | 1 + google/google.py | 48 ++++++++++++++++++++++++++----------- google/images.py | 4 ++-- google/tests/test_google.py | 21 ++++++++++++++-- 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/TODO.md b/TODO.md index 006c880..65b345f 100644 --- a/TODO.md +++ b/TODO.md @@ -8,6 +8,7 @@ Todo list for Google-Search-API - [ ] Write tests for future methods development (they should failed and be skipped up to the proper method development moment) - [ ] Be able to manage both Chrome and Firefox, and maybe Ie too, depending in what browser the user has - [ ] Write a full suite of docs test and unit tests +- [ ] Reconvert all comments following the google python style guide https://google-styleguide.googlecode.com/svn/trunk/pyguide.html ## Stuff to check out later on diff --git a/google/google.py b/google/google.py index 83506e1..0f2d7de 100644 --- a/google/google.py +++ b/google/google.py @@ -201,21 +201,41 @@ def shopping(query, pages=1): """ @staticmethod def convert_currency(amount, from_currency, to_currency): + """Method to convert currency. + + Args: + amount: numeric amount to convert + from_currency: currency denomination of the amount to convert + to_currency: currency denomination to convert to + """ + + # same currency, no conversion if from_currency == to_currency: - return 1.0 - conn = httplib.HTTPSConnection("www.google.com") - req_url = "/ig/calculator?hl=en&q={0}{1}=?{2}".format( - amount, from_currency.replace(" ", "%20"), to_currency.replace(" ", "%20")) - headers = { - "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101"} - conn.request("GET", req_url, "", headers) - response = conn.getresponse() - rval = response.read().decode("utf-8").replace(u"\xa0", "") - conn.close() - rhs = rval.split(",")[1].strip() - s = rhs[rhs.find('"') + 1:] - rate = s[:s.find(" ")] - return float(rate) + return amount * 1.0 + + req_url = Google._get_currency_req_url(amount, + from_currency, to_currency) + response = Google._do_currency_req(req_url) + rate = Google._parse_currency_response(response, to_currency) + + return rate + + @staticmethod + def _get_currency_req_url(amount, from_currency, to_currency): + return "https://www.google.com/finance/converter?a={0}&from={1}&to={2}".format( + amount, from_currency.replace(" ", "%20"), + to_currency.replace(" ", "%20")) + + @staticmethod + def _do_currency_req(req_url): + return urllib2.urlopen(req_url).read() + + @staticmethod + def _parse_currency_response(response, to_currency): + bs = BeautifulSoup(response) + str_rate = bs.find(id="currency_converter_result").span.get_text() + rate = float(str_rate.replace(to_currency, "").strip()) + return rate """ Gets the exchange rate of one currency to another. diff --git a/google/images.py b/google/images.py index 0b16645..f96495a 100644 --- a/google/images.py +++ b/google/images.py @@ -179,7 +179,7 @@ def _parse_image_format(image_link): return parsed_format -def _get_image_search_url(query, image_options=None, page=0, per_page=20): +def _get_images_req_url(query, image_options=None, page=0, per_page=20): query = query.strip().replace(":", "%3A").replace( "+", "%2B").replace("&", "%26").replace(" ", "+") @@ -255,7 +255,7 @@ def search(query, image_options=None, images=50): while curr_img < images: page += 1 - url = _get_image_search_url(query, image_options, page) + url = _get_images_req_url(query, image_options, page) html = get_html_from_dynamic_site(url) # write_html_to_file(html, "test_search_images.html") diff --git a/google/tests/test_google.py b/google/tests/test_google.py index e3a5954..10fd787 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -19,7 +19,7 @@ def test_search_images(self): res = google.Google.search_images("apple", num_images=10) self.assertEqual(len(res), 10) - # @unittest.skip("skip") + @unittest.skip("skip") def test_exchange_rate(self): """Test method to get an exchange rate in google.""" @@ -33,7 +33,7 @@ def test_convert_currency(self): euros = google.Google.convert_currency(5.0, "USD", "EUR") self.assertGreater(euros, 0.0) - # @unittest.skip("skip") + @unittest.skip("skip") def test_calculate(self): """Test method to calculate in google.""" @@ -53,5 +53,22 @@ def test_shopping(self): self.assertNotEqual(len(shop), 0) +class ConvertCurrencyTest(unittest.TestCase): + + # @unittest.skip("skip") + def test_get_currency_req_url(self): + """Test method to get currency conversion request url.""" + + amount = 10 + from_currency = "USD" + to_currency = "EUR" + req_url = google.Google._get_currency_req_url(amount, from_currency, + to_currency) + + expected_req_url = "https://www.google.com/finance/converter?a=10&from=USD&to=EUR" + + self.assertEqual(req_url, expected_req_url) + + if __name__ == '__main__': nose.main() From d0aacb84bebe5a39887a85c926486c819fa3e7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 24 Feb 2015 12:29:12 -0500 Subject: [PATCH 015/106] Unskip exchange rate conversion, since its working now. --- google/tests/test_google.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/google/tests/test_google.py b/google/tests/test_google.py index 10fd787..e60d042 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -19,11 +19,11 @@ def test_search_images(self): res = google.Google.search_images("apple", num_images=10) self.assertEqual(len(res), 10) - @unittest.skip("skip") + # @unittest.skip("skip") def test_exchange_rate(self): """Test method to get an exchange rate in google.""" - usd_to_eur = google.Google.convert_currency("USD", "EUR") + usd_to_eur = google.Google.exchange_rate("USD", "EUR") self.assertGreater(usd_to_eur, 0.0) # @unittest.skip("skip") From 57302106bcdd926825e48cea7d3c2ec3705fa644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 24 Feb 2015 13:27:15 -0500 Subject: [PATCH 016/106] Update TODO file. Clean up and reorganize google module. Move image classes to images module. Write tests for future currency module. --- TODO.md | 13 +- google/google.py | 272 +++++++++++----------------------- google/images.py | 43 ++++++ google/tests/test_currency.py | 42 ++++++ google/tests/test_google.py | 20 +-- 5 files changed, 185 insertions(+), 205 deletions(-) create mode 100644 google/tests/test_currency.py diff --git a/TODO.md b/TODO.md index 65b345f..969e305 100644 --- a/TODO.md +++ b/TODO.md @@ -3,8 +3,9 @@ Todo list for Google-Search-API ## Next tasks -- [x] Mock html test for search images -- [ ] Refactor and split main modules in separated ones +- [x] Mock html test for search images - https://pypi.python.org/pypi/mock/ +- [ ] Test and implement the use of images options in search images method (is broken right now, in the middle of a refactor) +- [ ] Refactor and split main module in separated ones - [ ] Write tests for future methods development (they should failed and be skipped up to the proper method development moment) - [ ] Be able to manage both Chrome and Firefox, and maybe Ie too, depending in what browser the user has - [ ] Write a full suite of docs test and unit tests @@ -15,6 +16,8 @@ Todo list for Google-Search-API * vcrpy to record web requests and make more automated mocks * SauceLabs to do UI tests * Sphinx to generate documentation - readthedocs style -* Cookiecutter-pythonpackage to make an instant and complete python package structure -* PhantomJS to use browsers without actually opening them (works with selenium) -* Understand UTF-8 encoding in the context of getting html, writing it into a file and getting it back \ No newline at end of file +* Cookiecutter-pythonpackage to make an instant and complete python package structure - https://github.com/audreyr/cookiecutter-pypackage and https://github.com/audreyr/cookiecutter +* PhantomJS to use browsers without actually opening them (works with selenium) - http://stackoverflow.com/questions/13287490/is-there-a-way-to-use-phantomjs-in-python +* Understand UTF-8 encoding in the context of getting html, writing it into a file and getting it back +* Make TODOs with an automatic application for sublime text that makes .todo files with awesome features +* Automatic PEP8/Flake8 formatting style when pushed to GitHub \ No newline at end of file diff --git a/google/google.py b/google/google.py index 0f2d7de..5bfb90c 100644 --- a/google/google.py +++ b/google/google.py @@ -1,7 +1,6 @@ from bs4 import BeautifulSoup import httplib import urllib2 -import sys import re import images @@ -15,6 +14,68 @@ __version__ = "1.0.0" +# GLOBAL METHODS +def normalize_query(query): + return query.strip().replace(":", "%3A").replace("+", "%2B").replace("&", "%26").replace(" ", "+") + + +def add_to_tbs(tbs, name, value): + if tbs: + return "%s,%s:%s" % (tbs, name, value) + else: + return "&tbs=%s:%s" % (name, value) + + +def parse_calc_result(string): + result = CalculatorResult() + result.fullstring = string + string = string.strip().replace(u"\xa0", " ") + if string.find("=") != -1: + result.expr = string[:string.rfind("=")].strip() + string = string[string.rfind("=") + 2:] + result.result = string + tokens = string.split(" ") + if len(tokens) > 0: + result.value = "" + for token in tokens: + if is_number(token): + result.value = result.value + token + else: + if result.unit: + result.unit = result.unit + " " + token + else: + result.unit = token + return result + return None + + +def is_number(s): + try: + float(s) + return True + except ValueError: + return False + + +def get_html(url): + try: + request = urllib2.Request(url) + request.add_header( + "User-Agent", "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101") + html = urllib2.urlopen(request).read() + return html + except: + print "Error accessing:", url + return None + + +def write_html_to_file(html, filename): + of = open(filename, "w") + of.write(html) + of.flush() + of.close() + + # RESULT CLASSES class GoogleResult: @@ -65,21 +126,20 @@ def __repr__(self): return self.name +# PUBLIC CLASS class Google: """Defines the public static api methods.""" DEBUG_MODE = False - - """ - Returns a list of GoogleResult - """ @staticmethod def search(query, pages=1): + """Returns a list of GoogleResult.""" + results = [] for i in range(pages): - url = get_search_url(query, i) + url = Google._get_search_url(query, i) html = get_html(url) if html: if Google.DEBUG_MODE: @@ -105,12 +165,15 @@ def search(query, pages=1): j = j + 1 return results - """ - OLD WAY OF DOING THIS. Attempts to use google calculator to calculate the result of expr - """ + @staticmethod + def _get_search_url(query, page=0, per_page=10): + # note: num per page might not be supported by google anymore (because of + # google instant) + return "http://www.google.com/search?hl=en&q=%s&start=%i&num=%i" % (normalize_query(query), page * per_page, per_page) + @staticmethod def calculate_old(expr): - url = get_search_url(expr) + url = Google._get_search_url(expr) html = get_html(url) if html: soup = BeautifulSoup(html) @@ -149,7 +212,7 @@ def search_images(query, image_options=None, num_images=50): def shopping(query, pages=1): results = [] for i in range(pages): - url = get_shopping_url(query, i) + url = Google._get_shopping_url(query, i) html = get_html(url) if html: if Google.DEBUG_MODE: @@ -195,10 +258,10 @@ def shopping(query, pages=1): j = j + 1 return results - """ - Converts one currency to another. - [amount] from_curreny = [return_value] to_currency - """ + @staticmethod + def _get_shopping_url(query, page=0, per_page=10): + return "http://www.google.com/search?hl=en&q={0}&tbm=shop&start={1}&num={2}".format(normalize_query(query), page * per_page, per_page) + @staticmethod def convert_currency(amount, from_currency, to_currency): """Method to convert currency. @@ -206,7 +269,7 @@ def convert_currency(amount, from_currency, to_currency): Args: amount: numeric amount to convert from_currency: currency denomination of the amount to convert - to_currency: currency denomination to convert to + to_currency: target currency denomination to convert to """ # same currency, no conversion @@ -223,8 +286,8 @@ def convert_currency(amount, from_currency, to_currency): @staticmethod def _get_currency_req_url(amount, from_currency, to_currency): return "https://www.google.com/finance/converter?a={0}&from={1}&to={2}".format( - amount, from_currency.replace(" ", "%20"), - to_currency.replace(" ", "%20")) + amount, from_currency.replace(" ", "%20"), + to_currency.replace(" ", "%20")) @staticmethod def _do_currency_req(req_url): @@ -237,17 +300,19 @@ def _parse_currency_response(response, to_currency): rate = float(str_rate.replace(to_currency, "").strip()) return rate - """ - Gets the exchange rate of one currency to another. - 1 from_curreny = [return_value] to_currency - """ @staticmethod def exchange_rate(from_currency, to_currency): + """Gets the exchange rate of one currency to another. + + Args: + from_currency: starting currency denomination (1) + to_currency: target currency denomination to convert to (rate) + + Returns: + rate / 1 to convert from_currency in to_currency + """ return Google.convert_currency(1, from_currency, to_currency) - """ - Attempts to use google calculator to calculate the result of expr - """ @staticmethod def calculate(expr): conn = httplib.HTTPSConnection("www.google.com") @@ -266,163 +331,6 @@ def calculate(expr): return parse_calc_result(js["lhs"] + " = " + js["rhs"]) -def normalize_query(query): - return query.strip().replace(":", "%3A").replace("+", "%2B").replace("&", "%26").replace(" ", "+") - - -def get_search_url(query, page=0, per_page=10): - # note: num per page might not be supported by google anymore (because of - # google instant) - return "http://www.google.com/search?hl=en&q=%s&start=%i&num=%i" % (normalize_query(query), page * per_page, per_page) - - -def get_shopping_url(query, page=0, per_page=10): - return "http://www.google.com/search?hl=en&q={0}&tbm=shop&start={1}&num={2}".format(normalize_query(query), page * per_page, per_page) - - -class ImageType: - NONE = None - FACE = "face" - PHOTO = "photo" - CLIPART = "clipart" - LINE_DRAWING = "lineart" - - -class SizeCategory: - NONE = None - ICON = "i" - LARGE = "l" - MEDIUM = "m" - SMALL = "s" - LARGER_THAN = "lt" - EXACTLY = "ex" - - -class LargerThan: - NONE = None - QSVGA = "qsvga" # 400 x 300 - VGA = "vga" # 640 x 480 - SVGA = "svga" # 800 x 600 - XGA = "xga" # 1024 x 768 - MP_2 = "2mp" # 2 MP (1600 x 1200) - MP_4 = "4mp" # 4 MP (2272 x 1704) - MP_6 = "6mp" # 6 MP (2816 x 2112) - MP_8 = "8mp" # 8 MP (3264 x 2448) - MP_10 = "10mp" # 10 MP (3648 x 2736) - MP_12 = "12mp" # 12 MP (4096 x 3072) - MP_15 = "15mp" # 15 MP (4480 x 3360) - MP_20 = "20mp" # 20 MP (5120 x 3840) - MP_40 = "40mp" # 40 MP (7216 x 5412) - MP_70 = "70mp" # 70 MP (9600 x 7200) - - -class ColorType: - NONE = None - COLOR = "color" - BLACK_WHITE = "gray" - SPECIFIC = "specific" - - -def add_to_tbs(tbs, name, value): - if tbs: - return "%s,%s:%s" % (tbs, name, value) - else: - return "&tbs=%s:%s" % (name, value) - - -def parse_calc_result(string): - result = CalculatorResult() - result.fullstring = string - string = string.strip().replace(u"\xa0", " ") - if string.find("=") != -1: - result.expr = string[:string.rfind("=")].strip() - string = string[string.rfind("=") + 2:] - result.result = string - tokens = string.split(" ") - if len(tokens) > 0: - result.value = "" - for token in tokens: - if is_number(token): - result.value = result.value + token - else: - if result.unit: - result.unit = result.unit + " " + token - else: - result.unit = token - return result - return None - - -def is_number(s): - try: - float(s) - return True - except ValueError: - return False - - -def get_html(url): - try: - request = urllib2.Request(url) - request.add_header( - "User-Agent", "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101") - html = urllib2.urlopen(request).read() - return html - except: - print "Error accessing:", url - return None - - -def write_html_to_file(html, filename): - of = open(filename, "w") - of.write(html) - of.flush() - of.close() - - -def test(): - search = Google.search("github") - if search is None or len(search) == 0: - print "ERROR: No Search Results!" - else: - print "PASSED: {0} Search Results".format(len(search)) - - shop = Google.shopping("Disgaea 4") - if shop is None or len(shop) == 0: - print "ERROR: No Shopping Results!" - else: - print "PASSED: {0} Shopping Results".format(len(shop)) - - options = ImageOptions() - options.image_type = ImageType.CLIPART - options.larger_than = LargerThan.MP_4 - options.color = "green" - images = Google.search_images("banana", options) - if images is None or len(images) == 0: - print "ERROR: No Image Results!" - else: - print "PASSED: {0} Image Results".format(len(images)) - - calc = Google.calculate("157.3kg in grams") - if calc is not None and int(calc.value) == 157300: - print "PASSED: Calculator passed" - else: - print "ERROR: Calculator failed!" - - euros = Google.convert_currency(5.0, "USD", "EUR") - if euros is not None and euros > 0.0: - print "PASSED: Currency convert passed" - else: - print "ERROR: Currency convert failed!" - - -def main(): - if len(sys.argv) > 1 and sys.argv[1] == "--debug": - Google.DEBUG_MODE = True - print "DEBUG_MODE ENABLED" - test() - if __name__ == "__main__": - # main() import doctest doctest.testmod() diff --git a/google/images.py b/google/images.py index f96495a..6ffe9f7 100644 --- a/google/images.py +++ b/google/images.py @@ -16,6 +16,49 @@ def write_html_to_file(html, filename): "jpeg", "jif", "jfif", "jp2", "jpx", "j2k", "j2c", "fpx", "pcd", "png", "pdf"] +class ImageType: + NONE = None + FACE = "face" + PHOTO = "photo" + CLIPART = "clipart" + LINE_DRAWING = "lineart" + + +class SizeCategory: + NONE = None + ICON = "i" + LARGE = "l" + MEDIUM = "m" + SMALL = "s" + LARGER_THAN = "lt" + EXACTLY = "ex" + + +class LargerThan: + NONE = None + QSVGA = "qsvga" # 400 x 300 + VGA = "vga" # 640 x 480 + SVGA = "svga" # 800 x 600 + XGA = "xga" # 1024 x 768 + MP_2 = "2mp" # 2 MP (1600 x 1200) + MP_4 = "4mp" # 4 MP (2272 x 1704) + MP_6 = "6mp" # 6 MP (2816 x 2112) + MP_8 = "8mp" # 8 MP (3264 x 2448) + MP_10 = "10mp" # 10 MP (3648 x 2736) + MP_12 = "12mp" # 12 MP (4096 x 3072) + MP_15 = "15mp" # 15 MP (4480 x 3360) + MP_20 = "20mp" # 20 MP (5120 x 3840) + MP_40 = "40mp" # 40 MP (7216 x 5412) + MP_70 = "70mp" # 70 MP (9600 x 7200) + + +class ColorType: + NONE = None + COLOR = "color" + BLACK_WHITE = "gray" + SPECIFIC = "specific" + + class ImageOptions: """Allows passing options to filter a google images search.""" diff --git a/google/tests/test_currency.py b/google/tests/test_currency.py new file mode 100644 index 0000000..8d1ec37 --- /dev/null +++ b/google/tests/test_currency.py @@ -0,0 +1,42 @@ +import unittest +import nose +from google import currency + + +class ConvertCurrencyTest(unittest.TestCase): + + def test_convert_currency(self): + """Test method to convert currency in currency module.""" + + euros = currency.convert_currency(5.0, "USD", "EUR") + self.assertGreater(euros, 0.0) + + def test_exchange_rate(self): + """Test method to get an exchange rate in currency module.""" + + usd_to_eur = currency.exchange_rate("USD", "EUR") + self.assertGreater(usd_to_eur, 0.0) + + # @unittest.skip("skip") + def test_get_currency_req_url(self): + """Test method to get currency conversion request url.""" + + amount = 10 + from_currency = "USD" + to_currency = "EUR" + req_url = currency._get_currency_req_url(amount, from_currency, + to_currency) + + expected_req_url = "https://www.google.com/finance/converter?a=10&from=USD&to=EUR" + + self.assertEqual(req_url, expected_req_url) + + @unittest.skip("skip") + def test_parse_currency_response(self): + """Test method to parse currency response.""" + pass + + +if __name__ == '__main__': + # nose.main() + nose.run(defaultTest=__name__) diff --git a/google/tests/test_google.py b/google/tests/test_google.py index e60d042..b6344c6 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -53,22 +53,6 @@ def test_shopping(self): self.assertNotEqual(len(shop), 0) -class ConvertCurrencyTest(unittest.TestCase): - - # @unittest.skip("skip") - def test_get_currency_req_url(self): - """Test method to get currency conversion request url.""" - - amount = 10 - from_currency = "USD" - to_currency = "EUR" - req_url = google.Google._get_currency_req_url(amount, from_currency, - to_currency) - - expected_req_url = "https://www.google.com/finance/converter?a=10&from=USD&to=EUR" - - self.assertEqual(req_url, expected_req_url) - - if __name__ == '__main__': - nose.main() + # nose.main() + nose.run(defaultTest=__name__) From 8243fdcf5bb7ff54c8f2175679483210cf0ba14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 24 Feb 2015 13:36:37 -0500 Subject: [PATCH 017/106] Refactor currency moving out of google module all convert currency methods. --- google/currency.py | 53 +++++++++++++++++++++++++++++++++++ google/google.py | 32 ++------------------- google/tests/test_currency.py | 3 +- 3 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 google/currency.py diff --git a/google/currency.py b/google/currency.py new file mode 100644 index 0000000..a580570 --- /dev/null +++ b/google/currency.py @@ -0,0 +1,53 @@ +import urllib2 +from bs4 import BeautifulSoup + + +def convert_currency(amount, from_currency, to_currency): + """Method to convert currency. + + Args: + amount: numeric amount to convert + from_currency: currency denomination of the amount to convert + to_currency: target currency denomination to convert to + """ + + # same currency, no conversion + if from_currency == to_currency: + return amount * 1.0 + + req_url = _get_currency_req_url(amount, + from_currency, to_currency) + response = _do_currency_req(req_url) + rate = _parse_currency_response(response, to_currency) + + return rate + + +def exchange_rate(from_currency, to_currency): + """Gets the exchange rate of one currency to another. + + Args: + from_currency: starting currency denomination (1) + to_currency: target currency denomination to convert to (rate) + + Returns: + rate / 1 to convert from_currency in to_currency + """ + return convert_currency(1, from_currency, to_currency) + + +def _get_currency_req_url(amount, from_currency, to_currency): + return "https://www.google.com/finance/converter?a={0}&from={1}&to={2}".format( + amount, from_currency.replace(" ", "%20"), + to_currency.replace(" ", "%20")) + + +def _do_currency_req(req_url): + return urllib2.urlopen(req_url).read() + + +def _parse_currency_response(response, to_currency): + bs = BeautifulSoup(response) + str_rate = bs.find(id="currency_converter_result").span.get_text() + rate = float(str_rate.replace(to_currency, "").strip()) + return rate diff --git a/google/google.py b/google/google.py index 5bfb90c..36f1eb4 100644 --- a/google/google.py +++ b/google/google.py @@ -3,6 +3,7 @@ import urllib2 import re import images +import currency try: import json @@ -271,34 +272,7 @@ def convert_currency(amount, from_currency, to_currency): from_currency: currency denomination of the amount to convert to_currency: target currency denomination to convert to """ - - # same currency, no conversion - if from_currency == to_currency: - return amount * 1.0 - - req_url = Google._get_currency_req_url(amount, - from_currency, to_currency) - response = Google._do_currency_req(req_url) - rate = Google._parse_currency_response(response, to_currency) - - return rate - - @staticmethod - def _get_currency_req_url(amount, from_currency, to_currency): - return "https://www.google.com/finance/converter?a={0}&from={1}&to={2}".format( - amount, from_currency.replace(" ", "%20"), - to_currency.replace(" ", "%20")) - - @staticmethod - def _do_currency_req(req_url): - return urllib2.urlopen(req_url).read() - - @staticmethod - def _parse_currency_response(response, to_currency): - bs = BeautifulSoup(response) - str_rate = bs.find(id="currency_converter_result").span.get_text() - rate = float(str_rate.replace(to_currency, "").strip()) - return rate + return currency.convert_currency(amount, from_currency, to_currency) @staticmethod def exchange_rate(from_currency, to_currency): @@ -311,7 +285,7 @@ def exchange_rate(from_currency, to_currency): Returns: rate / 1 to convert from_currency in to_currency """ - return Google.convert_currency(1, from_currency, to_currency) + return currency.exchange_rate(from_currency, to_currency) @staticmethod def calculate(expr): diff --git a/google/tests/test_currency.py b/google/tests/test_currency.py index 8d1ec37..4e15784 100644 --- a/google/tests/test_currency.py +++ b/google/tests/test_currency.py @@ -11,6 +11,7 @@ def test_convert_currency(self): euros = currency.convert_currency(5.0, "USD", "EUR") self.assertGreater(euros, 0.0) + # @unittest.skip("skip") def test_exchange_rate(self): """Test method to get an exchange rate in currency module.""" @@ -33,7 +34,7 @@ def test_get_currency_req_url(self): @unittest.skip("skip") def test_parse_currency_response(self): - """Test method to parse currency response.""" + """Test method to parse currency response. TODO!""" pass From 83ad9edfceb67d147d17c43a7e36e679e7c55702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 24 Feb 2015 13:59:33 -0500 Subject: [PATCH 018/106] Clean up and reorder a bit. Upload some design questions to think about. --- DESIGN_QUESTIONS.md | 16 ++++++++++++++++ google/currency.py | 36 +++++++++++++++++++----------------- google/images.py | 8 +++++--- 3 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 DESIGN_QUESTIONS.md diff --git a/DESIGN_QUESTIONS.md b/DESIGN_QUESTIONS.md new file mode 100644 index 0000000..affb806 --- /dev/null +++ b/DESIGN_QUESTIONS.md @@ -0,0 +1,16 @@ +Design questions about the package +==== + +* **Relocating methods of Google class in separated modules** Encapsulate the logic and functions of each kind of search method outside the public class used by the user to be able to maintain them and repair them without touching the main module and interface. (More maintainable or overkilling?) + +* **Duplicated docstrings between methods in the main class and methods in the modules** Updates in main module docstrings should be followed by duplicated updates in auxiliary modules docstrings. (should I put docstrings only in the main module?) + +* **Private methods "floating" in a module as global methods** It feels uncomfortable to have private methods not wrapped into a class, but adding classes inside auxiliary methods complicates the interface of the modules unnecessarily (should I keep them like this? Without "wrapping" classes?) + +* **Order of methods in the modules** Private methods before public methods or vice-versa? First approach shows first the bricks of the wall and then the wall, second approach shows first what the user can use about the module and "hide" some implementation details. + +* **Top down approach in currency: overkilling?** How far would be desirable to go in breaking down public methods into private ones to make more clear the algorithm followed by the main public method? + +* **Closely related tests between main module and auxiliary modules** Some tests in the google module seems to test just the same than some tests in the auxiliary modules. At some point this is duplicating code and work (specially when test must be changed) but they respond to different compartments of the package. + +* **Is it ok to upload something so informal as these DESIGN_QUESTIONS to github?** Transparency vs. overpopulate the package?? \ No newline at end of file diff --git a/google/currency.py b/google/currency.py index a580570..feb8a8f 100644 --- a/google/currency.py +++ b/google/currency.py @@ -2,6 +2,25 @@ from bs4 import BeautifulSoup +# PRIVATE +def _get_currency_req_url(amount, from_currency, to_currency): + return "https://www.google.com/finance/converter?a={0}&from={1}&to={2}".format( + amount, from_currency.replace(" ", "%20"), + to_currency.replace(" ", "%20")) + + +def _do_currency_req(req_url): + return urllib2.urlopen(req_url).read() + + +def _parse_currency_response(response, to_currency): + bs = BeautifulSoup(response) + str_rate = bs.find(id="currency_converter_result").span.get_text() + rate = float(str_rate.replace(to_currency, "").strip()) + return rate + + +# PUBLIC def convert_currency(amount, from_currency, to_currency): """Method to convert currency. @@ -34,20 +53,3 @@ def exchange_rate(from_currency, to_currency): rate / 1 to convert from_currency in to_currency """ return convert_currency(1, from_currency, to_currency) - - -def _get_currency_req_url(amount, from_currency, to_currency): - return "https://www.google.com/finance/converter?a={0}&from={1}&to={2}".format( - amount, from_currency.replace(" ", "%20"), - to_currency.replace(" ", "%20")) - - -def _do_currency_req(req_url): - return urllib2.urlopen(req_url).read() - - -def _parse_currency_response(response, to_currency): - bs = BeautifulSoup(response) - str_rate = bs.find(id="currency_converter_result").span.get_text() - rate = float(str_rate.replace(to_currency, "").strip()) - return rate diff --git a/google/images.py b/google/images.py index 6ffe9f7..d23d2ab 100644 --- a/google/images.py +++ b/google/images.py @@ -3,6 +3,7 @@ import urlparse +# GLOBAL AUXILIARY METHODS AND DATA def write_html_to_file(html, filename): of = open(filename, "w") of.write(html.encode("utf-8")) @@ -10,12 +11,13 @@ def write_html_to_file(html, filename): of.close() - IMAGE_FORMATS = ["bmp", "gif", "jpg", "png", "psd", "pspimage", "thm", "tif", "yuv", "ai", "drw", "eps", "ps", "svg", "tiff", "jpeg", "jif", "jfif", "jp2", "jpx", "j2k", "j2c", "fpx", "pcd", "png", "pdf"] + +# AUXILIARY CLASSES class ImageType: NONE = None FACE = "face" @@ -193,7 +195,7 @@ def _create_path(self, path): os.mkdir(path) -# PRIVATE METHODS +# PRIVATE def _parse_image_format(image_link): """Parse an image format from a download link. @@ -242,7 +244,7 @@ def _get_images_req_url(query, image_options=None, page=0, per_page=20): return url -# PUBLIC METHODS +# PUBLIC def search_old(query, image_options=None, pages=1): results = [] for i in range(pages): From 58642944b2c40f6b410342427064d236d10dad44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 24 Feb 2015 16:22:24 -0500 Subject: [PATCH 019/106] Change some names. Repair tests after name changes. Replace public methods in the main module with references to the methods written in the auxiliary modules. --- DESIGN_QUESTIONS.md | 16 ------------ DESIGN_THOUGHTS.md | 25 +++++++++++++++++++ TODO.md | 2 +- google/currency.py | 42 ++++++++++++++++---------------- google/google.py | 46 +++-------------------------------- google/images.py | 10 ++++---- google/tests/test_currency.py | 4 +-- 7 files changed, 58 insertions(+), 87 deletions(-) delete mode 100644 DESIGN_QUESTIONS.md create mode 100644 DESIGN_THOUGHTS.md diff --git a/DESIGN_QUESTIONS.md b/DESIGN_QUESTIONS.md deleted file mode 100644 index affb806..0000000 --- a/DESIGN_QUESTIONS.md +++ /dev/null @@ -1,16 +0,0 @@ -Design questions about the package -==== - -* **Relocating methods of Google class in separated modules** Encapsulate the logic and functions of each kind of search method outside the public class used by the user to be able to maintain them and repair them without touching the main module and interface. (More maintainable or overkilling?) - -* **Duplicated docstrings between methods in the main class and methods in the modules** Updates in main module docstrings should be followed by duplicated updates in auxiliary modules docstrings. (should I put docstrings only in the main module?) - -* **Private methods "floating" in a module as global methods** It feels uncomfortable to have private methods not wrapped into a class, but adding classes inside auxiliary methods complicates the interface of the modules unnecessarily (should I keep them like this? Without "wrapping" classes?) - -* **Order of methods in the modules** Private methods before public methods or vice-versa? First approach shows first the bricks of the wall and then the wall, second approach shows first what the user can use about the module and "hide" some implementation details. - -* **Top down approach in currency: overkilling?** How far would be desirable to go in breaking down public methods into private ones to make more clear the algorithm followed by the main public method? - -* **Closely related tests between main module and auxiliary modules** Some tests in the google module seems to test just the same than some tests in the auxiliary modules. At some point this is duplicating code and work (specially when test must be changed) but they respond to different compartments of the package. - -* **Is it ok to upload something so informal as these DESIGN_QUESTIONS to github?** Transparency vs. overpopulate the package?? \ No newline at end of file diff --git a/DESIGN_THOUGHTS.md b/DESIGN_THOUGHTS.md new file mode 100644 index 0000000..f9b84a1 --- /dev/null +++ b/DESIGN_THOUGHTS.md @@ -0,0 +1,25 @@ +Design thoughts about the package +==== + +*This is a list of design thoughts that have appeared in building/modifying this package and their provisional resolution. They should be taken as useful indicators of what was the developer maintaining this package thinking when making design decisions.* + +* **Relocating methods of public Google class in separated modules** The idea is to encapsulate the logic and functions of each kind of search method outside the public class used by the user to be able to maintain them on an individual basis and repair them without touching the main module and its interface. (Is this more maintainable or is overkilling the problem?) + - As the project may grow or the logic of the used methods may change, it is more maintainable to modularize. This shouldn't add more complication to the package since developers are used to deal with many files, if the structure of the modules make sense. + +* **Duplicated docstrings between methods in the main class and methods in the modules** Updates in main module (google) docstrings should be followed by duplicated updates in auxiliary module's docstrings. (Should I put docstrings only in the main module or manage both, even in a duplicated basis?) + - It is better to make the methods in the main public module be just a reference to the methods hold in the auxiliary modules, so only docstrings and signature of the method written in the module need to be taken care. + +* **Private methods "floating" in a module as global methods** It feels uncomfortable to have private methods not wrapped into a class, but adding classes inside auxiliary methods complicates the interface of the modules unnecessarily (should I keep them like this? Without putting them inside "wrapping" classes?) + - There is nothing wrong about having public and private methods in a module. The most important thing here is to build a clear user interface for the module. + +* **Order of methods in the modules** Private methods before public methods or vice-versa? First approach shows the bricks of the wall first, and then the wall. The second approach shows first the methods that are expected to be used by the user, delaying the reading of some implementation details. + - I'm not really sure about the convenience of one approach over the other one. Provisionally I'm taking the second approach as the better one, because I thing could improve readability. + +* **Top down approach in currency: overkilling?** How far would be desirable to go in breaking down public methods into private ones to make more clear the algorithm followed by the main public method? + - Making private methods with declarative names shows clearer algorithms, encapsulate issues than therefore can be dealt separately and avoid the need of in-line comments. Although tightly related private methods may not be separated, if it's unclear that they can truly be dealt separately. + +* **Closely related tests between main module and auxiliary modules** Some tests in the google module seems to test just the same than some tests in the auxiliary modules. At some point this is duplicating code and work (specially when test must be changed) but they respond to different compartments of the package. + - I am not really sure about the subject. The safest approach would be to say that those tests are really testing different things, even if the tests of the main class are just testing that the reference to the methods written inside auxiliary classes are working fine. + +* **Is it ok to upload something so informal as these DESIGN_THOUGHTS to Github?** Is this the proper place to state these thoughts? Are these thoughts something I should rather keep to myself in a private file? + - The design thoughts that a developer has when building a package (even, as is this case, with a newbie developer) can be useful for someone to understand, improve or discuss any productive changes that can be made in the package. They may sound silly sometimes, they may be even just totally wrong, but they offer (I think) a quick insight on what was this person thinking when taking these design decisions. \ No newline at end of file diff --git a/TODO.md b/TODO.md index 969e305..f006bb6 100644 --- a/TODO.md +++ b/TODO.md @@ -9,7 +9,7 @@ Todo list for Google-Search-API - [ ] Write tests for future methods development (they should failed and be skipped up to the proper method development moment) - [ ] Be able to manage both Chrome and Firefox, and maybe Ie too, depending in what browser the user has - [ ] Write a full suite of docs test and unit tests -- [ ] Reconvert all comments following the google python style guide https://google-styleguide.googlecode.com/svn/trunk/pyguide.html +- [ ] Reconvert all comments following the google python style guide - https://google-styleguide.googlecode.com/svn/trunk/pyguide.html ## Stuff to check out later on diff --git a/google/currency.py b/google/currency.py index feb8a8f..f18c1d4 100644 --- a/google/currency.py +++ b/google/currency.py @@ -2,26 +2,8 @@ from bs4 import BeautifulSoup -# PRIVATE -def _get_currency_req_url(amount, from_currency, to_currency): - return "https://www.google.com/finance/converter?a={0}&from={1}&to={2}".format( - amount, from_currency.replace(" ", "%20"), - to_currency.replace(" ", "%20")) - - -def _do_currency_req(req_url): - return urllib2.urlopen(req_url).read() - - -def _parse_currency_response(response, to_currency): - bs = BeautifulSoup(response) - str_rate = bs.find(id="currency_converter_result").span.get_text() - rate = float(str_rate.replace(to_currency, "").strip()) - return rate - - # PUBLIC -def convert_currency(amount, from_currency, to_currency): +def convert(amount, from_currency, to_currency): """Method to convert currency. Args: @@ -36,7 +18,7 @@ def convert_currency(amount, from_currency, to_currency): req_url = _get_currency_req_url(amount, from_currency, to_currency) - response = _do_currency_req(req_url) + response = _do_req(req_url) rate = _parse_currency_response(response, to_currency) return rate @@ -52,4 +34,22 @@ def exchange_rate(from_currency, to_currency): Returns: rate / 1 to convert from_currency in to_currency """ - return convert_currency(1, from_currency, to_currency) + return convert(1, from_currency, to_currency) + + +# PRIVATE +def _get_currency_req_url(amount, from_currency, to_currency): + return "https://www.google.com/finance/converter?a={0}&from={1}&to={2}".format( + amount, from_currency.replace(" ", "%20"), + to_currency.replace(" ", "%20")) + + +def _do_req(req_url): + return urllib2.urlopen(req_url).read() + + +def _parse_currency_response(response, to_currency): + bs = BeautifulSoup(response) + str_rate = bs.find(id="currency_converter_result").span.get_text() + rate = float(str_rate.replace(to_currency, "").strip()) + return rate diff --git a/google/google.py b/google/google.py index 36f1eb4..f265795 100644 --- a/google/google.py +++ b/google/google.py @@ -187,27 +187,13 @@ def calculate_old(expr): return parse_calc_result(h2.text) return None - @staticmethod - def search_images_old(query, image_options=None, pages=1): - """Old method to search images in google.""" - - return images.search_old(query, image_options, pages) + search_images_old = staticmethod(images.search_old) - @staticmethod - def search_images(query, image_options=None, num_images=50): - """Search images in google. + search_images = staticmethod(images.search) - # >>> results = Google.search_images("banana") - # 'style' index= 97 - # 'style' index= 98 - # 'style' index= 99 - # >>> len(results) - # 100 - # >>> isinstance(results[0], ImageResult) - # True - """ + convert_currency = staticmethod(currency.convert) - return images.search(query, image_options, num_images) + exchange_rate = staticmethod(currency.exchange_rate) @staticmethod def shopping(query, pages=1): @@ -263,30 +249,6 @@ def shopping(query, pages=1): def _get_shopping_url(query, page=0, per_page=10): return "http://www.google.com/search?hl=en&q={0}&tbm=shop&start={1}&num={2}".format(normalize_query(query), page * per_page, per_page) - @staticmethod - def convert_currency(amount, from_currency, to_currency): - """Method to convert currency. - - Args: - amount: numeric amount to convert - from_currency: currency denomination of the amount to convert - to_currency: target currency denomination to convert to - """ - return currency.convert_currency(amount, from_currency, to_currency) - - @staticmethod - def exchange_rate(from_currency, to_currency): - """Gets the exchange rate of one currency to another. - - Args: - from_currency: starting currency denomination (1) - to_currency: target currency denomination to convert to (rate) - - Returns: - rate / 1 to convert from_currency in to_currency - """ - return currency.exchange_rate(from_currency, to_currency) - @staticmethod def calculate(expr): conn = httplib.HTTPSConnection("www.google.com") diff --git a/google/images.py b/google/images.py index d23d2ab..c25c162 100644 --- a/google/images.py +++ b/google/images.py @@ -291,13 +291,13 @@ def search_old(query, image_options=None, pages=1): return results -def search(query, image_options=None, images=50): +def search(query, image_options=None, num_images=50): """Main method to search images in google.""" results = set() - curr_img = 0 + curr_num_img = 0 page = 0 - while curr_img < images: + while curr_num_img < num_images: page += 1 url = _get_images_req_url(query, image_options, page) @@ -361,8 +361,8 @@ def search(query, image_options=None, images=50): # increment image counter only if new image was added images_added = curr_num_results - prev_num_results - curr_img += images_added - if curr_img >= images: + curr_num_img += images_added + if curr_num_img >= num_images: break j = j + 1 diff --git a/google/tests/test_currency.py b/google/tests/test_currency.py index 4e15784..e4a4971 100644 --- a/google/tests/test_currency.py +++ b/google/tests/test_currency.py @@ -5,10 +5,10 @@ class ConvertCurrencyTest(unittest.TestCase): - def test_convert_currency(self): + def test_convert(self): """Test method to convert currency in currency module.""" - euros = currency.convert_currency(5.0, "USD", "EUR") + euros = currency.convert(5.0, "USD", "EUR") self.assertGreater(euros, 0.0) # @unittest.skip("skip") From 5d0c25dec3ea72abd7116e4f648aa6f8d5a759c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 24 Feb 2015 17:13:31 -0500 Subject: [PATCH 020/106] Update readme with warning about heavy refactoring in course and changes in the user interface. --- README.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8135d81..2830171 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Google Search API *The original package was developed by Anthony Casagrande and can be downloaded at https://github.com/BirdAPI This is a forked package that I will continue maintaining in the foreseeable future* -Google Search API is a python based library for searching various functionalities of google. It uses screen scraping to retrieve the results, and thus is unreliable if the way google's web pages are returned change in the future. +Google Search API is a python based library for searching various functionalities of google. It uses screen scraping to retrieve the results, and thus is unreliable if the way google's web pages are returned change in the future. This package is currently under heavy refactoring so changes in the user interface should be expected for the time being. *Disclaimer: This software uses screen scraping to retrieve search results from google.com, and therefore this software may stop working at any given time. Use this software at your own risk. I assume no responsibility for how this software API is used by others.* @@ -14,13 +14,13 @@ The repo is structured like a package, so it can be installed from pip using github clone url. From command line type: ``` -pip install git+https://github.com/abenassi/Google-Search-API.git +pip install Google-Search-API ``` To upgrade the package if you have already installed it: ``` -pip install git+https://github.com/abenassi/Google-Search-API.git --upgrade +pip install Google-Search-API --upgrade ``` You could also just download or clone the repo and import the package from @@ -29,14 +29,15 @@ Google-Search-API folder. ```python import os os.chdir("C:\Path_where_repo_is") -import google_search_api +import google ``` ## Google Web Search You can search google web in the following way: ```python -search_results = Google.search("This is my query") +from google import google +search_results = google.Google.search("This is my query") ``` `search_results` will contain a list of `GoogleResult` objects @@ -57,7 +58,8 @@ GoogleResult: Attempts to search google calculator for the result of an expression. Returns a `CalculatorResult` if successful or `None` if it fails. ```python -Google.calculate("157.3kg in grams") +from google import google +google.Google.calculate("157.3kg in grams") ``` ```python @@ -70,7 +72,8 @@ Google.calculate("157.3kg in grams") ```python -Google.calculate("cos(25 pi) / 17.4") +from google import google +google.Google.calculate("cos(25 pi) / 17.4") ``` ```python @@ -172,7 +175,8 @@ Convert between one currency and another using google calculator. Results are re Convert 5 US Dollars to Euros using the official 3 letter currency acronym: ```python -euros = Google.convert_currency(5.0, "USD", "EUR") +from google import google +euros = google.Google.convert_currency(5.0, "USD", "EUR") print "5.0 USD = {0} EUR".format(euros) ``` @@ -183,7 +187,7 @@ print "5.0 USD = {0} EUR".format(euros) Convert 1000 Japanese Yen to US Dollars: ```python -yen = Google.convert_currency(1000, "yen", "us dollars") +yen = google.Google.convert_currency(1000, "yen", "us dollars") print "1000 yen = {0} us dollars".format(yen) ``` @@ -194,7 +198,7 @@ print "1000 yen = {0} us dollars".format(yen) Instead you can get the exchange rate which returns what 1 `from_currency` equals in `to_currency` and do your own math: ```python -rate = Google.exchange_rate("dollars", "pesos") +rate = google.Google.exchange_rate("dollars", "pesos") print "dollars -> pesos exchange rate = {0}".format(rate) ``` @@ -205,11 +209,11 @@ dollars -> pesos exchange rate = 13.1580679 Perform your own math. The following 2 statements are equal: ```python -5.0 * Google.exchange_rate("USD", "EUR") +5.0 * google.Google.exchange_rate("USD", "EUR") ``` ```python -Google.convert_currency(5.0, "USD", "EUR") +google.Google.convert_currency(5.0, "USD", "EUR") ``` As a side note, `convert_currency` is always more accurate than performing your own math on `exchange_rate` because of possible rounding errors. However if you have more than one value to convert it is best to call `exchange_rate` and cache the result to use for multiple calculations instead of querying the google server for each one. From 170ac8ee17dae6162c77932260f8f5281ed4fe29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Wed, 25 Feb 2015 18:10:26 -0500 Subject: [PATCH 021/106] Add .bmp to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0670f65..f0724c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc *.egg -*.egg-info \ No newline at end of file +*.egg-info +*.dmp From cfc0f99ea921ff565526e61d3c2eeabf8f0d770a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Wed, 25 Feb 2015 19:58:44 -0500 Subject: [PATCH 022/106] Repair calculator method. --- google/google.py | 31 +++---- google/tests/test_calculator.html | 138 ++++++++++++++++++++++++++++++ google/tests/test_google.py | 14 ++- 3 files changed, 162 insertions(+), 21 deletions(-) create mode 100644 google/tests/test_calculator.html diff --git a/google/google.py b/google/google.py index f265795..a737739 100644 --- a/google/google.py +++ b/google/google.py @@ -4,6 +4,7 @@ import re import images import currency +from utils import get_html_from_dynamic_site try: import json @@ -72,11 +73,10 @@ def get_html(url): def write_html_to_file(html, filename): of = open(filename, "w") - of.write(html) - of.flush() + of.write(html.encode("utf-8")) + # of.flush() of.close() - # RESULT CLASSES class GoogleResult: @@ -251,20 +251,17 @@ def _get_shopping_url(query, page=0, per_page=10): @staticmethod def calculate(expr): - conn = httplib.HTTPSConnection("www.google.com") - req_url = "/ig/calculator?hl=en&q={0}".format(expr.replace(" ", "%20")) - headers = { - "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101"} - conn.request("GET", req_url, "", headers) - response = conn.getresponse() - j = response.read().decode("utf-8").replace(u"\xa0", "") - conn.close() - j = re.sub(r"{\s*'?(\w)", r'{"\1', j) - j = re.sub(r",\s*'?(\w)", r',"\1', j) - j = re.sub(r"(\w)'?\s*:", r'\1":', j) - j = re.sub(r":\s*'(\w)'\s*([,}])", r':"\1"\2', j) - js = json.loads(j) - return parse_calc_result(js["lhs"] + " = " + js["rhs"]) + url = Google._get_search_url(expr) + html = get_html_from_dynamic_site(url) + bs = BeautifulSoup(html) + + from_value = float(bs.find("input", {"id":"ucw_lhs_d"})["value"]) + to_value = float(bs.find("input", {"id":"ucw_rhs_d"})["value"]) + + gr = GoogleResult() + gr.value = to_value + + return gr if __name__ == "__main__": diff --git a/google/tests/test_calculator.html b/google/tests/test_calculator.html new file mode 100644 index 0000000..7247f0e --- /dev/null +++ b/google/tests/test_calculator.html @@ -0,0 +1,138 @@ + +157.3kg in grams - Google Search
    ×
    Get to Google faster. Switch your default search engine to Google.
    Sure
    No thanks
    Screen reader users, click here to turn off Google Instant.
    About 1,100 results (0.15 seconds) 
    Manhattan, New York, NY - From your Internet address - Use precise location
     - Learn more   
    \ No newline at end of file diff --git a/google/tests/test_google.py b/google/tests/test_google.py index b6344c6..abe43b2 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -7,15 +7,16 @@ class GoogleTest(unittest.TestCase): def setUp(self): + pass + + def test_search_images(self): + """Test method to search images.""" # replace method to get html with a test html file f = open('test_search_images.html', 'r') google.images.get_html_from_dynamic_site = \ Mock(return_value=f.read().decode('utf8')) - def test_search_images(self): - """Test method to search images.""" - res = google.Google.search_images("apple", num_images=10) self.assertEqual(len(res), 10) @@ -33,10 +34,15 @@ def test_convert_currency(self): euros = google.Google.convert_currency(5.0, "USD", "EUR") self.assertGreater(euros, 0.0) - @unittest.skip("skip") + # @unittest.skip("skip") def test_calculate(self): """Test method to calculate in google.""" + # replace method to get html with a test html file + f = open('test_calculator.html', 'r') + google.get_html_from_dynamic_site = \ + Mock(return_value=f.read().decode('utf8')) + calc = google.Google.calculate("157.3kg in grams") self.assertEqual(calc.value, 157300) From 5f1bb86b71b9b231b1b45898ad07afb1b75d8222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Thu, 26 Feb 2015 11:56:32 -0500 Subject: [PATCH 023/106] Relocate calculator methods in a new module. Create test. Create dummy private methods to implement later. --- TODO.md | 2 +- google/calculator.py | 64 +++++++++++++++++++ google/google.py | 103 ++---------------------------- google/images.py | 10 +-- google/tests/test_calculator.html | 10 +-- google/tests/test_calculator.py | 23 +++++++ google/tests/test_google.py | 2 +- google/utils.py | 34 +++++++++- 8 files changed, 132 insertions(+), 116 deletions(-) create mode 100644 google/calculator.py create mode 100644 google/tests/test_calculator.py diff --git a/TODO.md b/TODO.md index f006bb6..934ad79 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,7 @@ Todo list for Google-Search-API ## Next tasks -- [x] Mock html test for search images - https://pypi.python.org/pypi/mock/ +- [x] Mock html test for all search methods - https://pypi.python.org/pypi/mock/ - [ ] Test and implement the use of images options in search images method (is broken right now, in the middle of a refactor) - [ ] Refactor and split main module in separated ones - [ ] Write tests for future methods development (they should failed and be skipped up to the proper method development moment) diff --git a/google/calculator.py b/google/calculator.py new file mode 100644 index 0000000..b452f3c --- /dev/null +++ b/google/calculator.py @@ -0,0 +1,64 @@ +from utils import get_html_from_dynamic_site +from utils import _get_search_url +from bs4 import BeautifulSoup + + +class CalculatorResult: + + """Represents a result returned from google calculator.""" + + def __init__(self): + self.value = None + self.from_value = None + self.unit = None + self.from_unit = None + self.expr = None + self.result = None + self.fullstring = None + + +# PUBLIC +def calculate(expr): + url = _get_search_url(expr) + html = get_html_from_dynamic_site(url) + bs = BeautifulSoup(html) + + cr = CalculatorResult() + cr.value = _get_to_value(bs) + cr.from_value = _get_from_value(bs) + cr.unit = _get_to_unit(bs) + cr.from_unit = _get_from_unit(bs) + cr.expr = _get_expr(bs) + cr.result = _get_result(bs) + cr.fullstring = _get_fullstring(bs) + + return cr + + +# PRIVATE +def _get_to_value(bs): + return float(bs.find("input", {"id": "ucw_rhs_d"})["value"]) + + +def _get_from_value(bs): + return float(bs.find("input", {"id": "ucw_lhs_d"})["value"]) + + +def _get_to_unit(bs): + return None + + +def _get_from_unit(bs): + return None + + +def _get_expr(bs): + return None + + +def _get_result(bs): + return None + + +def _get_fullstring(bs): + return None diff --git a/google/google.py b/google/google.py index a737739..061cca0 100644 --- a/google/google.py +++ b/google/google.py @@ -1,15 +1,10 @@ from bs4 import BeautifulSoup -import httplib -import urllib2 import re import images import currency -from utils import get_html_from_dynamic_site +import calculator +from utils import write_html_to_file, _get_search_url, get_html, normalize_query -try: - import json -except ImportError: - import simplejson as json __author__ = "Anthony Casagrande , " + \ "Agustin Benassi " @@ -17,8 +12,6 @@ # GLOBAL METHODS -def normalize_query(query): - return query.strip().replace(":", "%3A").replace("+", "%2B").replace("&", "%26").replace(" ", "+") def add_to_tbs(tbs, name, value): @@ -28,29 +21,6 @@ def add_to_tbs(tbs, name, value): return "&tbs=%s:%s" % (name, value) -def parse_calc_result(string): - result = CalculatorResult() - result.fullstring = string - string = string.strip().replace(u"\xa0", " ") - if string.find("=") != -1: - result.expr = string[:string.rfind("=")].strip() - string = string[string.rfind("=") + 2:] - result.result = string - tokens = string.split(" ") - if len(tokens) > 0: - result.value = "" - for token in tokens: - if is_number(token): - result.value = result.value + token - else: - if result.unit: - result.unit = result.unit + " " + token - else: - result.unit = token - return result - return None - - def is_number(s): try: float(s) @@ -59,24 +29,6 @@ def is_number(s): return False -def get_html(url): - try: - request = urllib2.Request(url) - request.add_header( - "User-Agent", "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101") - html = urllib2.urlopen(request).read() - return html - except: - print "Error accessing:", url - return None - - -def write_html_to_file(html, filename): - of = open(filename, "w") - of.write(html.encode("utf-8")) - # of.flush() - of.close() - # RESULT CLASSES class GoogleResult: @@ -97,18 +49,6 @@ def __repr__(self): return "".join(list_google) -class CalculatorResult: - - """Represents a result returned from google calculator.""" - - def __init__(self): - self.value = None - self.unit = None - self.expr = None - self.result = None - self.fullstring = None - - class ShoppingResult: """Represents a shopping result.""" @@ -140,7 +80,7 @@ def search(query, pages=1): results = [] for i in range(pages): - url = Google._get_search_url(query, i) + url = _get_search_url(query, i) html = get_html(url) if html: if Google.DEBUG_MODE: @@ -166,27 +106,6 @@ def search(query, pages=1): j = j + 1 return results - @staticmethod - def _get_search_url(query, page=0, per_page=10): - # note: num per page might not be supported by google anymore (because of - # google instant) - return "http://www.google.com/search?hl=en&q=%s&start=%i&num=%i" % (normalize_query(query), page * per_page, per_page) - - @staticmethod - def calculate_old(expr): - url = Google._get_search_url(expr) - html = get_html(url) - if html: - soup = BeautifulSoup(html) - topstuff = soup.find("div", id="topstuff") - if topstuff: - a = topstuff.find("a") - if a and a["href"].find("calculator") != -1: - h2 = topstuff.find("h2") - if h2: - return parse_calc_result(h2.text) - return None - search_images_old = staticmethod(images.search_old) search_images = staticmethod(images.search) @@ -195,6 +114,8 @@ def calculate_old(expr): exchange_rate = staticmethod(currency.exchange_rate) + calculate = staticmethod(calculator.calculate) + @staticmethod def shopping(query, pages=1): results = [] @@ -249,20 +170,6 @@ def shopping(query, pages=1): def _get_shopping_url(query, page=0, per_page=10): return "http://www.google.com/search?hl=en&q={0}&tbm=shop&start={1}&num={2}".format(normalize_query(query), page * per_page, per_page) - @staticmethod - def calculate(expr): - url = Google._get_search_url(expr) - html = get_html_from_dynamic_site(url) - bs = BeautifulSoup(html) - - from_value = float(bs.find("input", {"id":"ucw_lhs_d"})["value"]) - to_value = float(bs.find("input", {"id":"ucw_rhs_d"})["value"]) - - gr = GoogleResult() - gr.value = to_value - - return gr - if __name__ == "__main__": import doctest diff --git a/google/images.py b/google/images.py index c25c162..77ff3e8 100644 --- a/google/images.py +++ b/google/images.py @@ -1,16 +1,8 @@ -from utils import get_html_from_dynamic_site +from utils import get_html_from_dynamic_site, write_html_to_file from bs4 import BeautifulSoup import urlparse -# GLOBAL AUXILIARY METHODS AND DATA -def write_html_to_file(html, filename): - of = open(filename, "w") - of.write(html.encode("utf-8")) - # of.flush() - of.close() - - IMAGE_FORMATS = ["bmp", "gif", "jpg", "png", "psd", "pspimage", "thm", "tif", "yuv", "ai", "drw", "eps", "ps", "svg", "tiff", "jpeg", "jif", "jfif", "jp2", "jpx", "j2k", "j2c", "fpx", diff --git a/google/tests/test_calculator.html b/google/tests/test_calculator.html index 7247f0e..b9afe82 100644 --- a/google/tests/test_calculator.html +++ b/google/tests/test_calculator.html @@ -1,5 +1,5 @@ -157.3kg in grams - Google Search
    ×
    Get to Google faster. Switch your default search engine to Google.
    Sure
    No thanks
    Screen reader users, click here to turn off Google Instant.
    Screen reader users, click here to turn off Google Instant.
    About 1,100 results (0.15 seconds) 
    About 1,040 results (0.57 seconds) 
    Manhattan, New York, NY - From your Internet address - Use precise location
     - Learn more   
    Manhattan, New York, NY - From your Internet address - Use precise location
     - Learn more   

     
    \ No newline at end of file diff --git a/google/tests/html_files/test_standard_search.html b/google/tests/html_files/test_standard_search.html new file mode 100644 index 0000000..2b9279a --- /dev/null +++ b/google/tests/html_files/test_standard_search.html @@ -0,0 +1,10 @@ +github - Google Search

     
    About 123,000,000 results
    \ No newline at end of file diff --git a/google/tests/test_calculator.py b/google/tests/test_calculator.py deleted file mode 100644 index 45fadc9..0000000 --- a/google/tests/test_calculator.py +++ /dev/null @@ -1,23 +0,0 @@ -import unittest -import nose -from google import calculator -from mock import Mock - - -class CalculatorTest(unittest.TestCase): - - def test_calculate(self): - """Test method to use google as a calculator.""" - - # replace method to get html with a test html file - f = open('test_calculator.html', 'r') - calculator.get_html_from_dynamic_site = \ - Mock(return_value=f.read().decode('utf8')) - - calc = calculator.calculate("157.3kg in grams") - self.assertEqual(calc.value, 157300) - - -if __name__ == '__main__': - # nose.main() - nose.run(defaultTest=__name__) diff --git a/google/tests/test_currency.py b/google/tests/test_currency.py deleted file mode 100644 index e4a4971..0000000 --- a/google/tests/test_currency.py +++ /dev/null @@ -1,43 +0,0 @@ -import unittest -import nose -from google import currency - - -class ConvertCurrencyTest(unittest.TestCase): - - def test_convert(self): - """Test method to convert currency in currency module.""" - - euros = currency.convert(5.0, "USD", "EUR") - self.assertGreater(euros, 0.0) - - # @unittest.skip("skip") - def test_exchange_rate(self): - """Test method to get an exchange rate in currency module.""" - - usd_to_eur = currency.exchange_rate("USD", "EUR") - self.assertGreater(usd_to_eur, 0.0) - - # @unittest.skip("skip") - def test_get_currency_req_url(self): - """Test method to get currency conversion request url.""" - - amount = 10 - from_currency = "USD" - to_currency = "EUR" - req_url = currency._get_currency_req_url(amount, from_currency, - to_currency) - - expected_req_url = "https://www.google.com/finance/converter?a=10&from=USD&to=EUR" - - self.assertEqual(req_url, expected_req_url) - - @unittest.skip("skip") - def test_parse_currency_response(self): - """Test method to parse currency response. TODO!""" - pass - - -if __name__ == '__main__': - # nose.main() - nose.run(defaultTest=__name__) diff --git a/google/tests/test_google.py b/google/tests/test_google.py index e14e5c3..535e09b 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -1,18 +1,18 @@ import unittest import nose from google import google +from google import currency, images from mock import Mock +import os class GoogleTest(unittest.TestCase): - def setUp(self): - pass - + # @unittest.skip("skip") def test_search_images(self): """Test method to search images.""" - # replace method to get html with a test html file + # replace method to get html from a test html file f = open('test_search_images.html', 'r') google.images.get_html_from_dynamic_site = \ Mock(return_value=f.read().decode('utf8')) @@ -20,10 +20,27 @@ def test_search_images(self): res = google.search_images("apple", num_images=10) self.assertEqual(len(res), 10) + # @unittest.skip("skip") + def test_calculate(self): + """Test method to calculate in google.""" + + # replace method to get html from a test html file + f = open('test_calculator.html', 'r') + google.calculator.get_html_from_dynamic_site = \ + Mock(return_value=f.read().decode('utf8')) + + calc = google.calculate("157.3kg in grams") + self.assertEqual(calc.value, 157300) + # @unittest.skip("skip") def test_exchange_rate(self): """Test method to get an exchange rate in google.""" + # replace method to get html from a test html file + f = open('test_exchange_rate.html', 'r') + google.currency.get_html = \ + Mock(return_value=f.read().decode('utf8')) + usd_to_eur = google.exchange_rate("USD", "EUR") self.assertGreater(usd_to_eur, 0.0) @@ -31,34 +48,77 @@ def test_exchange_rate(self): def test_convert_currency(self): """Test method to convert currency in google.""" - euros = google.convert_currency(5.0, "USD", "EUR") - self.assertGreater(euros, 0.0) - - # @unittest.skip("skip") - def test_calculate(self): - """Test method to calculate in google.""" - - # replace method to get html with a test html file - f = open('test_calculator.html', 'r') - google.calculator.get_html_from_dynamic_site = \ + # replace method to get html from a test html file + f = open('test_convert_currency.html', 'r') + google.currency.get_html = \ Mock(return_value=f.read().decode('utf8')) - calc = google.calculate("157.3kg in grams") - self.assertEqual(calc.value, 157300) + euros = google.convert_currency(5.0, "USD", "EUR") + self.assertGreater(euros, 0.0) def test_search(self): """Test method to search in google.""" + # replace method to get html from a test html file + f = open('test_standard_search.html', 'r') + google.standard_search.get_html = \ + Mock(return_value=f.read().decode('utf8')) + search = google.search("github") self.assertNotEqual(len(search), 0) def test_shopping(self): """Test method for google shopping.""" + # replace method to get html from a test html file + f = open('test_shopping_search.html', 'r') + google.shopping_search.get_html = \ + Mock(return_value=f.read().decode('utf8')) + shop = google.shopping("Disgaea 4") self.assertNotEqual(len(shop), 0) +class ConvertCurrencyTest(unittest.TestCase): + + # @unittest.skip("skip") + def test_get_currency_req_url(self): + """Test method to get currency conversion request url.""" + + amount = 10 + from_currency = "USD" + to_currency = "EUR" + req_url = currency._get_currency_req_url(amount, from_currency, + to_currency) + + exp_req_url = "https://www.google.com/finance/converter?a=10&from=USD&to=EUR" + + self.assertEqual(req_url, exp_req_url) + + @unittest.skip("skip") + def test_parse_currency_response(self): + """Test method to parse currency response. TODO!""" + pass + + +class SearchImagesTest(unittest.TestCase): + + def test_get_images_req_url(self): + + query = "banana" + options = images.ImageOptions() + options.image_type = images.ImageType.CLIPART + options.larger_than = images.LargerThan.MP_4 + options.color = "green" + + req_url = images._get_images_req_url(query, options) + + exp_req_url = 'https://www.google.com.ar/search?q=banana&es_sm=122&source=lnms&tbm=isch&sa=X&ei=DDdUVL-fE4SpNq-ngPgK&ved=0CAgQ_AUoAQ&biw=1024&bih=719&dpr=1.25&tbs=itp:clipart,isz:lt,islt:4mp,ic:specific,isc:green' + + self.assertEqual(req_url, exp_req_url) + + if __name__ == '__main__': # nose.main() + os.chdir("./html_files") nose.run(defaultTest=__name__) From 6a91c96873279fe53c1b74bf2b75ffce9a7fbe0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Fri, 27 Feb 2015 14:08:49 -0500 Subject: [PATCH 027/106] Skip all tests --- build/lib/google/__init__.py | 2 ++ build/lib/google/google.py | 23 +++++++++++++++++++++++ dist/Google-Search-API-1.1.0.zip | Bin 0 -> 2915 bytes google/tests/test_google.py | 6 +++--- 4 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 build/lib/google/__init__.py create mode 100644 build/lib/google/google.py create mode 100644 dist/Google-Search-API-1.1.0.zip diff --git a/build/lib/google/__init__.py b/build/lib/google/__init__.py new file mode 100644 index 0000000..70578ec --- /dev/null +++ b/build/lib/google/__init__.py @@ -0,0 +1,2 @@ +from modules import calculator, currency, images, utils +from modules import standard_search, shopping_search diff --git a/build/lib/google/google.py b/build/lib/google/google.py new file mode 100644 index 0000000..f17c2ed --- /dev/null +++ b/build/lib/google/google.py @@ -0,0 +1,23 @@ +from modules import images +from modules import currency +from modules import calculator +from modules import standard_search +from modules import shopping_search + +__author__ = "Anthony Casagrande , " + \ + "Agustin Benassi " +__version__ = "1.1.0" + + +"""Defines the public inteface of the API.""" + +search = standard_search.search +search_images = images.search +convert_currency = currency.convert +exchange_rate = currency.exchange_rate +calculate = calculator.calculate +shopping = shopping_search.shopping + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/dist/Google-Search-API-1.1.0.zip b/dist/Google-Search-API-1.1.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..3a8cc5a00612ec8fcd179b40748b1fea9f657dd1 GIT binary patch literal 2915 zcmWIWW@Zs#U|`^2V9t+rQxVZ!at_EdWMp7a0MhRH`RO^Sy1}W5Madbujsc#!hI)p2 z2KoWs?z*0SZvMIb=lu>F@U*|5_=&HsAvN{-91Vru&b2;L_2=@kgS>r%RNp1$`(JCk z(KtEv_3xjX`BiQ#Uhs3l6iYVl*{6KIG}2aZ#1W zcI|?);%P~hJC4;SI?GjVxlr_~@KJSnrv4()1kXPw{vC^YyL0h`(}nGKXNUPu*?nZP z&i=jT`e$FKFTPsn^0VvY{hob{0VrY7%OCu@3m68V@KD4P7R9M0r3HG)Y3aIsfqaJy zINI*d{=&E0p;zv}WSz9k@U|n%bf%oGsE+>F`<}UOhPHV?#x1#fa}_$JU#9Q!H-B%G zv4qL8fy3MQr&?{>)ZZB{245F%IQ})2Yv#rSH~iVldSAgtzs{p zd1m(adp6I`?hX_vnsH-iV~eEv^PeqGBIhkV-`Xme{qw;bxeYaLA6DtlUOrdnlh2ZJ z7yn~7qw>sepS{|}ZhvJ{`n(srR&9A`Cu4T1S5)Q#)5MkkKE9bS#avkZzObW&c1h40 z6OKF9@u!a+%d$UeE0Dpz?XZ)Hm4N%%yP3D+1G|-HNPI}BVV}J7YO(cmZ5EfOmXEpC z|2n9#MI`^zj_R2gS?4?KZFXA~|J3sNzvCvdo4@`L?D3a*&5{4=XVUNdY_~_#njXHe zJOB7mqQlX1r{iU!dd}Hx5?DFylG#JFcS#StO7IWpSq{P|m6rod3FW@2E_z>_4> z!P!P1Lg^J$mIfXKWs|+(uk0f%8yy69FKP^XaHyBxq3h0-T;;8vQI(*Qf_=37463U^QvYrFv#T}b!1bq6;cRO zXR#186{*NVHWyn7gD}^W6}Pz%Ul3j1pj(Pw+#oESOP-~$!Ux?V^bCuz=m;BGzC+5s p=ysv!C4^m5f!PaJyyFQ;l$;gd%?fle0|O@z1_IO3A5fNM000M;FGv6Y literal 0 HcmV?d00001 diff --git a/google/tests/test_google.py b/google/tests/test_google.py index 535e09b..24ecffa 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -5,7 +5,7 @@ from mock import Mock import os - +@unittest.skip("skip") class GoogleTest(unittest.TestCase): # @unittest.skip("skip") @@ -78,7 +78,7 @@ def test_shopping(self): shop = google.shopping("Disgaea 4") self.assertNotEqual(len(shop), 0) - +@unittest.skip("skip") class ConvertCurrencyTest(unittest.TestCase): # @unittest.skip("skip") @@ -100,7 +100,7 @@ def test_parse_currency_response(self): """Test method to parse currency response. TODO!""" pass - +@unittest.skip("skip") class SearchImagesTest(unittest.TestCase): def test_get_images_req_url(self): From 50d8f17d3a3c79434ee87b914c7607e52594f619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Sat, 28 Feb 2015 20:00:49 -0500 Subject: [PATCH 028/106] Restore tests. --- TODO.md | 1 + google/tests/test_google.py | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/TODO.md b/TODO.md index b4bc762..f8fd169 100644 --- a/TODO.md +++ b/TODO.md @@ -10,6 +10,7 @@ Todo list for Google-Search-API - [ ] Be able to manage both Chrome and Firefox, and maybe Ie too, depending in what browser the user has - [ ] Write a full suite of docs test and unit tests - [ ] Reconvert all comments following the google python style guide - https://google-styleguide.googlecode.com/svn/trunk/pyguide.html +- Decorator para load html files ## Stuff to check out later on diff --git a/google/tests/test_google.py b/google/tests/test_google.py index 24ecffa..3a6ba52 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -3,9 +3,8 @@ from google import google from google import currency, images from mock import Mock -import os -@unittest.skip("skip") + class GoogleTest(unittest.TestCase): # @unittest.skip("skip") @@ -13,7 +12,7 @@ def test_search_images(self): """Test method to search images.""" # replace method to get html from a test html file - f = open('test_search_images.html', 'r') + f = open('./html_files/test_search_images.html', 'r') google.images.get_html_from_dynamic_site = \ Mock(return_value=f.read().decode('utf8')) @@ -25,7 +24,7 @@ def test_calculate(self): """Test method to calculate in google.""" # replace method to get html from a test html file - f = open('test_calculator.html', 'r') + f = open('./html_files/test_calculator.html', 'r') google.calculator.get_html_from_dynamic_site = \ Mock(return_value=f.read().decode('utf8')) @@ -37,7 +36,7 @@ def test_exchange_rate(self): """Test method to get an exchange rate in google.""" # replace method to get html from a test html file - f = open('test_exchange_rate.html', 'r') + f = open('./html_files/test_exchange_rate.html', 'r') google.currency.get_html = \ Mock(return_value=f.read().decode('utf8')) @@ -49,7 +48,7 @@ def test_convert_currency(self): """Test method to convert currency in google.""" # replace method to get html from a test html file - f = open('test_convert_currency.html', 'r') + f = open('./html_files/test_convert_currency.html', 'r') google.currency.get_html = \ Mock(return_value=f.read().decode('utf8')) @@ -60,7 +59,7 @@ def test_search(self): """Test method to search in google.""" # replace method to get html from a test html file - f = open('test_standard_search.html', 'r') + f = open('./html_files/test_standard_search.html', 'r') google.standard_search.get_html = \ Mock(return_value=f.read().decode('utf8')) @@ -71,14 +70,16 @@ def test_shopping(self): """Test method for google shopping.""" # replace method to get html from a test html file - f = open('test_shopping_search.html', 'r') + f = open('./html_files/test_shopping_search.html', 'r') google.shopping_search.get_html = \ Mock(return_value=f.read().decode('utf8')) shop = google.shopping("Disgaea 4") self.assertNotEqual(len(shop), 0) -@unittest.skip("skip") +# @unittest.skip("skip") + + class ConvertCurrencyTest(unittest.TestCase): # @unittest.skip("skip") @@ -95,12 +96,14 @@ def test_get_currency_req_url(self): self.assertEqual(req_url, exp_req_url) - @unittest.skip("skip") + # @unittest.skip("skip") def test_parse_currency_response(self): """Test method to parse currency response. TODO!""" pass -@unittest.skip("skip") +# @unittest.skip("skip") + + class SearchImagesTest(unittest.TestCase): def test_get_images_req_url(self): @@ -120,5 +123,4 @@ def test_get_images_req_url(self): if __name__ == '__main__': # nose.main() - os.chdir("./html_files") nose.run(defaultTest=__name__) From ed77bfdaff416ba93c08087693503088763b6ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Sun, 1 Mar 2015 23:34:25 -0500 Subject: [PATCH 029/106] Change way to load html files in tests, so they can pass travel CI. --- google/tests/test_google.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/google/tests/test_google.py b/google/tests/test_google.py index 3a6ba52..f7feeda 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -3,16 +3,29 @@ from google import google from google import currency, images from mock import Mock +import os + + +def load_html_file(file_name): + + dir_path = os.path.join(os.path.dirname(__file__), "html_files") + # file_name = func.__name__ + ".html" + file_path = os.path.join(dir_path, file_name) + + f = open(file_path, "r") + + return f class GoogleTest(unittest.TestCase): # @unittest.skip("skip") + # @load_html_file def test_search_images(self): """Test method to search images.""" # replace method to get html from a test html file - f = open('./html_files/test_search_images.html', 'r') + f = load_html_file("test_search_images.html") google.images.get_html_from_dynamic_site = \ Mock(return_value=f.read().decode('utf8')) @@ -24,7 +37,7 @@ def test_calculate(self): """Test method to calculate in google.""" # replace method to get html from a test html file - f = open('./html_files/test_calculator.html', 'r') + f = load_html_file("test_calculator.html") google.calculator.get_html_from_dynamic_site = \ Mock(return_value=f.read().decode('utf8')) @@ -36,7 +49,7 @@ def test_exchange_rate(self): """Test method to get an exchange rate in google.""" # replace method to get html from a test html file - f = open('./html_files/test_exchange_rate.html', 'r') + f = load_html_file('test_exchange_rate.html') google.currency.get_html = \ Mock(return_value=f.read().decode('utf8')) @@ -48,7 +61,7 @@ def test_convert_currency(self): """Test method to convert currency in google.""" # replace method to get html from a test html file - f = open('./html_files/test_convert_currency.html', 'r') + f = load_html_file('test_convert_currency.html') google.currency.get_html = \ Mock(return_value=f.read().decode('utf8')) @@ -59,7 +72,7 @@ def test_search(self): """Test method to search in google.""" # replace method to get html from a test html file - f = open('./html_files/test_standard_search.html', 'r') + f = load_html_file('test_standard_search.html',) google.standard_search.get_html = \ Mock(return_value=f.read().decode('utf8')) @@ -70,7 +83,7 @@ def test_shopping(self): """Test method for google shopping.""" # replace method to get html from a test html file - f = open('./html_files/test_shopping_search.html', 'r') + f = load_html_file('test_shopping_search.html') google.shopping_search.get_html = \ Mock(return_value=f.read().decode('utf8')) From 7d9c471e3b8da8140872db1873c57a966b2e6ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Sun, 1 Mar 2015 23:39:21 -0500 Subject: [PATCH 030/106] Not passing python 2.6 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 29202ff..9ee4145 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python sudo: false python: - - '2.6' - '2.7' - pypy deploy: From ab4d7b7a80f2708d020a5ae113bddb099ae589cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Mon, 2 Mar 2015 12:04:25 -0500 Subject: [PATCH 031/106] Clean up a little bit the standard search module. Modify README file to show what is done what is still to be done. --- README.md | 26 ++++-- google/modules/standard_search.py | 127 +++++++++++++++++++++++++----- 2 files changed, 127 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 783a477..ae5370b 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Google-Search-API folder. ```python import os os.chdir("C:\Path_where_repo_is") -import google +from google import google ``` ## Google Web Search @@ -44,22 +44,27 @@ You can search google web in the following way: ```python from google import google -search_results = google.search("This is my query") +num_page = 3 +search_results = google.search("This is my query", num_page) ``` -`search_results` will contain a list of `GoogleResult` objects +`search_results` will contain a list of `GoogleResult` objects. num_page parameter is optional (default is 1 page) ```python GoogleResult: self.name # The title of the link - self.link # The link url + self.link # The external link (NOT implemented yet) + self.google_link # The google link self.description # The description of the link - self.thumb # The link to a thumbnail of the website (not implemented yet) - self.cached # A link to the cached version of the page + self.thumb # The link to a thumbnail of the website (NOT implemented yet) + self.cached # A link to the cached version of the page (NOT implemented yet) self.page # What page this result was on (When searching more than one page) self.index # What index on this page it was on ``` +*Description text parsing has some encoding problems to be resolved.* +*Only google link of the search is being parsed right now, parse the external link is an implementation priority.* + ## Google Calculator Attempts to search google calculator for the result of an expression. Returns a `CalculatorResult` if successful or `None` if it fails. @@ -224,3 +229,12 @@ google.convert_currency(5.0, "USD", "EUR") ``` As a side note, `convert_currency` is always more accurate than performing your own math on `exchange_rate` because of possible rounding errors. However if you have more than one value to convert it is best to call `exchange_rate` and cache the result to use for multiple calculations instead of querying the google server for each one. + + +## Contributions + +All contributions are very welcome! As you have seen, there is still some methods that are not implemented. The structure of the package is intended to facilitate that you can contribute implementing or improving any method without changing other code. + +Other interesting things that you may do is to build a good command line interface for the package. You can also take a look to the [TODO list](https://github.com/abenassi/Google-Search-API/blob/master/TODO.md) + +For all contributions, we intend to follow the [Google Ptyhon Style Guide](https://google-styleguide.googlecode.com/svn/trunk/pyguide.html) diff --git a/google/modules/standard_search.py b/google/modules/standard_search.py index 4c398ce..3bdf08f 100644 --- a/google/modules/standard_search.py +++ b/google/modules/standard_search.py @@ -1,5 +1,6 @@ from utils import _get_search_url, get_html from bs4 import BeautifulSoup +import urlparse class GoogleResult: @@ -7,44 +8,130 @@ class GoogleResult: """Represents a google search result.""" def __init__(self): - self.name = None - self.link = None - self.description = None - self.thumb = None - self.cached = None - self.page = None - self.index = None + self.name # The title of the link + self.link # The external link + self.google_link # The google link + self.description # The description of the link + self.thumb # Link to a thumbnail of the website (NOT implemented yet) + self.cached # Link to cached version of the page (NOT implemented yet) + self.page # Page this result was on (When searching more than one) + self.index # What index on this page it was on def __repr__(self): - list_google = ["Name: ", self.name, - "\nLink: ", self.link] + name = self._limit_str_size(self.name, 55) + description = self._limit_str_size(self.description, 49) + + list_google = ["GoogleResult(", + "name={}".format(name), "\n", " " * 13, + "description={}".format(description)] + return "".join(list_google) + def _limit_str_size(self, str_element, size_limit): + """Limit the characters of the string, adding .. at the end.""" + + if not str_element: + return None + + elif len(str_element) > size_limit: + return str_element.decode("utf-8")[:size_limit] + ".." + + else: + return str_element.decode("utf-8") + +# PUBLIC def search(query, pages=1): - """Returns a list of GoogleResult.""" + """Returns a list of GoogleResult. + + Args: + query: String to search in google. + pages: Number of pages where results must be taken. + + Returns: + A GoogleResult object.""" results = [] for i in range(pages): url = _get_search_url(query, i) html = get_html(url) + if html: soup = BeautifulSoup(html) lis = soup.findAll("li", attrs={"class": "g"}) + j = 0 for li in lis: res = GoogleResult() res.page = i res.index = j - a = li.find("a") - res.name = a.text.strip() - res.link = a["href"] - if res.link.startswith("/search?"): - # this is not an external link, so skip it - continue - sdiv = li.find("div", attrs={"class": "s"}) - if sdiv: - res.description = sdiv.text.strip() + + res.name = _get_name(li) + res.link = _get_link(li) + res.google_link = _get_google_link(li) + res.description = _get_description(li) + res.thumb = _get_description() + res.cached = _get_description() + results.append(res) - j = j + 1 + j += 1 + return results + + +# PRIVATE +def _get_name(li): + """Return the name of a google search.""" + a = li.find("a") + return a.text.strip() + + +def _get_link(li): + """Return external link from a search.""" + + a = li.find("a") + link = a["href"] + + if link.startswith("/url?") or link.startswith("/search?"): + return None + + else: + return link + + +def _get_google_link(li): + """Return google link from a search.""" + + a = li.find("a") + link = a["href"] + + if link.startswith("/url?") or link.startswith("/search?"): + return urlparse.urljoin("http://www.google.com", link) + + else: + return None + + +def _get_description(li): + """Return the description of a google search. + + TODO: There are some text encoding problems to resovle.""" + + sdiv = li.find("div", attrs={"class": "s"}) + if sdiv: + stspan = sdiv.find("span", attrs={"class": "st"}) + + return stspan.text.encode("utf-8").strip() + + else: + return None + + +def _get_thumb(): + """Return the link to a thumbnail of the website.""" + pass + + +def _get_cached(): + """Return a link to the cached version of the page.""" + pass From 52fafe3fff9bebd486748a2f682a6eb9671f535d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Mon, 2 Mar 2015 12:23:20 -0500 Subject: [PATCH 032/106] Clean up a little bit the calculator search module. Modify README file to show what is done what is still to be done. --- README.md | 27 +++++++++------------------ google/modules/calculator.py | 25 ++++++++++++++++++------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index ae5370b..319bf1b 100644 --- a/README.md +++ b/README.md @@ -75,26 +75,17 @@ google.calculate("157.3kg in grams") ``` ```python -{'expr': u'157.3 kilograms', - 'fullstring': u'157.3 kilograms = 157\xa0300 grams', - 'result': u'157 300 grams', - 'unit': u'grams', - 'value': u'157300'} +CalculatorResult + self.value = None # Result value (eg. 157300.0) + self.from_value = None # Initial value (eg. 157.3) + self.unit = None # Result unit (eg. u'grams') (NOT implemented yet) + self.from_unit = None # Initial unit (eg. u'kilograms') (NOT implemented yet) + self.expr = None # Initial expression (eg. u'157.3 grams') (NOT implemented yet) + self.result = None # Result expression (eg. u'157300 kilograms') (NOT implemented yet) + self.fullstring = None # Result unit (eg. u'157.3 kilograms = 157300 grams') (NOT implemented yet) ``` - -```python -from google import google -google.calculate("cos(25 pi) / 17.4") -``` - -```python -{'expr': u'cos(25 * pi) / 17.4', - 'fullstring': u'cos(25 * pi) / 17.4 = -0.0574712644', - 'result': u'-0.0574712644', - 'unit': None, - 'value': u'-0.0574712644'} -``` +*Parsing of the units must be implemented. The rest of the data members of CalculatorResult can be build from the values and units of the calculation.* ## Google Image Search Searches google images for a list of images. Image searches can be filtered to produce better results. diff --git a/google/modules/calculator.py b/google/modules/calculator.py index b452f3c..1e1f2e6 100644 --- a/google/modules/calculator.py +++ b/google/modules/calculator.py @@ -8,17 +8,28 @@ class CalculatorResult: """Represents a result returned from google calculator.""" def __init__(self): - self.value = None - self.from_value = None - self.unit = None - self.from_unit = None - self.expr = None - self.result = None - self.fullstring = None + self.value = None # Result value (eg. 157300.0) + self.from_value = None # Initial value (eg. 157.3) + self.unit = None # Result unit (eg. u'grams') (NOT implemented yet) + self.from_unit = None # Initial unit (eg. u'kilograms') (NOT implemented yet) + self.expr = None # Initial expression (eg. u'157.3 grams') (NOT implemented yet) + self.result = None # Result expression (eg. u'157300 kilograms') (NOT implemented yet) + self.fullstring = None # Complete expression (eg. u'157.3 kilograms = 157300 grams') (NOT implemented yet) # PUBLIC def calculate(expr): + """Search for a calculation expression in google. + + Attempts to search google calculator for the result of an expression. + Returns a `CalculatorResult` if successful or `None` if it fails. + + Args: + expr: Calculation expression (eg. "cos(25 pi) / 17.4") + + Returns: + CalculatorResult object.""" + url = _get_search_url(expr) html = get_html_from_dynamic_site(url) bs = BeautifulSoup(html) From 42b96a8dcbd968437ea4568a865e564bf35b683c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Mon, 2 Mar 2015 12:29:29 -0500 Subject: [PATCH 033/106] Fix bugs. --- google/modules/standard_search.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/google/modules/standard_search.py b/google/modules/standard_search.py index 3bdf08f..08cb67d 100644 --- a/google/modules/standard_search.py +++ b/google/modules/standard_search.py @@ -8,14 +8,14 @@ class GoogleResult: """Represents a google search result.""" def __init__(self): - self.name # The title of the link - self.link # The external link - self.google_link # The google link - self.description # The description of the link - self.thumb # Link to a thumbnail of the website (NOT implemented yet) - self.cached # Link to cached version of the page (NOT implemented yet) - self.page # Page this result was on (When searching more than one) - self.index # What index on this page it was on + self.name = None # The title of the link + self.link = None # The external link + self.google_link = None # The google link + self.description = None # The description of the link + self.thumb = None # Thumbnail link of website (NOT implemented yet) + self.cached = None # Cached version link of page (NOT implemented yet) + self.page = None # Results page this one was on + self.index = None # What index on this page it was on def __repr__(self): name = self._limit_str_size(self.name, 55) @@ -70,8 +70,8 @@ def search(query, pages=1): res.link = _get_link(li) res.google_link = _get_google_link(li) res.description = _get_description(li) - res.thumb = _get_description() - res.cached = _get_description() + res.thumb = _get_thumb() + res.cached = _get_cached() results.append(res) j += 1 From e8b1d50d3f7653400aab94bfdcac6ebbabfdf4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Mon, 2 Mar 2015 14:53:07 -0500 Subject: [PATCH 034/106] Update images readme file and relocate a method. --- README.md | 49 ++++++++++++++++++++++++-------------- google/modules/images.py | 51 +++++++++++++++++++++++++--------------- 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 319bf1b..2b1f75e 100644 --- a/README.md +++ b/README.md @@ -76,19 +76,19 @@ google.calculate("157.3kg in grams") ```python CalculatorResult - self.value = None # Result value (eg. 157300.0) - self.from_value = None # Initial value (eg. 157.3) - self.unit = None # Result unit (eg. u'grams') (NOT implemented yet) - self.from_unit = None # Initial unit (eg. u'kilograms') (NOT implemented yet) - self.expr = None # Initial expression (eg. u'157.3 grams') (NOT implemented yet) - self.result = None # Result expression (eg. u'157300 kilograms') (NOT implemented yet) - self.fullstring = None # Result unit (eg. u'157.3 kilograms = 157300 grams') (NOT implemented yet) + value = None # Result value (eg. 157300.0) + from_value = None # Initial value (eg. 157.3) + unit = None # Result unit (eg. u'grams') (NOT implemented yet) + from_unit = None # Initial unit (eg. u'kilograms') (NOT implemented yet) + expr = None # Initial expression (eg. u'157.3 grams') (NOT implemented yet) + result = None # Result expression (eg. u'157300 kilograms') (NOT implemented yet) + fullstring = None # Result unit (eg. u'157.3 kilograms = 157300 grams') (NOT implemented yet) ``` *Parsing of the units must be implemented. The rest of the data members of CalculatorResult can be build from the values and units of the calculation.* ## Google Image Search -Searches google images for a list of images. Image searches can be filtered to produce better results. +Searches google images for a list of images. Image searches can be filtered to produce better results. Image searches can be downloaded. Perform a google image search on "banana" and filter it: @@ -104,18 +104,23 @@ results = google.search_images("banana", options) Sample Result: ```python -{'domain': u'exitrealworld.com', - 'filesize': u'4054k', - 'format': u'jpg', - 'height': u'3103', - 'index': 0, - 'link': u'http://www.exitrealworld.com/tools_v2/resources/9e55471ba84686ade677ffe595c45992/upload_images/YELLOW_BANANA.jpg', - 'name': u'Lib Tech Skate Banana BTX', - 'page': 0, - 'thumb': u'http://t3.gstatic.com/images?q=tbn:ANd9GcRzvAUW0en9eZTag3giWelcQ_xbrnBMXVChb3RU3v4HtEgxN3RMS0bSdidf', - 'width': u'3104'} +{'domain': 'shop.tradedoubler.com', + 'filesize': None, + 'format': None, + 'height': '2000', + 'index': 15, + 'link': 'http://tesco.scene7.com/is/image/tesco/210-8446_PI_1000013MN%3Fwid%3D2000%26hei%3D2000', + 'name': None, + 'page': 1, + 'site': 'http://shop.tradedoubler.com/shop/uk-01/a/2058674/productName/banana/sortBy/price/sortReverse/false', + 'thumb': 'https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcS8JPH_bgyvvyf5X67k32ZZYjf9MlWlxHIEXXxi91TVrNafpokI', + 'thumb_height': '199px', + 'thumb_width': '199px', + 'width': '2000'} ``` +*filesize is to be implemented. format works, but sometimes the link of the image doesn't show the format. Google images right now seems to not have a names, so the method for that is not implemented.* + Filter options: ```python @@ -172,6 +177,14 @@ class ColorType: SPECIFIC = "specific" ``` +You can download a list of images. + +```python +images.download(image_results, path = "path/to/download/images") +``` + +Path is an optional argument, if you don't specify a path, images will be downloaded to an "images" folder inside the working directory. + ## Google Currency Converter (Exchange Rates) Convert between one currency and another using google calculator. Results are real time and can change at any time based on the current exchange rate according to google. diff --git a/google/modules/images.py b/google/modules/images.py index 368273c..fb0acd3 100644 --- a/google/modules/images.py +++ b/google/modules/images.py @@ -4,6 +4,7 @@ import sys import requests import shutil +import os IMAGE_FORMATS = ["bmp", "gif", "jpg", "png", "psd", "pspimage", "thm", @@ -69,30 +70,39 @@ def __init__(self): self.color_type = None self.color = None + def __repr__(self): + return self.__dict__ + def get_tbs(self): tbs = None if self.image_type: # clipart - tbs = _add_to_tbs(tbs, "itp", self.image_type) + tbs = self._add_to_tbs(tbs, "itp", self.image_type) if self.size_category and not (self.larger_than or (self.exact_width and self.exact_height)): # i = icon, l = large, m = medium, lt = larger than, ex = exact - tbs = _add_to_tbs(tbs, "isz", self.size_category) + tbs = self._add_to_tbs(tbs, "isz", self.size_category) if self.larger_than: # qsvga,4mp - tbs = _add_to_tbs(tbs, "isz", SizeCategory.LARGER_THAN) - tbs = _add_to_tbs(tbs, "islt", self.larger_than) + tbs = self._add_to_tbs(tbs, "isz", SizeCategory.LARGER_THAN) + tbs = self._add_to_tbs(tbs, "islt", self.larger_than) if self.exact_width and self.exact_height: - tbs = _add_to_tbs(tbs, "isz", SizeCategory.EXACTLY) - tbs = _add_to_tbs(tbs, "iszw", self.exact_width) - tbs = _add_to_tbs(tbs, "iszh", self.exact_height) + tbs = self._add_to_tbs(tbs, "isz", SizeCategory.EXACTLY) + tbs = self._add_to_tbs(tbs, "iszw", self.exact_width) + tbs = self._add_to_tbs(tbs, "iszh", self.exact_height) if self.color_type and not self.color: # color = color, gray = black and white, specific = user defined - tbs = _add_to_tbs(tbs, "ic", self.color_type) + tbs = self._add_to_tbs(tbs, "ic", self.color_type) if self.color: - tbs = _add_to_tbs(tbs, "ic", ColorType.SPECIFIC) - tbs = _add_to_tbs(tbs, "isc", self.color) + tbs = self._add_to_tbs(tbs, "ic", ColorType.SPECIFIC) + tbs = self._add_to_tbs(tbs, "isc", self.color) return tbs + def _add_to_tbs(self, tbs, name, value): + if tbs: + return "%s,%s:%s" % (tbs, name, value) + else: + return "&tbs=%s:%s" % (name, value) + class ImageResult: @@ -128,7 +138,7 @@ def __repr__(self): "domain={}, link={})".format(self.domain, self.link) return string - def download(self, path="download"): + def download(self, path="images"): """Download an image to a given path.""" self._create_path(path) @@ -197,13 +207,6 @@ def _create_path(self, path): # PRIVATE -def _add_to_tbs(tbs, name, value): - if tbs: - return "%s,%s:%s" % (tbs, name, value) - else: - return "&tbs=%s:%s" % (name, value) - - def _parse_image_format(image_link): """Parse an image format from a download link. @@ -255,6 +258,14 @@ def _find_divs_with_images(soup): return div_container.find_all("div", {"class": "rg_di"}) +def _get_name(): + pass + + +def _get_filesize(): + pass + + def _get_image_data(res, a): """Parse image data and write it to an ImageResult object. @@ -265,12 +276,14 @@ def _get_image_data(res, a): google_middle_link = a["href"] url_parsed = urlparse.urlparse(google_middle_link) qry_parsed = urlparse.parse_qs(url_parsed.query) + res.name = _get_name() res.link = qry_parsed["imgurl"][0] res.format = _parse_image_format(res.link) res.width = qry_parsed["w"][0] res.height = qry_parsed["h"][0] res.site = qry_parsed["imgrefurl"][0] res.domain = urlparse.urlparse(res.site).netloc + res.filesize = _get_filesize() def _get_thumb_data(res, img): @@ -403,7 +416,7 @@ def search(query, image_options=None, num_images=50): if curr_num_img >= num_images: break - return results + return list(results) def download(image_results, path=None): From 729603754269680a0e50a05029ce5dd37a820aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Mon, 2 Mar 2015 16:31:02 -0500 Subject: [PATCH 035/106] Improving tests with decorators. --- TODO.md | 8 ++++---- google/tests/__init__.py | 0 google/tests/test_google.py | 17 ++++++++++++++--- 3 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 google/tests/__init__.py diff --git a/TODO.md b/TODO.md index f8fd169..0c7f799 100644 --- a/TODO.md +++ b/TODO.md @@ -4,13 +4,13 @@ Todo list for Google-Search-API ## Next tasks - [x] Mock html test for all search methods - https://pypi.python.org/pypi/mock/ -- [x] Test and implement the use of images options in search images method (is broken right now, in the middle of a re-factor) +- [x] Test and implement the use of images options in search images method - [x] Re-factor and split main module in separated ones -- [ ] Write tests for future methods development (they should failed and be skipped up to the proper method development moment) +- [ ] Implement google search external link scraping - [ ] Be able to manage both Chrome and Firefox, and maybe Ie too, depending in what browser the user has -- [ ] Write a full suite of docs test and unit tests +- [ ] Implement PhantomJS for not showing the explorer opening +- [ ] Write a full suite of docs tests and unit tests - [ ] Reconvert all comments following the google python style guide - https://google-styleguide.googlecode.com/svn/trunk/pyguide.html -- Decorator para load html files ## Stuff to check out later on diff --git a/google/tests/__init__.py b/google/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/google/tests/test_google.py b/google/tests/test_google.py index f7feeda..f3565e2 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -17,15 +17,26 @@ def load_html_file(file_name): return f +def dec_load_html_file(func): + + dir_path = os.path.join(os.path.dirname(__file__), "html_files") + file_name = func.__name__ + ".html" + file_path = os.path.join(dir_path, file_name) + + f = open(file_path, "r") + + func(f) + + class GoogleTest(unittest.TestCase): # @unittest.skip("skip") - # @load_html_file - def test_search_images(self): + @dec_load_html_file + def test_search_images(self, f): """Test method to search images.""" # replace method to get html from a test html file - f = load_html_file("test_search_images.html") + # f = load_html_file("test_search_images.html") google.images.get_html_from_dynamic_site = \ Mock(return_value=f.read().decode('utf8')) From ffafc4fbfc829e3972f43dee9bb4b04e3fa66f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 3 Mar 2015 11:31:27 -0500 Subject: [PATCH 036/106] Load html files with decorators. --- google/tests/test_google.py | 72 ++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/google/tests/test_google.py b/google/tests/test_google.py index f3565e2..25e0fda 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -6,107 +6,97 @@ import os -def load_html_file(file_name): +def load_html_file(path): + """Call test with a html file of the same name. - dir_path = os.path.join(os.path.dirname(__file__), "html_files") - # file_name = func.__name__ + ".html" - file_path = os.path.join(dir_path, file_name) + Args: + path: Relative path where the html file is located.""" - f = open(file_path, "r") + def test_decorator(fn): + base_path = os.path.join(os.path.dirname(__file__), path) + file_name = fn.__name__ + ".html" + file_path = os.path.join(base_path, file_name) - return f + html_f = open(file_path, "r") + def test_decorated(self): + fn(self, html_f) -def dec_load_html_file(func): - - dir_path = os.path.join(os.path.dirname(__file__), "html_files") - file_name = func.__name__ + ".html" - file_path = os.path.join(dir_path, file_name) - - f = open(file_path, "r") - - func(f) + return test_decorated + return test_decorator class GoogleTest(unittest.TestCase): - # @unittest.skip("skip") - @dec_load_html_file - def test_search_images(self, f): + @load_html_file("html_files") + def test_search_images(self, html_f): """Test method to search images.""" # replace method to get html from a test html file - # f = load_html_file("test_search_images.html") google.images.get_html_from_dynamic_site = \ - Mock(return_value=f.read().decode('utf8')) + Mock(return_value=html_f.read().decode('utf8')) res = google.search_images("apple", num_images=10) self.assertEqual(len(res), 10) - # @unittest.skip("skip") - def test_calculate(self): + @load_html_file("html_files") + def test_calculator(self, html_f): """Test method to calculate in google.""" # replace method to get html from a test html file - f = load_html_file("test_calculator.html") google.calculator.get_html_from_dynamic_site = \ - Mock(return_value=f.read().decode('utf8')) + Mock(return_value=html_f.read().decode('utf8')) calc = google.calculate("157.3kg in grams") self.assertEqual(calc.value, 157300) - # @unittest.skip("skip") - def test_exchange_rate(self): + @load_html_file("html_files") + def test_exchange_rate(self, html_f): """Test method to get an exchange rate in google.""" # replace method to get html from a test html file - f = load_html_file('test_exchange_rate.html') google.currency.get_html = \ - Mock(return_value=f.read().decode('utf8')) + Mock(return_value=html_f.read().decode('utf8')) usd_to_eur = google.exchange_rate("USD", "EUR") self.assertGreater(usd_to_eur, 0.0) - # @unittest.skip("skip") - def test_convert_currency(self): + @load_html_file("html_files") + def test_convert_currency(self, html_f): """Test method to convert currency in google.""" # replace method to get html from a test html file - f = load_html_file('test_convert_currency.html') google.currency.get_html = \ - Mock(return_value=f.read().decode('utf8')) + Mock(return_value=html_f.read().decode('utf8')) euros = google.convert_currency(5.0, "USD", "EUR") self.assertGreater(euros, 0.0) - def test_search(self): + @load_html_file("html_files") + def test_standard_search(self, html_f): """Test method to search in google.""" # replace method to get html from a test html file - f = load_html_file('test_standard_search.html',) google.standard_search.get_html = \ - Mock(return_value=f.read().decode('utf8')) + Mock(return_value=html_f.read().decode('utf8')) search = google.search("github") self.assertNotEqual(len(search), 0) - def test_shopping(self): + @load_html_file("html_files") + def test_shopping_search(self, html_f): """Test method for google shopping.""" # replace method to get html from a test html file - f = load_html_file('test_shopping_search.html') google.shopping_search.get_html = \ - Mock(return_value=f.read().decode('utf8')) + Mock(return_value=html_f.read().decode('utf8')) shop = google.shopping("Disgaea 4") self.assertNotEqual(len(shop), 0) -# @unittest.skip("skip") - class ConvertCurrencyTest(unittest.TestCase): - # @unittest.skip("skip") def test_get_currency_req_url(self): """Test method to get currency conversion request url.""" From fe16791c1dc737c88820efa45be9a9b5e21863fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 3 Mar 2015 18:08:44 -0500 Subject: [PATCH 037/106] Add a coverage batch! --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2b1f75e..9b697cc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Coverage Status](https://coveralls.io/repos/abenassi/Google-Search-API/badge.svg?branch=master)](https://coveralls.io/r/abenassi/Google-Search-API?branch=master) + Google Search API ===== From 9c2de961fd9cf058ddf4c9f5f3d9ddcd65d274da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Tue, 3 Mar 2015 18:24:02 -0500 Subject: [PATCH 038/106] Add more badges. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9b697cc..e057c30 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -[![Coverage Status](https://coveralls.io/repos/abenassi/Google-Search-API/badge.svg?branch=master)](https://coveralls.io/r/abenassi/Google-Search-API?branch=master) - Google Search API ===== +[![Coverage Status](https://coveralls.io/repos/abenassi/Google-Search-API/badge.svg?branch=master)](https://coveralls.io/r/abenassi/Google-Search-API?branch=master) +[![Build Status](https://travis-ci.org/abenassi/Google-Search-API.svg?branch=master)](https://travis-ci.org/abenassi/Google-Search-API) +[![](https://img.shields.io/pypi/v/Google-Search-API.svg)](https://pypi.python.org/pypi/Google-Search-API) + *The original package was developed by Anthony Casagrande and can be downloaded at https://github.com/BirdAPI This is a forked package that I will continue maintaining in the foreseeable future. I will try to maintain a strongly modularized design so when something is broken anyone can quickly repair it. All contributions are very welcome.* Google Search API is a python based library for searching various functionalities of google. It uses screen scraping to retrieve the results, and thus is unreliable if the way google's web pages are returned change in the future. This package is currently under heavy refactoring so changes in the user interface should be expected for the time being. From db0c4a20693b11412b48c8a486e46c4f92fb162b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Wed, 4 Mar 2015 18:02:58 -0500 Subject: [PATCH 039/106] Add fast_download method to download images using multithreading and change the way the selenium browser is managed when scraping more than one page of search images results. --- google/modules/images.py | 77 ++++++++++++++++++++++++++++++++++--- google/modules/utils.py | 62 ++++++++++++++++++++++++++++- google/tests/test_google.py | 25 ++++++++++-- 3 files changed, 153 insertions(+), 11 deletions(-) diff --git a/google/modules/images.py b/google/modules/images.py index fb0acd3..529b26f 100644 --- a/google/modules/images.py +++ b/google/modules/images.py @@ -1,10 +1,12 @@ -from utils import get_html_from_dynamic_site, write_html_to_file +from utils import get_browser_with_url, write_html_to_file, measure_time from bs4 import BeautifulSoup import urlparse import sys import requests import shutil import os +import threading +import Queue IMAGE_FORMATS = ["bmp", "gif", "jpg", "png", "psd", "pspimage", "thm", @@ -375,11 +377,14 @@ def search(query, image_options=None, num_images=50): results = set() curr_num_img = 1 page = 0 + browser = get_browser_with_url("http://www.google.com") while curr_num_img <= num_images: page += 1 url = _get_images_req_url(query, image_options, page) - html = get_html_from_dynamic_site(url) + # html = get_html_from_dynamic_site(url) + browser.get(url) + html = browser.page_source if html: soup = BeautifulSoup(html) @@ -416,9 +421,20 @@ def search(query, image_options=None, num_images=50): if curr_num_img >= num_images: break + browser.quit() + return list(results) +def _download_image(image_result, path): + + if path: + image_result.download(path) + else: + image_result.download() + + +@measure_time def download(image_results, path=None): """Download a list of images. @@ -430,11 +446,60 @@ def download(image_results, path=None): total_images = len(image_results) i = 1 for image_result in image_results: + progress = "".join(["Downloading image ", str(i), " (", str(total_images), ")"]) print progress - if path: - image_result.download(path) - else: - image_result.download() + + _download_image(image_result, path) + i += 1 + + +class ThreadUrl(threading.Thread): + + """Threaded Url Grab""" + + def __init__(self, queue, path, total): + threading.Thread.__init__(self) + self.queue = queue + self.path = "images" + self.total = total + + def run(self): + while True: + # grabs host from queue + image_result = self.queue.get() + + counter = self.total - self.queue.qsize() + progress = "".join(["Downloading image ", str(counter), + " (", str(self.total), ")"]) + print progress + sys.stdout.flush() + _download_image(image_result, self.path) + + # signals to queue job is done + self.queue.task_done() + + +@measure_time +def fast_download(image_results, path=None, threads=12): + + # print "starting!" + queue = Queue.Queue() + total = len(image_results) + + # print "Putting images in queue..." + for image_result in image_results: + queue.put(image_result) + + # spawn a pool of threads, and pass them queue instance + for i in range(threads): + # print "start thread number", i + sys.stdout.flush() + t = ThreadUrl(queue, path, total) + t.setDaemon(True) + t.start() + + # wait on the queue until everything has been processed + queue.join() diff --git a/google/modules/utils.py b/google/modules/utils.py index ff72530..c7c518b 100644 --- a/google/modules/utils.py +++ b/google/modules/utils.py @@ -1,6 +1,22 @@ import time from selenium import webdriver import urllib2 +from functools import wraps + + +def measure_time(fn): + + def decorator(*args, **kwargs): + start = time.time() + + res = fn(*args, **kwargs) + + elapsed = time.time() - start + print fn.__name__, "took", elapsed, "seconds" + + return res + + return decorator def normalize_query(query): @@ -51,7 +67,7 @@ def get_browser_with_url(url, timeout=120, driver="firefox"): # open a browser with given url browser.get(url) - time.sleep(5) + time.sleep(0.5) return browser @@ -69,7 +85,7 @@ def get_html_from_dynamic_site(url, timeout=120, browser = get_browser_with_url(url, timeout, driver) # get html - time.sleep(5) + time.sleep(2) content = browser.page_source # try again if there is no content @@ -87,3 +103,45 @@ def get_html_from_dynamic_site(url, timeout=120, time.sleep(5) return RV + + +def timeit(func=None, loops=1, verbose=False): + if func: + def inner(*args, **kwargs): + + sums = 0.0 + mins = 1.7976931348623157e+308 + maxs = 0.0 + print '====%s Timing====' % func.__name__ + for i in range(0, loops): + t0 = time.time() + result = func(*args, **kwargs) + dt = time.time() - t0 + mins = dt if dt < mins else mins + maxs = dt if dt > maxs else maxs + sums += dt + if verbose: + print '\t%r ran in %2.9f sec on run %s' % (func.__name__, dt, i) + print '%r min run time was %2.9f sec' % (func.__name__, mins) + print '%r max run time was %2.9f sec' % (func.__name__, maxs) + print '%r avg run time was %2.9f sec in %s runs' % (func.__name__, sums / loops, loops) + print '==== end ====' + return result + + return inner + else: + def partial_inner(func): + return timeit(func, loops, verbose) + return partial_inner + + +def timing(f): + @wraps(f) + def wrap(*args, **kw): + ts = time.time() + result = f(*args, **kw) + te = time.time() + print 'func:%r args:[%r, %r] took: %2.4f sec' % \ + (f.__name__, args, kw, te - ts) + return result + return wrap diff --git a/google/tests/test_google.py b/google/tests/test_google.py index 25e0fda..7207cfd 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -29,12 +29,25 @@ def test_decorated(self): class GoogleTest(unittest.TestCase): @load_html_file("html_files") + # @unittest.skip("skip") def test_search_images(self, html_f): """Test method to search images.""" - # replace method to get html from a test html file - google.images.get_html_from_dynamic_site = \ - Mock(return_value=html_f.read().decode('utf8')) + class MockBrowser(object): + + """Mock browser to replace selenium driver.""" + + def __init__(self, html_f): + self.page_source = html_f.read().decode('utf8') + + def get(self, url): + pass + + def quit(self): + pass + + google.images.get_browser_with_url = \ + Mock(return_value=MockBrowser(html_f)) res = google.search_images("apple", num_images=10) self.assertEqual(len(res), 10) @@ -134,6 +147,12 @@ def test_get_images_req_url(self): self.assertEqual(req_url, exp_req_url) + def test_download(self): + pass + + def test_fast_download(self): + pass + if __name__ == '__main__': # nose.main() From bcbf1f9eb8485f89e5597a320e0755c4c055f15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Wed, 4 Mar 2015 18:08:04 -0500 Subject: [PATCH 040/106] Update readme file. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index e057c30..29575b3 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,14 @@ images.download(image_results, path = "path/to/download/images") Path is an optional argument, if you don't specify a path, images will be downloaded to an "images" folder inside the working directory. +If you want to download a large list of images, the previous method could be slow. A better method using multithreading is provided for this case. + +```python +images.fast_download(image_results, path = "path/to/download/images", threads=12) +``` + +You may change the number of threads, 12 is the number that has offered the best speed after a number of informal tests that I've done. + ## Google Currency Converter (Exchange Rates) Convert between one currency and another using google calculator. Results are real time and can change at any time based on the current exchange rate according to google. From 8cd50f2212fb958d2b9142ee770010137f0e8a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Thu, 5 Mar 2015 13:54:12 -0500 Subject: [PATCH 041/106] Change to version 1.1.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3221a18..2ddf143 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='Google-Search-API', - version='1.1.0', + version='1.1.1', url='https://github.com/abenassi/Google-Search-API', description='Search in google', author='Anthony Casagrande, Agustin Benassi', From e9c93c0732922f3518806fd3a32189ebb276b891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Fri, 6 Mar 2015 15:51:27 -0500 Subject: [PATCH 042/106] Fix bugs in images. Minor change in calculator docstring. Exploring new .todo format --- TODO.todo | 25 ++++++++ google/modules/calculator.py | 3 +- google/modules/images.py | 119 ++++++++++++++++++++++------------- 3 files changed, 103 insertions(+), 44 deletions(-) create mode 100644 TODO.todo diff --git a/TODO.todo b/TODO.todo new file mode 100644 index 0000000..f0a7192 --- /dev/null +++ b/TODO.todo @@ -0,0 +1,25 @@ +Todo list for Google-Search-API +==== + +Next tasks: + ☐ Implement google search external link scraping + ☐ Be able to manage Chrome, Firefox and Ie, depending in what browser the user has installed + ☐ Implement PhantomJS for not showing the explorer opening + ☐ Write a full suite of docs tests and unit tests + ☐ Reconvert all comments following the google python style guide - https://google-styleguide.googlecode.com/svn/trunk/pyguide.html + +Stuff to check out later on: + ☐ vcrpy to record web requests and make more automated mocks + ☐ SauceLabs to do UI tests + ☐ Sphinx to generate documentation - readthedocs style + ☐ Cookiecutter-pythonpackage to make an instant and complete python package structure - https://github.com/audreyr/cookiecutter-pypackage and https://github.com/audreyr/cookiecutter + ☐ PhantomJS to use browsers without actually opening them (works with selenium) - http://stackoverflow.com/questions/13287490/is-there-a-way-to-use-phantomjs-in-python + ☐ Understand UTF-8 encoding in the context of getting html, writing it into a file and getting it back + ☐ Make TODOs with an automatic application for sublime text that makes .todo files with awesome features + ☐ Automatic PEP8/Flake8 formatting style when pushed to GitHub + +___________________ +Archive: + ✔ Test and implement the use of images options in search images method @done (15-03-06 15:32) @project(Next tasks) + ✔ Re-factor and split main module in separated ones @done (15-03-06 15:32) @project(Next tasks) + ✔ Mock html test for all search methods - https://pypi.python.org/pypi/mock/ @done (15-03-06 15:32) @project(Next tasks) diff --git a/google/modules/calculator.py b/google/modules/calculator.py index 1e1f2e6..272a733 100644 --- a/google/modules/calculator.py +++ b/google/modules/calculator.py @@ -25,7 +25,8 @@ def calculate(expr): Returns a `CalculatorResult` if successful or `None` if it fails. Args: - expr: Calculation expression (eg. "cos(25 pi) / 17.4") + expr: Calculation expression (eg. "cos(25 pi) / 17.4" or + "157.3kg in grams") Returns: CalculatorResult object.""" diff --git a/google/modules/images.py b/google/modules/images.py index 529b26f..bdbedd8 100644 --- a/google/modules/images.py +++ b/google/modules/images.py @@ -3,6 +3,7 @@ import urlparse import sys import requests +import urllib2 import shutil import os import threading @@ -115,6 +116,7 @@ class ImageResult: def __init__(self): self.name = None + self.file_name = None self.link = None self.thumb = None self.thumb_width = None @@ -144,13 +146,25 @@ def download(self, path="images"): """Download an image to a given path.""" self._create_path(path) + # print path try: response = requests.get(self.link, stream=True) - - path_filename = self._get_path_filename(path) - with open(path_filename, 'wb') as output_file: - shutil.copyfileobj(response.raw, output_file) + # request a protected image (adding a referer to the request) + # referer = self.domain + # image = self.link + + # req = urllib2.Request(image) + # req.add_header('Referer', referer) # here is the trick + # response = urllib2.urlopen(req) + + if "image" in response.headers['content-type']: + path_filename = self._get_path_filename(path) + with open(path_filename, 'wb') as output_file: + shutil.copyfileobj(response.raw, output_file) + # output_file.write(response.content) + else: + print "\r\rskiped! cached image" del response @@ -176,8 +190,8 @@ def _get_path_filename(self, path): path_filename = None # preserve the original name - if self.name and self.format: - original_filename = self.name + "." + self.format + if self.file_name: + original_filename = self.file_name path_filename = os.path.join(path, original_filename) # create a default name if there is no original name @@ -220,11 +234,11 @@ def _parse_image_format(image_link): >>> link = "http://minionslovebananas.com/images/gallery/preview/Chiquita-DM2-minion-banana-3.jpg%3Fw%3D300%26h%3D429" >>> Google._parse_image_format(link) - 'jpg' """ parsed_format = image_link[image_link.rfind(".") + 1:] + # OLD: identify formats even with weird final characters if parsed_format not in IMAGE_FORMATS: for image_format in IMAGE_FORMATS: if image_format in parsed_format: @@ -256,8 +270,27 @@ def _get_images_req_url(query, image_options=None, page=0, def _find_divs_with_images(soup): - div_container = soup.find("div", {"id": "rg_s"}) - return div_container.find_all("div", {"class": "rg_di"}) + + try: + div_container = soup.find("div", {"id": "rg_s"}) + divs = div_container.find_all("div", {"class": "rg_di"}) + except: + divs = None + return divs + + +def _get_file_name(link): + + temp_name = link.rsplit('/', 1)[1] + image_format = _parse_image_format(link) + + if image_format and temp_name.rsplit(".", 1)[1] != image_format: + file_name = temp_name.rsplit(".", 1)[0] + "." + image_format + + else: + file_name = temp_name + + return file_name def _get_name(): @@ -280,6 +313,7 @@ def _get_image_data(res, a): qry_parsed = urlparse.parse_qs(url_parsed.query) res.name = _get_name() res.link = qry_parsed["imgurl"][0] + res.file_name = _get_file_name(res.link) res.format = _parse_image_format(res.link) res.width = qry_parsed["w"][0] res.height = qry_parsed["h"][0] @@ -377,7 +411,7 @@ def search(query, image_options=None, num_images=50): results = set() curr_num_img = 1 page = 0 - browser = get_browser_with_url("http://www.google.com") + browser = get_browser_with_url("") while curr_num_img <= num_images: page += 1 @@ -391,35 +425,37 @@ def search(query, image_options=None, num_images=50): # iterate over the divs containing images in one page divs = _find_divs_with_images(soup) - for div in divs: - res = ImageResult() + if divs: + for div in divs: + + res = ImageResult() - # store indexing paramethers - res.page = page - res.index = curr_num_img + # store indexing paramethers + res.page = page + res.index = curr_num_img - # get url of image and its secondary data - a = div.find("a") - if a: - _get_image_data(res, a) + # get url of image and its secondary data + a = div.find("a") + if a: + _get_image_data(res, a) - # get url of thumb and its size paramethers - img = a.find_all("img") - if img: - _get_thumb_data(res, img) + # get url of thumb and its size paramethers + img = a.find_all("img") + if img: + _get_thumb_data(res, img) - # increment image counter only if a new image was added - prev_num_results = len(results) - results.add(res) - curr_num_results = len(results) + # increment image counter only if a new image was added + prev_num_results = len(results) + results.add(res) + curr_num_results = len(results) - if curr_num_results > prev_num_results: - curr_num_img += 1 + if curr_num_results > prev_num_results: + curr_num_img += 1 - # break the loop when limit of images is reached - if curr_num_img >= num_images: - break + # break the loop when limit of images is reached + if curr_num_img >= num_images: + break browser.quit() @@ -428,10 +464,11 @@ def search(query, image_options=None, num_images=50): def _download_image(image_result, path): - if path: - image_result.download(path) - else: - image_result.download() + if image_result.format: + if path: + image_result.download(path) + else: + image_result.download() @measure_time @@ -463,7 +500,7 @@ class ThreadUrl(threading.Thread): def __init__(self, queue, path, total): threading.Thread.__init__(self) self.queue = queue - self.path = "images" + self.path = path self.total = total def run(self): @@ -483,20 +520,16 @@ def run(self): @measure_time -def fast_download(image_results, path=None, threads=12): - - # print "starting!" +def fast_download(image_results, path=None, threads=10): + # print path queue = Queue.Queue() total = len(image_results) - # print "Putting images in queue..." for image_result in image_results: queue.put(image_result) # spawn a pool of threads, and pass them queue instance for i in range(threads): - # print "start thread number", i - sys.stdout.flush() t = ThreadUrl(queue, path, total) t.setDaemon(True) t.start() From 4d03cd3286768d508356d72892791d5a9e17e2fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Fri, 6 Mar 2015 15:51:55 -0500 Subject: [PATCH 043/106] Update gitignore with .zip --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f0724c4..ae22a4d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.egg *.egg-info *.dmp +*.zip From cc45112da81ce95773e56dc5c3a650f049e41dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Fri, 6 Mar 2015 15:55:19 -0500 Subject: [PATCH 044/106] Remove TODO.todo, lets keep .md as is the only one Github can render --- TODO.todo | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 TODO.todo diff --git a/TODO.todo b/TODO.todo deleted file mode 100644 index f0a7192..0000000 --- a/TODO.todo +++ /dev/null @@ -1,25 +0,0 @@ -Todo list for Google-Search-API -==== - -Next tasks: - ☐ Implement google search external link scraping - ☐ Be able to manage Chrome, Firefox and Ie, depending in what browser the user has installed - ☐ Implement PhantomJS for not showing the explorer opening - ☐ Write a full suite of docs tests and unit tests - ☐ Reconvert all comments following the google python style guide - https://google-styleguide.googlecode.com/svn/trunk/pyguide.html - -Stuff to check out later on: - ☐ vcrpy to record web requests and make more automated mocks - ☐ SauceLabs to do UI tests - ☐ Sphinx to generate documentation - readthedocs style - ☐ Cookiecutter-pythonpackage to make an instant and complete python package structure - https://github.com/audreyr/cookiecutter-pypackage and https://github.com/audreyr/cookiecutter - ☐ PhantomJS to use browsers without actually opening them (works with selenium) - http://stackoverflow.com/questions/13287490/is-there-a-way-to-use-phantomjs-in-python - ☐ Understand UTF-8 encoding in the context of getting html, writing it into a file and getting it back - ☐ Make TODOs with an automatic application for sublime text that makes .todo files with awesome features - ☐ Automatic PEP8/Flake8 formatting style when pushed to GitHub - -___________________ -Archive: - ✔ Test and implement the use of images options in search images method @done (15-03-06 15:32) @project(Next tasks) - ✔ Re-factor and split main module in separated ones @done (15-03-06 15:32) @project(Next tasks) - ✔ Mock html test for all search methods - https://pypi.python.org/pypi/mock/ @done (15-03-06 15:32) @project(Next tasks) From 76438d083c11beb1586d201bddb0aef18d676d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Fri, 6 Mar 2015 16:30:50 -0500 Subject: [PATCH 045/106] Update todo. --- TODO.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index 0c7f799..74516c0 100644 --- a/TODO.md +++ b/TODO.md @@ -11,14 +11,11 @@ Todo list for Google-Search-API - [ ] Implement PhantomJS for not showing the explorer opening - [ ] Write a full suite of docs tests and unit tests - [ ] Reconvert all comments following the google python style guide - https://google-styleguide.googlecode.com/svn/trunk/pyguide.html +- [ ] Add support for Google Scholar search (check out Py3 library google-scholar) ## Stuff to check out later on * vcrpy to record web requests and make more automated mocks * SauceLabs to do UI tests * Sphinx to generate documentation - readthedocs style -* Cookiecutter-pythonpackage to make an instant and complete python package structure - https://github.com/audreyr/cookiecutter-pypackage and https://github.com/audreyr/cookiecutter -* PhantomJS to use browsers without actually opening them (works with selenium) - http://stackoverflow.com/questions/13287490/is-there-a-way-to-use-phantomjs-in-python -* Understand UTF-8 encoding in the context of getting html, writing it into a file and getting it back -* Make TODOs with an automatic application for sublime text that makes .todo files with awesome features -* Automatic PEP8/Flake8 formatting style when pushed to GitHub \ No newline at end of file +* PhantomJS to use browsers without actually opening them (works with selenium) - http://stackoverflow.com/questions/13287490/is-there-a-way-to-use-phantomjs-in-python \ No newline at end of file From c6d4359dc153419ab1ba548f482ccf0fd3d61ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Benassi?= Date: Sat, 28 Mar 2015 21:47:29 -0400 Subject: [PATCH 046/106] Add TOC to README. --- README.md | 21 ++++++++++++++++++++- TODO.md | 1 + google/modules/images.py | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 29575b3..dc63685 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + Google Search API ===== @@ -11,12 +12,30 @@ Google Search API is a python based library for searching various functionalitie *Disclaimer: This software uses screen scraping to retrieve search results from google.com, and therefore this software may stop working at any given time. Use this software at your own risk. I assume no responsibility for how this software API is used by others.* + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Google Search API +](#google-search-api) + - [Development current status +](#development-current-status) + - [Installation +](#installation) + - [Google Web Search](#google-web-search) + - [Google Calculator](#google-calculator) + - [Google Image Search](#google-image-search) + - [Google Currency Converter (Exchange Rates)](#google-currency-converter-exchange-rates) + - [Contributions](#contributions) + + + Development current status -------------------------- All methods are currently functioning and returning its primary target data. Although, some of the secondary data that is supposed to be collected in the result objects is not yet working. -Redesign of the package is still a work in progress. After completed, I will attempt to repair the gathering of secondary data. +Redesign of the package is still a work in progress. After completed, I will attempt to repair the gathering of secondary data. Contributions are welcome! Installation ------------ diff --git a/TODO.md b/TODO.md index 74516c0..311b0fe 100644 --- a/TODO.md +++ b/TODO.md @@ -6,6 +6,7 @@ Todo list for Google-Search-API - [x] Mock html test for all search methods - https://pypi.python.org/pypi/mock/ - [x] Test and implement the use of images options in search images method - [x] Re-factor and split main module in separated ones +- [ ] Implement fast download using concurrent futures - https://gist.github.com/mangecoeur/9540178 - https://docs.python.org/3/library/concurrent.futures.html - [ ] Implement google search external link scraping - [ ] Be able to manage both Chrome and Firefox, and maybe Ie too, depending in what browser the user has - [ ] Implement PhantomJS for not showing the explorer opening diff --git a/google/modules/images.py b/google/modules/images.py index bdbedd8..b821d3a 100644 --- a/google/modules/images.py +++ b/google/modules/images.py @@ -487,6 +487,7 @@ def download(image_results, path=None): progress = "".join(["Downloading image ", str(i), " (", str(total_images), ")"]) print progress + sys.stdout.flush() _download_image(image_result, path) From 1659c711a29c0ad9a66ebb9a69e62697acade6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Wed, 8 Jul 2015 18:39:55 -0300 Subject: [PATCH 047/106] Fix mistakes in setup.py and update travis for automatic deployment. --- .travis.yml | 26 +++++++++++++++++--------- setup.py | 29 ++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9ee4145..7af53d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,27 @@ language: python -sudo: false +sudo: required python: - - '2.7' - - pypy +- '2.7' +matrix: + allow_failures: + - python: '2.6' + - python: '3.4' + - python: pypy deploy: provider: pypi user: beni password: - secure: ib4iaqycgcF3ijSC... + secure: HPJvi6YtjXerH/e7GaVqzbwlu6X/3f/3HjJn7AvDiNP+y4okibqEsmDfPUDklPNTq+YW7q5y3ZEJFDnB9TnqXEzkOrCwYSTdqfAZbW3ncH9aJiV20tkL8sTgJIXPK3T/XNjZYz+SxuGD4cCE2/vVHY1x9AZNalGUyPl7RC+jiPc= on: tags: true + repo: abenassi/Google-Search-API install: - - pip install -r requirements.txt - - python setup.py install - - pip install coveralls +- pip install -r requirements.txt +- python setup.py install +- pip install coveralls script: - - nosetests -after_success: coveralls \ No newline at end of file +- nosetests +after_success: coveralls +os: +- linux +- osx diff --git a/setup.py b/setup.py index 2ddf143..741c89e 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,35 @@ from setuptools import setup +test_requirements = [ + "nose", + "nose-cov" +] + setup(name='Google-Search-API', - version='1.1.1', + version='1.1.2', url='https://github.com/abenassi/Google-Search-API', description='Search in google', author='Anthony Casagrande, Agustin Benassi', author_email='birdapi@gmail.com, agusbenassi@gmail.com', + maintainer="Agustin Benassi", + maintainer_email='agusbenassi@gmail.com', license='MIT', - - packages=['google'], - + packages=[ + 'google', + 'google.modules', + 'google.test' + ], + package_dir={'google': 'google'}, + include_package_data=True, + keywords="google search images api", + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7' + ], setup_requires=['nose>=1.0'], test_suite='nose.collector', - tests_require=['nose', 'nose-cov'] + tests_require=test_requirements ) From b356e714c9a5ab6a4a148b5b08d26056aef50067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Wed, 8 Jul 2015 18:48:06 -0300 Subject: [PATCH 048/106] fix typo --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 741c89e..6f6fce1 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ packages=[ 'google', 'google.modules', - 'google.test' + 'google.tests' ], package_dir={'google': 'google'}, include_package_data=True, From af4c8f2e799948cb287b663d761749610ae76b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Wed, 8 Jul 2015 18:55:09 -0300 Subject: [PATCH 049/106] Fix requirements --- setup.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 6f6fce1..0f916a2 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,14 @@ -from setuptools import setup +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +with open("requirements.txt") as f: + requirements = [req.strip() for req in f.readlines()] test_requirements = [ "nose", @@ -6,7 +16,7 @@ ] setup(name='Google-Search-API', - version='1.1.2', + version='1.1.3', url='https://github.com/abenassi/Google-Search-API', description='Search in google', author='Anthony Casagrande, Agustin Benassi', @@ -21,6 +31,7 @@ ], package_dir={'google': 'google'}, include_package_data=True, + install_requires=requirements, keywords="google search images api", classifiers=[ 'Development Status :: 3 - Alpha', From 2e0d2c5ce7768be9c3f9284cffa2354ab1ec0e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Wed, 8 Jul 2015 19:19:47 -0300 Subject: [PATCH 050/106] Fix some encoding issues. --- google/google.py | 2 ++ google/modules/calculator.py | 2 ++ google/modules/currency.py | 2 ++ google/modules/images.py | 10 ++++++---- google/modules/shopping_search.py | 5 ++++- google/modules/standard_search.py | 7 +++++-- google/modules/utils.py | 2 ++ requirements.txt | 2 ++ 8 files changed, 25 insertions(+), 7 deletions(-) diff --git a/google/google.py b/google/google.py index f17c2ed..18f8827 100644 --- a/google/google.py +++ b/google/google.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from modules import images from modules import currency from modules import calculator diff --git a/google/modules/calculator.py b/google/modules/calculator.py index 272a733..75f5f89 100644 --- a/google/modules/calculator.py +++ b/google/modules/calculator.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from utils import get_html_from_dynamic_site from utils import _get_search_url from bs4 import BeautifulSoup diff --git a/google/modules/currency.py b/google/modules/currency.py index 5b691f5..e3fa405 100644 --- a/google/modules/currency.py +++ b/google/modules/currency.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from utils import get_html from bs4 import BeautifulSoup diff --git a/google/modules/images.py b/google/modules/images.py index b821d3a..cdb9343 100644 --- a/google/modules/images.py +++ b/google/modules/images.py @@ -1,9 +1,11 @@ +from __future__ import unicode_literals +from unidecode import unidecode + from utils import get_browser_with_url, write_html_to_file, measure_time from bs4 import BeautifulSoup import urlparse import sys import requests -import urllib2 import shutil import os import threading @@ -74,7 +76,7 @@ def __init__(self): self.color = None def __repr__(self): - return self.__dict__ + return unidecode(self.__dict__) def get_tbs(self): tbs = None @@ -138,8 +140,8 @@ def __hash__(self): def __repr__(self): string = "ImageResult(" + \ - "index={}, page={}, ".format(self.index, self.page) + \ - "domain={}, link={})".format(self.domain, self.link) + "index={}, page={}, ".format(unidecode(self.index), unidecode(self.page)) + \ + "domain={}, link={})".format(unidecode(self.domain), unidecode(self.link)) return string def download(self, path="images"): diff --git a/google/modules/shopping_search.py b/google/modules/shopping_search.py index fbfe424..492d63f 100644 --- a/google/modules/shopping_search.py +++ b/google/modules/shopping_search.py @@ -1,6 +1,9 @@ +from __future__ import unicode_literals + from utils import get_html, normalize_query from bs4 import BeautifulSoup import re +from unidecode import unidecode class ShoppingResult: @@ -18,7 +21,7 @@ def __init__(self): self.min_price = None def __repr__(self): - return self.name + return unidecode(self.name) def shopping(query, pages=1): diff --git a/google/modules/standard_search.py b/google/modules/standard_search.py index 08cb67d..c12907d 100644 --- a/google/modules/standard_search.py +++ b/google/modules/standard_search.py @@ -1,6 +1,9 @@ +from __future__ import unicode_literals + from utils import _get_search_url, get_html from bs4 import BeautifulSoup import urlparse +from unidecode import unidecode class GoogleResult: @@ -34,10 +37,10 @@ def _limit_str_size(self, str_element, size_limit): return None elif len(str_element) > size_limit: - return str_element.decode("utf-8")[:size_limit] + ".." + return unidecode(str_element[:size_limit]) + ".." else: - return str_element.decode("utf-8") + return unidecode(str_element) # PUBLIC diff --git a/google/modules/utils.py b/google/modules/utils.py index c7c518b..dddc605 100644 --- a/google/modules/utils.py +++ b/google/modules/utils.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import time from selenium import webdriver import urllib2 diff --git a/requirements.txt b/requirements.txt index bf02217..80dc2b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ beautifulsoup4 selenium>=2.44.0,<3.0.0 +requests +unidecode From ebd937d77cf7e59fc6d5576d5095186d2be5e52d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Wed, 8 Jul 2015 19:20:52 -0300 Subject: [PATCH 051/106] change pip version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0f916a2..0bab6a7 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ ] setup(name='Google-Search-API', - version='1.1.3', + version='1.1.4', url='https://github.com/abenassi/Google-Search-API', description='Search in google', author='Anthony Casagrande, Agustin Benassi', From 865528a5cf818ee17cd07dae3ce522855f17249a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Wed, 8 Jul 2015 19:28:24 -0300 Subject: [PATCH 052/106] Include MANIFEST with requirements.txt --- MANIFEST.in | 1 + setup.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f9bd145 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include requirements.txt diff --git a/setup.py b/setup.py index 0bab6a7..0796e0f 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,6 @@ "nose", "nose-cov" ] - setup(name='Google-Search-API', version='1.1.4', url='https://github.com/abenassi/Google-Search-API', From 981ad9565e196f02aa71a406830569823f769c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Wed, 8 Jul 2015 19:33:08 -0300 Subject: [PATCH 053/106] change version in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0796e0f..cdea47a 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ "nose-cov" ] setup(name='Google-Search-API', - version='1.1.4', + version='1.1.5', url='https://github.com/abenassi/Google-Search-API', description='Search in google', author='Anthony Casagrande, Agustin Benassi', From b9882bab49069554fbbb6dbc589d96ef049b77ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Thu, 9 Jul 2015 00:54:39 -0300 Subject: [PATCH 054/106] Fix a warning about BeautifulSoup and add __repr__ for calculator results. --- google/modules/calculator.py | 4 ++++ google/modules/standard_search.py | 2 +- setup.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/google/modules/calculator.py b/google/modules/calculator.py index 75f5f89..9dee9ac 100644 --- a/google/modules/calculator.py +++ b/google/modules/calculator.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from unidecode import unidecode from utils import get_html_from_dynamic_site from utils import _get_search_url @@ -18,6 +19,9 @@ def __init__(self): self.result = None # Result expression (eg. u'157300 kilograms') (NOT implemented yet) self.fullstring = None # Complete expression (eg. u'157.3 kilograms = 157300 grams') (NOT implemented yet) + def __repr__(self): + return unidecode(self.value) + # PUBLIC def calculate(expr): diff --git a/google/modules/standard_search.py b/google/modules/standard_search.py index c12907d..90b060a 100644 --- a/google/modules/standard_search.py +++ b/google/modules/standard_search.py @@ -60,7 +60,7 @@ def search(query, pages=1): html = get_html(url) if html: - soup = BeautifulSoup(html) + soup = BeautifulSoup(html, "html.parser") lis = soup.findAll("li", attrs={"class": "g"}) j = 0 diff --git a/setup.py b/setup.py index cdea47a..db53653 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ "nose-cov" ] setup(name='Google-Search-API', - version='1.1.5', + version='1.1.6', url='https://github.com/abenassi/Google-Search-API', description='Search in google', author='Anthony Casagrande, Agustin Benassi', From 190cf652c72ae1a57df73719df0b10b0b66836e7 Mon Sep 17 00:00:00 2001 From: Tao Sauvage Date: Fri, 10 Jul 2015 19:46:51 +0200 Subject: [PATCH 055/106] [utils] Explicit error when GoogleCaptcha. Would for instance output in the case where google is asking for a captcha: >>> google.search('site:bleh.meh inurl:foo') Error accessing: http://www.google.com/search?hl=en&q=site%3Ableh.meh+inurl%3Afoo&start=0&num=10 Google is requiring a Captcha. For more information see: 'https://support.google.com/websearch/answer/86640' [] --- google/modules/utils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/google/modules/utils.py b/google/modules/utils.py index dddc605..13e7568 100644 --- a/google/modules/utils.py +++ b/google/modules/utils.py @@ -38,8 +38,15 @@ def get_html(url): "User-Agent", "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101") html = urllib2.urlopen(request).read() return html - except: + except urllib2.HTTPError as e: print "Error accessing:", url + if e.code == 503 and 'CaptchaRedirect' in e.read(): + print "Google is requiring a Captcha. " \ + "For more information see: 'https://support.google.com/websearch/answer/86640'" + return None + except Exception as e: + print "Error accessing:", url + print e return None From c74c8faa224524e590078fa978635f8d74dee187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Fri, 10 Jul 2015 16:06:03 -0300 Subject: [PATCH 056/106] Fix setup.py to use new Travis CI containers buildings. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7af53d3..a5e31bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: python -sudo: required +sudo: false python: - '2.7' matrix: From 6d955c31e34cdf3aa30043ddce5fa1606704e759 Mon Sep 17 00:00:00 2001 From: isaacbernat Date: Sun, 19 Jul 2015 22:53:10 +0200 Subject: [PATCH 057/106] Add cached search result link --- README.md | 18 +++++++++--------- google/modules/standard_search.py | 15 ++++++++++----- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index dc63685..6370f52 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ GoogleResult: self.google_link # The google link self.description # The description of the link self.thumb # The link to a thumbnail of the website (NOT implemented yet) - self.cached # A link to the cached version of the page (NOT implemented yet) + self.cached # A link to the cached version of the page self.page # What page this result was on (When searching more than one page) self.index # What index on this page it was on ``` @@ -148,13 +148,13 @@ Filter options: ```python ImageOptions: - image_type # face, body, clipart, line drawing - size_category # large, small, icon - larger_than # the well known name of the smallest image size you want - exact_width # the exact width of the image you want - exact_height # the exact height of the image you want - color_type # color, b&w, specific - color # blue, green, red + image_type # face, body, clipart, line drawing + size_category # large, small, icon + larger_than # the well known name of the smallest image size you want + exact_width # the exact width of the image you want + exact_height # the exact height of the image you want + color_type # color, b&w, specific + color # blue, green, red ``` Enums of values that can be used to filter image searches: @@ -219,7 +219,7 @@ You may change the number of threads, 12 is the number that has offered the best ## Google Currency Converter (Exchange Rates) Convert between one currency and another using google calculator. Results are real time and can change at any time based on the current exchange rate according to google. -Convert 5 US Dollars to Euros using the official 3 letter currency acronym: +Convert 5 US Dollars to Euros using the official 3 letter currency acronym ([ISO 4217](https://en.wikipedia.org/wiki/ISO_4217)): ```python from google import google diff --git a/google/modules/standard_search.py b/google/modules/standard_search.py index 90b060a..62fec4b 100644 --- a/google/modules/standard_search.py +++ b/google/modules/standard_search.py @@ -16,7 +16,7 @@ def __init__(self): self.google_link = None # The google link self.description = None # The description of the link self.thumb = None # Thumbnail link of website (NOT implemented yet) - self.cached = None # Cached version link of page (NOT implemented yet) + self.cached = None # Cached version link of page self.page = None # Results page this one was on self.index = None # What index on this page it was on @@ -74,7 +74,7 @@ def search(query, pages=1): res.google_link = _get_google_link(li) res.description = _get_description(li) res.thumb = _get_thumb() - res.cached = _get_cached() + res.cached = _get_cached(li) results.append(res) j += 1 @@ -118,7 +118,7 @@ def _get_google_link(li): def _get_description(li): """Return the description of a google search. - TODO: There are some text encoding problems to resovle.""" + TODO: There are some text encoding problems to resolve.""" sdiv = li.find("div", attrs={"class": "s"}) if sdiv: @@ -135,6 +135,11 @@ def _get_thumb(): pass -def _get_cached(): +def _get_cached(li): """Return a link to the cached version of the page.""" - pass + links = li.find_all("a") + if len(links) > 1 and links[1].text == "Cached": + link = links[1]["href"] + if link.startswith("/url?") or link.startswith("/search?"): + return urlparse.urljoin("http://www.google.com", link) + return None From f982cfa27b3f7eb54e603daa184556664fb51b63 Mon Sep 17 00:00:00 2001 From: Alfonso de la Guarda Reyes Date: Fri, 28 Aug 2015 13:49:50 -0500 Subject: [PATCH 058/106] Better unicode support, added language processing in queries --- google/modules/standard_search.py | 14 +++++++------- google/modules/utils.py | 15 +++++++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/google/modules/standard_search.py b/google/modules/standard_search.py index 62fec4b..8be84df 100644 --- a/google/modules/standard_search.py +++ b/google/modules/standard_search.py @@ -32,7 +32,6 @@ def __repr__(self): def _limit_str_size(self, str_element, size_limit): """Limit the characters of the string, adding .. at the end.""" - if not str_element: return None @@ -44,7 +43,7 @@ def _limit_str_size(self, str_element, size_limit): # PUBLIC -def search(query, pages=1): +def search(query, pages=1, lang='en'): """Returns a list of GoogleResult. Args: @@ -56,16 +55,17 @@ def search(query, pages=1): results = [] for i in range(pages): - url = _get_search_url(query, i) + url = _get_search_url(query, i, lang=lang) html = get_html(url) if html: soup = BeautifulSoup(html, "html.parser") lis = soup.findAll("li", attrs={"class": "g"}) - + j = 0 for li in lis: res = GoogleResult() + res.page = i res.index = j @@ -86,6 +86,7 @@ def search(query, pages=1): def _get_name(li): """Return the name of a google search.""" a = li.find("a") + #return a.text.encode("utf-8").strip() return a.text.strip() @@ -123,9 +124,8 @@ def _get_description(li): sdiv = li.find("div", attrs={"class": "s"}) if sdiv: stspan = sdiv.find("span", attrs={"class": "st"}) - - return stspan.text.encode("utf-8").strip() - + #return stspan.text.encode("utf-8").strip() + return stspan.text.strip() else: return None diff --git a/google/modules/utils.py b/google/modules/utils.py index 13e7568..03f4059 100644 --- a/google/modules/utils.py +++ b/google/modules/utils.py @@ -4,6 +4,9 @@ from selenium import webdriver import urllib2 from functools import wraps +#import requests +from urllib import urlencode + def measure_time(fn): @@ -25,17 +28,21 @@ def normalize_query(query): return query.strip().replace(":", "%3A").replace("+", "%2B").replace("&", "%26").replace(" ", "+") -def _get_search_url(query, page=0, per_page=10): +def _get_search_url(query, page=0, per_page=10, lang='en'): # note: num per page might not be supported by google anymore (because of # google instant) - return "http://www.google.com/search?hl=en&q=%s&start=%i&num=%i" % (normalize_query(query), page * per_page, per_page) + params = {'nl': lang, 'q': normalize_query(query).encode('utf8'), 'start':page * per_page, 'num':per_page} + params = urlencode(params) + url = u"http://www.google.com/search?" + params + #return u"http://www.google.com/search?hl=%s&q=%s&start=%i&num=%i" % (lang, normalize_query(query), page * per_page, per_page) + return url def get_html(url): + header = "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101" try: request = urllib2.Request(url) - request.add_header( - "User-Agent", "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101") + request.add_header("User-Agent", header) html = urllib2.urlopen(request).read() return html except urllib2.HTTPError as e: From 128d572c5383e3e9903c0bae0cca36523b0b680a Mon Sep 17 00:00:00 2001 From: Alfonso de la Guarda Reyes Date: Fri, 28 Aug 2015 15:06:18 -0500 Subject: [PATCH 059/106] Fix bug for None description --- google/modules/standard_search.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/google/modules/standard_search.py b/google/modules/standard_search.py index 8be84df..c15cb50 100644 --- a/google/modules/standard_search.py +++ b/google/modules/standard_search.py @@ -87,7 +87,9 @@ def _get_name(li): """Return the name of a google search.""" a = li.find("a") #return a.text.encode("utf-8").strip() - return a.text.strip() + if a is not None: + return a.text.strip() + return None def _get_link(li): @@ -124,8 +126,9 @@ def _get_description(li): sdiv = li.find("div", attrs={"class": "s"}) if sdiv: stspan = sdiv.find("span", attrs={"class": "st"}) + if stspan is not None: #return stspan.text.encode("utf-8").strip() - return stspan.text.strip() + return stspan.text.strip() else: return None From bd74348bd468545957bf4b30c2abf5e85c2184d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Fri, 28 Aug 2015 20:54:13 -0300 Subject: [PATCH 060/106] Change setup.py to 1.1.8 due because I forgot that with the 1.1.7 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index db53653..1eba093 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ "nose-cov" ] setup(name='Google-Search-API', - version='1.1.6', + version='1.1.8', url='https://github.com/abenassi/Google-Search-API', description='Search in google', author='Anthony Casagrande, Agustin Benassi', From 505187267ae1c7bbabe6bca9f9b902a4736ab9e3 Mon Sep 17 00:00:00 2001 From: Gordon Quad Date: Mon, 19 Oct 2015 11:22:54 +0100 Subject: [PATCH 061/106] external link for standard search implemented --- README.md | 2 +- google/modules/standard_search.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6370f52..d6a4d80 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ search_results = google.search("This is my query", num_page) ```python GoogleResult: self.name # The title of the link - self.link # The external link (NOT implemented yet) + self.link # The external link self.google_link # The google link self.description # The description of the link self.thumb # The link to a thumbnail of the website (NOT implemented yet) diff --git a/google/modules/standard_search.py b/google/modules/standard_search.py index c15cb50..3ac4ebc 100644 --- a/google/modules/standard_search.py +++ b/google/modules/standard_search.py @@ -3,7 +3,9 @@ from utils import _get_search_url, get_html from bs4 import BeautifulSoup import urlparse +from urllib2 import unquote from unidecode import unidecode +from re import match class GoogleResult: @@ -98,11 +100,12 @@ def _get_link(li): a = li.find("a") link = a["href"] - if link.startswith("/url?") or link.startswith("/search?"): - return None + if link.startswith("/url?"): + m = match('/url\?(url|q)=(.+?)&', link) + if m and len(m.groups()) == 2: + return unquote(m.group(2)) - else: - return link + return None def _get_google_link(li): From ad073335c7a41ffb14edce9c1c85ff769c850853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Mon, 19 Oct 2015 10:55:36 -0300 Subject: [PATCH 062/106] Removing OS X environment from Travis because is not working properly. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a5e31bb..55c3f1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,4 +24,3 @@ script: after_success: coveralls os: - linux -- osx From 9e4bf87614ef127835ed0e4e4646d226bbb24954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Mon, 19 Oct 2015 10:57:05 -0300 Subject: [PATCH 063/106] New version to PIP. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1eba093..9fccdc7 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ "nose-cov" ] setup(name='Google-Search-API', - version='1.1.8', + version='1.1.9', url='https://github.com/abenassi/Google-Search-API', description='Search in google', author='Anthony Casagrande, Agustin Benassi', From dff4a35291cabe93a9206db98e7b7114982f14b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Mon, 19 Oct 2015 11:06:16 -0300 Subject: [PATCH 064/106] Update travis so it deploys automatically to pip, again. --- .travis.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 55c3f1c..8839d70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ deploy: provider: pypi user: beni password: - secure: HPJvi6YtjXerH/e7GaVqzbwlu6X/3f/3HjJn7AvDiNP+y4okibqEsmDfPUDklPNTq+YW7q5y3ZEJFDnB9TnqXEzkOrCwYSTdqfAZbW3ncH9aJiV20tkL8sTgJIXPK3T/XNjZYz+SxuGD4cCE2/vVHY1x9AZNalGUyPl7RC+jiPc= + secure: XfUbc5Tnjq8mUHnv/rrQvcQ5m+k7mvk2sAwhS1Hzi2NFXaiPQF0YR2er0BDDQOFYba+MBd57l4zHdyti8Y39uVI2ZfY10c5KYio3VzXDU2doycLf7hY8cqKs8UioabVehrPU96GErVEUyA2Jj1cqrsIUX7Smj8qby0DfX+igJtM= on: tags: true repo: abenassi/Google-Search-API diff --git a/setup.py b/setup.py index 9fccdc7..de51333 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ "nose-cov" ] setup(name='Google-Search-API', - version='1.1.9', + version='1.1.10', url='https://github.com/abenassi/Google-Search-API', description='Search in google', author='Anthony Casagrande, Agustin Benassi', From 564694c25684c0f236a8023e6281f52ca2250b63 Mon Sep 17 00:00:00 2001 From: hugsy Date: Thu, 17 Dec 2015 11:45:21 +1100 Subject: [PATCH 065/106] in _get_search_url(), we removed the call to normalize_query() for the `q` parameter. The normalization is already performed by urllib.urlencode (L36). This simple hack allows Google keywords (site:, filetype:, etc.) to work as expected. --- google/modules/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/modules/utils.py b/google/modules/utils.py index 03f4059..e002da2 100644 --- a/google/modules/utils.py +++ b/google/modules/utils.py @@ -32,7 +32,7 @@ def _get_search_url(query, page=0, per_page=10, lang='en'): # note: num per page might not be supported by google anymore (because of # google instant) - params = {'nl': lang, 'q': normalize_query(query).encode('utf8'), 'start':page * per_page, 'num':per_page} + params = {'nl': lang, 'q': query.encode('utf8'), 'start':page * per_page, 'num':per_page} params = urlencode(params) url = u"http://www.google.com/search?" + params #return u"http://www.google.com/search?hl=%s&q=%s&start=%i&num=%i" % (lang, normalize_query(query), page * per_page, per_page) From c0ef17b29691a765b4922d6ce5a2196e25d191c7 Mon Sep 17 00:00:00 2001 From: Pavel Savchenko Date: Sun, 3 Jan 2016 12:30:40 +0100 Subject: [PATCH 066/106] doc(README): Mention Firefox is required for image search --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d6a4d80..c6c4e2d 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,10 @@ CalculatorResult *Parsing of the units must be implemented. The rest of the data members of CalculatorResult can be build from the values and units of the calculation.* ## Google Image Search -Searches google images for a list of images. Image searches can be filtered to produce better results. Image searches can be downloaded. +Searches google images for a list of images. Image searches can be filtered to produce better results. Image searches can be downloaded. + +### Requirement +Image search uses the selenium & the Firefox driver, therefor you MUST have [Firefox installed](https://www.mozilla.org/en-US/firefox/new/) to use it. Perform a google image search on "banana" and filter it: From 97fca540e3de62da8a7847307948e6e472184100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Sun, 3 Jan 2016 16:14:26 -0300 Subject: [PATCH 067/106] Fixing auth mistakes. --- .gitignore | 68 +++++++++++++++++++++++++++++++ .travis.yml | 0 build/lib/google/google.py | 2 + google/modules/standard_search.py | 22 ++++++---- google/modules/utils.py | 2 +- 5 files changed, 85 insertions(+), 9 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .travis.yml diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index ae22a4d..fa9e9a7 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,71 @@ +### Linux template +*~ + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + *.pyc *.egg *.egg-info diff --git a/.travis.yml b/.travis.yml old mode 100644 new mode 100755 diff --git a/build/lib/google/google.py b/build/lib/google/google.py index f17c2ed..18f8827 100644 --- a/build/lib/google/google.py +++ b/build/lib/google/google.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from modules import images from modules import currency from modules import calculator diff --git a/google/modules/standard_search.py b/google/modules/standard_search.py index 3ac4ebc..771086a 100644 --- a/google/modules/standard_search.py +++ b/google/modules/standard_search.py @@ -45,7 +45,7 @@ def _limit_str_size(self, str_element, size_limit): # PUBLIC -def search(query, pages=1, lang='en'): +def search(query, pages=1, lang='en', void=True): """Returns a list of GoogleResult. Args: @@ -77,7 +77,9 @@ def search(query, pages=1, lang='en'): res.description = _get_description(li) res.thumb = _get_thumb() res.cached = _get_cached(li) - + if void is True: + if res.description is None: + continue results.append(res) j += 1 @@ -96,9 +98,11 @@ def _get_name(li): def _get_link(li): """Return external link from a search.""" - - a = li.find("a") - link = a["href"] + try: + a = li.find("a") + link = a["href"] + except: + return None if link.startswith("/url?"): m = match('/url\?(url|q)=(.+?)&', link) @@ -110,9 +114,11 @@ def _get_link(li): def _get_google_link(li): """Return google link from a search.""" - - a = li.find("a") - link = a["href"] + try: + a = li.find("a") + link = a["href"] + except: + return None if link.startswith("/url?") or link.startswith("/search?"): return urlparse.urljoin("http://www.google.com", link) diff --git a/google/modules/utils.py b/google/modules/utils.py index 03f4059..f2eb49c 100644 --- a/google/modules/utils.py +++ b/google/modules/utils.py @@ -32,7 +32,7 @@ def _get_search_url(query, page=0, per_page=10, lang='en'): # note: num per page might not be supported by google anymore (because of # google instant) - params = {'nl': lang, 'q': normalize_query(query).encode('utf8'), 'start':page * per_page, 'num':per_page} + params = {'hl': lang, 'q': normalize_query(query).encode('utf8'), 'start':page * per_page, 'num':per_page} params = urlencode(params) url = u"http://www.google.com/search?" + params #return u"http://www.google.com/search?hl=%s&q=%s&start=%i&num=%i" % (lang, normalize_query(query), page * per_page, per_page) From d854c9efd03d8f85fc131c9d1af49058080b1ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Sun, 3 Jan 2016 16:32:06 -0300 Subject: [PATCH 068/106] Add firefox warning. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c6c4e2d..830b24d 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ To upgrade the package if you have already installed it: pip install Google-Search-API --upgrade ``` +Please note that you should also install **Firefox browser** in order to use images search. + You could also just download or clone the repo and import the package from Google-Search-API folder. From 53920afb742d5866c7383b7d2a45c31dccbc80ae Mon Sep 17 00:00:00 2001 From: Pavel Savchenko Date: Mon, 4 Jan 2016 09:43:17 +0100 Subject: [PATCH 069/106] Allow filtering image search by license --- README.md | 7 +++++++ google/modules/images.py | 11 +++++++++++ google/tests/test_google.py | 3 ++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 830b24d..3c1f447 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,13 @@ class ColorType: COLOR = "color" BLACK_WHITE = "gray" SPECIFIC = "specific" + +class License: + NONE = None + REUSE = "fc" + REUSE_WITH_MOD = "fmc" + REUSE_NON_COMMERCIAL = "f" + REUSE_WITH_MOD_NON_COMMERCIAL = "fm" ``` You can download a list of images. diff --git a/google/modules/images.py b/google/modules/images.py index cdb9343..a5c505e 100644 --- a/google/modules/images.py +++ b/google/modules/images.py @@ -62,6 +62,14 @@ class ColorType: SPECIFIC = "specific" +class License: + NONE = None + REUSE = "fc" + REUSE_WITH_MOD = "fmc" + REUSE_NON_COMMERCIAL = "f" + REUSE_WITH_MOD_NON_COMMERCIAL = "fm" + + class ImageOptions: """Allows passing options to filter a google images search.""" @@ -74,6 +82,7 @@ def __init__(self): self.exact_height = None self.color_type = None self.color = None + self.license = None def __repr__(self): return unidecode(self.__dict__) @@ -100,6 +109,8 @@ def get_tbs(self): if self.color: tbs = self._add_to_tbs(tbs, "ic", ColorType.SPECIFIC) tbs = self._add_to_tbs(tbs, "isc", self.color) + if self.license: + tbs = self._add_to_tbs(tbs, "sur", self.license) return tbs def _add_to_tbs(self, tbs, name, value): diff --git a/google/tests/test_google.py b/google/tests/test_google.py index 7207cfd..90523af 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -140,10 +140,11 @@ def test_get_images_req_url(self): options.image_type = images.ImageType.CLIPART options.larger_than = images.LargerThan.MP_4 options.color = "green" + options.license = images.License.REUSE_WITH_MOD req_url = images._get_images_req_url(query, options) - exp_req_url = 'https://www.google.com.ar/search?q=banana&es_sm=122&source=lnms&tbm=isch&sa=X&ei=DDdUVL-fE4SpNq-ngPgK&ved=0CAgQ_AUoAQ&biw=1024&bih=719&dpr=1.25&tbs=itp:clipart,isz:lt,islt:4mp,ic:specific,isc:green' + exp_req_url = 'https://www.google.com.ar/search?q=banana&es_sm=122&source=lnms&tbm=isch&sa=X&ei=DDdUVL-fE4SpNq-ngPgK&ved=0CAgQ_AUoAQ&biw=1024&bih=719&dpr=1.25&tbs=itp:clipart,isz:lt,islt:4mp,ic:specific,isc:green,sur:fmc' self.assertEqual(req_url, exp_req_url) From 823a4ea4aab1cd5543aed34f48de61dcdf0bb8c7 Mon Sep 17 00:00:00 2001 From: Pavel Savchenko Date: Mon, 4 Jan 2016 09:59:25 +0100 Subject: [PATCH 070/106] Fix ImageResult representation * this should handle cases where result properties are not yet set (empty object) as well as normal result case where index/page integers could not be passed to `unidecode` directly --- google/modules/images.py | 9 ++++++--- google/tests/test_google.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/google/modules/images.py b/google/modules/images.py index cdb9343..6d044ca 100644 --- a/google/modules/images.py +++ b/google/modules/images.py @@ -139,9 +139,12 @@ def __hash__(self): return id(self.link) def __repr__(self): - string = "ImageResult(" + \ - "index={}, page={}, ".format(unidecode(self.index), unidecode(self.page)) + \ - "domain={}, link={})".format(unidecode(self.domain), unidecode(self.link)) + string = "ImageResult(index={i}, page={p}, domain={d}, link={l})".format( + i=str(self.index), + p=str(self.page), + d=unidecode(self.domain) if self.domain else None, + l=unidecode(self.link) if self.link else None + ) return string def download(self, path="images"): diff --git a/google/tests/test_google.py b/google/tests/test_google.py index 7207cfd..021db60 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -147,6 +147,18 @@ def test_get_images_req_url(self): self.assertEqual(req_url, exp_req_url) + def test_repr(self): + res = images.ImageResult() + assert repr(res) == 'ImageResult(index=None, page=None, domain=None, link=None)' + res.page = 1 + res.index = 11 + res.name = 'test' + res.thumb = 'test' + res.format = 'test' + res.domain = 'test' + res.link = 'http://aa.com' + assert repr(res) == 'ImageResult(index=11, page=1, domain=test, link=http://aa.com)' + def test_download(self): pass From cda5e3aefc710f3b0df8756149c4c69d7a4cc389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Mon, 4 Jan 2016 11:02:32 -0300 Subject: [PATCH 071/106] Remove build and dist folders, they shouldn't be in the repo. --- build/lib/google/__init__.py | 2 -- build/lib/google/google.py | 25 ------------------------- dist/Google-Search-API-1.1.0.zip | Bin 2915 -> 0 bytes 3 files changed, 27 deletions(-) delete mode 100644 build/lib/google/__init__.py delete mode 100644 build/lib/google/google.py delete mode 100644 dist/Google-Search-API-1.1.0.zip diff --git a/build/lib/google/__init__.py b/build/lib/google/__init__.py deleted file mode 100644 index 70578ec..0000000 --- a/build/lib/google/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from modules import calculator, currency, images, utils -from modules import standard_search, shopping_search diff --git a/build/lib/google/google.py b/build/lib/google/google.py deleted file mode 100644 index 18f8827..0000000 --- a/build/lib/google/google.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import unicode_literals - -from modules import images -from modules import currency -from modules import calculator -from modules import standard_search -from modules import shopping_search - -__author__ = "Anthony Casagrande , " + \ - "Agustin Benassi " -__version__ = "1.1.0" - - -"""Defines the public inteface of the API.""" - -search = standard_search.search -search_images = images.search -convert_currency = currency.convert -exchange_rate = currency.exchange_rate -calculate = calculator.calculate -shopping = shopping_search.shopping - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/dist/Google-Search-API-1.1.0.zip b/dist/Google-Search-API-1.1.0.zip deleted file mode 100644 index 3a8cc5a00612ec8fcd179b40748b1fea9f657dd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2915 zcmWIWW@Zs#U|`^2V9t+rQxVZ!at_EdWMp7a0MhRH`RO^Sy1}W5Madbujsc#!hI)p2 z2KoWs?z*0SZvMIb=lu>F@U*|5_=&HsAvN{-91Vru&b2;L_2=@kgS>r%RNp1$`(JCk z(KtEv_3xjX`BiQ#Uhs3l6iYVl*{6KIG}2aZ#1W zcI|?);%P~hJC4;SI?GjVxlr_~@KJSnrv4()1kXPw{vC^YyL0h`(}nGKXNUPu*?nZP z&i=jT`e$FKFTPsn^0VvY{hob{0VrY7%OCu@3m68V@KD4P7R9M0r3HG)Y3aIsfqaJy zINI*d{=&E0p;zv}WSz9k@U|n%bf%oGsE+>F`<}UOhPHV?#x1#fa}_$JU#9Q!H-B%G zv4qL8fy3MQr&?{>)ZZB{245F%IQ})2Yv#rSH~iVldSAgtzs{p zd1m(adp6I`?hX_vnsH-iV~eEv^PeqGBIhkV-`Xme{qw;bxeYaLA6DtlUOrdnlh2ZJ z7yn~7qw>sepS{|}ZhvJ{`n(srR&9A`Cu4T1S5)Q#)5MkkKE9bS#avkZzObW&c1h40 z6OKF9@u!a+%d$UeE0Dpz?XZ)Hm4N%%yP3D+1G|-HNPI}BVV}J7YO(cmZ5EfOmXEpC z|2n9#MI`^zj_R2gS?4?KZFXA~|J3sNzvCvdo4@`L?D3a*&5{4=XVUNdY_~_#njXHe zJOB7mqQlX1r{iU!dd}Hx5?DFylG#JFcS#StO7IWpSq{P|m6rod3FW@2E_z>_4> z!P!P1Lg^J$mIfXKWs|+(uk0f%8yy69FKP^XaHyBxq3h0-T;;8vQI(*Qf_=37463U^QvYrFv#T}b!1bq6;cRO zXR#186{*NVHWyn7gD}^W6}Pz%Ul3j1pj(Pw+#oESOP-~$!Ux?V^bCuz=m;BGzC+5s p=ysv!C4^m5f!PaJyyFQ;l$;gd%?fle0|O@z1_IO3A5fNM000M;FGv6Y From e7444604be04337e7f7a98490ec369dd76e77b9f Mon Sep 17 00:00:00 2001 From: Artem Rodichev Date: Thu, 7 Jan 2016 18:21:16 -0800 Subject: [PATCH 072/106] fixed infinite loop for image search --- google/modules/images.py | 69 +++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/google/modules/images.py b/google/modules/images.py index 9a2658f..068419d 100644 --- a/google/modules/images.py +++ b/google/modules/images.py @@ -62,14 +62,6 @@ class ColorType: SPECIFIC = "specific" -class License: - NONE = None - REUSE = "fc" - REUSE_WITH_MOD = "fmc" - REUSE_NON_COMMERCIAL = "f" - REUSE_WITH_MOD_NON_COMMERCIAL = "fm" - - class ImageOptions: """Allows passing options to filter a google images search.""" @@ -82,7 +74,6 @@ def __init__(self): self.exact_height = None self.color_type = None self.color = None - self.license = None def __repr__(self): return unidecode(self.__dict__) @@ -109,8 +100,6 @@ def get_tbs(self): if self.color: tbs = self._add_to_tbs(tbs, "ic", ColorType.SPECIFIC) tbs = self._add_to_tbs(tbs, "isc", self.color) - if self.license: - tbs = self._add_to_tbs(tbs, "sur", self.license) return tbs def _add_to_tbs(self, tbs, name, value): @@ -150,12 +139,9 @@ def __hash__(self): return id(self.link) def __repr__(self): - string = "ImageResult(index={i}, page={p}, domain={d}, link={l})".format( - i=str(self.index), - p=str(self.page), - d=unidecode(self.domain) if self.domain else None, - l=unidecode(self.link) if self.link else None - ) + string = "ImageResult(" + \ + "index={}, page={}, ".format(unidecode(self.index), unidecode(self.page)) + \ + "domain={}, link={})".format(unidecode(self.domain), unidecode(self.link)) return string def download(self, path="images"): @@ -442,36 +428,39 @@ def search(query, image_options=None, num_images=50): # iterate over the divs containing images in one page divs = _find_divs_with_images(soup) - if divs: - for div in divs: + # empty search result page case + if not divs: + break - res = ImageResult() + for div in divs: + + res = ImageResult() - # store indexing paramethers - res.page = page - res.index = curr_num_img + # store indexing paramethers + res.page = page + res.index = curr_num_img - # get url of image and its secondary data - a = div.find("a") - if a: - _get_image_data(res, a) + # get url of image and its secondary data + a = div.find("a") + if a: + _get_image_data(res, a) - # get url of thumb and its size paramethers - img = a.find_all("img") - if img: - _get_thumb_data(res, img) + # get url of thumb and its size paramethers + img = a.find_all("img") + if img: + _get_thumb_data(res, img) - # increment image counter only if a new image was added - prev_num_results = len(results) - results.add(res) - curr_num_results = len(results) + # increment image counter only if a new image was added + prev_num_results = len(results) + results.add(res) + curr_num_results = len(results) - if curr_num_results > prev_num_results: - curr_num_img += 1 + if curr_num_results > prev_num_results: + curr_num_img += 1 - # break the loop when limit of images is reached - if curr_num_img >= num_images: - break + # break the loop when limit of images is reached + if curr_num_img >= num_images: + break browser.quit() From a2a649e298723eb616c63dccf36b01acb14c3861 Mon Sep 17 00:00:00 2001 From: Artem Rodichev Date: Thu, 7 Jan 2016 18:40:48 -0800 Subject: [PATCH 073/106] update src --- google/modules/images.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/google/modules/images.py b/google/modules/images.py index 068419d..d668aab 100644 --- a/google/modules/images.py +++ b/google/modules/images.py @@ -62,6 +62,14 @@ class ColorType: SPECIFIC = "specific" +class License: + NONE = None + REUSE = "fc" + REUSE_WITH_MOD = "fmc" + REUSE_NON_COMMERCIAL = "f" + REUSE_WITH_MOD_NON_COMMERCIAL = "fm" + + class ImageOptions: """Allows passing options to filter a google images search.""" @@ -74,6 +82,7 @@ def __init__(self): self.exact_height = None self.color_type = None self.color = None + self.license = None def __repr__(self): return unidecode(self.__dict__) @@ -100,6 +109,8 @@ def get_tbs(self): if self.color: tbs = self._add_to_tbs(tbs, "ic", ColorType.SPECIFIC) tbs = self._add_to_tbs(tbs, "isc", self.color) + if self.license: + tbs = self._add_to_tbs(tbs, "sur", self.license) return tbs def _add_to_tbs(self, tbs, name, value): @@ -139,9 +150,12 @@ def __hash__(self): return id(self.link) def __repr__(self): - string = "ImageResult(" + \ - "index={}, page={}, ".format(unidecode(self.index), unidecode(self.page)) + \ - "domain={}, link={})".format(unidecode(self.domain), unidecode(self.link)) + string = "ImageResult(index={i}, page={p}, domain={d}, link={l})".format( + i=str(self.index), + p=str(self.page), + d=unidecode(self.domain) if self.domain else None, + l=unidecode(self.link) if self.link else None + ) return string def download(self, path="images"): From e6439ff0cb742dc40d94c1762259fc4bf84dade2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Fri, 8 Jan 2016 00:00:08 -0300 Subject: [PATCH 074/106] Upload new version to PyPi. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index de51333..6de523e 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ "nose-cov" ] setup(name='Google-Search-API', - version='1.1.10', + version='1.1.11', url='https://github.com/abenassi/Google-Search-API', description='Search in google', author='Anthony Casagrande, Agustin Benassi', From 28d08a820b0d3dfedbfb4362e10a66bb1675f3a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Tue, 22 Mar 2016 23:13:27 -0300 Subject: [PATCH 075/106] Fix standard search method. Results where "li" tags and now they are "div" tags. --- .gitignore | 1 + google/.DS_Store | Bin 0 -> 6148 bytes google/modules/calculator.py | 13 +- google/modules/standard_search.py | 10 +- google/modules/utils.py | 2 +- google/tests/test_google.py | 23 +- google/tests/test_utils.py | 25 + .../vcr_cassetes/test_search_images.yaml | 11923 ++++++++++++++++ requirements.py | 230 + setup.cfg | 3 - setup.py | 5 +- test_requirements.txt | 2 + test_standard_search.yaml | 732 + 13 files changed, 12947 insertions(+), 22 deletions(-) create mode 100644 google/.DS_Store create mode 100644 google/tests/test_utils.py create mode 100644 google/tests/vcr_cassetes/test_search_images.yaml create mode 100644 requirements.py create mode 100644 test_requirements.txt create mode 100644 test_standard_search.yaml diff --git a/.gitignore b/.gitignore index fa9e9a7..e3d7cd0 100755 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,4 @@ target/ *.egg-info *.dmp *.zip +.DS_Store diff --git a/google/.DS_Store b/google/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..190550528b56ffdd05c7d94a8fb5d2f2434ebef9 GIT binary patch literal 6148 zcmeHKyG{c!5S)cbM50NV(!anTSW)-_egMIxyXcB2{Z)JypT_K?AUf!h0yHbF$6oK) z@)WnX0Bm<2o`4O2HQf;(J}k}m-4}LO5hK!h#)tz>!|8o|n-Sn7B#o~6J-y$8>6E#W!DR8O4eJ(d% z|DWl<^#7M6t)zey_*V+pYX7p|@kv!%7mxE=+vu-!&-tXgaUK*7QI3gGj=Au1yp5#H aYd+_GFPsyD&Uny?`WbLtWK!VP3VZ=HMHQR? literal 0 HcmV?d00001 diff --git a/google/modules/calculator.py b/google/modules/calculator.py index 9dee9ac..263297d 100644 --- a/google/modules/calculator.py +++ b/google/modules/calculator.py @@ -14,10 +14,15 @@ def __init__(self): self.value = None # Result value (eg. 157300.0) self.from_value = None # Initial value (eg. 157.3) self.unit = None # Result unit (eg. u'grams') (NOT implemented yet) - self.from_unit = None # Initial unit (eg. u'kilograms') (NOT implemented yet) - self.expr = None # Initial expression (eg. u'157.3 grams') (NOT implemented yet) - self.result = None # Result expression (eg. u'157300 kilograms') (NOT implemented yet) - self.fullstring = None # Complete expression (eg. u'157.3 kilograms = 157300 grams') (NOT implemented yet) + # Initial unit (eg. u'kilograms') (NOT implemented yet) + self.from_unit = None + # Initial expression (eg. u'157.3 grams') (NOT implemented yet) + self.expr = None + # Result expression (eg. u'157300 kilograms') (NOT implemented yet) + self.result = None + # Complete expression (eg. u'157.3 kilograms = 157300 grams') (NOT + # implemented yet) + self.fullstring = None def __repr__(self): return unidecode(self.value) diff --git a/google/modules/standard_search.py b/google/modules/standard_search.py index 771086a..885daec 100644 --- a/google/modules/standard_search.py +++ b/google/modules/standard_search.py @@ -62,10 +62,10 @@ def search(query, pages=1, lang='en', void=True): if html: soup = BeautifulSoup(html, "html.parser") - lis = soup.findAll("li", attrs={"class": "g"}) - + divs = soup.findAll("div", attrs={"class": "g"}) + j = 0 - for li in lis: + for li in divs: res = GoogleResult() res.page = i @@ -90,7 +90,7 @@ def search(query, pages=1, lang='en', void=True): def _get_name(li): """Return the name of a google search.""" a = li.find("a") - #return a.text.encode("utf-8").strip() + # return a.text.encode("utf-8").strip() if a is not None: return a.text.strip() return None @@ -136,7 +136,7 @@ def _get_description(li): if sdiv: stspan = sdiv.find("span", attrs={"class": "st"}) if stspan is not None: - #return stspan.text.encode("utf-8").strip() + # return stspan.text.encode("utf-8").strip() return stspan.text.strip() else: return None diff --git a/google/modules/utils.py b/google/modules/utils.py index b9fa3ae..74bb965 100644 --- a/google/modules/utils.py +++ b/google/modules/utils.py @@ -4,7 +4,7 @@ from selenium import webdriver import urllib2 from functools import wraps -#import requests +# import requests from urllib import urlencode diff --git a/google/tests/test_google.py b/google/tests/test_google.py index d456a4d..7ccadab 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -4,6 +4,9 @@ from google import currency, images from mock import Mock import os +import vcr + +BASE_DIR = os.path.dirname(__file__) def load_html_file(path): @@ -26,6 +29,11 @@ def test_decorated(self): return test_decorator +# HELPERS +def get_dir_vcr(name): + return os.path.join(BASE_DIR, "vcr_cassetes", name) + + class GoogleTest(unittest.TestCase): @load_html_file("html_files") @@ -85,14 +93,11 @@ def test_convert_currency(self, html_f): euros = google.convert_currency(5.0, "USD", "EUR") self.assertGreater(euros, 0.0) - @load_html_file("html_files") - def test_standard_search(self, html_f): + # @load_html_file("html_files") + @vcr.use_cassette("test_standard_search.yaml") + def test_standard_search(self): """Test method to search in google.""" - # replace method to get html from a test html file - google.standard_search.get_html = \ - Mock(return_value=html_f.read().decode('utf8')) - search = google.search("github") self.assertNotEqual(len(search), 0) @@ -150,7 +155,8 @@ def test_get_images_req_url(self): def test_repr(self): res = images.ImageResult() - assert repr(res) == 'ImageResult(index=None, page=None, domain=None, link=None)' + assert repr( + res) == 'ImageResult(index=None, page=None, domain=None, link=None)' res.page = 1 res.index = 11 res.name = 'test' @@ -158,7 +164,8 @@ def test_repr(self): res.format = 'test' res.domain = 'test' res.link = 'http://aa.com' - assert repr(res) == 'ImageResult(index=11, page=1, domain=test, link=http://aa.com)' + assert repr( + res) == 'ImageResult(index=11, page=1, domain=test, link=http://aa.com)' def test_download(self): pass diff --git a/google/tests/test_utils.py b/google/tests/test_utils.py new file mode 100644 index 0000000..5e2e781 --- /dev/null +++ b/google/tests/test_utils.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Tests helper methods.""" + +from __future__ import unicode_literals +from __future__ import print_function +from __future__ import with_statement +import unittest +import nose + +from google.modules.utils import _get_search_url + + +class UtilsTestCase(unittest.TestCase): + """Tests for helper methods.""" + + def test_get_search_url(self): + url = _get_search_url("apple", 0, 10, "en") + exp_url = "http://www.google.com/search?q=apple&start=0&num=10&nl=en" + self.assertEqual(url, exp_url) + + +if __name__ == '__main__': + nose.run(defaultTest=__name__) diff --git a/google/tests/vcr_cassetes/test_search_images.yaml b/google/tests/vcr_cassetes/test_search_images.yaml new file mode 100644 index 0000000..66e879d --- /dev/null +++ b/google/tests/vcr_cassetes/test_search_images.yaml @@ -0,0 +1,11923 @@ +interactions: +- request: + body: '{"desiredCapabilities": {"platform": "ANY", "browserName": "firefox", "version": + "", "marionette": false, "javascriptEnabled": true}}' + headers: + Accept: [application/json] + Connection: [keep-alive] + Content-type: [application/json;charset="UTF-8"] + POST: [/hub/session] + User-Agent: [Python http auth] + method: POST + uri: http://127.0.0.1:64104/hub/session + response: + body: {string: !!python/unicode '{"name":"newSession","sessionId":"f0baf8c0-8109-8745-b27e-205763034e37","status":0,"value":{"cssSelectorsEnabled":true,"browserName":"firefox","handlesAlerts":true,"javascriptEnabled":true,"nativeEvents":false,"platform":"Darwin","rotatable":false,"takesScreenshot":true,"version":"40.0.2","webStorageEnabled":true,"applicationCacheEnabled":true,"databaseEnabled":true,"locationContextEnabled":true,"acceptSslCerts":true}}'} + headers: + connection: [close] + content-length: ['422'] + content-type: [application/json; charset=UTF-8] + date: ['Wed, 23 Mar 2016 01:46:21 GMT'] + server: [httpd.js] + status: {code: 200, message: OK} +- request: + body: '{"sessionId": "f0baf8c0-8109-8745-b27e-205763034e37", "type": "page load", + "ms": 120000.0}' + headers: + Accept: [application/json] + Connection: [keep-alive] + Content-type: [application/json;charset="UTF-8"] + POST: [!!python/unicode '/hub/session/f0baf8c0-8109-8745-b27e-205763034e37/timeouts'] + User-Agent: [Python http auth] + method: POST + uri: http://127.0.0.1:64104/hub/session/f0baf8c0-8109-8745-b27e-205763034e37/timeouts + response: + body: {string: !!python/unicode '{"name":"setTimeout","sessionId":"f0baf8c0-8109-8745-b27e-205763034e37","status":0,"value":""}'} + headers: + connection: [close] + content-length: ['94'] + content-type: [application/json; charset=UTF-8] + date: ['Wed, 23 Mar 2016 01:46:21 GMT'] + server: [httpd.js] + status: {code: 200, message: OK} +- request: + body: '{"url": "", "sessionId": "f0baf8c0-8109-8745-b27e-205763034e37"}' + headers: + Accept: [application/json] + Connection: [keep-alive] + Content-type: [application/json;charset="UTF-8"] + POST: [!!python/unicode '/hub/session/f0baf8c0-8109-8745-b27e-205763034e37/url'] + User-Agent: [Python http auth] + method: POST + uri: http://127.0.0.1:64104/hub/session/f0baf8c0-8109-8745-b27e-205763034e37/url + response: + body: {string: !!python/unicode '{"name":"get","sessionId":"f0baf8c0-8109-8745-b27e-205763034e37","status":0,"value":""}'} + headers: + connection: [close] + content-length: ['87'] + content-type: [application/json; charset=UTF-8] + date: ['Wed, 23 Mar 2016 01:46:21 GMT'] + server: [httpd.js] + status: {code: 200, message: OK} +- request: + body: '{"url": "https://www.google.com.ar/search?q=apple&es_sm=122&source=lnms&tbm=isch&sa=X&ei=DDdUVL-fE4SpNq-ngPgK&ved=0CAgQ_AUoAQ&biw=1024&bih=719&dpr=1.25", + "sessionId": "f0baf8c0-8109-8745-b27e-205763034e37"}' + headers: + Accept: [application/json] + Connection: [keep-alive] + Content-type: [application/json;charset="UTF-8"] + POST: [!!python/unicode '/hub/session/f0baf8c0-8109-8745-b27e-205763034e37/url'] + User-Agent: [Python http auth] + method: POST + uri: http://127.0.0.1:64104/hub/session/f0baf8c0-8109-8745-b27e-205763034e37/url + response: + body: {string: !!python/unicode '{"name":"get","sessionId":"f0baf8c0-8109-8745-b27e-205763034e37","status":0,"value":""}'} + headers: + connection: [close] + content-length: ['87'] + content-type: [application/json; charset=UTF-8] + date: ['Wed, 23 Mar 2016 01:46:24 GMT'] + server: [httpd.js] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: [application/json] + Connection: [keep-alive] + Content-type: [application/json;charset="UTF-8"] + GET: [!!python/unicode '/hub/session/f0baf8c0-8109-8745-b27e-205763034e37/source'] + User-Agent: [Python http auth] + method: GET + uri: http://127.0.0.1:64104/hub/session/f0baf8c0-8109-8745-b27e-205763034e37/source + response: + body: {string: "{\"name\":\"getPageSource\",\"sessionId\":\"f0baf8c0-8109-8745-b27e-205763034e37\"\ + ,\"status\":0,\"value\":\"\\napple\ + \ - Buscar con Google #gbsfw{min-width:400px;overflow:visible}.gb_Hb,#gbsfw.gb_g{display:block;outline:none}#gbsfw.gb_qa\ + \ iframe{display:none}.gb_Ib{padding:118px 0;text-align:center}.gb_Jb{background:no-repeat\ + \ center 0;color:#aaa;font-size:13px;line-height:20px;padding-top:76px;background-image:url('//ssl.gstatic.com/gb/images/a/f5cdd88b65.png')}.gb_Jb\ + \ a{color:#4285f4;text-decoration:none}@-moz-keyframes gb__a{0%{opacity:0}50%{opacity:1}}@keyframes\ + \ gb__a{0%{opacity:0}50%{opacity:1}}.gb_0a{display:none!important}.gb_4c{display:inline-block;padding:0\ + \ 0 0 15px;vertical-align:middle}.gb_4c:first-child,#gbsfw:first-child+.gb_4c{padding-left:0}.gb_nc{position:relative}.gb_b{display:inline-block;outline:none;vertical-align:middle;-moz-border-radius:2px;border-radius:2px;-moz-box-sizing:border-box;box-sizing:border-box;height:30px;width:30px;color:#000;cursor:default;text-decoration:none}#gb#gb\ + \ a.gb_b{color:#000;cursor:default;text-decoration:none}.gb_cb{border-color:transparent;border-bottom-color:#fff;border-style:dashed\ + \ dashed solid;border-width:0 8.5px 8.5px;display:none;position:absolute;left:6.5px;top:37px;z-index:1;height:0;width:0;-moz-animation:gb__a\ + \ .2s;animation:gb__a .2s}.gb_db{border-color:transparent;border-style:dashed\ + \ dashed solid;border-width:0 8.5px 8.5px;display:none;position:absolute;left:6.5px;z-index:1;height:0;width:0;-moz-animation:gb__a\ + \ .2s;animation:gb__a .2s;border-bottom-color:#ccc;border-bottom-color:rgba(0,0,0,.2);top:36px}x:-o-prefocus,div.gb_db{border-bottom-color:#ccc}.gb_ga{background:#fff;border:1px\ + \ solid #ccc;border-color:rgba(0,0,0,.2);color:#000;-moz-box-shadow:0 2px\ + \ 10px rgba(0,0,0,.2);box-shadow:0 2px 10px rgba(0,0,0,.2);display:none;outline:none;overflow:hidden;position:absolute;right:0;top:44px;-moz-animation:gb__a\ + \ .2s;animation:gb__a .2s;-moz-border-radius:2px;border-radius:2px;-moz-user-select:text}.gb_4c.gb_g\ + \ .gb_cb,.gb_4c.gb_g .gb_db,.gb_4c.gb_g .gb_ga,.gb_g.gb_ga{display:block}.gb_4c.gb_g.gb_Ld\ + \ .gb_cb,.gb_4c.gb_g.gb_Ld .gb_db{display:none}.gb_Md{position:absolute;right:0;top:44px;z-index:-1}.gb_3a\ + \ .gb_cb,.gb_3a .gb_db,.gb_3a .gb_ga{margin-top:-10px}.gb_wb .gb_db{border:0;border-left:1px\ + \ solid rgba(0,0,0,.2);border-top:1px solid rgba(0,0,0,.2);height:14px;width:14px;-moz-transform:rotate(45deg);transform:rotate(45deg)}.gb_wb\ + \ .gb_cb{border:0;border-left:1px solid rgba(0,0,0,.2);border-top:1px solid\ + \ rgba(0,0,0,.2);height:14px;width:14px;-moz-transform:rotate(45deg);transform:rotate(45deg);border-color:#fff;background:#fff}.gb_ce\ + \ ::-webkit-scrollbar{height:15px;width:15px}.gb_ce ::-webkit-scrollbar-button{height:0;width:0}.gb_ce\ + \ ::-webkit-scrollbar-thumb{background-clip:padding-box;background-color:rgba(0,0,0,.3);border:5px\ + \ solid transparent;-moz-border-radius:10px;border-radius:10px;min-height:20px;min-width:20px;height:5px;width:5px}.gb_ce\ + \ ::-webkit-scrollbar-thumb:hover,.gb_ce ::-webkit-scrollbar-thumb:active{background-color:rgba(0,0,0,.4)}.gb_ea\ + \ .gb_b{background-position:-35px -311px;opacity:.55}.gb_fa .gb_ea .gb_b{background-position:-35px\ + \ -311px}.gb_X .gb_ea .gb_b{background-position:-60px -1675px;opacity:1}.gb_ga.gb_ha{min-height:196px;overflow-y:auto;width:320px}.gb_ia{-moz-transition:height\ + \ .2s ease-in-out;transition:height .2s ease-in-out}.gb_ja{background:#fff;margin:0;min-height:100px;padding:28px;padding-right:27px;text-align:left;white-space:normal;width:265px}.gb_ka{background:#f5f5f5;cursor:pointer;height:40px;overflow:hidden}.gb_la{position:relative}.gb_ka{display:block;line-height:40px;text-align:center;width:320px}.gb_la{display:block;line-height:40px;text-align:center}.gb_la.gb_ma{line-height:0}.gb_ka,.gb_ka:visited,.gb_ka:active,.gb_la,.gb_la:visited{color:#737373;text-decoration:none}.gb_la:active{color:#737373}#gb\ + \ a.gb_ka,#gb a.gb_ka:visited,#gb a.gb_ka:active,#gb a.gb_la,#gb a.gb_la:visited{color:#737373;text-decoration:none}#gb\ + \ a.gb_la:active{color:#737373}.gb_la,.gb_ja{display:none}.gb_ca,.gb_ca+.gb_la,.gb_na\ + \ .gb_la,.gb_na .gb_ja{display:block}.gb_la:hover,.gb_la:active,#gb a.gb_la:hover,#gb\ + \ a.gb_la:active{text-decoration:underline}.gb_la{border-bottom:1px solid\ + \ #ebebeb;left:28px;width:264px}.gb_na .gb_ka{display:none}.gb_la:last-child{border-bottom-width:0}.gb_oa\ + \ .gb_O{display:initial}.gb_oa.gb_pa{height:100px;text-align:center}.gb_oa.gb_pa\ + \ img{padding:34px 0;height:32px;width:32px}.gb_oa .gb_3{background-image:url('//ssl.gstatic.com/gb/images/v1_8758f7bb.png');background-size:92px\ + \ 2541px;background-position:0 -690px}.gb_oa .gb_3+img{border:0;margin:8px;height:48px;width:48px}.gb_oa\ + \ div.gb_qa{background:#ffa;-moz-border-radius:5px;border-radius:5px;padding:5px;text-align:center}.gb_oa.gb_ra,.gb_oa.gb_sa{padding-bottom:0}.gb_oa.gb_ta,.gb_oa.gb_sa{padding-top:0}.gb_oa.gb_sa\ + \ a,.gb_oa.gb_ta a{top:0}.gb_ua .gb_ka{margin-top:0;position:static}.gb_va{display:inline-block}.gb_wa{margin:-12px\ + \ 28px 28px;position:relative;width:264px;-moz-border-radius:2px;border-radius:2px;-moz-box-shadow:0\ + \ 1px 2px rgba(0,0,0,0.1),0 0 1px rgba(0,0,0,0.1);box-shadow:0 1px 2px rgba(0,0,0,0.1),0\ + \ 0 1px rgba(0,0,0,0.1)}.gb_5{background-image:url('//ssl.gstatic.com/gb/images/v1_8758f7bb.png');background-size:92px\ + \ 2541px;display:inline-block;margin:8px;vertical-align:middle;height:64px;width:64px}.gb_xa{color:#262626;display:inline-block;font:13px/18px\ + \ Arial,sans-serif;margin-right:80px;padding:10px 10px 10px 0;vertical-align:middle;white-space:normal}.gb_ya{font:16px/24px\ + \ Arial,sans-serif}.gb_za,#gb#gb .gb_za{color:#427fed;text-decoration:none}.gb_za:hover,#gb#gb\ + \ .gb_za:hover{text-decoration:underline}.gb_Aa .gb_ja{position:relative}.gb_Aa\ + \ .gb_O{position:absolute;top:28px;left:28px}.gb_ka.gb_Ba{display:none;height:0}.gb_N\ + \ .gb_ea .gb_b::before,.gb_N.gb_fa .gb_ea .gb_b::before{left:-35px;top:-311px}.gb_N.gb_X\ + \ .gb_ea .gb_b::before{left:-60px;top:-1675px}.gb_wb .gb_ka{position:relative}.gb_ea\ + \ .gb_b:hover,.gb_ea .gb_b:focus{opacity:.85}.gb_X .gb_ea .gb_b:hover,.gb_X\ + \ .gb_ea .gb_b:focus{opacity:1}@media (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_oa\ + \ .gb_3{background-image:url('//ssl.gstatic.com/gb/images/v2_0bc49e87.png')}}#gb#gb\ + \ a.gb_O{color:#404040;text-decoration:none}#gb#gb a.gb_P,#gb#gb span.gb_P{text-decoration:none}#gb#gb\ + \ a.gb_P,#gb#gb span.gb_P{color:#000}.gb_P{opacity:.75}#gb#gb a.gb_P:hover,#gb#gb\ + \ a.gb_P:focus{opacity:.85;text-decoration:underline}.gb_Q.gb_R{display:none;padding-left:15px;vertical-align:middle}.gb_Q.gb_R:first-child{padding-left:0}.gb_S.gb_R{display:inline-block}.gb_Q\ + \ span{opacity:.55;-moz-user-select:text}.gb_T .gb_S.gb_R{flex:0 1 auto;flex:0\ + \ 1 main-size;display:-webkit-flex;display:flex}.gb_U .gb_S.gb_R{display:none}.gb_Q\ + \ .gb_P{display:inline-block;line-height:24px;outline:none;vertical-align:middle}.gb_S\ + \ .gb_P{min-width:60px;overflow:hidden;flex:0 1 auto;flex:0 1 main-size;text-overflow:ellipsis}.gb_V\ + \ .gb_S .gb_P{min-width:0}.gb_W .gb_S .gb_P{width:0!important}#gb#gb.gb_X\ + \ a.gb_P,#gb#gb.gb_X span.gb_P,#gb#gb .gb_X a.gb_P,#gb#gb .gb_X span.gb_P{color:#fff}#gb#gb.gb_X\ + \ span.gb_P,#gb#gb .gb_X span.gb_P{opacity:.7}.gb_M.gb_M{background-size:64px\ + \ 64px}#gb2 .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/3a1e625196.png')}.gb_N\ + \ #gb2 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/3a1e625196.png')}#gb22\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/3daf4c1f88.png')}.gb_N\ + \ #gb22 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/3daf4c1f88.png')}#gb45\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/f420d06f66.png')}.gb_N\ + \ #gb45 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/f420d06f66.png')}#gb72\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/78b3d46de1.png')}.gb_N\ + \ #gb72 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/78b3d46de1.png')}#gb117\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/142da27578.png')}.gb_N\ + \ #gb117 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/142da27578.png')}#gb136\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/911e3628e6.png')}.gb_N\ + \ #gb136 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/911e3628e6.png')}#gb166\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/5d03e9e245.png')}.gb_N\ + \ #gb166 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/5d03e9e245.png')}#gb171\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/4244245d7e.png')}.gb_N\ + \ #gb171 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/4244245d7e.png')}#gb177\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/4653513b7d.png')}.gb_N\ + \ #gb177 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/4653513b7d.png')}#gb206\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/ad330d8459.png')}.gb_N\ + \ #gb206 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/ad330d8459.png')}#gb207\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/2c21041e16.png')}.gb_N\ + \ #gb207 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/2c21041e16.png')}#gb211\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/c03dda0b34.png')}.gb_N\ + \ #gb211 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/c03dda0b34.png')}#gb217\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/71060be5b3.png')}.gb_N\ + \ #gb217 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/71060be5b3.png')}#gb228\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/74aa55e0c2.png')}.gb_N\ + \ #gb228 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/74aa55e0c2.png')}#gb249\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/afa40f6e42.png')}.gb_N\ + \ #gb249 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/afa40f6e42.png')}#gb260\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/ea554714e7.png')}.gb_N\ + \ #gb260 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/ea554714e7.png')}#gb261\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/0b26f6f8e4.png')}.gb_N\ + \ #gb261 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/0b26f6f8e4.png')}#gb108\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/dfbeb24785.png')}.gb_N\ + \ #gb108 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/dfbeb24785.png')}#gb60\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/85bb99a341.png')}.gb_N\ + \ #gb60 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/85bb99a341.png')}#gb175\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/eacd033c28.png')}.gb_N\ + \ #gb175 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/eacd033c28.png')}@media\ + \ (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){#gb2\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/438087d3df.png')}.gb_N\ + \ #gb2 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/438087d3df.png')}#gb22\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/cfa67efcd3.png')}.gb_N\ + \ #gb22 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/cfa67efcd3.png')}#gb45\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/9c561d4392.png')}.gb_N\ + \ #gb45 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/9c561d4392.png')}#gb72\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/90f42e515b.png')}.gb_N\ + \ #gb72 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/90f42e515b.png')}#gb117\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/e3cbb9b858.png')}.gb_N\ + \ #gb117 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/e3cbb9b858.png')}#gb136\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/17bdcddea9.png')}.gb_N\ + \ #gb136 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/17bdcddea9.png')}#gb166\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/56c3072e8e.png')}.gb_N\ + \ #gb166 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/56c3072e8e.png')}#gb171\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/1b217ae532.png')}.gb_N\ + \ #gb171 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/1b217ae532.png')}#gb177\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/188f0d697b.png')}.gb_N\ + \ #gb177 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/188f0d697b.png')}#gb206\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/20808fb750.png')}.gb_N\ + \ #gb206 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/20808fb750.png')}#gb207\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/6d9eaee7f9.png')}.gb_N\ + \ #gb207 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/6d9eaee7f9.png')}#gb211\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/2d7fffa981.png')}.gb_N\ + \ #gb211 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/2d7fffa981.png')}#gb217\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/e2c0b463b4.png')}.gb_N\ + \ #gb217 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/e2c0b463b4.png')}#gb228\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/fe8c881457.png')}.gb_N\ + \ #gb228 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/fe8c881457.png')}#gb249\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/d54db42004.png')}.gb_N\ + \ #gb249 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/d54db42004.png')}#gb260\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/99be7c5086.png')}.gb_N\ + \ #gb260 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/99be7c5086.png')}#gb261\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/9001dae971.png')}.gb_N\ + \ #gb261 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/9001dae971.png')}#gb108\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/ca7b209615.png')}.gb_N\ + \ #gb108 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/ca7b209615.png')}#gb60\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/e000432278.png')}.gb_N\ + \ #gb60 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/e000432278.png')}#gb175\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/84d52a8885.png')}.gb_N\ + \ #gb175 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/84d52a8885.png')}}.gb_Z{padding:1px;display:inline-block;vertical-align:top;color:black;z-index:999;height:98px;width:86px}.gb_Z\ + \ a{text-decoration:none}.gb_Z[aria-grabbed=true]{visibility:hidden}.gb_Z:hover:not(.gb_0){z-index:1001}.gb_Z:hover:not(.gb_0)\ + \ a{border:1px solid #e5e5e5;-moz-border-radius:2px;border-radius:2px;margin:7px\ + \ 1px}.gb_Z.gb_1:not(.gb_0) a{border:1px solid #e5e5e5;-moz-box-shadow:0 1px\ + \ 2px rgba(0,0,0,0.1);box-shadow:0 1px 2px rgba(0,0,0,0.1)}.gb_Z.gb_1 a{background:#fff;cursor:-moz-grabbing;cursor:-webkit-grabbing;margin:-1px;visibility:visible;z-index:1001}.gb_2{opacity:.5}.gb_Z.gb_1\ + \ a{color:#404040!important;cursor:-moz-grabbing;cursor:-webkit-grabbing;font:13px/27px\ + \ Arial,sans-serif;text-decoration:none!important}.gb_O{color:#404040;display:inline-block;font-size:13px;margin:8px\ + \ 2px;text-align:center;outline:none}.gb_O .gb_3,.gb_O .gb_M{display:inline-block;vertical-align:top;height:64px;width:64px}.gb_4{display:block;line-height:20px;overflow:hidden;white-space:nowrap;width:84px;text-overflow:ellipsis}.gb_Z:hover\ + \ .gb_O{z-index:1}.gb_Z:hover .gb_4{background:rgba(255,255,255,.9);white-space:normal;overflow-wrap:break-word;word-wrap:break-word}.gb_O.gb_0{cursor:default;filter:url(\\\ + \"data:image/svg+xml;utf8,\\\\00003csvg xmlns=\\\\000027http://www.w3.org/2000/svg\\\ + \\000027\\\\00003e\\\\00003cfilter id=\\\\000027g\\\\000027\\\\00003e\\\\\ + 00003cfeColorMatrix values=\\\\0000270.3333 0.3333 0.3333 0 0 0.3333 0.3333\ + \ 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0\\\\000027/\\\\00003e\\\\00003c/filter\\\ + \\00003e\\\\00003c/svg\\\\00003e#g\\\");opacity:.4}.gb_O .gb_3{background-image:url('//ssl.gstatic.com/gb/images/v1_8758f7bb.png');background-size:92px\ + \ 2541px}.gb_N .gb_O .gb_3,.gb_N .gb_5.gb_3{background-image:none;overflow:hidden;position:relative}.gb_N\ + \ .gb_O .gb_3::before,.gb_N .gb_5.gb_3::before{content:url('//ssl.gstatic.com/gb/images/v1_8758f7bb.png');position:absolute}.gb_N\ + \ .gb_M{background-image:none!important;position:relative}.gb_N .gb_M::before{left:0;position:absolute;top:0}@media\ + \ (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_O\ + \ .gb_3{background-image:url('//ssl.gstatic.com/gb/images/v2_0bc49e87.png')}.gb_N\ + \ .gb_O .gb_3::before{content:url('//ssl.gstatic.com/gb/images/v2_0bc49e87.png');-moz-transform:scale(.5);transform:scale(.5);-moz-transform-origin:0\ + \ 0;transform-origin:0 0}.gb_N .gb_O .gb_M::before{-moz-transform:scale(.5);transform:scale(.5);-moz-transform-origin:0\ + \ 0;transform-origin:0 0}}.gb_6 .gb_O:focus,#gb#gb .gb_6 a.gb_O:focus{text-decoration:underline}.gb_Z[aria-grabbed=true].gb_7{visibility:visible}.gb_8,.gb_9{position:relative;top:27px;visibility:hidden}.gb_aa,.gb_ba{left:37px;visibility:hidden}.gb_8{float:left;width:0;height:0;border-top:5px\ + \ solid transparent;border-bottom:5px solid transparent;border-right:5px solid\ + \ #4273db}.gb_9{float:right;width:0;height:0;border-top:5px solid transparent;border-bottom:5px\ + \ solid transparent;border-left:5px solid #4273db}.gb_aa{position:absolute;top:0;width:0;height:0;border-left:5px\ + \ solid transparent;border-right:5px solid transparent;border-bottom:5px solid\ + \ #4273db}.gb_ba{position:absolute;top:59px;width:0;height:0;border-left:5px\ + \ solid transparent;border-right:5px solid transparent;border-top:5px solid\ + \ #4273db}ul.gb_ca li.gb_7:not(:first-child) .gb_8,ul.gb_ca li.gb_7:not(:nth-child(-n+3))\ + \ .gb_aa,ul.gb_ca li.gb_7 .gb_9,ul.gb_ca li.gb_7 .gb_ba,ul.gb_da li.gb_7 .gb_8,ul.gb_da\ + \ li.gb_7 .gb_aa,ul.gb_da li.gb_7:not(:last-child) .gb_9,ul.gb_da li.gb_7:not(:nth-last-child(-n+3))\ + \ .gb_ba{visibility:visible}.gb_Ca{border:none;color:#4285f4;cursor:default;font-weight:bold;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap;-moz-user-select:none}.gb_Ca:hover{background-color:rgba(153,153,153,.2)}.gb_Ca:active{background-color:rgba(153,153,153,.4)}.gb_Ca,.gb_Da,.gb_Ea{display:inline-block;line-height:28px;padding:0\ + \ 12px;-moz-border-radius:2px;border-radius:2px}.gb_Da{background:#f8f8f8;border:1px\ + \ solid #c6c6c6}.gb_Ea{background:#f8f8f8}.gb_Da,#gb a.gb_Da.gb_Da,.gb_Ea{color:#666;cursor:default;text-decoration:none}#gb\ + \ a.gb_Ea.gb_Ea{cursor:default;text-decoration:none}.gb_Ea{border:1px solid\ + \ #4285f4;font-weight:bold;outline:none;background:#4285f4;background:-moz-linear-gradient(top,#4387fd,#4683ea);background:linear-gradient(top,#4387fd,#4683ea);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4387fd,endColorstr=#4683ea,GradientType=0)}#gb\ + \ a.gb_Ea.gb_Ea{color:#fff}.gb_Ea:hover{-moz-box-shadow:0 1px 0 rgba(0,0,0,.15);box-shadow:0\ + \ 1px 0 rgba(0,0,0,.15)}.gb_Ea:active{-moz-box-shadow:inset 0 2px 0 rgba(0,0,0,.15);box-shadow:inset\ + \ 0 2px 0 rgba(0,0,0,.15);background:#3c78dc;background:-moz-linear-gradient(top,#3c7ae4,#3f76d3);background:linear-gradient(top,#3c7ae4,#3f76d3);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3c7ae4,endColorstr=#3f76d3,GradientType=0)}.gb_Nd{display:inline-block;line-height:normal;position:relative;z-index:987}.gb_1a{background-size:32px\ + \ 32px;-moz-border-radius:50%;border-radius:50%;display:block;margin:-1px;overflow:hidden;position:relative;height:32px;width:32px}@media\ + \ (min-resolution:1.25dppx),(-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_1a::before{-moz-transform:scale(.5);transform:scale(.5);-moz-transform-origin:left\ + \ 0;transform-origin:left 0}.gb_lb::before{-moz-transform:scale(.5);transform:scale(.5);-moz-transform-origin:left\ + \ 0;transform-origin:left 0}}.gb_1a:hover,.gb_1a:focus{-moz-box-shadow:0 1px\ + \ 0 rgba(0,0,0,.15);box-shadow:0 1px 0 rgba(0,0,0,.15)}.gb_1a:active{-moz-box-shadow:inset\ + \ 0 2px 0 rgba(0,0,0,.15);box-shadow:inset 0 2px 0 rgba(0,0,0,.15)}.gb_1a:active::after{background:rgba(0,0,0,.1);-moz-border-radius:50%;border-radius:50%;content:'';display:block;height:100%}.gb_2a{cursor:pointer;line-height:30px;min-width:30px;opacity:.75;overflow:hidden;vertical-align:middle;text-overflow:ellipsis}.gb_b.gb_2a{width:auto}.gb_2a:hover,.gb_2a:focus{opacity:.85}.gb_3a\ + \ .gb_2a,.gb_3a .gb_4a{line-height:26px}#gb#gb.gb_3a a.gb_2a,.gb_3a .gb_4a{font-size:11px;height:auto}.gb_5a{border-top:4px\ + \ solid #000;border-left:4px dashed transparent;border-right:4px dashed transparent;display:inline-block;margin-left:6px;opacity:.75;vertical-align:middle}.gb_6a:hover\ + \ .gb_5a{opacity:.85}.gb_X .gb_2a,.gb_X .gb_5a{opacity:1}#gb#gb.gb_X.gb_X\ + \ a.gb_2a,#gb#gb .gb_X.gb_X a.gb_2a{color:#fff}.gb_X.gb_X .gb_5a{border-top-color:#fff;opacity:1}.gb_fa\ + \ .gb_1a:hover,.gb_X .gb_1a:hover,.gb_fa .gb_1a:focus,.gb_X .gb_1a:focus{-moz-box-shadow:0\ + \ 1px 0 rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 0 rgba(0,0,0,.15),0\ + \ 1px 2px rgba(0,0,0,.2)}.gb_7a .gb_8a,.gb_9a .gb_8a{position:absolute;right:1px}.gb_8a.gb_R,.gb_ab.gb_R,.gb_6a.gb_R{flex:0\ + \ 1 auto;flex:0 1 main-size}.gb_bb.gb_W .gb_2a{width:30px!important}.gb_2a~.gb_cb,.gb_2a~.gb_db{left:auto;right:6.5px}.gb_eb{outline:none}.gb_fb,#gb\ + \ a.gb_fb.gb_fb,.gb_gb a,#gb .gb_gb.gb_gb a{color:#36c;text-decoration:none}.gb_fb:active,#gb\ + \ a.gb_fb:active,.gb_fb:hover,#gb a.gb_fb:hover,.gb_gb a:active,#gb .gb_gb\ + \ a:active,.gb_gb a:hover,#gb .gb_gb a:hover{text-decoration:underline}.gb_hb{margin:20px}.gb_ib,.gb_jb{display:inline-block;vertical-align:top}.gb_ib{margin-right:20px;position:relative}.gb_kb{-moz-border-radius:50%;border-radius:50%;overflow:hidden}.gb_lb{background-size:96px\ + \ 96px;border:none;vertical-align:top;height:96px;width:96px}.gb_mb{background:rgba(78,144,254,.7);bottom:0;color:#fff;font-size:9px;font-weight:bold;left:0;line-height:9px;position:absolute;padding:7px\ + \ 0;text-align:center;width:96px}.gb_kb .gb_mb{background:rgba(0,0,0,.54)}.gb_nb{font-weight:bold;margin:-4px\ + \ 0 1px 0}.gb_ob{color:#666}.gb_gb{color:#ccc;margin:6px 0}.gb_gb a{margin:0\ + \ 10px}.gb_gb a:first-child{margin-left:0}.gb_gb a:last-child{margin-right:0}.gb_jb\ + \ .gb_pb{background:#4d90fe;border-color:#3079ed;font-weight:bold;margin:10px\ + \ 0 0 0;color:#fff}#gb .gb_jb a.gb_pb.gb_pb{color:#fff}.gb_jb .gb_pb:hover{background:#357ae8;border-color:#2f5bb7}.gb_qb{background:#f5f5f5;border-top:1px\ + \ solid #ccc;border-color:rgba(0,0,0,.2);padding:10px 0;width:100%;display:table}.gb_qb\ + \ .gb_pb{margin:0 20px}.gb_qb>div{display:table-cell;text-align:right}.gb_qb>div:first-child{text-align:left}.gb_qb\ + \ .gb_rb{display:block;text-align:center}.gb_sb .gb_cb{border-bottom-color:#fef9db}.gb_tb{background:#fef9db;font-size:11px;padding:10px\ + \ 20px;white-space:normal}.gb_tb b,.gb_fb{white-space:nowrap}.gb_ub{background:#f5f5f5;border-top:1px\ + \ solid #ccc;border-top-color:rgba(0,0,0,.2);max-height:230px;overflow:auto}.gb_vb{border-top:1px\ + \ solid #ccc;border-top-color:rgba(0,0,0,.2);display:block;padding:10px 20px}.gb_wb\ + \ .gb_vb:focus .gb_xb{outline:1px dotted #fff}.gb_vb:hover{background:#eee}.gb_vb:first-child,.gb_yb:first-child+.gb_vb{border-top:0}.gb_yb{display:none}.gb_zb{cursor:default}.gb_zb:hover{background:transparent}.gb_Ab{border:none;vertical-align:top;height:48px;width:48px}.gb_xb{display:inline-block;margin:6px\ + \ 0 0 10px}.gb_zb .gb_Ab,.gb_zb .gb_xb{opacity:.4}.gb_Bb{color:#000}.gb_zb\ + \ .gb_Bb{color:#666}.gb_Cb{color:#666}.gb_Db{background:#f5f5f5;border-top:1px\ + \ solid #ccc;border-top-color:rgba(0,0,0,.2);display:block;padding:10px 20px}.gb_Eb{background-position:0\ + \ -1421px;display:inline-block;margin:1px 0;vertical-align:middle;height:25px;width:25px}.gb_N\ + \ .gb_Eb::before{left:0;top:-1421px}.gb_Fb{color:#427fed;display:inline-block;padding:0\ + \ 25px 0 10px;vertical-align:middle;white-space:normal}.gb_Db:hover .gb_Fb{text-decoration:underline}.gb_2e{color:#000;font:13px/27px\ + \ Arial,sans-serif;left:0;min-width:1152px;position:absolute;top:0;-moz-user-select:-moz-none;width:100%}.gb_ce{font:13px/27px\ + \ Arial,sans-serif;position:relative;height:60px;width:100%}.gb_3a .gb_ce{height:28px}#gba{height:60px}#gba.gb_3a{height:28px}#gba.gb_3e{height:90px}#gba.gb_3e.gb_3a{height:58px}.gb_ce>.gb_R{height:60px;line-height:58px;vertical-align:middle}.gb_3a\ + \ .gb_ce>.gb_R{height:28px;line-height:26px}.gb_ce::before{background:#e5e5e5;bottom:0;content:'';display:none;height:1px;left:0;position:absolute;right:0}.gb_ce{background:#f1f1f1}.gb_4e\ + \ .gb_ce{background:#fff}.gb_4e .gb_ce::before,.gb_3a .gb_ce::before{display:none}.gb_fa\ + \ .gb_ce,.gb_X .gb_ce,.gb_3a .gb_ce{background:transparent}.gb_fa .gb_ce::before{background:#e1e1e1;background:rgba(0,0,0,.12)}.gb_X\ + \ .gb_ce::before{background:#333;background:rgba(255,255,255,.2)}.gb_R{display:inline-block;flex:0\ + \ 0 auto;flex:0 0 main-size}.gb_R.gb_5e{float:right;order:1}.gb_6e{white-space:nowrap}.gb_T\ + \ .gb_6e{display:-webkit-flex;display:flex}.gb_6e,.gb_R{margin-left:0!important;margin-right:0!important}.gb_Pb{background-image:url('//ssl.gstatic.com/gb/images/v1_8758f7bb.png');background-size:92px\ + \ 2541px}@media (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_Pb{background-image:url('//ssl.gstatic.com/gb/images/v2_0bc49e87.png')}}.gb_bb:not(.gb_N)\ + \ .gb_1a::before,.gb_bb:not(.gb_N) .gb_lb::before{content:none}.gb_N .gb_Nb\ + \ .gb_Pb::before{left:0;top:-762px}.gb_N.gb_X .gb_Nb .gb_Pb::before{left:0;top:-2439px}.gb_N.gb_fa\ + \ .gb_Nb .gb_Pb::before{left:0;top:-1883px}.gb_N .gb_Qb{background-image:none!important}.gb_N\ + \ .gb_Rb{visibility:visible}.gb_wb .gb_Qc span{background:transparent}.gb_Kb{min-width:127px;overflow:hidden;position:relative;z-index:987}.gb_Lb{position:absolute;padding:0\ + \ 20px 0 15px}.gb_Mb .gb_Lb{right:100%;margin-right:-127px}.gb_Nb{display:inline-block;outline:none;vertical-align:middle}.gb_Ob\ + \ .gb_Nb{position:relative;top:2px}.gb_Nb .gb_Pb,.gb_Qb{display:block}.gb_Rb{border:none;display:block;visibility:hidden}.gb_Nb\ + \ .gb_Pb{background-position:0 -762px;height:33px;width:92px}.gb_Qb{background-repeat:no-repeat}.gb_X\ + \ .gb_Nb .gb_Pb{background-position:0 -2439px}.gb_fa .gb_Nb .gb_Pb{background-position:0\ + \ -1883px;opacity:.54}@-moz-keyframes gb__nb{0%{-moz-transform:scale(0,0);transform:scale(0,0)}20%{-moz-transform:scale(1.4,1.4);transform:scale(1.4,1.4)}50%{-moz-transform:scale(.8,.8);transform:scale(.8,.8)}85%{-moz-transform:scale(1.1,1.1);transform:scale(1.1,1.1)}to{-moz-transform:scale(1.0,1.0);transform:scale(1.0,1.0)}}@keyframes\ + \ gb__nb{0%{-moz-transform:scale(0,0);transform:scale(0,0)}20%{-moz-transform:scale(1.4,1.4);transform:scale(1.4,1.4)}50%{-moz-transform:scale(.8,.8);transform:scale(.8,.8)}85%{-moz-transform:scale(1.1,1.1);transform:scale(1.1,1.1)}to{-moz-transform:scale(1.0,1.0);transform:scale(1.0,1.0)}}.gb_ec{background-position:-35px\ + \ -658px;opacity:.55;height:100%;width:100%}.gb_b:hover .gb_ec,.gb_b:focus\ + \ .gb_ec{opacity:.85}.gb_fc .gb_ec{background-position:-35px -1848px}.gb_gc{background-color:#cb4437;-moz-border-radius:8px;border-radius:8px;font:bold\ + \ 11px Arial;color:#fff;line-height:16px;min-width:14px;padding:0 1px;position:absolute;right:0;text-align:center;text-shadow:0\ + \ 1px 0 rgba(0,0,0,0.1);top:0;visibility:hidden;z-index:990}.gb_hc .gb_gc,.gb_hc\ + \ .gb_ic,.gb_hc .gb_ic.gb_jc{visibility:visible}.gb_ic{padding:0 2px;visibility:hidden}.gb_kc:not(.gb_lc)\ + \ .gb_db,.gb_kc:not(.gb_lc) .gb_cb{left:3px}.gb_gc.gb_mc{-moz-animation:gb__nb\ + \ .6s 1s both ease-in-out;animation:gb__nb .6s 1s both ease-in-out;-moz-perspective-origin:top\ + \ right;perspective-origin:top right;-moz-transform:scale(1,1);transform:scale(1,1);-moz-transform-origin:top\ + \ right;transform-origin:top right}.gb_mc .gb_ic{visibility:visible}.gb_fa\ + \ .gb_b .gb_ec{background-position:0 -311px;opacity:.7}.gb_fa .gb_fc .gb_ec{background-position:0\ + \ -1658px}.gb_fa .gb_b:hover .gb_ec,.gb_fa .gb_b:focus .gb_ec{opacity:.85}.gb_X\ + \ .gb_b .gb_ec{background-position:-35px -623px;opacity:1}.gb_X .gb_fc .gb_ec{background-position:-35px\ + \ -276px}.gb_fa .gb_gc,.gb_X .gb_gc{border:none}.gb_kc .gb_nc{font-size:14px;font-weight:bold;top:0;right:0}.gb_kc\ + \ .gb_b{display:inline-block;vertical-align:middle;-moz-box-sizing:border-box;box-sizing:border-box;height:30px;width:30px}.gb_kc\ + \ .gb_cb{border-bottom-color:#e5e5e5}.gb_oc{background-color:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:bold;line-height:20px;margin:5px;padding:0\ + \ 2px;text-align:center;-moz-box-sizing:border-box;box-sizing:border-box;-moz-border-radius:50%;border-radius:50%;height:20px;width:20px}.gb_oc.gb_pc{background-position:-35px\ + \ -1675px}.gb_oc.gb_qc{background-position:-69px 0}.gb_b:hover .gb_oc,.gb_b:focus\ + \ .gb_oc{background-color:rgba(0,0,0,.85)}#gbsfw.gb_rc{background:#e5e5e5;border-color:#ccc}.gb_fa\ + \ .gb_oc{background-color:rgba(0,0,0,.7)}.gb_X .gb_oc.gb_oc,.gb_X .gb_hc .gb_oc.gb_oc,.gb_X\ + \ .gb_hc .gb_b:hover .gb_oc,.gb_X .gb_hc .gb_b:focus .gb_oc{background-color:#fff;color:#404040}.gb_X\ + \ .gb_oc.gb_pc{background-position:0 -1921px}.gb_X .gb_oc.gb_qc{background-position:-69px\ + \ -869px}.gb_hc .gb_oc.gb_oc{background-color:#db4437;color:#fff}.gb_hc .gb_b:hover\ + \ .gb_oc,.gb_hc .gb_b:focus .gb_oc{background-color:#a52714}.gb_hc .gb_oc.gb_qc{background-position:-69px\ + \ 0}.gb_N .gb_ec::before{left:-35px;top:-658px}.gb_N .gb_fc .gb_ec::before{left:-35px;top:-1848px}.gb_N.gb_fa\ + \ .gb_b .gb_ec::before{left:0;top:-311px}.gb_N.gb_fa .gb_fc .gb_ec::before{left:0;top:-1658px}.gb_N.gb_X\ + \ .gb_b .gb_ec::before{left:-35px;top:-623px}.gb_N.gb_X .gb_fc .gb_ec::before{left:-35px;top:-276px}.gb_wb\ + \ .gb_oc{border:1px solid #fff;color:#fff}.gb_wb.gb_fa .gb_oc{border-color:#000;color:#000}.gb_N\ + \ .gb_oc.gb_pc::before,.gb_wb.gb_N.gb_X .gb_oc.gb_pc::before{left:-35px;top:-1675px}.gb_N\ + \ .gb_oc.gb_qc::before,.gb_wb.gb_N.gb_X .gb_oc.gb_qc::before{left:-69px;top:0}.gb_N.gb_X\ + \ .gb_oc.gb_pc::before,.gb_wb.gb_N.gb_fa .gb_oc.gb_pc::before{left:0;top:-1921px}.gb_N.gb_X\ + \ .gb_oc.gb_qc::before,.gb_wb.gb_N.gb_fa .gb_oc.gb_qc::before{left:-69px;top:-869px}.gb_wc{color:#ffffff;font-size:13px;font-weight:bold;height:25px;line-height:19px;padding-top:5px;padding-left:12px;position:relative;background-color:#4d90fe}.gb_wc\ + \ .gb_xc{color:#ffffff;cursor:default;font-size:22px;font-weight:normal;position:absolute;right:12px;top:5px}.gb_wc\ + \ .gb_yc,.gb_wc .gb_zc{color:#ffffff;display:inline-block;font-size:11px;margin-left:16px;padding:0\ + \ 8px;white-space:nowrap}.gb_Ac{background:none;background-image:-moz-linear-gradient(top,rgba(0,0,0,0.16),rgba(0,0,0,0.2));background-image:linear-gradient(top,rgba(0,0,0,0.16),rgba(0,0,0,0.2));background-image:-moz-linear-gradient(top,rgba(0,0,0,0.16),rgba(0,0,0,0.2));-moz-border-radius:2px;border-radius:2px;border:1px\ + \ solid #dcdcdc;border:1px solid rgba(0,0,0,0.1);cursor:default!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#160000ff,endColorstr=#220000ff);text-decoration:none!important}.gb_Ac:hover{background:none;background-image:-moz-linear-gradient(top,rgba(0,0,0,0.14),rgba(0,0,0,0.2));background-image:linear-gradient(top,rgba(0,0,0,0.14),rgba(0,0,0,0.2));background-image:-moz-linear-gradient(top,rgba(0,0,0,0.14),rgba(0,0,0,0.2));border:1px\ + \ solid rgba(0,0,0,0.2);box-shadow:0 1px 1px rgba(0,0,0,0.1);-moz-box-shadow:0\ + \ 1px 1px rgba(0,0,0,0.1);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#14000000,endColorstr=#22000000)}.gb_Ac:active{box-shadow:inset\ + \ 0 1px 2px rgba(0,0,0,0.3);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.3)}.gb_4c.gb_Od{padding:0}.gb_Od\ + \ .gb_ga{padding:26px 26px 22px 13px;background:#ffffff}.gb_Pd.gb_Od .gb_ga{background:#4d90fe}a.gb_Qd{color:#666666!important;font-size:22px;height:9px;opacity:.8;position:absolute;right:14px;top:4px;text-decoration:none!important;width:9px}.gb_Pd\ + \ a.gb_Qd{color:#c1d1f4!important}a.gb_Qd:hover,a.gb_Qd:active{opacity:1}.gb_Rd{padding:0;width:258px;white-space:normal;display:table}.gb_Sd\ + \ .gb_ga{top:36px;border:0;padding:18px 24px 16px 20px;-moz-box-shadow:4px\ + \ 4px 12px rgba(0,0,0,0.4);box-shadow:4px 4px 12px rgba(0,0,0,0.4)}.gb_Sd\ + \ .gb_Rd{width:356px}.gb_Sd .gb_Ea,.gb_Sd .gb_Td,.gb_Sd .gb_Ud,.gb_Sd .gb_Ca,.gb_Vd{line-height:normal;font-family:Roboto,RobotoDraft,Helvetica,Arial,sans-serif}.gb_Sd\ + \ .gb_Ea,.gb_Sd .gb_Td,.gb_Sd .gb_Ca{font-weight:500}.gb_Sd .gb_Ea,.gb_Sd\ + \ .gb_Ca{border:0;padding:10px 8px}.gb_Od .gb_Ea:active{outline:none;-moz-box-shadow:0\ + \ 4px 5px rgba(0,0,0,.16);box-shadow:0 4px 5px rgba(0,0,0,.16)}.gb_Sd .gb_Td{margin-bottom:8px}.gb_Sd\ + \ .gb_Ud{color:#808080;font-size:14px}.gb_Wd{text-align:right;font-size:14px;padding-bottom:10px}.gb_Wd\ + \ .gb_yc{margin-left:16px}.gb_Vd{background-color:#404040;color:#fff;padding:16px\ + \ 24px 16px 20px;position:absolute;top:36px;width:356px;right:0;-moz-border-radius:2px;border-radius:2px;-moz-box-shadow:4px\ + \ 4px 12px rgba(0,0,0,0.4);box-shadow:4px 4px 12px rgba(0,0,0,0.4)}.gb_Vd\ + \ a,.gb_Vd a:visited{color:#5e97f6;text-decoration:none}.gb_Xd{position:absolute;right:20px;text-transform:uppercase}.gb_Pd\ + \ .gb_Rd{width:200px}.gb_Td{color:#333333;font-size:16px;line-height:20px;margin:0;margin-bottom:16px}.gb_Pd\ + \ .gb_Td{color:#ffffff}.gb_Ud{color:#666666;line-height:17px;margin:0;margin-bottom:5px}.gb_Pd\ + \ .gb_Ud{color:#ffffff}.gb_Zd{position:absolute;background:transparent;top:-999px;z-index:-1;visibility:hidden;margin-top:1px;margin-left:1px}#gb\ + \ .gb_Od{margin:0}.gb_Od .gb_pb{background:#4d90fe;border-color:#3079ed;margin-top:15px}#gb\ + \ .gb_Od a.gb_pb.gb_pb{color:#ffffff}.gb_Od .gb_pb:hover{background:#357ae8;border-color:#2f5bb7}.gb_0d\ + \ .gb_nc .gb_cb{border-bottom-color:#ffffff;display:block}.gb_1d .gb_nc .gb_cb{border-bottom-color:#4d90fe;display:block}.gb_0d\ + \ .gb_nc .gb_db,.gb_1d .gb_nc .gb_db{display:block}.gb_2d,.gb_3d{display:table-cell}.gb_2d{vertical-align:middle}.gb_Sd\ + \ .gb_2d{vertical-align:top}.gb_3d{padding-left:13px;width:100%}.gb_Sd .gb_3d{padding-left:20px}.gb_5d{margin-bottom:32px;font-size:small}.gb_5d\ + \ .gb_6d{margin-right:5px}.gb_5d .gb_7d{color:red}.gb_vc{display:none}.gb_vc.gb_g{display:block}.gb_8d{position:relative;width:650px;z-index:986}#gbq2{padding-top:15px}.gb_T\ + \ .gb_8d{min-width:200px;flex:0 2 auto;flex:0 2 main-size}.gb_V~.gb_8d{min-width:0}.gb_T\ + \ #gbqf{margin-right:0;display:-webkit-flex;display:flex}.gb_T .gbqff{min-width:0;flex:1\ + \ 1 auto;flex:1 1 main-size}.gb_N .gbqfi::before{left:0;top:-415px}.gb_wb\ + \ .gbqfb:focus .gbqfi{outline:1px dotted #fff}#gbq2{display:block}#gbqf{display:block;margin:0;margin-right:60px;white-space:nowrap}.gbqff{border:none;display:inline-block;margin:0;padding:0;vertical-align:top;width:100%}.gbqfqw,#gbqfb,.gbqfwa{vertical-align:top}#gbqfaa,#gbqfab,#gbqfqwb{position:absolute}#gbqfaa{left:0}#gbqfab{right:0}.gbqfqwb,.gbqfqwc{right:0;left:0;height:100%}.gbqfqwb{padding:0\ + \ 8px}#gbqfbw{display:inline-block;vertical-align:top}#gbqfb{border:1px solid\ + \ transparent;border-bottom-left-radius:0;border-top-left-radius:0;height:30px;margin:0;outline:none;padding:0\ + \ 0;width:60px;-moz-box-shadow:none;box-shadow:none;-moz-box-sizing:border-box;box-sizing:border-box;background:#4285f4;background:-moz-linear-gradient(top,#4387fd,#4683ea);background:linear-gradient(top,#4387fd,#4683ea);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4387fd,endColorstr=#4683ea,GradientType=1)}#gbqfb:hover{-moz-box-shadow:0\ + \ 1px 0 rgba(0,0,0,.15);box-shadow:0 1px 0 rgba(0,0,0,.15)}#gbqfb:focus{-moz-box-shadow:inset\ + \ 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}#gbqfb:hover:focus{-moz-box-shadow:0\ + \ 1px 0 rgba(0,0,0,.15),inset 0 0 0 1px #fff;box-shadow:0 1px 0 rgba(0,0,0,.15),inset\ + \ 0 0 0 1px #fff}#gbqfb:active:active{border:1px solid transparent;-moz-box-shadow:inset\ + \ 0 2px 0 rgba(0,0,0,.15);box-shadow:inset 0 2px 0 rgba(0,0,0,.15);background:#3c78dc;background:-moz-linear-gradient(top,#3c7ae4,#3f76d3);background:linear-gradient(top,#3c7ae4,#3f76d3);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3c7ae4,endColorstr=#3f76d3,GradientType=1)}.gbqfi{background-position:0\ + \ -415px;display:inline-block;margin:-1px;height:30px;width:30px}.gbqfqw{background:#fff;background-clip:padding-box;border:1px\ + \ solid #cdcdcd;border-color:rgba(0,0,0,.15);border-right-width:0;height:30px;-moz-box-sizing:border-box;box-sizing:border-box}#gbfwc\ + \ .gbqfqw{border-right-width:1px}#gbqfqw{position:relative}.gbqfqw.gbqfqw:hover{border-color:#a9a9a9;border-color:rgba(0,0,0,.3)}.gbqfwa{display:inline-block;width:100%}.gbqfwb{width:40%}.gbqfwc{width:60%}.gbqfwb\ + \ .gbqfqw{margin-left:10px}.gbqfqw.gbqfqw:active,.gbqfqw.gbqfqwf.gbqfqwf{border-color:#4285f4}#gbqfq,#gbqfqb,#gbqfqc{background:transparent;border:none;height:20px;margin-top:4px;padding:0;vertical-align:top;width:100%}#gbqfq:focus,#gbqfqb:focus,#gbqfqc:focus{outline:none}.gbqfif,.gbqfsf{color:#222;font:16px\ + \ arial,sans-serif}#gbqfbwa{display:none;text-align:center;height:0}#gbqfbwa\ + \ .gbqfba{margin:16px 8px}#gbqfsa,#gbqfsb{font:bold 11px/27px Arial,sans-serif!important;vertical-align:top}.gb_fa\ + \ .gbqfqw.gbqfqw,.gb_X .gbqfqw.gbqfqw{border-color:rgba(255,255,255,1);-moz-box-shadow:0\ + \ 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 2px rgba(0,0,0,.2)}.gb_fa #gbqfb,.gb_X\ + \ #gbqfb{-moz-box-shadow:0 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 2px rgba(0,0,0,.2)}.gb_fa\ + \ #gbqfb:hover,.gb_X #gbqfb:hover{-moz-box-shadow:0 1px 0 rgba(0,0,0,.15),0\ + \ 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 0 rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.2)}.gb_fa\ + \ #gbqfb:active,.gb_X #gbqfb:active{-moz-box-shadow:inset 0 2px 0 rgba(0,0,0,.15),0\ + \ 1px 2px rgba(0,0,0,.2);box-shadow:inset 0 2px 0 rgba(0,0,0,.15),0 1px 2px\ + \ rgba(0,0,0,.2)}.gbqfb,.gbqfba,.gbqfbb{cursor:default!important;display:inline-block;font-weight:bold;height:29px;line-height:29px;min-width:54px;padding:0\ + \ 8px;text-align:center;text-decoration:none!important;-moz-border-radius:2px;border-radius:2px;-moz-user-select:-moz-none}.gbqfba:focus{border:1px\ + \ solid #4d90fe;outline:none;-moz-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset\ + \ 0 0 0 1px #fff}.gbqfba:hover{border-color:#c6c6c6;color:#222!important;-moz-box-shadow:0\ + \ 1px 0 rgba(0,0,0,.15);box-shadow:0 1px 0 rgba(0,0,0,.15);background:#f8f8f8;background:-moz-linear-gradient(top,#f8f8f8,#f1f1f1);background:linear-gradient(top,#f8f8f8,#f1f1f1);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#f8f8f8,endColorstr=#f1f1f1,GradientType=1)}.gbqfba:hover:focus{-moz-box-shadow:0\ + \ 1px 0 rgba(0,0,0,.15),inset 0 0 0 1px #fff;box-shadow:0 1px 0 rgba(0,0,0,.15),inset\ + \ 0 0 0 1px #fff}.gbqfb::-moz-focus-inner{border:0}.gbqfba::-moz-focus-inner{border:0}.gbqfba{border:1px\ + \ solid #dcdcdc;border-color:rgba(0,0,0,0.1);color:#444!important;font-size:11px;background:#f5f5f5;background:-moz-linear-gradient(top,#f5f5f5,#f1f1f1);background:linear-gradient(top,#f5f5f5,#f1f1f1);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#f5f5f5,endColorstr=#f1f1f1,GradientType=1)}.gbqfba:active{-moz-box-shadow:inset\ + \ 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.gb_9d\ + \ .gb_b{background-position:0 -623px;opacity:.55;height:30px;width:30px}.gb_9d\ + \ .gb_b:hover,.gb_9d .gb_b:focus{opacity:.85}.gb_9d .gb_cb{border-bottom-color:#f5f5f5}#gbsfw.gb_ae{background:#f5f5f5;border-color:#ccc}.gb_X\ + \ .gb_9d .gb_b{background-position:0 -2404px;opacity:1}.gb_fa .gb_9d .gb_b{background-position:0\ + \ -1848px;opacity:.7}.gb_fa .gb_9d .gb_b:hover,.gb_fa .gb_9d .gb_b:focus{opacity:.85}.gb_N\ + \ .gb_9d .gb_b::before{left:0;top:-623px}.gb_N.gb_fa .gb_9d .gb_b::before{left:0;top:-1848px}.gb_N.gb_X\ + \ .gb_9d .gb_b::before{left:0;top:-2404px}.gb_ve{width:480px}.gb_we{background:#e7e7e7;background:rgba(0,0,0,.04);border-bottom-right-radius:0;line-height:30px;position:relative;text-align:center;width:100%}.gb_we:hover{background:#dbdbdb;background:rgba(0,0,0,.08)}.gb_we\ + \ .gb_xe{margin:0 10px}.gb_ye{position:relative;z-index:1}.gb_ze{background:#eee;border-bottom:1px\ + \ solid #e3e3e3;border-left:1px solid #e3e3e3;display:inline-block;line-height:32px;text-align:center;width:160px}.gb_ye\ + \ .gb_ze:first-child{border-left:none}.gb_ye .gb_g{background:#fff;border-bottom:none}.gb_Ae{display:none;text-align:center}.gb_Ae.gb_g{display:block}.gb_Be{color:inherit;display:inline-block;padding:15px;text-decoration:none}.gb_Ce{background-clip:content-box;background-origin:content-box;display:inherit;height:64px;width:64px}.gb_De{display:block;text-align:center}.gb_Ee{border-top:none;top:78px;z-index:1;-moz-border-radius:0\ + \ 0 2px 2px;border-radius:0 0 2px 2px}.gb_Fe{display:inline-block;vertical-align:middle}.gb_He{display:inline-block;vertical-align:middle;background-size:100%;height:20px;width:20px}.gb_Ie{background-image:url('//ssl.gstatic.com/gb/images/a/5a1c013d3d.png')}.gb_Je{background-image:url('//ssl.gstatic.com/gb/images/a/de580e5330.png')}.gb_Ke{background-image:url('//ssl.gstatic.com/gb/images/a/451603daf6.png')}.gb_Fe{margin-left:4px}.gb_Le{margin:5px;width:470px}.gb_Me{border:none;display:block;margin:0\ + \ 5px;outline:none;padding:0 5px;height:30px;width:450px}.gb_Ne{border:none;display:block;margin:0\ + \ 5px;outline:none;padding:0 5px;height:30px;width:450px;border-top:1px solid\ + \ #e3e3e3}.gb_Oe{border-color:#e3e3e3;display:block;font:inherit;margin:0\ + \ 5px;outline:none;padding:5px;text-align:left;height:320px;width:450px}.gb_Pe,.gb_Qe{border:1px\ + \ solid #e3e3e3;-moz-border-radius:2px;border-radius:2px;cursor:pointer;line-height:27px;margin:5px;padding:0\ + \ 8px;width:54px}.gb_Pe{float:left}.gb_Qe{float:right}.gb_bb{min-width:315px;padding-left:30px;padding-right:30px;position:relative;text-align:right;z-index:986;align-items:center;justify-content:flex-end;-moz-user-select:-moz-none}.gb_3a\ + \ .gb_bb{min-width:0}.gb_bb.gb_R{flex:1 1 auto;flex:1 1 main-size}.gb_cc{line-height:normal;position:relative;text-align:left}.gb_cc.gb_R,.gb_Re.gb_R,.gb_4a.gb_R{flex:0\ + \ 1 auto;flex:0 1 main-size}.gb_Se,.gb_Te{display:inline-block;padding:0 0\ + \ 0 15px;position:relative;vertical-align:middle}.gb_Re{line-height:normal;padding-right:15px}.gb_bb\ + \ .gb_Re.gb_U{padding-right:0}.gb_4a{color:#404040;line-height:30px;min-width:30px;overflow:hidden;vertical-align:middle;text-overflow:ellipsis}#gb.gb_3a.gb_3a\ + \ .gb_ne,#gb.gb_3a.gb_3a .gb_cc>.gb_Te .gb_oe{background:none;border:none;color:#36c;cursor:pointer;filter:none;font-size:11px;line-height:26px;padding:0;-moz-box-shadow:none;box-shadow:none}#gb.gb_3a.gb_X\ + \ .gb_ne,#gb.gb_3a.gb_X .gb_cc>.gb_Te .gb_oe{color:#fff}.gb_3a .gb_ne{text-transform:uppercase}.gb_bb.gb_V{padding-left:0;padding-right:29px}.gb_bb.gb_Ue{max-width:400px}.gb_Ve{background-clip:content-box;background-origin:content-box;opacity:.27;padding:22px;height:16px;width:16px}.gb_Ve.gb_R{display:none}.gb_Ve:hover,.gb_Ve:focus{opacity:.55}.gb_We{background-position:-70px\ + \ -623px}.gb_Xe{background-position:-70px -519px;padding-left:30px;padding-right:14px;position:absolute;right:0;top:0;z-index:990}.gb_7a:not(.gb_9a)\ + \ .gb_Xe,.gb_V .gb_We{display:inline-block}.gb_7a .gb_We{padding-left:30px;padding-right:0;width:0}.gb_7a:not(.gb_9a)\ + \ .gb_Ze{display:none}.gb_bb.gb_R.gb_V,.gb_V:not(.gb_9a) .gb_cc{flex:0 0 auto;flex:0\ + \ 0 main-size}.gb_Ve,.gb_V .gb_Re,.gb_9a .gb_cc{overflow:hidden}.gb_7a .gb_Re{padding-right:0}.gb_V\ + \ .gb_cc{padding:1px 1px 1px 0}.gb_7a .gb_cc{width:75px}.gb_bb.gb_0e,.gb_bb.gb_0e\ + \ .gb_We,.gb_bb.gb_0e .gb_We::before,.gb_bb.gb_0e .gb_Re,.gb_bb.gb_0e .gb_cc{-moz-transition:width\ + \ .5s ease-in-out,min-width .5s ease-in-out,max-width .5s ease-in-out,padding\ + \ .5s ease-in-out,left .5s ease-in-out;transition:width .5s ease-in-out,min-width\ + \ .5s ease-in-out,max-width .5s ease-in-out,padding .5s ease-in-out,left .5s\ + \ ease-in-out}.gb_T .gb_bb{min-width:0}.gb_bb.gb_W,.gb_bb.gb_W .gb_cc,.gb_bb.gb_1e,.gb_bb.gb_1e\ + \ .gb_cc{min-width:0!important}.gb_bb.gb_W,.gb_bb.gb_W .gb_R{-moz-box-flex:0\ + \ 0 auto!important;flex:0 0 auto!important}.gb_bb.gb_W .gb_4a{width:30px!important}.gb_N\ + \ .gb_We::before{clip:rect(623px 86px 639px 70px);left:-48px;top:-601px}.gb_N\ + \ .gb_Pb.gb_Xe{position:absolute}.gb_N .gb_Xe::before{clip:rect(519px 86px\ + \ 535px 70px);left:-40px;top:-497px}.gb_N .gb_7a .gb_We::before{left:-40px}@media\ + \ (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_N\ + \ .gb_We::before{clip:rect(1246px 172px 1278px 140px)}.gb_N .gb_Xe::before{clip:rect(1038px\ + \ 172px 1070px 140px)}}.gb_N .gb_Pb,.gb_N .gbii,.gb_N .gbip{background-image:none;overflow:hidden;position:relative}.gb_N\ + \ .gb_Pb::before{content:url('//ssl.gstatic.com/gb/images/v1_8758f7bb.png');position:absolute}@media\ + \ (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_N\ + \ .gb_Pb::before{content:url('//ssl.gstatic.com/gb/images/v2_0bc49e87.png');-moz-transform:scale(.5);transform:scale(.5);-moz-transform-origin:0\ + \ 0;transform-origin:0 0}}.gb_wb a:focus{outline:1px dotted #fff!important}#gb.gb_9e{min-width:980px}#gb.gb_9e\ + \ .gb_8d{min-width:0;position:static;width:0}.gb_9e .gb_ce{background:transparent;border-bottom-color:transparent}.gb_9e\ + \ .gb_ce::before{display:none}.gb_9e.gb_9e .gb_Q{display:inline-block}.gb_9e.gb_bb\ + \ .gb_Re.gb_U{padding-right:15px}.gb_T.gb_9e .gb_S.gb_R{display:-webkit-flex;display:flex}.gb_9e.gb_T\ + \ #gbqf{display:block}.gb_9e #gbq{height:0;position:absolute}.gb_9e.gb_bb{z-index:987}sentinel{}#gbq\ + \ .gbgt-hvr,#gbq .gbgt:focus{background-color:transparent;background-image:none}.gbqfh#gbq1{display:none}.gbxx{display:none\ + \ !important}#gbq{line-height:normal;position:relative;top:0px;white-space:nowrap}#gbq{left:0;width:100%}#gbq2{top:0px;z-index:986}#gbq4{display:inline-block;max-height:29px;overflow:hidden;position:relative}.gbqfh#gbq2{z-index:985}.gbqfh#gbq2{margin:0;margin-left:0\ + \ !important;padding-top:0;position:relative;top:310px}.gbqfh #gbqf{margin:auto;min-width:534px;padding:0\ + \ !important}.gbqfh #gbqfbw{display:none}.gbqfh #gbqfbwa{display:block}.gbqfh\ + \ #gbqf{max-width:572px;min-width:572px}.gbqfh .gbqfqw{border-right-width:1px}\\\ + n.gbii::before{content:url(//ssl.gstatic.com/gb/images/silhouette_27.png);position:absolute}.gbip::before{content:url(//ssl.gstatic.com/gb/images/silhouette_96.png);position:absolute}@media\ + \ (min-resolution:1.25dppx),(-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gbii::before{content:url(//ssl.gstatic.com/gb/images/silhouette_27.png)}.gbip::before{content:url(//ssl.gstatic.com/gb/images/silhouette_96.png)}}\\\ + n.gbii{background-image:url(//ssl.gstatic.com/gb/images/silhouette_27.png)}.gbip{background-image:url(//ssl.gstatic.com/gb/images/silhouette_96.png)}@media\ + \ (min-resolution:1.25dppx),(-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gbii{background-image:url(//ssl.gstatic.com/gb/images/silhouette_27.png)}.gbip{background-image:url(//ssl.gstatic.com/gb/images/silhouette_96.png)}}\\\ + n#gb192 .gb_3::before{left:0px;top:-1451px}
    Usuario lector de pantalla, clic aqu\xED para desact. Google Instant.\ + \
    Noticias
  • Gmail
  • Drive
  • Calendario
  • Google+
  • Traductor
  • Fotos
  • M\xE1s
  • Documentos
  • Libros
  • Blogger
  • Contactos
  • HangoutsM\xE1s de Google
    \xD7

    \xBFVienes aqu\xED seguido? Convierte\ + \ a Google en tu p\xE1gina de inicio.

    Claro
    Error
    Enviando
    Enviado axxx-xxx-xxxUpdate phone number \ + \

    \ + \
    Google Instant\ + \ no est\xE1 disponible. Ingresa para realizar una b\xFAsqueda.\_M\xE1\ + s informaci\xF3n
    Google Google\ + \ Instant est\xE1 desactivado debido a la velocidad de conexi\xF3n. Presiona\ + \ Aceptar para buscar.
    \\\"Presionar Enter para\ + \ buscar\\\"
    Color
  • Cualquier color
  • A todo color
  • En blanco y negro
  • Transparentes
  • Tipo
    Fecha
    Derechos de uso
    M\xE1s herramientas
    {\\\"tw\\\":203, \\\"th\\\":249}
    {\\\"tw\\\":213, \\\"th\\\":237}
    {\\\"cl\\\":21,\\\"cr\\\":21,\\\"tw\\\":287,\ + \ \\\"th\\\":176}
    {\\\"ct\\\":21,\\\"cb\\\":21,\\\"cl\\\":21,\\\ + \"cr\\\":21,\\\"tw\\\":259, \\\"th\\\":194}

    \_
    \_

    Resultados de b\xFAsqueda

    \\\
    1200\_\xD7\_1200 - apple.com
    {\\\"cb\\\":21,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\":21,\\\"\ + id\\\":\\\"DV49ugmyPgIGVM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"jpg\\\",\\\"oh\\\":1200,\\\"ou\\\":\\\"http://images.apple.com/euro/home/n/generic/images/og.jpg?201602090601\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"Apple (Espa\xF1a)\\\",\\\"rid\\\":\\\"T6Md6OcWwrF27M\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/es/\\\",\\\"s\\\":\\\"\\\",\\\"sc\\\ + \":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTo9brcxalOzWUhweYTXf2PEC7Pb_0JL-agMnlYe0rOmVPXl4TXSg\\\"\ + ,\\\"tw\\\":225}
    \\\
    1200\_\xD7\_1200 - apple.com
    {\\\"cb\\\":21,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\":21,\\\"\ + id\\\":\\\"LL-KayQ-mXwalM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"jpg\\\",\\\"oh\\\":1200,\\\"ou\\\":\\\"http://www.apple.com/mx/ipod/home/images/social/og.jpg?201601060706\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"iPod - Apple (MX)\\\",\\\"rid\\\":\\\"BWRPDRg7qEj7GM\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/mx/ipod/\\\",\\\"s\\\":\\\"\\\",\\\"\ + sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcRZTmyZ3XfUHG6p_gpT3051lFd5Jvzk6BtssnCFKhBWGiBS5Aiz\\\",\\\ + \"tw\\\":225}
    \\\
    2000\_\xD7\_2000 - marcianophone.com
    {\\\"cl\\\":9,\\\"cr\\\":12,\\\"id\\\":\\\"DwmtNsGZ03Ox1M:\\\ + \",\\\"isu\\\":\\\"marcianophone.com\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\"\ + :2000,\\\"ou\\\":\\\"http://marcianophone.com/wp-content/uploads/2016/02/Apple_logo_black.svg_.png\\\ + \",\\\"ow\\\":2000,\\\"pt\\\":\\\"Apple lanza una versi\xF3n \\\\u201csecundaria\\\ + \\u201d de iOS 9.2.1 para solucionar ...\\\",\\\"rid\\\":\\\"xBLvzab3LBGgOM\\\ + \",\\\"ru\\\":\\\"https://www.marcianophone.com/archives/78703\\\",\\\"s\\\ + \":\\\"Con este error, Apple simplemente pretende mejorar la seguridad de\ + \ sus dispositivos para que nadie pueda acceder a tus huellas ni claves mediante\ + \ un cambio ...\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcRtgWAjhXTSujaMbdY00xXQWeXYI04RAaa_konBm4X5Hut7Q8zLww\\\"\ + ,\\\"tw\\\":225}
    \\\
    220\_\xD7\_220 - techcrunch.com
    {\\\"cb\\\":9,\\\"cl\\\":15,\\\"cr\\\":15,\\\"ct\\\ + \":9,\\\"id\\\":\\\"J1fpJxAeccKurM:\\\",\\\"isu\\\":\\\"techcrunch.com\\\"\ + ,\\\"ity\\\":\\\"png\\\",\\\"oh\\\":220,\\\"ou\\\":\\\"https://tctechcrunch2011.files.wordpress.com/2014/06/apple_topic.png?w\\\ + \\u003d220\\\",\\\"ow\\\":220,\\\"pt\\\":\\\"Apple | TechCrunch\\\",\\\"rid\\\ + \":\\\"BqOlv-WNmHzf7M\\\",\\\"ru\\\":\\\"http://techcrunch.com/topic/company/apple/\\\ + \",\\\"s\\\":\\\"Apple\\\",\\\"sc\\\":1,\\\"th\\\":176,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSzn62n_Wlcy36VwSK_e3AsBjsY40Ijut5tRNVzPmOmqZHgkPQMcQ\\\"\ + ,\\\"tw\\\":176}
    \\\
    512\_\xD7\_512 - sites.google.com
    {\\\"cb\\\":3,\\\"cl\\\":9,\\\"cr\\\":9,\\\"ct\\\"\ + :3,\\\"id\\\":\\\"UUm8pWjKFq2SiM:\\\",\\\"isu\\\":\\\"sites.google.com\\\"\ + ,\\\"ity\\\":\\\"png\\\",\\\"oh\\\":512,\\\"ou\\\":\\\"http://www.beevoz.com/wp-content/uploads/2015/03/Apple-Logo.png\\\ + \",\\\"ow\\\":512,\\\"pt\\\":\\\"Apple - AppleAndAndroidApps\\\",\\\"rid\\\ + \":\\\"YmCLsQZHUwq9nM\\\",\\\"ru\\\":\\\"https://sites.google.com/site/myappleandandroidapps/home/apple\\\ + \",\\\"s\\\":\\\"App Store es un servicio para el iPhone, el iPod Touche,\ + \ el iPad y Mac OS Ox Snow Leopar o posterior, creado por Apple Inc. Que permite\ + \ a los usuarios ...\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQRIvL1O51kqoqTK9ENxEGy8asczpQ73G_L1dPa3jzdZtHuqxkJOw\\\"\ + ,\\\"tw\\\":225}
    \\\
    436\_\xD7\_276 - support.apple.com
    {\\\"id\\\":\\\"JoGvoZ4pniOIKM:\\\",\\\"isu\\\":\\\ + \"support.apple.com\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":276,\\\"ou\\\"\ + :\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/osx/mac-apple-logo-screen-icon.png\\\ + \",\\\"ow\\\":436,\\\"pt\\\":\\\"Acerca de las pantallas que aparecen al arrancar\ + \ el Mac - Soporte ...\\\",\\\"rid\\\":\\\"mA_Jb9NO-al6hM\\\",\\\"ru\\\":\\\ + \"https://support.apple.com/es-es/HT204156\\\",\\\"s\\\":\\\"Logotipo de Apple\\\ + \",\\\"sc\\\":1,\\\"th\\\":179,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcRKlZWYkvm7q1qsyLSLSeChIzbt6DAe4XIUU4MLITt2AWl4AdmblQ\\\"\ + ,\\\"tw\\\":282}
    \\\
    1499\_\xD7\_739 - axpe-blogs.com
    {\\\"cb\\\":6,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\ + \":3,\\\"id\\\":\\\"LHInqsKlGi_SUM:\\\",\\\"isu\\\":\\\"axpe-blogs.com\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":739,\\\"ou\\\":\\\"http://www.axpe-blogs.com/wp-content/uploads/Apple_logo.jpg\\\ + \",\\\"ow\\\":1499,\\\"pt\\\":\\\"La \\\\u0026#39;iPhone-dependencia\\\\u0026#39;\ + \ de Apple - Blog oficial de Axpe Consulting\\\",\\\"rid\\\":\\\"3vOK_u0DBJi8lM\\\ + \",\\\"ru\\\":\\\"http://www.axpe-blogs.com/tecnologia/la-iphone-dependencia-de-apple/\\\ + \",\\\"s\\\":\\\"Apple ha presentado hoy sus resultados financieros correspondientes\ + \ al cuarto trimestre fiscal de 2015, que finaliz\xF3 el pasado 26 de septiembre.\\\ + \",\\\"th\\\":157,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQafnUIXr287x5jFMI4JsfwW2TcvPIRMPbpn8NVxGDEjM4hEBtwJw\\\"\ + ,\\\"tw\\\":320}
    \\\
    874\_\xD7\_1024 - impactony.com
    {\\\"id\\\":\\\"6GOKElj0paqeLM:\\\",\\\"isu\\\":\\\ + \"impactony.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1024,\\\"ou\\\":\\\"\ + http://www.impactony.com/wp-content/uploads/2014/08/Apple-logo1.jpg\\\",\\\ + \"ow\\\":874,\\\"pt\\\":\\\"Apple | Impacto Latin News \u2122\\\",\\\"rid\\\ + \":\\\"tI1BGO8hSFhT7M\\\",\\\"ru\\\":\\\"http://www.impactony.com/tag/apple/\\\ + \",\\\"s\\\":\\\"Apple-logo\\\",\\\"sc\\\":1,\\\"th\\\":243,\\\"tu\\\":\\\"\ + https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcSXwGE8t1A52kM4YUpUU-rmi5WQwz2JE6VqzYU5jkeb0_VC4fBj\\\ + \",\\\"tw\\\":207}
    \\\
    2290\_\xD7\_2290 - weknowyourdreamz.com
    {\\\"cb\\\":3,\\\"cl\\\":15,\\\"cr\\\":18,\\\"id\\\ + \":\\\"lglsoGJZIAC58M:\\\",\\\"isu\\\":\\\"weknowyourdreamz.com\\\",\\\"ity\\\ + \":\\\"jpg\\\",\\\"oh\\\":2290,\\\"ou\\\":\\\"http://weknowyourdreamz.com/images/apple/apple-06.jpg\\\ + \",\\\"ow\\\":2290,\\\"pt\\\":\\\"Interpretation of a dream in which you saw\ + \ \xABApple\xBB\\\",\\\"rid\\\":\\\"ArhdgBHqRcsKGM\\\",\\\"ru\\\":\\\"http://weknowyourdreamz.com/apple.html\\\ + \",\\\"s\\\":\\\"1 | 2 | 3 | 4 | 5. Dreams Interpretation - Apple\\\",\\\"\ + sc\\\":1,\\\"th\\\":224,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcR9dfnC_DQaO7aQ5lSe9MSnmYd7K67TYfOiWhVvUNrD--lUXdyB\\\",\\\ + \"tw\\\":224}
    \\\
    1920\_\xD7\_1080 - guillermoalegre.es
    {\\\"cb\\\":6,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\ + \":9,\\\"id\\\":\\\"zXLuYWTt7HqqhM:\\\",\\\"isu\\\":\\\"guillermoalegre.es\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1080,\\\"ou\\\":\\\"http://www.guillermoalegre.es/wp-content/uploads/2014/02/apple-wallpaper-logo-1.jpg\\\ + \",\\\"ow\\\":1920,\\\"pt\\\":\\\"Apple Inc. y su ventaja competitiva | Guillermo\ + \ Alegre\\\",\\\"rid\\\":\\\"oMnFa4U1GBb5gM\\\",\\\"ru\\\":\\\"http://www.guillermoalegre.es/apple-inc-y-su-ventaja-competitiva/\\\ + \",\\\"s\\\":\\\"Apple Inc. y su ventaja competitiva\\\",\\\"th\\\":168,\\\ + \"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQ0_twW6whESTN_D5VbMLZcY8B59iWJDfLW6zKXWzfJUuAX9IBlyg\\\ + \",\\\"tw\\\":300}
    \\\
    2272\_\xD7\_1704 - girabsas.com
    {\\\"cb\\\":9,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\ + \":9,\\\"id\\\":\\\"o62v3mff1wC9rM:\\\",\\\"isu\\\":\\\"girabsas.com\\\",\\\ + \"ity\\\":\\\"png\\\",\\\"oh\\\":1704,\\\"ou\\\":\\\"http://www.girabsas.com/files/image/14/14336/553082a17eb08.png\\\ + \",\\\"ow\\\":2272,\\\"pt\\\":\\\"Las m\xE1s dif\xEDciles e ins\xF3litas preguntas\ + \ de Apple para conseguir ...\\\",\\\"rid\\\":\\\"ms4LdUbvtmNgNM\\\",\\\"\ + ru\\\":\\\"http://www.girabsas.com/nota/7790/\\\",\\\"s\\\":\\\"Las m\xE1\ + s dif\xEDciles e ins\xF3litas preguntas de Apple para conseguir trabajo -\ + \ Gira BsAs\\\",\\\"sc\\\":1,\\\"th\\\":194,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSCp0aPsHO_gE5nbHITgeiTa01mSkuyCSmqF80yJRBzEDA5MupcPA\\\"\ + ,\\\"tw\\\":259}
    \\\
    500\_\xD7\_500 - macrumors.com
    {\\\"id\\\":\\\"xSvwXDgGp0WrNM:\\\",\\\"isu\\\":\\\ + \"macrumors.com\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":500,\\\"ou\\\":\\\"\ + http://cdn.macrumors.com/article-new/2015/02/Apple-Logo.png?retina\\\",\\\"\ + ow\\\":500,\\\"pt\\\":\\\"Apple to Open $25 Million Technology Development\ + \ Site in India ...\\\",\\\"rid\\\":\\\"w9CqPdSQ58lbtM\\\",\\\"ru\\\":\\\"\ + http://www.macrumors.com/2016/02/12/apple-india-hyderabad-development/\\\"\ + ,\\\"s\\\":\\\"Apple to Open $25 Million Technology Development Site in India\ + \ [Updated] - Mac Rumors\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTsHIFkFjyZQyBec-QnGUAHkXh886Pqy9esMTBuJ7ilvuYEIpDGAw\\\"\ + ,\\\"tw\\\":225}
    \\\
    3172\_\xD7\_2709 - weknowyourdreamz.com
    {\\\"cb\\\":12,\\\"cl\\\":21,\\\"cr\\\":18,\\\"ct\\\ + \":9,\\\"id\\\":\\\"Mxd6eMffENefZM:\\\",\\\"isu\\\":\\\"weknowyourdreamz.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":2709,\\\"ou\\\":\\\"http://weknowyourdreamz.com/images/apple/apple-05.jpg\\\ + \",\\\"ow\\\":3172,\\\"pt\\\":\\\"Interpretation of a dream in which you saw\ + \ \xABApple\xBB\\\",\\\"rid\\\":\\\"ArhdgBHqRcsKGM\\\",\\\"ru\\\":\\\"http://weknowyourdreamz.com/apple.html\\\ + \",\\\"s\\\":\\\"1 | 2 | 3 | 4 | 5. Dreams Interpretation - Apple\\\",\\\"\ + sc\\\":1,\\\"th\\\":207,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQGzX9EkXBBLwxQTF4jvQeLqGrNu-eJOwjrD20WDoMnv8UqJ9HG\\\",\\\ + \"tw\\\":243}
    \\\
    1200\_\xD7\_630 - apple.com
    {\\\"id\\\":\\\"uopsmMz6OOkjAM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":630,\\\"ou\\\":\\\"http://images.apple.com/euro/tv/d/generic/images/og.jpg??201602090348\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"Apple TV - Apple (ES)\\\",\\\"rid\\\":\\\ + \"qo4gFzd07jZS2M\\\",\\\"ru\\\":\\\"http://www.apple.com/es/tv/\\\",\\\"s\\\ + \":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":163,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQ1YtxJTInDE0djJEnPNCUGuzdEgfyvlzZLlHmWvf1VWW048GYU\\\",\\\ + \"tw\\\":310}
    \\\
    848\_\xD7\_480 - apple.com
    {\\\"cb\\\":6,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\":3,\\\"\ + id\\\":\\\"sISfrWsL97eylM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"jpg\\\",\\\"oh\\\":480,\\\"ou\\\":\\\"http://images.apple.com/es/apple-events/static/apple-events/september-2013/video/poster_large.jpg\\\ + \",\\\"ow\\\":848,\\\"pt\\\":\\\"Apple - Eventos de Apple - Evento especial,\ + \ septiembre de 2013\\\",\\\"rid\\\":\\\"4ByrtT7-eAIMVM\\\",\\\"ru\\\":\\\"\ + http://www.apple.com/es/apple-events/september-2013/\\\",\\\"s\\\":\\\"Evento\ + \ especial de Apple. 10 de septiembre de 2013.\\\",\\\"sc\\\":1,\\\"th\\\"\ + :169,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcTCTDh-wzJSeUM1XRVWVcJuMNBdbZvB_jsljpE5KlRjAZSZCdcb0A\\\ + \",\\\"tw\\\":299}
    \\\
    3968\_\xD7\_4496 - blog.loseit.com
    {\\\"cl\\\":6,\\\"cr\\\":9,\\\"ct\\\":3,\\\"id\\\"\ + :\\\"rTI6opaq4cHh-M:\\\",\\\"isu\\\":\\\"blog.loseit.com\\\",\\\"ity\\\":\\\ + \"jpg\\\",\\\"oh\\\":4496,\\\"ou\\\":\\\"https://loseitapp.files.wordpress.com/2014/09/istock_000014459318_double.jpg\\\ + \",\\\"ow\\\":3968,\\\"pt\\\":\\\"The Apple: A Perfect Fruit for Weight Loss?\ + \ |\\\",\\\"rid\\\":\\\"7-a-U6e0cIeHhM\\\",\\\"ru\\\":\\\"http://blog.loseit.com/the-apple-a-perfect-fruit-for-weight-loss/\\\ + \",\\\"s\\\":\\\"It\\\\u0026#39;s no secret that apples offer a nutrient goldmine.\ + \ Here are three ways that the nutrients in apples keep those doctors away\ + \ and help keep your body ...\\\",\\\"sc\\\":1,\\\"th\\\":239,\\\"tu\\\":\\\ + \"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQvqH1aYdncoxPWHtKwsHkmnTKs1wK_0VrwiCV8JdwME8nFNX1Kvg\\\ + \",\\\"tw\\\":211}
    \\\
    1200\_\xD7\_909 - cellz.com
    {\\\"cl\\\":15,\\\"cr\\\":21,\\\"ct\\\":6,\\\"id\\\":\\\"PFkVTzUtCvQXzM:\\\ + \",\\\"isu\\\":\\\"cellz.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":909,\\\ + \"ou\\\":\\\"http://www.cellz.com/blog/wp-content/uploads/2014/11/3D-Apple-full.jpg\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"Apple | Cellz Blog\\\",\\\"rid\\\":\\\"\ + z68zmbgCvLn5jM\\\",\\\"ru\\\":\\\"http://www.cellz.com/blog/category/apple/\\\ + \",\\\"s\\\":\\\"3D-Apple-full\\\",\\\"sc\\\":1,\\\"th\\\":195,\\\"tu\\\"\ + :\\\"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcTn-wiVhRC5Krp_lxWNqTxbnlu_KbAejdbUNJmOJ-JCaKGnC3p1xQ\\\ + \",\\\"tw\\\":258}
    \\\
    2218\_\xD7\_2216 - quotesgram.com
    {\\\"cb\\\":6,\\\"cl\\\":15,\\\"cr\\\":15,\\\"ct\\\ + \":6,\\\"id\\\":\\\"WpLAV4HbrSK3PM:\\\",\\\"isu\\\":\\\"quotesgram.com\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":2216,\\\"ou\\\":\\\"http://assisted-living.benchmarkseniorliving.com/wp-content/uploads/2012/11/iStock_000017473165Large.jpg\\\ + \",\\\"ow\\\":2218,\\\"pt\\\":\\\"Apple Quotes. QuotesGram\\\",\\\"rid\\\"\ + :\\\"UEXwaprkxpH9WM\\\",\\\"ru\\\":\\\"http://quotesgram.com/apple-quotes/\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":224,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQhKUDrTJVFrLRx7UWNtTKDP7qppvJon8ULIMU2cT61WyxBeiYozQ\\\"\ + ,\\\"tw\\\":225}
    \\\
    894\_\xD7\_893 - weknowyourdreamz.com
    {\\\"cb\\\":9,\\\"cl\\\":15,\\\"cr\\\":15,\\\"ct\\\ + \":3,\\\"id\\\":\\\"Ig4W18BX-UQMyM:\\\",\\\"isu\\\":\\\"weknowyourdreamz.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":893,\\\"ou\\\":\\\"http://weknowyourdreamz.com/images/apple/apple-01.jpg\\\ + \",\\\"ow\\\":894,\\\"pt\\\":\\\"Interpretation of a dream in which you saw\ + \ \xABApple\xBB\\\",\\\"rid\\\":\\\"ArhdgBHqRcsKGM\\\",\\\"ru\\\":\\\"http://weknowyourdreamz.com/apple.html\\\ + \",\\\"s\\\":\\\"Photo Gallery of Dream - Apple\\\",\\\"sc\\\":1,\\\"th\\\"\ + :224,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcRL2A1ILpxvXdO-6edv_L6WMkPXH8lVRBIaRbchg1Go_wXS6AAK\\\ + \",\\\"tw\\\":225}
    \\\
    400\_\xD7\_400 - apple.com
    {\\\"cb\\\":9,\\\"cl\\\":12,\\\"cr\\\":9,\\\"ct\\\":21,\\\"\ + id\\\":\\\"e4fbZpwSReF4EM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"png\\\",\\\"oh\\\":400,\\\"ou\\\":\\\"https://www.apple.com/support/assets/images/home/2015/apple_tv_icon_2x.png\\\ + \",\\\"ow\\\":400,\\\"pt\\\":\\\"Soporte t\xE9cnico de Apple oficial\\\",\\\ + \"rid\\\":\\\"P_uuKPQm6B6M6M\\\",\\\"ru\\\":\\\"https://www.apple.com/es/support/\\\ + \",\\\"s\\\":\\\"Soporte t\xE9cnico Apple TV\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\ + \"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcT3vjh4izEwVDMmuUbwOT7U-_jPRDwKxAgT1OhC4u7iSkfHORTr\\\ + \",\\\"tw\\\":225}
    \\\
    1920\_\xD7\_1080 - infoes.net
    {\\\"cl\\\":9,\\\"cr\\\":18,\\\"id\\\":\\\"cPRrDYIJiZARYM:\\\ + \",\\\"isu\\\":\\\"infoes.net\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1080,\\\ + \"ou\\\":\\\"http://infoes.net/wp-content/uploads/2016/02/logo-apple.jpg\\\ + \",\\\"ow\\\":1920,\\\"pt\\\":\\\"Apple | INFOES\\\",\\\"rid\\\":\\\"FJRXt-ru8QFZzM\\\ + \",\\\"ru\\\":\\\"http://infoes.net/apple/\\\",\\\"s\\\":\\\"Zoom en Leer\ + \ M\xE1s\\\",\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTPsJJwtcqvoi59J7C_w0_lg8tvk1i1UBrE12HaTzyQLKiCJaQAeA\\\"\ + ,\\\"tw\\\":300}
    \\\
    1600\_\xD7\_1613 - dreamatico.com
    {\\\"cb\\\":9,\\\"cl\\\":12,\\\"cr\\\":15,\\\"ct\\\ + \":18,\\\"id\\\":\\\"xA0yo66Zq3LyRM:\\\",\\\"isu\\\":\\\"dreamatico.com\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1613,\\\"ou\\\":\\\"http://dreamatico.com/data_images/apple/apple-6.jpg\\\ + \",\\\"ow\\\":1600,\\\"pt\\\":\\\"The meaning of the dream in which you saw\ + \ \xABApple\xBB\\\",\\\"rid\\\":\\\"rK_GSJwJSTW-2M\\\",\\\"ru\\\":\\\"http://dreamatico.com/apple.html\\\ + \",\\\"s\\\":\\\"Photo Gallery of \xABApple\xBB:\\\",\\\"sc\\\":1,\\\"th\\\ + \":225,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcRSDr5MwSpmZPzbpxQLX91KySEWe8ilY7rf7Zt08Fa_owoxacua\\\ + \",\\\"tw\\\":224}
    \\\
    416\_\xD7\_416 - forbes.com
    {\\\"cb\\\":6,\\\"cl\\\":9,\\\"cr\\\":15,\\\"id\\\":\\\"lZgE0JQs4FnkPM:\\\ + \",\\\"isu\\\":\\\"forbes.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":416,\\\ + \"ou\\\":\\\"http://i.forbesimg.com/media/lists/companies/apple_416x416.jpg\\\ + \",\\\"ow\\\":416,\\\"pt\\\":\\\"Apple on the Forbes Canada\\\\u0026#39;s\ + \ Best Employers List\\\",\\\"rid\\\":\\\"SiS47bI6wAuB-M\\\",\\\"ru\\\":\\\ + \"http://www.forbes.com/companies/apple/\\\",\\\"s\\\":\\\"\\\",\\\"th\\\"\ + :225,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcRWxiHrMmPGGzjxEZ-v8bY1heYy5AVK23HXlMv48wm8ZFxY-fZR\\\ + \",\\\"tw\\\":225}
    \\\
    350\_\xD7\_440 - apple.com
    {\\\"id\\\":\\\"q-6dAcRCQySwiM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":440,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/appletv/witb/appletv-witb-tv-201511?wid\\\ + \\u003d350\\\\u0026hei\\\\u003d440\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453547347338\\\",\\\"ow\\\":350,\\\"pt\\\":\\\"Comprar\ + \ el Apple TV - Apple (MX)\\\",\\\"rid\\\":\\\"6IDJ4bn9VJDNDM\\\",\\\"ru\\\ + \":\\\"http://www.apple.com/mx/shop/buy-tv/apple-tv\\\",\\\"s\\\":\\\"Apple\ + \ TV\\\",\\\"sc\\\":1,\\\"th\\\":252,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQv4iBNvS4LKE5N_uRo9SZiFAkUIf-_t_NGVHWJn0fnYwteRIom\\\",\\\ + \"tw\\\":200}
    \\\
    1920\_\xD7\_1080 - youtube.com
    {\\\"id\\\":\\\"BOpOjlg3u4mo2M:\\\",\\\"isu\\\":\\\ + \"youtube.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1080,\\\"ou\\\":\\\"\ + https://i.ytimg.com/vi/OON2bZdqVzs/maxresdefault.jpg\\\",\\\"ow\\\":1920,\\\ + \"pt\\\":\\\"Apple - YouTube\\\",\\\"rid\\\":\\\"pcOjmCN79JHQsM\\\",\\\"ru\\\ + \":\\\"https://www.youtube.com/user/Apple\\\",\\\"s\\\":\\\"\\\",\\\"sc\\\"\ + :1,\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSVYkDPIeL6E9jPphXHytVNob1yjF7CGCAQmh3JSqCGxIGE6rV6\\\",\\\ + \"tw\\\":300}
    \\\
    600\_\xD7\_315 - apple.com
    {\\\"cb\\\":9,\\\"cl\\\":9,\\\"cr\\\":9,\\\"id\\\":\\\"a-JcEP_-HgnXBM:\\\ + \",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":315,\\\ + \"ou\\\":\\\"http://www.apple.com/euro/ipad-pro/a/generic/images/overview_social.jpg?201602260611\\\ + \",\\\"ow\\\":600,\\\"pt\\\":\\\"iPad Pro - Apple (ES)\\\",\\\"rid\\\":\\\"\ + X4Sbq50L91-veM\\\",\\\"ru\\\":\\\"http://www.apple.com/es/ipad-pro/\\\",\\\ + \"s\\\":\\\"\\\",\\\"th\\\":163,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQCkiUl_bdJtgjCzsyl0LiAqUPkEbWrqyCErfk0JJZZSsnsymb0ig\\\"\ + ,\\\"tw\\\":310}
    \\\
    1600\_\xD7\_1000 - territoriomarketing.es
    {\\\"cl\\\":21,\\\"cr\\\":9,\\\"id\\\":\\\"YozCV0lX8L8YMM:\\\ + \",\\\"isu\\\":\\\"territoriomarketing.es\\\",\\\"ity\\\":\\\"jpg\\\",\\\"\ + oh\\\":1000,\\\"ou\\\":\\\"http://territoriomarketing.es/wp-content/uploads/2014/05/Apple.jpg\\\ + \",\\\"ow\\\":1600,\\\"pt\\\":\\\"Apple al desnudo - Territorio Marketing\\\ + \",\\\"rid\\\":\\\"sronnRmOQH_N1M\\\",\\\"ru\\\":\\\"http://territoriomarketing.es/apple/\\\ + \",\\\"s\\\":\\\"Apple al desnudo\\\",\\\"th\\\":177,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSera-XxQKL4EEhwXC2BCJNyo8xu7wcY9-xiqgtMxuG8OpujhWcyw\\\"\ + ,\\\"tw\\\":284}
    \\\
    765\_\xD7\_573 - dreamatico.com
    {\\\"cb\\\":6,\\\"cl\\\":18,\\\"cr\\\":18,\\\"ct\\\ + \":3,\\\"id\\\":\\\"QdKUhnZ187NRkM:\\\",\\\"isu\\\":\\\"dreamatico.com\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":573,\\\"ou\\\":\\\"http://dreamatico.com/data_images/apple/apple-1.jpg\\\ + \",\\\"ow\\\":765,\\\"pt\\\":\\\"The meaning of the dream in which you saw\ + \ \xABApple\xBB\\\",\\\"rid\\\":\\\"rK_GSJwJSTW-2M\\\",\\\"ru\\\":\\\"http://dreamatico.com/apple.html\\\ + \",\\\"s\\\":\\\"Photo Gallery of \xABApple\xBB:\\\",\\\"sc\\\":1,\\\"th\\\ + \":194,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcQT5HjB4zEtr4LqYwXKgYYpiXt6EfJKjWUcRfJEr25fTxZFaDpmsA\\\ + \",\\\"tw\\\":259}
    \\\
    2880\_\xD7\_2600 - en.wikipedia.org
    {\\\"cb\\\":3,\\\"cl\\\":6,\\\"cr\\\":6,\\\"ct\\\"\ + :6,\\\"id\\\":\\\"hpFDFOBGqc6NDM:\\\",\\\"isu\\\":\\\"en.wikipedia.org\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":2600,\\\"ou\\\":\\\"https://upload.wikimedia.org/wikipedia/commons/0/07/Honeycrisp-Apple.jpg\\\ + \",\\\"ow\\\":2880,\\\"pt\\\":\\\"Honeycrisp - Wikipedia, the free encyclopedia\\\ + \",\\\"rid\\\":\\\"PxTuVIuVeV8iiM\\\",\\\"ru\\\":\\\"https://en.wikipedia.org/wiki/Honeycrisp\\\ + \",\\\"s\\\":\\\"Honeycrisp-Apple.jpg\\\",\\\"sc\\\":1,\\\"th\\\":213,\\\"\ + tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcTm_FG6VDLzbLkW9u3nfqchN6A7-MHcKQleIpcbXSyo2p2u0qU\\\ + \",\\\"tw\\\":236}
    \\\
    300\_\xD7\_300 - marketingdirecto.com
    {\\\"cb\\\":6,\\\"cl\\\":6,\\\"cr\\\":9,\\\"ct\\\"\ + :3,\\\"id\\\":\\\"_geQub_qtPxkqM:\\\",\\\"isu\\\":\\\"marketingdirecto.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":300,\\\"ou\\\":\\\"http://www.marketingdirecto.com/wp-content/uploads/2012/12/apple4.jpg\\\ + \",\\\"ow\\\":300,\\\"pt\\\":\\\"Los momentos clave en la \\\\u0026quot;iRevoluci\xF3\ + n\\\\u0026quot; de Apple - Marketing Directo\\\",\\\"rid\\\":\\\"MqAfEDTVybvR3M\\\ + \",\\\"ru\\\":\\\"http://www.marketingdirecto.com/actualidad/checklists/los-momentos-clave-en-la-irevolucion-de-apple/\\\ + \",\\\"s\\\":\\\"Todo lo que toca se convierte oro pero, \xBFc\xF3mo ha logrado\ + \ Apple convertirse en el rey Midas del universo tecnol\xF3gico? En la llegada\ + \ de Apple al \\\\u0026quot;trono\\\\u0026quot; de la ...\\\",\\\"sc\\\":1,\\\ + \"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\\ + u003dtbn:ANd9GcSaHJawrul1VqmNc1ymMNuAZh5dwiZ1PrbexP9mBm1Qv06jElsYDg\\\",\\\ + \"tw\\\":225}
    \\\
    4481\_\xD7\_3753 - avereymcdowell.blogspot.com
    {\\\"cb\\\":3,\\\"cl\\\":15,\\\"cr\\\":12,\\\"ct\\\ + \":3,\\\"id\\\":\\\"wdQK2J4RZqsytM:\\\",\\\"isu\\\":\\\"avereymcdowell.blogspot.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":3753,\\\"ou\\\":\\\"http://www.public-domain-image.com/free-images/flora-plants/fruits/apple-pictures/red-apple-with-water-droplets.jpg\\\ + \",\\\"ow\\\":4481,\\\"pt\\\":\\\"Mythology and Folklore : Portfolio Week\ + \ 9 Storytelling: George and ...\\\",\\\"rid\\\":\\\"ydIm3jGN1TIKsM\\\",\\\ + \"ru\\\":\\\"http://avereymcdowell.blogspot.com/2015/10/week-9-storytelling-george-and-apple.html\\\ + \",\\\"s\\\":\\\"The Red Apple.\\\",\\\"th\\\":205,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQFYZlmEL8qnSy_2MUoQNx8adPRWfLY5fVYRBv0PMPMvUI5oHO7nw\\\"\ + ,\\\"tw\\\":245}
    \\\
    420\_\xD7\_288 - weknowyourdreamz.com
    {\\\"cb\\\":6,\\\"cl\\\":18,\\\"cr\\\":21,\\\"ct\\\ + \":3,\\\"id\\\":\\\"UapPQ47xiBcZDM:\\\",\\\"isu\\\":\\\"weknowyourdreamz.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":288,\\\"ou\\\":\\\"http://weknowyourdreamz.com/images/apple/apple-02.jpg\\\ + \",\\\"ow\\\":420,\\\"pt\\\":\\\"Interpretation of a dream in which you saw\ + \ \xABApple\xBB\\\",\\\"rid\\\":\\\"ArhdgBHqRcsKGM\\\",\\\"ru\\\":\\\"http://weknowyourdreamz.com/apple.html\\\ + \",\\\"s\\\":\\\"Photo Gallery of Dream - Apple\\\",\\\"sc\\\":1,\\\"th\\\"\ + :186,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcQmUvbQDo8IiMPEW-7MUcGINuFybUM2O0H-l9IYJk7YJR99FSZK7Q\\\ + \",\\\"tw\\\":271}
    \\\
    2000\_\xD7\_1536 - apple.com
    {\\\"cb\\\":18,\\\"cl\\\":15,\\\"cr\\\":15,\\\"ct\\\":18,\\\"\ + id\\\":\\\"NDoJoEaeDgF0BM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"\\\",\\\"oh\\\":1536,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/tv/apple-tv-gallery1-2015?wid\\\ + \\u003d2000\\\\u0026hei\\\\u003d1536\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\ + \\u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453545817238\\\",\\\"ow\\\":2000,\\\"pt\\\":\\\"Comprar\ + \ Apple TV (tercera generaci\xF3n) - Apple (MX)\\\",\\\"rid\\\":\\\"HKFtBFbXk_q1iM\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/mx/shop/buy-tv/apple-tv-3rd-gen\\\"\ + ,\\\"s\\\":\\\"Imagen 1 ...\\\",\\\"sc\\\":1,\\\"th\\\":197,\\\"tu\\\":\\\"\ + https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcSFoUxbL7oIR8Fce1tZuVGjAcixcehb6wItivIThUesi2q1yPMnDA\\\ + \",\\\"tw\\\":256}
    \\\
    300\_\xD7\_201 - macworld.com
    {\\\"id\\\":\\\"rbGHSGCFKlxhIM:\\\",\\\"isu\\\":\\\"macworld.com\\\ + \",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":201,\\\"ou\\\":\\\"http://core3.staticworld.net/images/article/2015/09/apple-logo-news-slide-background-100612702-medium.png\\\ + \",\\\"ow\\\":300,\\\"pt\\\":\\\"News and Insights on the Apple universe\\\ + \",\\\"rid\\\":\\\"E3FsAH7CnVud6M\\\",\\\"ru\\\":\\\"http://www.macworld.com/news\\\ + \",\\\"s\\\":\\\"apple\\\",\\\"sc\\\":1,\\\"th\\\":160,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQ5k4hnFufGdBBkSqHG9-gd4ekk9wFk2jAE915-Ml-LcMtXwjA5sA\\\"\ + ,\\\"tw\\\":240}
    \\\
    299\_\xD7\_877 - apple.com
    {\\\"id\\\":\\\"nwgdmimPIpOIjM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":877,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/s/ce/scene0/left/scene0-left-2x?wid\\\ + \\u003d299\\\\u0026hei\\\\u003d877\\\\u0026fmt\\\\u003dpng-alpha\\\\u0026qlt\\\ + \\u003d95\\\\u0026.v\\\\u003d1453543739065\\\",\\\"ow\\\":299,\\\"pt\\\":\\\ + \"Compra un iPhone 6 o un iPhone 6 Plus - Apple (ES)\\\",\\\"rid\\\":\\\"\ + ZhCjqxYEAMHSfM\\\",\\\"ru\\\":\\\"http://www.apple.com/es/shop/buy-iphone/iphone6\\\ + \",\\\"s\\\":\\\"Comprar un iPhone 6\\\",\\\"sc\\\":1,\\\"th\\\":239,\\\"\ + tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcS9OXTqa3l7h_mK_76NvcXvfJkL2BQOzRmilp6OBfmszmTME26H\\\ + \",\\\"tw\\\":81}
    \\\
    450\_\xD7\_565 - apple.com
    {\\\"cr\\\":3,\\\"ct\\\":3,\\\"id\\\":\\\"05uXkGEnkYe1qM:\\\"\ + ,\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"\\\",\\\"oh\\\":565,\\\"ou\\\ + \":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/i/ph/iphone6s/scene1/iphone6s-scene1?wid\\\ + \\u003d450\\\\u0026hei\\\\u003d565\\\\u0026fmt\\\\u003dpng-alpha\\\\u0026qlt\\\ + \\u003d95\\\\u0026.v\\\\u003d1453545831471\\\",\\\"ow\\\":450,\\\"pt\\\":\\\ + \"Compra un iPhone 6s o un iPhone 6s Plus - Apple (ES)\\\",\\\"rid\\\":\\\"\ + 4umIyG8WQehF3M\\\",\\\"ru\\\":\\\"http://www.apple.com/es/shop/buy-iphone/iphone6s\\\ + \",\\\"s\\\":\\\"Ver galer\xEDa\\\",\\\"sc\\\":1,\\\"th\\\":252,\\\"tu\\\"\ + :\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcQ7O91Hc0YNtfV-ir-uWuL5MQrqrp_NMvzIirq3nJl6QEw1_XH_aw\\\ + \",\\\"tw\\\":200}
    \\\
    1200\_\xD7\_630 - apple.com
    {\\\"id\\\":\\\"iO1Cvx7v_L4pAM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":630,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/tv/apple-tv-hero-select-201510?wid\\\ + \\u003d1200\\\\u0026hei\\\\u003d630\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\ + \\u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453547618625\\\",\\\"ow\\\":1200,\\\"pt\\\":\\\"Apple TV\ + \ (cuarta generaci\xF3n) 32 GB - Apple (MX)\\\",\\\"rid\\\":\\\"YTkVNlcbZshE3M\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/mx/shop/buy-tv/apple-tv/apple-tv-32-gb\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":163,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQR6R6JoEmy-WmZmiC51s5hAZa9dfGpI3vyE91TiNb2Ks6Pjc00Fg\\\"\ + ,\\\"tw\\\":310}
    \\\
    2560\_\xD7\_1600 - pandasecurity.com
    {\\\"id\\\":\\\"dSSeBcLQ-zLTwM:\\\",\\\"isu\\\":\\\ + \"pandasecurity.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1600,\\\"ou\\\"\ + :\\\"http://www.pandasecurity.com/spain/mediacenter/src/uploads/2015/07/apple.jpg\\\ + \",\\\"ow\\\":2560,\\\"pt\\\":\\\"Apple Phishing\\\",\\\"rid\\\":\\\"PNvLYGnhR2zYNM\\\ + \",\\\"ru\\\":\\\"http://www.pandasecurity.com/spain/mediacenter/noticias/phishing-apple/\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":177,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSdE3isiV02ix-7uq8kL0b26KRx9qQvRGPSNfDG1c1o99EOBDB1\\\",\\\ + \"tw\\\":284}
    \\\
    1200\_\xD7\_630 - huffingtonpost.com
    {\\\"cb\\\":3,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\ + \":3,\\\"id\\\":\\\"-q2gF09oo0a3lM:\\\",\\\"isu\\\":\\\"huffingtonpost.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":630,\\\"ou\\\":\\\"http://images.huffingtonpost.com/2015-06-10-1433944614-3985241-og.jpg\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"Apple Music and the Race to Coexist\\\"\ + ,\\\"rid\\\":\\\"Q6tf0w65S_pYOM\\\",\\\"ru\\\":\\\"http://www.huffingtonpost.com/seth-schachner/apple-music-and-the-race-to-coexist_b_7552334.html\\\ + \",\\\"s\\\":\\\"2015-06-10-1433944614-3985241-og.jpg\\\",\\\"sc\\\":1,\\\"\ + th\\\":163,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\\ + u003dtbn:ANd9GcR39JI3cNl5M8j1V8ZMHaNutmpTQUmxe9dGOXoK3VvZxCdEQ28E\\\",\\\"\ + tw\\\":310}
    \\\
    400\_\xD7\_400 - apple.com
    {\\\"cb\\\":6,\\\"cl\\\":6,\\\"cr\\\":6,\\\"ct\\\":15,\\\"id\\\ + \":\\\"Y-8NfJs6McPFaM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"\\\ + \",\\\"oh\\\":400,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/A/PP/APPLECARE/APPLECARE?wid\\\ + \\u003d400\\\\u0026hei\\\\u003d400\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453538181395\\\",\\\"ow\\\":400,\\\"pt\\\":\\\"Apple TV:\ + \ AppleCare Protection Plan - Apple (ES)\\\",\\\"rid\\\":\\\"eo_DL5NGv2aUgM\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/es/shop/product/MF219E/A/apple-tv-applecare-protection-plan\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcRVK3pTaPd-yogfrn_cX7xlOQLq-igU7VYzBg2-3qAoO1A6yBH7Ng\\\"\ + ,\\\"tw\\\":225}
    \\\
    600\_\xD7\_315 - apple.com
    {\\\"id\\\":\\\"QhuJO7vXO8FKNM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":315,\\\"ou\\\":\\\"http://www.apple.com/euro/apple-pencil/a/generic/images/pencil_social.jpg?201602260609\\\ + \",\\\"ow\\\":600,\\\"pt\\\":\\\"iPad Pro - Apple Pencil - Apple (ES)\\\"\ + ,\\\"rid\\\":\\\"38PgssJqz8kz0M\\\",\\\"ru\\\":\\\"http://www.apple.com/es/apple-pencil/\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":163,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcT9DAMsBlSPbZhyN-9hATkO9mYrWKKWvRt9GWb5r4kKAhwsZLVF\\\",\\\ + \"tw\\\":310}
    \\\
    620\_\xD7\_385 - cnnexpansion.com
    {\\\"cb\\\":15,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\ + \":3,\\\"id\\\":\\\"Ybd9yMPBZM-6cM:\\\",\\\"isu\\\":\\\"cnnexpansion.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":385,\\\"ou\\\":\\\"http://static.cnnexpansion.com/media/2014/09/15/apple-logotipo-manzana_5.jpg\\\ + \",\\\"ow\\\":620,\\\"pt\\\":\\\"Por qu\xE9 el logotipo de Apple es una manzana\ + \ mordida? - Especiales ...\\\",\\\"rid\\\":\\\"ln_PP90REXqUKM\\\",\\\"ru\\\ + \":\\\"http://www.cnnexpansion.com/especiales/2014/09/12/por-que-el-logotipo-de-apple-es-una-manzana-mordida\\\ + \",\\\"s\\\":\\\"cnnexpansion\\\",\\\"sc\\\":1,\\\"th\\\":177,\\\"tu\\\":\\\ + \"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcR9BY13NvnmadH1nqCjJU53YQ04p6SGE98RW1-ox28EqUk9XPXu\\\ + \",\\\"tw\\\":285}
    \\\
    700\_\xD7\_900 - support.apple.com
    {\\\"cb\\\":6,\\\"cl\\\":12,\\\"cr\\\":9,\\\"ct\\\"\ + :6,\\\"id\\\":\\\"BIRGK_rmVYbszM:\\\",\\\"isu\\\":\\\"support.apple.com\\\"\ + ,\\\"ity\\\":\\\"png\\\",\\\"oh\\\":900,\\\"ou\\\":\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/appletv/apple-tv-2-3-gen-remote-tech-spec.png\\\ + \",\\\"ow\\\":700,\\\"pt\\\":\\\"C\xF3mo utilizar el control remoto Apple\ + \ Remote con el Apple TV ...\\\",\\\"rid\\\":\\\"rcbUFmpz-qPoAM\\\",\\\"ru\\\ + \":\\\"https://support.apple.com/es-mx/HT200131\\\",\\\"s\\\":\\\"... viene\ + \ con el Apple TV (segunda y tercera generaci\xF3n) Si tienes un Apple TV\ + \ (cuarta generaci\xF3n), obt\xE9n informaci\xF3n sobre c\xF3mo utilizar el\ + \ control remoto ...\\\",\\\"sc\\\":1,\\\"th\\\":255,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcS_61vlXRAWi74wvhw7Pw8hTV9nbFVyJ1lr7y66PT-Ivf_A9ssj\\\",\\\ + \"tw\\\":198}
    \\\
    1200\_\xD7\_1200 - apple.com
    {\\\"cb\\\":18,\\\"cl\\\":6,\\\"cr\\\":6,\\\"ct\\\":15,\\\"\ + id\\\":\\\"4aDMyuuzNR3yxM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"jpg\\\",\\\"oh\\\":1200,\\\"ou\\\":\\\"http://images.apple.com/euro/macbook/a/generic/overview/images/macbook_overview_og.jpg?201601250709\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"MacBook - Apple (ES)\\\",\\\"rid\\\":\\\"\ + sFcb0SUPb2vg4M\\\",\\\"ru\\\":\\\"http://www.apple.com/es/macbook/\\\",\\\"\ + s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcT2xwpnMIBvHSeLu-kFkBZsILEGgI6F6nDXMQVWROhmCH3RUaC9eQ\\\"\ + ,\\\"tw\\\":225}
    \\\
    2000\_\xD7\_1536 - apple.com
    {\\\"cb\\\":21,\\\"cl\\\":15,\\\"cr\\\":15,\\\"ct\\\":21,\\\"\ + id\\\":\\\"hVYnBg1Z63tFcM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"\\\",\\\"oh\\\":1536,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/tv/apple-tv-gallery2-2015?wid\\\ + \\u003d2000\\\\u0026hei\\\\u003d1536\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\ + \\u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453545529871\\\",\\\"ow\\\":2000,\\\"pt\\\":\\\"Comprar\ + \ Apple TV (tercera generaci\xF3n) - Apple (MX)\\\",\\\"rid\\\":\\\"HKFtBFbXk_q1iM\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/mx/shop/buy-tv/apple-tv-3rd-gen\\\"\ + ,\\\"s\\\":\\\"Imagen 1 ...\\\",\\\"sc\\\":1,\\\"th\\\":197,\\\"tu\\\":\\\"\ + https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcSb4dMQ1Ujq3sBBxsyyCBpuUsU6YOnADGc0eTQ91kcBvJbgOhcS\\\ + \",\\\"tw\\\":256}
    \\\
    650\_\xD7\_450 - todoappleblog.com
    {\\\"id\\\":\\\"C5uRjSF9lNAuuM:\\\",\\\"isu\\\":\\\ + \"todoappleblog.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":450,\\\"ou\\\"\ + :\\\"http://www.todoappleblog.com/wp-content/uploads/2016/01/apple-interes-lifi.jpg\\\ + \",\\\"ow\\\":650,\\\"pt\\\":\\\"El c\xF3digo de iOS 9 muestra que Apple tiene\ + \ inter\xE9s en LiFi\\\",\\\"rid\\\":\\\"NKnc4ZoTnMbvnM\\\",\\\"ru\\\":\\\"\ + http://www.todoappleblog.com/ios-9-muestra-que-apple-tiene-interes-en-la-tecnologia-lifi/\\\ + \",\\\"s\\\":\\\"C\xF3digos localizados en iOS 9 demuestran que Apple est\xE1\ + \ trabajando con la tecnolog\xEDa LiFi\\\",\\\"sc\\\":1,\\\"th\\\":187,\\\"\ + tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcSfWHWXlhuaCzy4IamlKiZCbpIcp0PRl1UfvekEbJpilMTv4Epz\\\ + \",\\\"tw\\\":270}
    \\\
    300\_\xD7\_260 - apple.com
    {\\\"cb\\\":21,\\\"cl\\\":18,\\\"cr\\\":18,\\\"ct\\\":18,\\\"\ + id\\\":\\\"S_5-hZ-KcoHZcM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"png\\\",\\\"oh\\\":260,\\\"ou\\\":\\\"https://www.apple.com/support/assets/images/home/2015/promo_twitter_2x.png\\\ + \",\\\"ow\\\":300,\\\"pt\\\":\\\"Official Apple Support\\\",\\\"rid\\\":\\\ + \"OrunAFB7EkjeiM\\\",\\\"ru\\\":\\\"https://www.apple.com/support/\\\",\\\"\ + s\\\":\\\"Apple Support is on Twitter\\\",\\\"sc\\\":1,\\\"th\\\":208,\\\"\ + tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQdkx3HLjjq7DcWsjg4YIRu6Lq6EyB3bxHvS_noIlC97nnDr86TeA\\\ + \",\\\"tw\\\":240}
    \\\
    260\_\xD7\_220 - apple.com
    {\\\"id\\\":\\\"jXjaopmLP_nwqM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":220,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/appletv/2/appletv-2-compare-201509?wid\\\ + \\u003d260\\\\u0026hei\\\\u003d220\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453547643693\\\",\\\"ow\\\":260,\\\"pt\\\":\\\"Comprar\ + \ el Apple TV - Apple (MX)\\\",\\\"rid\\\":\\\"6IDJ4bn9VJDNDM\\\",\\\"ru\\\ + \":\\\"http://www.apple.com/mx/shop/buy-tv/apple-tv\\\",\\\"s\\\":\\\"Apple\ + \ TV (cuarta generaci\xF3n)\\\",\\\"sc\\\":1,\\\"th\\\":176,\\\"tu\\\":\\\"\ + https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQM-iP7Rnn60cvtWVk-nMg-EfW6TZmCWzGVhbdmxGwtPXn_uhI2\\\ + \",\\\"tw\\\":208}
    \\\
    1180\_\xD7\_1690 - support.apple.com
    {\\\"cl\\\":9,\\\"id\\\":\\\"P6YVXEKaU0c6JM:\\\",\\\ + \"isu\\\":\\\"support.apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1690,\\\ + \"ou\\\":\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/iOS/apple-pay-wrap-hero.jpg\\\ + \",\\\"ow\\\":1180,\\\"pt\\\":\\\"Set up and use Apple Pay with your Apple\ + \ Watch - Apple Support\\\",\\\"rid\\\":\\\"CAP1OgaYY1yZ_M\\\",\\\"ru\\\"\ + :\\\"https://support.apple.com/en-us/HT204506\\\",\\\"s\\\":\\\"\\\",\\\"\ + sc\\\":1,\\\"th\\\":269,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQSTkeO-XHf_E2fZbrvpFVsItRju2NIxhVnASyesGFQHpOChggu\\\",\\\ + \"tw\\\":188}
    \\\
    646\_\xD7\_484 - euroresidentes.com
    {\\\"cb\\\":6,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\ + \":3,\\\"id\\\":\\\"XZEDzuAAIuudtM:\\\",\\\"isu\\\":\\\"euroresidentes.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":484,\\\"ou\\\":\\\"http://2.bp.blogspot.com/-ex1W3uglrGY/UfoyG7DfHlI/AAAAAAAAJ8Y/YBcUNMwLdTc/s1600/logo-apple.jpg\\\ + \",\\\"ow\\\":646,\\\"pt\\\":\\\"Los beneficios de Apple superan expectativas\ + \ - \xC9xito Empresarial\\\",\\\"rid\\\":\\\"51qpPTIw92uKPM\\\",\\\"ru\\\"\ + :\\\"https://www.euroresidentes.com/empresa/exito-empresarial/los-beneficios-de-apple-superan\\\ + \",\\\"s\\\":\\\"Las acciones del fabricante de ordenadores y tel\xE9fonos\ + \ inteligentes Apple subieron casi un 5% en las operaciones electr\xF3nicas\ + \ fuera de hora tras informar de ...\\\",\\\"th\\\":194,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcR0iWriqxss3OEadgLJX80_xoKMpNa5hpHYMArK3x4KU5NrXLJp\\\",\\\ + \"tw\\\":259}
    \\\
    332\_\xD7\_392 - apple.com
    {\\\"cb\\\":9,\\\"cl\\\":9,\\\"cr\\\":12,\\\"ct\\\":18,\\\"\ + id\\\":\\\"NQ3MTkKXRFL-hM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"\\\",\\\"oh\\\":392,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/s/38/s38si/sbwh/s38si-sbwh-sel-201509?wid\\\ + \\u003d332\\\\u0026hei\\\\u003d392\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453545508020\\\",\\\"ow\\\":332,\\\"pt\\\":\\\"Apple Watch\ + \ Sport - Comprar un Apple Watch Sport - Apple (ES)\\\",\\\"rid\\\":\\\"UpP7DXQ0G0XH7M\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/es/shop/buy-watch/apple-watch-sport\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":244,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQd6u4vbLg6gwMvYorTvIfmH5u_pXYoHMyXisOuX9El4jhMEXCx\\\",\\\ + \"tw\\\":207}
    \\\
    4608\_\xD7\_3072 - pexels.com
    {\\\"cb\\\":6,\\\"cl\\\":21,\\\"cr\\\":12,\\\"ct\\\":9,\\\"\ + id\\\":\\\"Q-N-wDymnkiezM:\\\",\\\"isu\\\":\\\"pexels.com\\\",\\\"ity\\\"\ + :\\\"jpg\\\",\\\"oh\\\":3072,\\\"ou\\\":\\\"https://static.pexels.com/photos/8208/pexels-photo.jpg\\\ + \",\\\"ow\\\":4608,\\\"pt\\\":\\\"Free stock photo of healthy, apple, fruit\\\ + \",\\\"rid\\\":\\\"MSxvSQ_ypyb3sM\\\",\\\"ru\\\":\\\"https://www.pexels.com/photo/apple-fruit-healthy-8208/\\\ + \",\\\"s\\\":\\\"Free Download\\\",\\\"th\\\":183,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQZDwktuxT1KTxpa9NHiDD_EWKfzvn04wGT6QvKb8CnDgBZtIXe\\\",\\\ + \"tw\\\":275}
    \\\
    2742\_\xD7\_3504 - pngimg.com
    {\\\"id\\\":\\\"Yw10pHwD_jWUnM:\\\",\\\"isu\\\":\\\"pngimg.com\\\ + \",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":3504,\\\"ou\\\":\\\"http://pngimg.com/upload/apple_PNG38.png\\\ + \",\\\"ow\\\":2742,\\\"pt\\\":\\\"Apples png images, pictures, cliparts\\\"\ + ,\\\"rid\\\":\\\"fLV1t_6ynQtruM\\\",\\\"ru\\\":\\\"http://pngimg.com/img/fruits/apple\\\ + \",\\\"s\\\":\\\"Red apple PNG image\\\",\\\"sc\\\":1,\\\"th\\\":254,\\\"\ + tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcQVaOhC7yCTMLMwee3oguGqTZITA1-XPhvgptnL2Q2wxqs2HK6u7A\\\ + \",\\\"tw\\\":199}
    \\\
    160\_\xD7\_160 - apple.com
    {\\\"cb\\\":9,\\\"cl\\\":3,\\\"cr\\\":9,\\\"ct\\\":9,\\\"id\\\ + \":\\\"DVwi6uVneLCzYM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"\ + png\\\",\\\"oh\\\":160,\\\"ou\\\":\\\"https://www.apple.com/support/assets/images/home/2015/airport_2x.png\\\ + \",\\\"ow\\\":160,\\\"pt\\\":\\\"Soporte t\xE9cnico de Apple oficial\\\",\\\ + \"rid\\\":\\\"P_uuKPQm6B6M6M\\\",\\\"ru\\\":\\\"https://www.apple.com/es/support/\\\ + \",\\\"s\\\":\\\"Soporte t\xE9cnico AirPort + Wi-Fi\\\",\\\"th\\\":128,\\\"\ + tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQ_M9BNLYF15ChBu1Ye8OrX_WyWp3WcCJ-WbbGR9Os6ff_B4dN5\\\ + \",\\\"tw\\\":128}
    \\\
    245\_\xD7\_220 - apple.com
    {\\\"id\\\":\\\"exeog1gTt_I9dM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":220,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/appletv/compare/appletv-compare-201509?wid\\\ + \\u003d245\\\\u0026hei\\\\u003d220\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453547645529\\\",\\\"ow\\\":245,\\\"pt\\\":\\\"Buy Apple\ + \ TV (3rd gen) - Apple (ES)\\\",\\\"rid\\\":\\\"Bo-jGBURQ2fy4M\\\",\\\"ru\\\ + \":\\\"http://www.apple.com/es/shop/buy-tv/apple-tv-3rd-gen\\\",\\\"s\\\"\ + :\\\"Apple TV (3.\xAA generaci\xF3n)\\\",\\\"sc\\\":1,\\\"th\\\":176,\\\"\ + tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcQx6tua0dd007PlJ9VceXDTbYSPQz5QzId7xXotIHO50HYzgxOt\\\ + \",\\\"tw\\\":196}
    \\\
    1024\_\xD7\_683 - applesfera.com
    {\\\"cb\\\":12,\\\"cl\\\":21,\\\"cr\\\":21,\\\"id\\\ + \":\\\"edyg9hU0_4MynM:\\\",\\\"isu\\\":\\\"applesfera.com\\\",\\\"ity\\\"\ + :\\\"jpg\\\",\\\"oh\\\":683,\\\"ou\\\":\\\"http://i.blogs.es/4832ba/apple-music-periodo-prueba-trial-0/original.jpg\\\ + \",\\\"ow\\\":1024,\\\"pt\\\":\\\"Apple lanza la primera beta de Apple Music\ + \ para Android en la Play ...\\\",\\\"rid\\\":\\\"mQzjN7Nnh_1PoM\\\",\\\"\ + ru\\\":\\\"http://www.applesfera.com/itunes/apple-lanza-la-primera-beta-de-apple-music-para-android-en-la-play-store\\\ + \",\\\"s\\\":\\\"Apple lanza la primera beta de Apple Music para Android en\ + \ la Play Store\\\",\\\"th\\\":183,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcThEjCcdpabpb_2RtIuPe1OSaboPQ6j7l-wu8QWVq7V-XiLuNr3\\\",\\\ + \"tw\\\":275}
    \\\
    700\_\xD7\_950 - support.apple.com
    {\\\"cl\\\":6,\\\"cr\\\":6,\\\"id\\\":\\\"l0s4DqszX_5aiM:\\\ + \",\\\"isu\\\":\\\"support.apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\"\ + :950,\\\"ou\\\":\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/appletv/apple-tv-first-second-gen-remotes.jpg\\\ + \",\\\"ow\\\":700,\\\"pt\\\":\\\"Enlazar el control remoto Apple Remote al\ + \ Apple TV (tercera ...\\\",\\\"rid\\\":\\\"NHtOGI5ogTx0NM\\\",\\\"ru\\\"\ + :\\\"https://support.apple.com/es-la/HT201254\\\",\\\"s\\\":\\\"Antes de empezar\\\ + \",\\\"sc\\\":1,\\\"th\\\":262,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQ4YQmLUzYhJkbNKPxhhyQVwMzgv4NpkxfRaw9ahI1iSSVdjXLjtw\\\"\ + ,\\\"tw\\\":193}
    \\\
    620\_\xD7\_349 - cbc.ca
    {\\\"cl\\\":9,\\\"cr\\\":21,\\\"id\\\":\\\"CQPMd-J22UL_QM:\\\ + \",\\\"isu\\\":\\\"cbc.ca\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":349,\\\"\ + ou\\\":\\\"http://i.cbc.ca/1.2749556.1409245330!/httpImage/image.jpg_gen/derivatives/16x9_620/apple-logo-for-apple-event-story.jpg\\\ + \",\\\"ow\\\":620,\\\"pt\\\":\\\"Apple event: New iPhones, Apple TV, smarter\ + \ Siri expected on Sept ...\\\",\\\"rid\\\":\\\"dkYirhx-FttfTM\\\",\\\"ru\\\ + \":\\\"http://www.cbc.ca/news/technology/apple-event-new-iphones-apple-tv-smarter-siri-expected-on-sept-9-1.3218867\\\ + \",\\\"s\\\":\\\"Apple has scheduled an event for Sept. 9 at 10 a.m. PT (1\ + \ p.m.\\\",\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQdaNEVu_-wgHvACvYfYcIKIY3YdrYebJwgm-QSPz-bRpvGM7LhVg\\\"\ + ,\\\"tw\\\":299}
    \\\
    534\_\xD7\_401 - usatoday.com
    {\\\"cb\\\":21,\\\"cl\\\":6,\\\"cr\\\":21,\\\"ct\\\":6,\\\"\ + id\\\":\\\"J3sFHNe6hhxywM:\\\",\\\"isu\\\":\\\"usatoday.com\\\",\\\"ity\\\"\ + :\\\"jpg\\\",\\\"oh\\\":401,\\\"ou\\\":\\\"http://www.gannett-cdn.com/-mm-/9dbe32c1c0832c803dc30f788ea67265050d3876/c\\\ + \\u003d67-0-957-669\\\\u0026r\\\\u003dx404\\\\u0026c\\\\u003d534x401/local/-/media/2015/09/10/USATODAY/USATODAY/635774864480351068-AP-APPLE-75734234.JPG\\\ + \",\\\"ow\\\":534,\\\"pt\\\":\\\"Apple \\\\u0026#39;isn\\\\u0026#39;t reinventing\ + \ the world\\\\u0026#39; anymore\\\",\\\"rid\\\":\\\"gpyIHtxnbeK6BM\\\",\\\ + \"ru\\\":\\\"http://www.usatoday.com/story/money/markets/2015/09/10/apple-reinventing-world-stock-aapl/72005518/\\\ + \",\\\"s\\\":\\\"\\\",\\\"th\\\":194,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSR_2b-pQj5z6bVJkhanNeFTieS8EAbos6-9hVV_XWyV0NGG2E_\\\",\\\ + \"tw\\\":259}
    \\\
    520\_\xD7\_720 - pixabay.com
    {\\\"id\\\":\\\"6UNsD5b_FDEGgM:\\\",\\\"isu\\\":\\\"pixabay.com\\\ + \",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":720,\\\"ou\\\":\\\"https://pixabay.com/static/uploads/photo/2013/07/13/11/34/apple-158419_960_720.png\\\ + \",\\\"ow\\\":520,\\\"pt\\\":\\\"Vector gratis: Apple, Rojo, Frutas, Vitaminas\ + \ - Imagen gratis en ...\\\",\\\"rid\\\":\\\"eI0-muKinnc7wM\\\",\\\"ru\\\"\ + :\\\"https://pixabay.com/es/apple-rojo-frutas-vitaminas-158419/\\\",\\\"s\\\ + \":\\\"Apple, Rojo, Frutas, Vitaminas, Saludable\\\",\\\"sc\\\":1,\\\"th\\\ + \":264,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcRhGQqz3scUAg6kj_OFyXOixcHX830ys1WfjsQPnud6ocYmDnJi\\\ + \",\\\"tw\\\":191}
    \\\
    569\_\xD7\_405 - apple.com
    {\\\"cb\\\":3,\\\"id\\\":\\\"848NC_1EruqsKM:\\\",\\\"isu\\\"\ + :\\\"apple.com\\\",\\\"ity\\\":\\\"\\\",\\\"oh\\\":405,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/m/ac/macbook/box/macbook-box-hw-silver-201504?wid\\\ + \\u003d569\\\\u0026hei\\\\u003d405\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453495470706\\\",\\\"ow\\\":569,\\\"pt\\\":\\\"Compra un\ + \ MacBook - Apple (ES)\\\",\\\"rid\\\":\\\"k5kBXwtH5mh-6M\\\",\\\"ru\\\":\\\ + \"http://www.apple.com/es/shop/buy-mac/macbook\\\",\\\"s\\\":\\\"MacBook\\\ + \",\\\"sc\\\":1,\\\"th\\\":189,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSLmR0jcsSLug11ZE2fiIjZijHyed8ZjLwhhHwjc4s68pqiTRRheg\\\"\ + ,\\\"tw\\\":266}
    \\\
    960\_\xD7\_637 - pixabay.com
    {\\\"cb\\\":3,\\\"cr\\\":6,\\\"ct\\\":9,\\\"id\\\":\\\"3tUCeuPcYNVepM:\\\ + \",\\\"isu\\\":\\\"pixabay.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":637,\\\ + \"ou\\\":\\\"https://pixabay.com/static/uploads/photo/2015/04/05/20/41/apple-708521_960_720.jpg\\\ + \",\\\"ow\\\":960,\\\"pt\\\":\\\"Free photo: Apple, Fruit, Green Apple - Free\ + \ Image on Pixabay - 708521\\\",\\\"rid\\\":\\\"NsglyVzkG1dVsM\\\",\\\"ru\\\ + \":\\\"https://pixabay.com/en/apple-fruit-green-apple-red-apple-708521/\\\"\ + ,\\\"s\\\":\\\"Apple, Fruit, Green Apple, Red Apple\\\",\\\"th\\\":183,\\\"\ + tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcSl__saX29YTF-ccJdr1K2cRP7StAAgyzkyDDM0S3EM4TQr509U\\\ + \",\\\"tw\\\":276}
    \\\
    510\_\xD7\_490 - efresh.com
    {\\\"cl\\\":6,\\\"cr\\\":6,\\\"ct\\\":3,\\\"id\\\":\\\"xZmgFzJbFbRDYM:\\\ + \",\\\"isu\\\":\\\"efresh.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":490,\\\ + \"ou\\\":\\\"http://efresh.com/sites/default/files/Green-Apple_1.jpg\\\",\\\ + \"ow\\\":510,\\\"pt\\\":\\\"Green Apple | eFresh.com\\\",\\\"rid\\\":\\\"\ + A15NPK6ZukZbvM\\\",\\\"ru\\\":\\\"http://efresh.com/product/green-apple-658\\\ + \",\\\"s\\\":\\\"Green Apple\\\",\\\"sc\\\":1,\\\"th\\\":220,\\\"tu\\\":\\\ + \"https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcQrhooCWwIjN35rArY6dypUPpTXWUhz4TvMUtbaXITQQVV719QZ\\\ + \",\\\"tw\\\":229}
    \\\
    1200\_\xD7\_694 - macrumors.com
    {\\\"cl\\\":3,\\\"cr\\\":3,\\\"id\\\":\\\"38E_Rv4J54b4CM:\\\ + \",\\\"isu\\\":\\\"macrumors.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":694,\\\ + \"ou\\\":\\\"http://cdn.macrumors.com/article-new/2014/11/apple_product_red_gift_card.jpg?retina\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"Apple Details Special (RED) Shopping Day\ + \ Gift Card Amounts for ...\\\",\\\"rid\\\":\\\"kDxR8ZZrjL_aGM\\\",\\\"ru\\\ + \":\\\"http://www.macrumors.com/2014/11/24/apple-back-friday-red/\\\",\\\"\ + s\\\":\\\"Apple Details Special (RED) Shopping Day Gift Card Amounts for Black\ + \ Friday - Mac Rumors\\\",\\\"th\\\":171,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSJL7KyhBTeuOqDKYmGkKwAOBllaFpdOUG4wxlct-koiysUChyQ\\\",\\\ + \"tw\\\":295}
    \\\
    420\_\xD7\_900 - support.apple.com
    {\\\"id\\\":\\\"FFXD8byWfM0W_M:\\\",\\\"isu\\\":\\\ + \"support.apple.com\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":900,\\\"ou\\\"\ + :\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/appletv/apple-remote-aluminum-battery-diagram-2.png\\\ + \",\\\"ow\\\":420,\\\"pt\\\":\\\"C\xF3mo reemplazar la bater\xEDa en el control\ + \ remoto Apple Remote ...\\\",\\\"rid\\\":\\\"yycL5E6nf07wKM\\\",\\\"ru\\\"\ + :\\\"https://support.apple.com/es-mx/HT201531\\\",\\\"s\\\":\\\"Extracci\xF3\ + n de la bater\xEDa de tu Apple Remote (aluminio)\\\",\\\"sc\\\":1,\\\"th\\\ + \":204,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcRfzvvhUT1zsWwDHWmKzctLVXxV4Tx5jr_vPfdSeRiLOKO98d1K\\\ + \",\\\"tw\\\":95}
    \\\
    400\_\xD7\_400 - bloomberg.com
    {\\\"cb\\\":6,\\\"cl\\\":3,\\\"cr\\\":6,\\\"ct\\\"\ + :6,\\\"id\\\":\\\"aFO7NKng1nit_M:\\\",\\\"isu\\\":\\\"bloomberg.com\\\",\\\ + \"ity\\\":\\\"gif\\\",\\\"oh\\\":400,\\\"ou\\\":\\\"http://www.bloomberg.com/features/2015-how-apple-built-3d-touch-iphone-6s/img/animation2-final.gif\\\ + \",\\\"ow\\\":400,\\\"pt\\\":\\\"How Apple Built 3D Touch\\\",\\\"rid\\\"\ + :\\\"Z-2vHKrWAxNgNM\\\",\\\"ru\\\":\\\"http://www.bloomberg.com/features/2015-how-apple-built-3d-touch-iphone-6s/\\\ + \",\\\"s\\\":\\\"Some of this technology was first revealed in the Apple Watch,\ + \ which has a feature called Force Touch. But 3D Touch is to Force Touch as\ + \ ocean swimming is ...\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQHXJM2LgWE0dT9lxdlxjQB9l1aL1oR690uorjwQPxmBTwOVwnl\\\",\\\ + \"tw\\\":225}
    \\\
    640\_\xD7\_360 - itespresso.es
    {\\\"id\\\":\\\"78pAlu6ehW3FrM:\\\",\\\"isu\\\":\\\ + \"itespresso.es\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":360,\\\"ou\\\":\\\"\ + http://www.itespresso.es/wp-content/uploads/2014/10/Apple-logo-jpg.jpg\\\"\ + ,\\\"ow\\\":640,\\\"pt\\\":\\\"Apple bloquea todas sus relaciones comerciales\ + \ con Crimea\\\",\\\"rid\\\":\\\"aVl-f3cuTVssUM\\\",\\\"ru\\\":\\\"http://www.itespresso.es/apple-bloquea-todas-sus-relaciones-comerciales-con-crimea-133325.html\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTHJFOm82pdCINeuWTMvgVZfbaF9HL6fYqUGcLjaFG40wPBwbU7\\\",\\\ + \"tw\\\":300}
    \\\
    332\_\xD7\_392 - apple.com
    {\\\"cb\\\":9,\\\"cl\\\":9,\\\"cr\\\":12,\\\"ct\\\":18,\\\"\ + id\\\":\\\"Tfd9lrHePXm9xM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"\\\",\\\"oh\\\":392,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/8748/as-images.apple.com/is/image/AppleInc/aos/published/images/w/38/w38ss/sbwh/w38ss-sbwh-sel-201509?wid\\\ + \\u003d332\\\\u0026hei\\\\u003d392\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453545531685\\\",\\\"ow\\\":332,\\\"pt\\\":\\\"Apple Watch\ + \ - Shop Apple Watch - Apple (MY)\\\",\\\"rid\\\":\\\"rK9WiVjysmYRsM\\\",\\\ + \"ru\\\":\\\"http://www.apple.com/my/shop/buy-watch/apple-watch\\\",\\\"s\\\ + \":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":244,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcT3OE002RasR8ajU_9m8rtIy1BY4i2h09-6ynowFsytR3W7sAUd\\\",\\\ + \"tw\\\":207}
    \\\
    636\_\xD7\_358 - imfnd.com
    {\\\"cl\\\":3,\\\"cr\\\":15,\\\"id\\\":\\\"JDDMsq5KLnlpsM:\\\ + \",\\\"isu\\\":\\\"imfnd.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":358,\\\ + \"ou\\\":\\\"http://www.imfnd.com/wp-content/uploads/2015/05/ceuhkwdqlrne37qwattm.jpg\\\ + \",\\\"ow\\\":636,\\\"pt\\\":\\\"Apple has been ruling the charts as the most\ + \ valuable brand in the ...\\\",\\\"rid\\\":\\\"T4i0yiTRMhjsiM\\\",\\\"ru\\\ + \":\\\"http://www.imfnd.com/apple-has-been-ruling-the-charts-as-the-most-valuable-brand-in-the-world-2015/\\\ + \",\\\"s\\\":\\\"Apple has been ruling the charts as the most valuable brand\ + \ in the world 2015\\\",\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQS9RLjXTaVfFfKmwE_FHX574Kk83BrgKshHhPADlBW_VoMXWrI\\\",\\\ + \"tw\\\":299}
    \\\
    760\_\xD7\_368 - support.apple.com
    {\\\"cl\\\":21,\\\"cr\\\":21,\\\"id\\\":\\\"b_1VYsfpFb7pyM:\\\ + \",\\\"isu\\\":\\\"support.apple.com\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\"\ + :368,\\\"ou\\\":\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/ios/built_in/triple-nav-messages.png\\\ + \",\\\"ow\\\":760,\\\"pt\\\":\\\"Iniciar sesi\xF3n con un ID de Apple distinto\ + \ en tu iPhone, iPad o ...\\\",\\\"rid\\\":\\\"dzRSQ9LQ2nYs0M\\\",\\\"ru\\\ + \":\\\"https://support.apple.com/es-es/HT203983\\\",\\\"s\\\":\\\"Cambiar\ + \ ID de Apple en iPhone para Mensajes\\\",\\\"th\\\":156,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSwlKo-9-Iomq8WRpqr1M5zNNeDmpNPZgmsxnHexy05fTezLpbz\\\",\\\ + \"tw\\\":323}
    \\\
    700\_\xD7\_450 - support.apple.com
    {\\\"cb\\\":3,\\\"cl\\\":6,\\\"cr\\\":6,\\\"id\\\"\ + :\\\"ufcGQJlP0Z32ZM:\\\",\\\"isu\\\":\\\"support.apple.com\\\",\\\"ity\\\"\ + :\\\"png\\\",\\\"oh\\\":450,\\\"ou\\\":\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/apple-store-giftcard-2col.png\\\ + \",\\\"ow\\\":700,\\\"pt\\\":\\\"Qu\xE9 tipo de tarjeta regalo tengo? - Soporte\ + \ t\xE9cnico de Apple\\\",\\\"rid\\\":\\\"bNwYTayL2-0C9M\\\",\\\"ru\\\":\\\ + \"https://support.apple.com/es-es/HT204199\\\",\\\"s\\\":\\\"Tarjeta regalo\ + \ del Apple Store\\\",\\\"th\\\":180,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcT96G2G0ldPdk9zGJFSq430MVYVOMLs_DFB7uWkzsoAuQSr0wNn\\\",\\\ + \"tw\\\":280}
    \\\
    445\_\xD7\_445 - apple.com
    {\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\":3,\\\"id\\\":\\\"h8FyMWhlPsKiVM:\\\ + \",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"\\\",\\\"oh\\\":445,\\\"\ + ou\\\":\\\"http://store.storeimages.cdn-apple.com/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/M/C8/MC838/MC838?wid\\\ + \\u003d445\\\\u0026hei\\\\u003d445\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453466889710\\\",\\\"ow\\\":445,\\\"pt\\\":\\\"Cables y\ + \ cargadores - Todos los accesorios - Apple (MX)\\\",\\\"rid\\\":\\\"ut9ReUkx1Qd2kM\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/mx/shop/accessories/all-accessories/power-cables\\\ + \",\\\"s\\\":\\\"Cable de HDMI a HDMI de Apple (1.8 m) - Next Gallery Image\\\ + \",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQrYIkZqQMokVnh9r3jmTBWM38DXdzpCykFoqo9oNvLpXP974kDhg\\\"\ + ,\\\"tw\\\":225}
    \\\
    300\_\xD7\_300 - drinkre.com
    {\\\"cb\\\":3,\\\"cl\\\":6,\\\"cr\\\":9,\\\"ct\\\":12,\\\"id\\\ + \":\\\"bpjPAVIgZL6WBM:\\\",\\\"isu\\\":\\\"drinkre.com\\\",\\\"ity\\\":\\\"\ + png\\\",\\\"oh\\\":300,\\\"ou\\\":\\\"http://drinkre.com/wp-content/uploads/2013/12/apple1.png\\\ + \",\\\"ow\\\":300,\\\"pt\\\":\\\"Apple\\\",\\\"rid\\\":\\\"bs5xN09D8SGgmM\\\ + \",\\\"ru\\\":\\\"http://drinkre.com/fruit/apple/\\\",\\\"s\\\":\\\"\\\",\\\ + \"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQrRJKm561xYo-p1X2EiAJVlqU2mrE7aJhwIlAns7PsCTc5FG6fgQ\\\"\ + ,\\\"tw\\\":225}
    \\\
    840\_\xD7\_1125 - apple.com
    {\\\"id\\\":\\\"bWM1Hea2TFLzmM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":1125,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/i/pa/ipad/pro/ipad-pro-scene2-1?wid\\\ + \\u003d840\\\\u0026hei\\\\u003d1125\\\\u0026fmt\\\\u003dpng-alpha\\\\u0026qlt\\\ + \\u003d95\\\\u0026.v\\\\u003d1453547102660\\\",\\\"ow\\\":840,\\\"pt\\\":\\\ + \"Buy iPad Pro - Apple (UK)\\\",\\\"rid\\\":\\\"IKsdGtmkdiyTOM\\\",\\\"ru\\\ + \":\\\"http://www.apple.com/uk/shop/buy-ipad/ipad-pro\\\",\\\"s\\\":\\\"\\\ + \",\\\"sc\\\":1,\\\"th\\\":260,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcR272NuKmZOuhQkM7Rouwv8lD7oJlHiP1e43dm4JIXux9t2rvGaDg\\\"\ + ,\\\"tw\\\":194}
    \\\
    640\_\xD7\_1310 - support.apple.com
    {\\\"cb\\\":9,\\\"ct\\\":6,\\\"id\\\":\\\"il4WaE35pjJ2_M:\\\ + \",\\\"isu\\\":\\\"support.apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\"\ + :1310,\\\"ou\\\":\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/icloud/iphone6-watch-app-mywatch-apple-watch-mark-as-missing-full.jpg\\\ + \",\\\"ow\\\":640,\\\"pt\\\":\\\"Acerca del bloqueo de activaci\xF3n en el\ + \ Apple Watch - Soporte ...\\\",\\\"rid\\\":\\\"zvJBy456EG0QLM\\\",\\\"ru\\\ + \":\\\"https://support.apple.com/es-es/HT205009\\\",\\\"s\\\":\\\"Desactiva\ + \ las tarjetas de Apple Pay en el Apple Watch.\\\",\\\"th\\\":200,\\\"tu\\\ + \":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQNq5-UeXChRZGXKj6Y3IZwOh8Lue9G-dKyPFNVQIG0wtKznrpU\\\ + \",\\\"tw\\\":97}
    \\\
    300\_\xD7\_260 - apple.com
    {\\\"cb\\\":9,\\\"cl\\\":9,\\\"cr\\\":12,\\\"ct\\\":6,\\\"id\\\ + \":\\\"d0_l2YFn8QPj8M:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"\ + png\\\",\\\"oh\\\":260,\\\"ou\\\":\\\"https://www.apple.com/support/assets/images/home/2015/promo_communities_2x.png\\\ + \",\\\"ow\\\":300,\\\"pt\\\":\\\"Soporte t\xE9cnico de Apple oficial\\\",\\\ + \"rid\\\":\\\"P_uuKPQm6B6M6M\\\",\\\"ru\\\":\\\"https://www.apple.com/es/support/\\\ + \",\\\"s\\\":\\\"Comunidades de soporte t\xE9cnico de Apple\\\",\\\"sc\\\"\ + :1,\\\"th\\\":208,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcRU4ac6LmtbLd_oPkiZPam8z0OH0FgNoUk-fMnXW7BmVnoPuhIr\\\",\\\ + \"tw\\\":240}
    \\\
    849\_\xD7\_481 - apple.com
    {\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\":6,\\\"id\\\":\\\"n9VnN3bUQPDPxM:\\\ + \",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":481,\\\ + \"ou\\\":\\\"http://images.apple.com/es/apple-events/static/apple-events/september-2015/video/poster_large.jpg\\\ + \",\\\"ow\\\":849,\\\"pt\\\":\\\"Eventos de Apple - Evento especial de Apple,\ + \ septiembre de 2015 ...\\\",\\\"rid\\\":\\\"d26PYCdcheVbtM\\\",\\\"ru\\\"\ + :\\\"http://www.apple.com/es/apple-events/september-2015/\\\",\\\"s\\\":\\\ + \"Evento especial de Apple. 9 de septiembre de 2015\\\",\\\"sc\\\":1,\\\"\ + th\\\":169,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\\ + u003dtbn:ANd9GcSANlZRlm7kHQZ4uQPUolxu2u9ywda5qF45RhQJmZHM6xRt6QXmgw\\\",\\\ + \"tw\\\":298}
    \\\
    960\_\xD7\_720 - pixabay.com
    {\\\"cb\\\":12,\\\"cl\\\":18,\\\"cr\\\":21,\\\"ct\\\":3,\\\"\ + id\\\":\\\"5HrXwGkhvJPnCM:\\\",\\\"isu\\\":\\\"pixabay.com\\\",\\\"ity\\\"\ + :\\\"png\\\",\\\"oh\\\":720,\\\"ou\\\":\\\"https://pixabay.com/static/uploads/photo/2015/11/27/13/48/apple-1065563_960_720.png\\\ + \",\\\"ow\\\":960,\\\"pt\\\":\\\"Ilustraci\xF3n gratis: Apple, Frutas, Rojo,\ + \ Los Alimentos - Imagen ...\\\",\\\"rid\\\":\\\"f1RB8v-C0m8SSM\\\",\\\"ru\\\ + \":\\\"https://pixabay.com/es/apple-frutas-rojo-los-alimentos-1065563/\\\"\ + ,\\\"s\\\":\\\"Apple, Frutas, Rojo, Los Alimentos, Transparente\\\",\\\"sc\\\ + \":1,\\\"th\\\":194,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcS5d3b9HP01DbcFdZ2e35r9Kxy1TtpqicMleyRcwfNZwmQCzys0xA\\\"\ + ,\\\"tw\\\":259}
    \\\
    332\_\xD7\_392 - apple.com
    {\\\"cb\\\":6,\\\"cl\\\":9,\\\"cr\\\":9,\\\"ct\\\":15,\\\"id\\\ + \":\\\"Zif0slL9gc5ApM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"\\\ + \",\\\"oh\\\":392,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/s/38/s38si/sbor/s38si-sbor-sel-201509?wid\\\ + \\u003d332\\\\u0026hei\\\\u003d392\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453545887556\\\",\\\"ow\\\":332,\\\"pt\\\":\\\"Apple Watch\ + \ Sport - Comprar un Apple Watch Sport - Apple (ES)\\\",\\\"rid\\\":\\\"UpP7DXQ0G0XH7M\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/es/shop/buy-watch/apple-watch-sport\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":244,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcR_mUXbhcrehEG02EbaM9L_PqWuUPt3RNCNnhmfHjeuBAzbIc6m\\\",\\\ + \"tw\\\":207}
    \\\
    1920\_\xD7\_1080 - maclatino.com
    {\\\"cb\\\":6,\\\"cl\\\":21,\\\"cr\\\":6,\\\"ct\\\"\ + :3,\\\"id\\\":\\\"q7hkNDmBPJjLlM:\\\",\\\"isu\\\":\\\"maclatino.com\\\",\\\ + \"ity\\\":\\\"jpg\\\",\\\"oh\\\":1080,\\\"ou\\\":\\\"http://www.maclatino.com/wp-content/ups/2015/10/Apple-inc.jpg\\\ + \",\\\"ow\\\":1920,\\\"pt\\\":\\\"Apple contin\xFAa entre las empresas m\xE1\ + s valiosas del mundo ...\\\",\\\"rid\\\":\\\"tqqaHYVuzxZnzM\\\",\\\"ru\\\"\ + :\\\"http://www.maclatino.com/apple-continua-entre-las-empresas-mas-valiosas-del-mundo/\\\ + \",\\\"s\\\":\\\"Apple ha estado durante a\xF1os en los primeros puestos de\ + \ las empresas m\xE1s valiosas del mundo y a\xFAn contin\xFAa entre las m\xE1\ + s importantes.\\\",\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcT1Mao_tHUIEetJkVsaFIxdLnmn3yKbjK0hmQe3O15OsGqSq3hHvA\\\"\ + ,\\\"tw\\\":300}
    \\\
    780\_\xD7\_458 - support.apple.com
    {\\\"id\\\":\\\"rxtP8VJVRf72QM:\\\",\\\"isu\\\":\\\ + \"support.apple.com\\\",\\\"ity\\\":\\\"gif\\\",\\\"oh\\\":458,\\\"ou\\\"\ + :\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/applemusic/apple-music-suggestion-hero-animation.gif\\\ + \",\\\"ow\\\":780,\\\"pt\\\":\\\"Refine the For You suggestions in Apple Music\ + \ on your iPhone, iPad ...\\\",\\\"rid\\\":\\\"bEkQWAwesO_B4M\\\",\\\"ru\\\ + \":\\\"https://support.apple.com/en-us/HT204842\\\",\\\"s\\\":\\\"When you\ + \ join Apple Music, we ask you to choose genres and artists that you like.\ + \ We use these choices to give you music suggestions from our experts who\ + \ ...\\\",\\\"sc\\\":1,\\\"th\\\":172,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTokJ-L5uyJVEfGed5hxEAkqsKxo07I_F7KV03LEZlq4DW5Tn8\\\",\\\"\ + tw\\\":293}
    \\\
    2000\_\xD7\_1000 - foreveralone.es
    {\\\"cl\\\":6,\\\"cr\\\":6,\\\"id\\\":\\\"RZGq-DDul8dJQM:\\\ + \",\\\"isu\\\":\\\"foreveralone.es\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\"\ + :1000,\\\"ou\\\":\\\"http://i.huffpost.com/gen/3398918/images/o-LAPIZ-APPLE-facebook.jpg\\\ + \",\\\"ow\\\":2000,\\\"pt\\\":\\\"Foreveralone - Y Twitter se cachonde\xF3\ + \ del l\xE1piz de Apple de 100\\\\u20ac\\\",\\\"rid\\\":\\\"lXfPb6M2jjt2FM\\\ + \",\\\"ru\\\":\\\"http://www.foreveralone.es/post/128729429274/y-twitter-se-cachonde%C3%B3-del-l%C3%A1piz-de-apple-de-100\\\ + \",\\\"s\\\":\\\"la gran novedad de Apple era un lapiz de 100\\\\u20ac y como\ + \ no pod\xEDa ser menos se ha montado el cachondeo el Twitter, aqu\xED las\ + \ mejores co\xF1as de la red social al ...\\\",\\\"sc\\\":1,\\\"th\\\":159,\\\ + \"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcTy1V4CX-VOoAeh6jWck29LXbSQ0aC3NNSjzFvy6k_HxsuvfKjl\\\ + \",\\\"tw\\\":318}
    \\\
    1560\_\xD7\_900 - support.apple.com
    {\\\"id\\\":\\\"OrPKMIh7yCix0M:\\\",\\\"isu\\\":\\\ + \"support.apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":900,\\\"ou\\\"\ + :\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/appleid/apple-id-standard-reset-password-email-sent.jpg\\\ + \",\\\"ow\\\":1560,\\\"pt\\\":\\\"If you forgot your Apple ID password - Apple\ + \ Support\\\",\\\"rid\\\":\\\"pmU_bh5yqEkuMM\\\",\\\"ru\\\":\\\"https://support.apple.com/en-us/HT201487\\\ + \",\\\"s\\\":\\\"Use email authentication\\\",\\\"sc\\\":1,\\\"th\\\":170,\\\ + \"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcSShiYkCzf7d7nvMu4oFyERtEDVbN45WUFdsvJvsj4DiausGLN_\\\ + \",\\\"tw\\\":296}
    \\\
    1200\_\xD7\_1200 - apple.com
    {\\\"cb\\\":21,\\\"cl\\\":18,\\\"cr\\\":21,\\\"id\\\":\\\"vK7xTXvoO0cMTM:\\\ + \",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1200,\\\ + \"ou\\\":\\\"http://images.apple.com/euro/mac/home/n/generic/images/social/macbook_mac_og.jpg?201602220751\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"Mac - Apple (ES)\\\",\\\"rid\\\":\\\"RyeA513XrFw3SM\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/es/mac/\\\",\\\"s\\\":\\\"\\\",\\\"\ + th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\\\ + u003dtbn:ANd9GcRkiW3rAgl0att-1oLqVq5zCxBFlYKCy1mtuzT_1rr0hZJl8Wrv9Q\\\",\\\ + \"tw\\\":225}
    \\\
    160\_\xD7\_160 - apple.com
    {\\\"cb\\\":21,\\\"cl\\\":15,\\\"cr\\\":15,\\\"ct\\\":21,\\\"\ + id\\\":\\\"lxy0B1NHKslblM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"png\\\",\\\"oh\\\":160,\\\"ou\\\":\\\"https://www.apple.com/support/assets/images/home/2015/apple_pay_icon_2x.png\\\ + \",\\\"ow\\\":160,\\\"pt\\\":\\\"Official Apple Support\\\",\\\"rid\\\":\\\ + \"OrunAFB7EkjeiM\\\",\\\"ru\\\":\\\"https://www.apple.com/support/\\\",\\\"\ + s\\\":\\\"Apple Pay Support\\\",\\\"sc\\\":1,\\\"th\\\":128,\\\"tu\\\":\\\"\ + https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcSzUffJMY0g6XLxURAZ1a5zYUWnyivxKrS4OiTqis8hdd-vf13O\\\ + \",\\\"tw\\\":128}
    \\\
    232\_\xD7\_232 - apple.com
    {\\\"id\\\":\\\"xxBAPjtNbwTSZM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":232,\\\"ou\\\":\\\"https://www.apple.com/support/assets/images/products/programs/hero_programs.jpg\\\ + \",\\\"ow\\\":232,\\\"pt\\\":\\\"Programas de servicio de Apple - Soporte\ + \ t\xE9cnico de Apple\\\",\\\"rid\\\":\\\"rTLJhjdY0-GBDM\\\",\\\"ru\\\":\\\ + \"https://www.apple.com/es/support/programs/\\\",\\\"s\\\":\\\"Pie de p\xE1\ + gina de Apple\\\",\\\"sc\\\":1,\\\"th\\\":185,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQXNBCYASGiMVwF8OTzxZUCninXV8LKQdvZvGpVqlNfkHNMuIxm\\\",\\\ + \"tw\\\":185}
    \\\
    700\_\xD7\_700 - poderpda.com
    {\\\"cl\\\":21,\\\"cr\\\":21,\\\"id\\\":\\\"32v5Q1FDQ4I16M:\\\ + \",\\\"isu\\\":\\\"poderpda.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":700,\\\ + \"ou\\\":\\\"https://s3.amazonaws.com/poderpda/2015/05/apple-iphone-5c-700x700.jpg\\\ + \",\\\"ow\\\":700,\\\"pt\\\":\\\"Apple no crear\xE1 \\\\u201cbackdoor\\\\\ + u201d para el FBI | PoderPDA\\\",\\\"rid\\\":\\\"1_qQPCHnGml9SM\\\",\\\"ru\\\ + \":\\\"http://www.poderpda.com/plataformas/apple/apple-niega-crear-backdoor/\\\ + \",\\\"s\\\":\\\"apple iphone 5c\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\"\ + :\\\"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcQ_SS3hMDEbrv4CNp5uTDGdGMJxek3rAl70plzKpTZ4zOelHSal\\\ + \",\\\"tw\\\":225}
    \\\
    800\_\xD7\_799 - imore.com
    {\\\"cb\\\":18,\\\"cl\\\":3,\\\"id\\\":\\\"kJWvtPo8cEK0XM:\\\ + \",\\\"isu\\\":\\\"imore.com\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":799,\\\ + \"ou\\\":\\\"http://www.imore.com/sites/imore.com/files/styles/large/public/topic_images/2015/apple-tv-4-topic.png?itok\\\ + \\u003dqAEoCCk0\\\",\\\"ow\\\":800,\\\"pt\\\":\\\"Apple TV \\\\u2014 Everything\ + \ you need to know! | iMore\\\",\\\"rid\\\":\\\"BAWSc_QV6-2IjM\\\",\\\"ru\\\ + \":\\\"http://www.imore.com/apple-tv\\\",\\\"s\\\":\\\"Apple TV\\\",\\\"sc\\\ + \":1,\\\"th\\\":224,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTGo2WqDgxl6u9eQ31UJelwZda2_XHQm0l0RqMDyVv-4d0dNJk4Gw\\\"\ + ,\\\"tw\\\":225}
    \\\
    800\_\xD7\_800 - clipartpanda.com
    {\\\"cb\\\":3,\\\"cl\\\":9,\\\"cr\\\":9,\\\"ct\\\"\ + :3,\\\"id\\\":\\\"h-XL1DGOub3FAM:\\\",\\\"isu\\\":\\\"clipartpanda.com\\\"\ + ,\\\"ity\\\":\\\"png\\\",\\\"oh\\\":800,\\\"ou\\\":\\\"http://images.clipartpanda.com/apple-clipart-apple5.png\\\ + \",\\\"ow\\\":800,\\\"pt\\\":\\\"Apple Clipart | Clipart Panda - Free Clipart\ + \ Images\\\",\\\"rid\\\":\\\"4iAesQutRHaySM\\\",\\\"ru\\\":\\\"http://www.clipartpanda.com/categories/apple-clipart\\\ + \",\\\"s\\\":\\\"apple%20clipart\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\"\ + :\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcTirE7dOKgMJjG5s3V6iOxNDGbtjJjFgcJ_rhQultHxpXBsL5CZ\\\ + \",\\\"tw\\\":225}
    \\\
    2000\_\xD7\_2200 - en.wikipedia.org
    {\\\"id\\\":\\\"QU50BCxNTMxUyM:\\\",\\\"isu\\\":\\\ + \"en.wikipedia.org\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":2200,\\\"ou\\\"\ + :\\\"https://upload.wikimedia.org/wikipedia/commons/thumb/8/84/Apple_Computer_Logo_rainbow.svg/2000px-Apple_Computer_Logo_rainbow.svg.png\\\ + \",\\\"ow\\\":2000,\\\"pt\\\":\\\"History of Apple Inc. - Wikipedia, the free\ + \ encyclopedia\\\",\\\"rid\\\":\\\"XlwI6ynf58KZqM\\\",\\\"ru\\\":\\\"https://en.wikipedia.org/wiki/History_of_Apple_Inc.\\\ + \",\\\"s\\\":\\\"Created by Rob Janoff in 1977, the Apple logo with the rainbow\ + \ scheme was used from April of that year [10] until August 26, 1999.\\\"\ + ,\\\"sc\\\":1,\\\"th\\\":235,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcR78mBAvw3kSOj5QCGkdR275kT1R5yML0rDPOocQRDa7rDHj10o5w\\\"\ + ,\\\"tw\\\":214}
    \\\
    1102\_\xD7\_1289 - taringa.net
    {\\\"cl\\\":3,\\\"cr\\\":3,\\\"ct\\\":3,\\\"id\\\"\ + :\\\"Flp61hOT67GVrM:\\\",\\\"isu\\\":\\\"taringa.net\\\",\\\"ity\\\":\\\"\ + jpg\\\",\\\"oh\\\":1289,\\\"ou\\\":\\\"http://virket.com/wp-content/uploads/Manzana.jpg\\\ + \",\\\"ow\\\":1102,\\\"pt\\\":\\\"Los 10 fracasos de Apple - Taringa!\\\"\ + ,\\\"rid\\\":\\\"u88VDr8ef_5vIM\\\",\\\"ru\\\":\\\"http://www.taringa.net/post/economia-negocios/17553341/Los-10-fracasos-de-Apple.html\\\ + \",\\\"s\\\":\\\"Los 10 fracasos de Apple\\\",\\\"sc\\\":1,\\\"th\\\":243,\\\ + \"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcS4UeHfpR9_wJCqbZIkpHPsqHa7WzXiF8_Pv8u52fNaF9qZgkkdXA\\\ + \",\\\"tw\\\":208}
    \\\
    277\_\xD7\_784 - apple.com
    {\\\"id\\\":\\\"dptmCRz8ZaBozM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":784,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/s/ce/scene0/right/scene0-right-2x?wid\\\ + \\u003d277\\\\u0026hei\\\\u003d784\\\\u0026fmt\\\\u003dpng-alpha\\\\u0026qlt\\\ + \\u003d95\\\\u0026.v\\\\u003d1453543709552\\\",\\\"ow\\\":277,\\\"pt\\\":\\\ + \"Compra un iPhone 6 o un iPhone 6 Plus - Apple (ES)\\\",\\\"rid\\\":\\\"\ + ZhCjqxYEAMHSfM\\\",\\\"ru\\\":\\\"http://www.apple.com/es/shop/buy-iphone/iphone6\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":235,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcS0tvjg_ErVfnyvxnw-yePYXQ0i_u-OAUHg8gUBOWA25sMP8Ujq3w\\\"\ + ,\\\"tw\\\":83}
    \\\
    660\_\xD7\_410 - appleinsider.com
    {\\\"cb\\\":3,\\\"cl\\\":3,\\\"cr\\\":3,\\\"ct\\\"\ + :3,\\\"id\\\":\\\"r1B3wsjEJIg13M:\\\",\\\"isu\\\":\\\"appleinsider.com\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":410,\\\"ou\\\":\\\"http://photos.appleinsidercdn.com/gallery/14203-9543-Screen-Shot-2015-09-09-at-63530-PM-l.jpg\\\ + \",\\\"ow\\\":660,\\\"pt\\\":\\\"Hands On: Apple TV 2015 with tvOS apps, Remote\ + \ featuring touch ...\\\",\\\"rid\\\":\\\"qDV-u_J4ARIk5M\\\",\\\"ru\\\":\\\ + \"http://appleinsider.com/articles/15/09/12/hands-on-apple-tv-2015-with-tvos-apps-remote-featuring-touch-motion-siri\\\ + \",\\\"s\\\":\\\"It\\\\u0026#39;s not clear if Apple will actually support\ + \ 3D-TV output from the new Apple TV, but 1.4 does suggest that as a possibility,\ + \ even if only in a future ...\\\",\\\"sc\\\":1,\\\"th\\\":177,\\\"tu\\\"\ + :\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcQEaxtKUK-OYLzvJrpDGNninqvYZ7qrwKV76Ud9tYHTLOihEDPJ\\\ + \",\\\"tw\\\":285}
    \\\
    600\_\xD7\_450 - techtimes.com
    {\\\"cb\\\":6,\\\"cl\\\":9,\\\"cr\\\":3,\\\"ct\\\"\ + :3,\\\"id\\\":\\\"9d-cCahhPJ_8KM:\\\",\\\"isu\\\":\\\"techtimes.com\\\",\\\ + \"ity\\\":\\\"jpg\\\",\\\"oh\\\":450,\\\"ou\\\":\\\"http://images.techtimes.com/data/images/full/191029/iphone-7.jpg?w\\\ + \\u003d600\\\",\\\"ow\\\":600,\\\"pt\\\":\\\"What To Expect From Apple In\ + \ 2016: iPhone 7, Apple Watch 2 ...\\\",\\\"rid\\\":\\\"Z99CeMHdtW1QFM\\\"\ + ,\\\"ru\\\":\\\"http://www.techtimes.com/articles/121536/20160106/what-to-expect-from-apple-in-2016-iphone-7-apple-watch-2-macbook-air-and-more.htm\\\ + \",\\\"s\\\":\\\"A rumor circulating on the Internet suggests that Apple might\ + \ ditch the classic 3.5mm headphone jack from the iPhone 7 in favor of Lightning\ + \ ports or ...\\\",\\\"th\\\":194,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTPNKvOxk-O4Fo5mzYTkZWZeI-H3I1rgI4PAaXfpg50yZwQ985KMw\\\"\ + ,\\\"tw\\\":259}
    \\\
    701\_\xD7\_377 - apple.com
    {\\\"id\\\":\\\"78w6FTfuX5e8oM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":377,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/l/p/lp/reuse/lp-reuse-versa-201503a?wid\\\ + \\u003d701\\\\u0026hei\\\\u003d377\\\\u0026fmt\\\\u003dpng-alpha\\\\u0026qlt\\\ + \\u003d95\\\\u0026.v\\\\u003d1453517230273\\\",\\\"ow\\\":701,\\\"pt\\\":\\\ + \"Programa de Reciclaje y Reutilizaci\xF3n de Apple para iPhone, iPad ...\\\ + \",\\\"rid\\\":\\\"SdGRAJDxFn5fXM\\\",\\\"ru\\\":\\\"http://www.apple.com/es/shop/browse/reuse_and_recycle\\\ + \",\\\"s\\\":\\\"Seg\xFAn el tipo de dispositivo, tendr\xE1s que llevarlo\ + \ a un Apple Store o tramitarlo por Internet.\\\",\\\"sc\\\":1,\\\"th\\\"\ + :165,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQKveSq-75OF0-oofnDoXwq2AamHUx2ATDfCZll9pUDUxG2gJIv6A\\\ + \",\\\"tw\\\":306}
    \\\
    650\_\xD7\_430 - maclovers.universiablogs.net
    {\\\"cb\\\":3,\\\"cl\\\":21,\\\"cr\\\":21,\\\"id\\\ + \":\\\"d2h5X5Nsu8InmM:\\\",\\\"isu\\\":\\\"maclovers.universiablogs.net\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":430,\\\"ou\\\":\\\"http://maclovers.universiablogs.net/files/apple_beats2_6501.jpg\\\ + \",\\\"ow\\\":650,\\\"pt\\\":\\\"Apple se hace con Beats | MACLOVERS Aprendiendo\ + \ Mac\\\",\\\"rid\\\":\\\"fm9x4_clLsLXaM\\\",\\\"ru\\\":\\\"http://maclovers.universiablogs.net/2014/05/29/apple-se-hace-finalmente-con-beats/\\\ + \",\\\"s\\\":\\\"De hecho, los de la manzana se interesaron por esta compa\xF1\ + \xEDa para potenciar el negocio de iTunes gracias a su servicio de Streaming;\ + \ tomando la delantera a ...\\\",\\\"sc\\\":1,\\\"th\\\":183,\\\"tu\\\":\\\ + \"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcSf67tmBSG1HQXvmRj29qHjo60vqHV-6isLY6Mcka31gM5JUzSI3g\\\ + \",\\\"tw\\\":276}
    \\\
    770\_\xD7\_433 - infobae.com
    {\\\"cb\\\":3,\\\"cl\\\":3,\\\"cr\\\":3,\\\"ct\\\":3,\\\"id\\\ + \":\\\"23XZ4s06o6htBM:\\\",\\\"isu\\\":\\\"infobae.com\\\",\\\"ity\\\":\\\"\ + jpg\\\",\\\"oh\\\":433,\\\"ou\\\":\\\"http://cdn01.ib.infobae.com/adjuntos/162/imagenes/014/198/0014198501.jpg\\\ + \",\\\"ow\\\":770,\\\"pt\\\":\\\"Apple prepara varios cambios para el iPhone\ + \ 7 | Apple, iPhone ...\\\",\\\"rid\\\":\\\"I6DorJ_g87SmWM\\\",\\\"ru\\\"\ + :\\\"http://www.infobae.com/2016/02/03/1787552-apple-prepara-varios-cambios-el-iphone-7\\\ + \",\\\"s\\\":\\\"El dise\xF1o del iPhone 7 basado en los nuevos rumores\\\"\ + ,\\\"sc\\\":1,\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcRuByE98p0ZO66OIVaYggPHHkEbQWLaYJT-q2h6TdKWxx-PVsugjw\\\"\ + ,\\\"tw\\\":300}
    \\\
    1020\_\xD7\_681 - theverge.com
    {\\\"cl\\\":12,\\\"cr\\\":12,\\\"ct\\\":21,\\\"id\\\ + \":\\\"BIWXxSUcFAajWM:\\\",\\\"isu\\\":\\\"theverge.com\\\",\\\"ity\\\":\\\ + \"jpg\\\",\\\"oh\\\":681,\\\"ou\\\":\\\"https://cdn1.vox-cdn.com/thumbor/TT51CKCf623RakUIha5Qw0q3sek\\\ + \\u003d/1020x0/cdn0.vox-cdn.com/uploads/chorus_asset/file/4044620/apple-iphone-6s-live-_0824.0.jpg\\\ + \",\\\"ow\\\":1020,\\\"pt\\\":\\\"This is the stylus for Apple\\\\u0026#39;s\ + \ new iPad Pro | The Verge\\\",\\\"rid\\\":\\\"23jXcA0_UFNQZM\\\",\\\"ru\\\ + \":\\\"http://www.theverge.com/2015/9/9/9295901/apple-ipad-stylus-pen-announced-price-specs-date\\\ + \",\\\"s\\\":\\\"Apple iphone 6s live 0824.0\\\",\\\"th\\\":183,\\\"tu\\\"\ + :\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcTTVcwavQ3jGI_FF0Lqrbh7JP8tIoeR-X2n_mVwdeKs8VL8tgbQBA\\\ + \",\\\"tw\\\":275}
    \\\"Resultado
    2000\_\xD7\_1536 - ishopcentroamerica.com
    {\\\"cb\\\":15,\\\"cl\\\":18,\\\"cr\\\":21,\\\"ct\\\ + \":15,\\\"id\\\":\\\"VNIZ3ku7M6Q0EM:\\\",\\\"isu\\\":\\\"ishopcentroamerica.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":1536,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4528/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/tv/apple-tv-gallery4-2015?wid\\\ + \\u003d2000\\\\u0026hei\\\\u003d1536\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\ + \\u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1441801116406%E2%80%9C\\\",\\\"ow\\\":2000,\\\"pt\\\":\\\ + \"Apple TV 3G | iShop Costa Rica Distribuidor Apple Autorizado.\\\",\\\"rid\\\ + \":\\\"0NpLjao8bA9cZM\\\",\\\"ru\\\":\\\"http://www.ishopcentroamerica.com/shop/apple-tv/apple-tv/\\\ + \",\\\"s\\\":\\\"Apple TV 3G\\\",\\\"sc\\\":1,\\\"th\\\":197,\\\"tu\\\":\\\ + \"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcQ59HF9WQKLmPW83sf3XX8G8VDU37OQm7gza1jo4w0bAExEdyLz\\\ + \",\\\"tw\\\":256}
    \\\"Resultado
    310\_\xD7\_233 - economictimes.indiatimes.com
    {\\\"cb\\\":9,\\\"cr\\\":21,\\\"id\\\":\\\"1s-AZ8rVSTzUQM:\\\ + \",\\\"isu\\\":\\\"economictimes.indiatimes.com\\\",\\\"ity\\\":\\\"jpg\\\"\ + ,\\\"oh\\\":233,\\\"ou\\\":\\\"http://economictimes.indiatimes.com/thumb/msid-50660935,width-310,resizemode-4/apple-posts-record-india-sales-in-tough-october-december-quarter.jpg\\\ + \",\\\"ow\\\":310,\\\"pt\\\":\\\"Apple posts record India sales in tough October-December\ + \ quarter ...\\\",\\\"rid\\\":\\\"D9KChmpKb5GJjM\\\",\\\"ru\\\":\\\"http://economictimes.indiatimes.com/small-biz/apple-posts-record-india-sales-in-tough-october-december-quarter/articleshow/50660904.cms\\\ + \",\\\"s\\\":\\\"Apple\\\\u0026#39;s sales in India rose to a record in the\ + \ October-December period thanks to\\\",\\\"th\\\":186,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcR7RAQEXwKZjpRgUAbk0DRLOJmS7FNNWLzsHhdnyzB2EI4yw5v0\\\",\\\ + \"tw\\\":248}
    Im\xE1genes relacionadas:
    Ver m\xE1s
    Las im\xE1genes pueden estar sujetas a derechos de autor.Enviar comentarios
    Im\xE1genes relacionadas:
    Ver m\xE1s
    Las im\xE1genes pueden estar sujetas a derechos de autor.Enviar comentarios
    Im\xE1genes relacionadas:
    Ver m\xE1s
    Las im\xE1genes pueden estar sujetas a derechos de autor.Enviar comentarios
    Im\xE1genes relacionadas:
    Ver m\xE1s
    Las im\xE1genes pueden estar sujetas a derechos de autor.Enviar comentarios
      \ + \
      \ + \
      \ + \
      \"}"} + headers: + connection: [close] + content-length: ['675034'] + content-type: [application/json; charset=UTF-8] + date: ['Wed, 23 Mar 2016 01:46:24 GMT'] + server: [httpd.js] + status: {code: 200, message: OK} +- request: + body: '{"url": "https://www.google.com.ar/search?q=apple&es_sm=122&source=lnms&tbm=isch&sa=X&ei=DDdUVL-fE4SpNq-ngPgK&ved=0CAgQ_AUoAQ&biw=1024&bih=719&dpr=1.25", + "sessionId": "f0baf8c0-8109-8745-b27e-205763034e37"}' + headers: + Accept: [application/json] + Connection: [keep-alive] + Content-type: [application/json;charset="UTF-8"] + POST: [!!python/unicode '/hub/session/f0baf8c0-8109-8745-b27e-205763034e37/url'] + User-Agent: [Python http auth] + method: POST + uri: http://127.0.0.1:64104/hub/session/f0baf8c0-8109-8745-b27e-205763034e37/url + response: + body: {string: !!python/unicode '{"name":"get","sessionId":"f0baf8c0-8109-8745-b27e-205763034e37","status":0,"value":""}'} + headers: + connection: [close] + content-length: ['87'] + content-type: [application/json; charset=UTF-8] + date: ['Wed, 23 Mar 2016 01:46:25 GMT'] + server: [httpd.js] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: [application/json] + Connection: [keep-alive] + Content-type: [application/json;charset="UTF-8"] + GET: [!!python/unicode '/hub/session/f0baf8c0-8109-8745-b27e-205763034e37/source'] + User-Agent: [Python http auth] + method: GET + uri: http://127.0.0.1:64104/hub/session/f0baf8c0-8109-8745-b27e-205763034e37/source + response: + body: {string: "{\"name\":\"getPageSource\",\"sessionId\":\"f0baf8c0-8109-8745-b27e-205763034e37\"\ + ,\"status\":0,\"value\":\"\\napple\ + \ - Buscar con Google #gbsfw{min-width:400px;overflow:visible}.gb_Hb,#gbsfw.gb_g{display:block;outline:none}#gbsfw.gb_qa\ + \ iframe{display:none}.gb_Ib{padding:118px 0;text-align:center}.gb_Jb{background:no-repeat\ + \ center 0;color:#aaa;font-size:13px;line-height:20px;padding-top:76px;background-image:url('//ssl.gstatic.com/gb/images/a/f5cdd88b65.png')}.gb_Jb\ + \ a{color:#4285f4;text-decoration:none}@-moz-keyframes gb__a{0%{opacity:0}50%{opacity:1}}@keyframes\ + \ gb__a{0%{opacity:0}50%{opacity:1}}.gb_0a{display:none!important}.gb_4c{display:inline-block;padding:0\ + \ 0 0 15px;vertical-align:middle}.gb_4c:first-child,#gbsfw:first-child+.gb_4c{padding-left:0}.gb_nc{position:relative}.gb_b{display:inline-block;outline:none;vertical-align:middle;-moz-border-radius:2px;border-radius:2px;-moz-box-sizing:border-box;box-sizing:border-box;height:30px;width:30px;color:#000;cursor:default;text-decoration:none}#gb#gb\ + \ a.gb_b{color:#000;cursor:default;text-decoration:none}.gb_cb{border-color:transparent;border-bottom-color:#fff;border-style:dashed\ + \ dashed solid;border-width:0 8.5px 8.5px;display:none;position:absolute;left:6.5px;top:37px;z-index:1;height:0;width:0;-moz-animation:gb__a\ + \ .2s;animation:gb__a .2s}.gb_db{border-color:transparent;border-style:dashed\ + \ dashed solid;border-width:0 8.5px 8.5px;display:none;position:absolute;left:6.5px;z-index:1;height:0;width:0;-moz-animation:gb__a\ + \ .2s;animation:gb__a .2s;border-bottom-color:#ccc;border-bottom-color:rgba(0,0,0,.2);top:36px}x:-o-prefocus,div.gb_db{border-bottom-color:#ccc}.gb_ga{background:#fff;border:1px\ + \ solid #ccc;border-color:rgba(0,0,0,.2);color:#000;-moz-box-shadow:0 2px\ + \ 10px rgba(0,0,0,.2);box-shadow:0 2px 10px rgba(0,0,0,.2);display:none;outline:none;overflow:hidden;position:absolute;right:0;top:44px;-moz-animation:gb__a\ + \ .2s;animation:gb__a .2s;-moz-border-radius:2px;border-radius:2px;-moz-user-select:text}.gb_4c.gb_g\ + \ .gb_cb,.gb_4c.gb_g .gb_db,.gb_4c.gb_g .gb_ga,.gb_g.gb_ga{display:block}.gb_4c.gb_g.gb_Ld\ + \ .gb_cb,.gb_4c.gb_g.gb_Ld .gb_db{display:none}.gb_Md{position:absolute;right:0;top:44px;z-index:-1}.gb_3a\ + \ .gb_cb,.gb_3a .gb_db,.gb_3a .gb_ga{margin-top:-10px}.gb_wb .gb_db{border:0;border-left:1px\ + \ solid rgba(0,0,0,.2);border-top:1px solid rgba(0,0,0,.2);height:14px;width:14px;-moz-transform:rotate(45deg);transform:rotate(45deg)}.gb_wb\ + \ .gb_cb{border:0;border-left:1px solid rgba(0,0,0,.2);border-top:1px solid\ + \ rgba(0,0,0,.2);height:14px;width:14px;-moz-transform:rotate(45deg);transform:rotate(45deg);border-color:#fff;background:#fff}.gb_ce\ + \ ::-webkit-scrollbar{height:15px;width:15px}.gb_ce ::-webkit-scrollbar-button{height:0;width:0}.gb_ce\ + \ ::-webkit-scrollbar-thumb{background-clip:padding-box;background-color:rgba(0,0,0,.3);border:5px\ + \ solid transparent;-moz-border-radius:10px;border-radius:10px;min-height:20px;min-width:20px;height:5px;width:5px}.gb_ce\ + \ ::-webkit-scrollbar-thumb:hover,.gb_ce ::-webkit-scrollbar-thumb:active{background-color:rgba(0,0,0,.4)}.gb_ea\ + \ .gb_b{background-position:-35px -311px;opacity:.55}.gb_fa .gb_ea .gb_b{background-position:-35px\ + \ -311px}.gb_X .gb_ea .gb_b{background-position:-60px -1675px;opacity:1}.gb_ga.gb_ha{min-height:196px;overflow-y:auto;width:320px}.gb_ia{-moz-transition:height\ + \ .2s ease-in-out;transition:height .2s ease-in-out}.gb_ja{background:#fff;margin:0;min-height:100px;padding:28px;padding-right:27px;text-align:left;white-space:normal;width:265px}.gb_ka{background:#f5f5f5;cursor:pointer;height:40px;overflow:hidden}.gb_la{position:relative}.gb_ka{display:block;line-height:40px;text-align:center;width:320px}.gb_la{display:block;line-height:40px;text-align:center}.gb_la.gb_ma{line-height:0}.gb_ka,.gb_ka:visited,.gb_ka:active,.gb_la,.gb_la:visited{color:#737373;text-decoration:none}.gb_la:active{color:#737373}#gb\ + \ a.gb_ka,#gb a.gb_ka:visited,#gb a.gb_ka:active,#gb a.gb_la,#gb a.gb_la:visited{color:#737373;text-decoration:none}#gb\ + \ a.gb_la:active{color:#737373}.gb_la,.gb_ja{display:none}.gb_ca,.gb_ca+.gb_la,.gb_na\ + \ .gb_la,.gb_na .gb_ja{display:block}.gb_la:hover,.gb_la:active,#gb a.gb_la:hover,#gb\ + \ a.gb_la:active{text-decoration:underline}.gb_la{border-bottom:1px solid\ + \ #ebebeb;left:28px;width:264px}.gb_na .gb_ka{display:none}.gb_la:last-child{border-bottom-width:0}.gb_oa\ + \ .gb_O{display:initial}.gb_oa.gb_pa{height:100px;text-align:center}.gb_oa.gb_pa\ + \ img{padding:34px 0;height:32px;width:32px}.gb_oa .gb_3{background-image:url('//ssl.gstatic.com/gb/images/v1_8758f7bb.png');background-size:92px\ + \ 2541px;background-position:0 -690px}.gb_oa .gb_3+img{border:0;margin:8px;height:48px;width:48px}.gb_oa\ + \ div.gb_qa{background:#ffa;-moz-border-radius:5px;border-radius:5px;padding:5px;text-align:center}.gb_oa.gb_ra,.gb_oa.gb_sa{padding-bottom:0}.gb_oa.gb_ta,.gb_oa.gb_sa{padding-top:0}.gb_oa.gb_sa\ + \ a,.gb_oa.gb_ta a{top:0}.gb_ua .gb_ka{margin-top:0;position:static}.gb_va{display:inline-block}.gb_wa{margin:-12px\ + \ 28px 28px;position:relative;width:264px;-moz-border-radius:2px;border-radius:2px;-moz-box-shadow:0\ + \ 1px 2px rgba(0,0,0,0.1),0 0 1px rgba(0,0,0,0.1);box-shadow:0 1px 2px rgba(0,0,0,0.1),0\ + \ 0 1px rgba(0,0,0,0.1)}.gb_5{background-image:url('//ssl.gstatic.com/gb/images/v1_8758f7bb.png');background-size:92px\ + \ 2541px;display:inline-block;margin:8px;vertical-align:middle;height:64px;width:64px}.gb_xa{color:#262626;display:inline-block;font:13px/18px\ + \ Arial,sans-serif;margin-right:80px;padding:10px 10px 10px 0;vertical-align:middle;white-space:normal}.gb_ya{font:16px/24px\ + \ Arial,sans-serif}.gb_za,#gb#gb .gb_za{color:#427fed;text-decoration:none}.gb_za:hover,#gb#gb\ + \ .gb_za:hover{text-decoration:underline}.gb_Aa .gb_ja{position:relative}.gb_Aa\ + \ .gb_O{position:absolute;top:28px;left:28px}.gb_ka.gb_Ba{display:none;height:0}.gb_N\ + \ .gb_ea .gb_b::before,.gb_N.gb_fa .gb_ea .gb_b::before{left:-35px;top:-311px}.gb_N.gb_X\ + \ .gb_ea .gb_b::before{left:-60px;top:-1675px}.gb_wb .gb_ka{position:relative}.gb_ea\ + \ .gb_b:hover,.gb_ea .gb_b:focus{opacity:.85}.gb_X .gb_ea .gb_b:hover,.gb_X\ + \ .gb_ea .gb_b:focus{opacity:1}@media (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_oa\ + \ .gb_3{background-image:url('//ssl.gstatic.com/gb/images/v2_0bc49e87.png')}}#gb#gb\ + \ a.gb_O{color:#404040;text-decoration:none}#gb#gb a.gb_P,#gb#gb span.gb_P{text-decoration:none}#gb#gb\ + \ a.gb_P,#gb#gb span.gb_P{color:#000}.gb_P{opacity:.75}#gb#gb a.gb_P:hover,#gb#gb\ + \ a.gb_P:focus{opacity:.85;text-decoration:underline}.gb_Q.gb_R{display:none;padding-left:15px;vertical-align:middle}.gb_Q.gb_R:first-child{padding-left:0}.gb_S.gb_R{display:inline-block}.gb_Q\ + \ span{opacity:.55;-moz-user-select:text}.gb_T .gb_S.gb_R{flex:0 1 auto;flex:0\ + \ 1 main-size;display:-webkit-flex;display:flex}.gb_U .gb_S.gb_R{display:none}.gb_Q\ + \ .gb_P{display:inline-block;line-height:24px;outline:none;vertical-align:middle}.gb_S\ + \ .gb_P{min-width:60px;overflow:hidden;flex:0 1 auto;flex:0 1 main-size;text-overflow:ellipsis}.gb_V\ + \ .gb_S .gb_P{min-width:0}.gb_W .gb_S .gb_P{width:0!important}#gb#gb.gb_X\ + \ a.gb_P,#gb#gb.gb_X span.gb_P,#gb#gb .gb_X a.gb_P,#gb#gb .gb_X span.gb_P{color:#fff}#gb#gb.gb_X\ + \ span.gb_P,#gb#gb .gb_X span.gb_P{opacity:.7}.gb_M.gb_M{background-size:64px\ + \ 64px}#gb2 .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/3a1e625196.png')}.gb_N\ + \ #gb2 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/3a1e625196.png')}#gb22\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/3daf4c1f88.png')}.gb_N\ + \ #gb22 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/3daf4c1f88.png')}#gb45\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/f420d06f66.png')}.gb_N\ + \ #gb45 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/f420d06f66.png')}#gb72\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/78b3d46de1.png')}.gb_N\ + \ #gb72 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/78b3d46de1.png')}#gb117\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/142da27578.png')}.gb_N\ + \ #gb117 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/142da27578.png')}#gb136\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/911e3628e6.png')}.gb_N\ + \ #gb136 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/911e3628e6.png')}#gb166\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/5d03e9e245.png')}.gb_N\ + \ #gb166 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/5d03e9e245.png')}#gb171\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/4244245d7e.png')}.gb_N\ + \ #gb171 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/4244245d7e.png')}#gb177\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/4653513b7d.png')}.gb_N\ + \ #gb177 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/4653513b7d.png')}#gb206\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/ad330d8459.png')}.gb_N\ + \ #gb206 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/ad330d8459.png')}#gb207\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/2c21041e16.png')}.gb_N\ + \ #gb207 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/2c21041e16.png')}#gb211\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/c03dda0b34.png')}.gb_N\ + \ #gb211 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/c03dda0b34.png')}#gb217\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/71060be5b3.png')}.gb_N\ + \ #gb217 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/71060be5b3.png')}#gb228\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/74aa55e0c2.png')}.gb_N\ + \ #gb228 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/74aa55e0c2.png')}#gb249\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/afa40f6e42.png')}.gb_N\ + \ #gb249 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/afa40f6e42.png')}#gb260\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/ea554714e7.png')}.gb_N\ + \ #gb260 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/ea554714e7.png')}#gb261\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/0b26f6f8e4.png')}.gb_N\ + \ #gb261 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/0b26f6f8e4.png')}#gb108\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/dfbeb24785.png')}.gb_N\ + \ #gb108 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/dfbeb24785.png')}#gb60\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/85bb99a341.png')}.gb_N\ + \ #gb60 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/85bb99a341.png')}#gb175\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/eacd033c28.png')}.gb_N\ + \ #gb175 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/eacd033c28.png')}@media\ + \ (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){#gb2\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/438087d3df.png')}.gb_N\ + \ #gb2 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/438087d3df.png')}#gb22\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/cfa67efcd3.png')}.gb_N\ + \ #gb22 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/cfa67efcd3.png')}#gb45\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/9c561d4392.png')}.gb_N\ + \ #gb45 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/9c561d4392.png')}#gb72\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/90f42e515b.png')}.gb_N\ + \ #gb72 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/90f42e515b.png')}#gb117\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/e3cbb9b858.png')}.gb_N\ + \ #gb117 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/e3cbb9b858.png')}#gb136\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/17bdcddea9.png')}.gb_N\ + \ #gb136 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/17bdcddea9.png')}#gb166\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/56c3072e8e.png')}.gb_N\ + \ #gb166 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/56c3072e8e.png')}#gb171\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/1b217ae532.png')}.gb_N\ + \ #gb171 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/1b217ae532.png')}#gb177\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/188f0d697b.png')}.gb_N\ + \ #gb177 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/188f0d697b.png')}#gb206\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/20808fb750.png')}.gb_N\ + \ #gb206 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/20808fb750.png')}#gb207\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/6d9eaee7f9.png')}.gb_N\ + \ #gb207 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/6d9eaee7f9.png')}#gb211\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/2d7fffa981.png')}.gb_N\ + \ #gb211 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/2d7fffa981.png')}#gb217\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/e2c0b463b4.png')}.gb_N\ + \ #gb217 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/e2c0b463b4.png')}#gb228\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/fe8c881457.png')}.gb_N\ + \ #gb228 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/fe8c881457.png')}#gb249\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/d54db42004.png')}.gb_N\ + \ #gb249 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/d54db42004.png')}#gb260\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/99be7c5086.png')}.gb_N\ + \ #gb260 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/99be7c5086.png')}#gb261\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/9001dae971.png')}.gb_N\ + \ #gb261 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/9001dae971.png')}#gb108\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/ca7b209615.png')}.gb_N\ + \ #gb108 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/ca7b209615.png')}#gb60\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/e000432278.png')}.gb_N\ + \ #gb60 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/e000432278.png')}#gb175\ + \ .gb_M{background-image:url('//ssl.gstatic.com/gb/images/a/84d52a8885.png')}.gb_N\ + \ #gb175 .gb_M::before{content:url('//ssl.gstatic.com/gb/images/a/84d52a8885.png')}}.gb_Z{padding:1px;display:inline-block;vertical-align:top;color:black;z-index:999;height:98px;width:86px}.gb_Z\ + \ a{text-decoration:none}.gb_Z[aria-grabbed=true]{visibility:hidden}.gb_Z:hover:not(.gb_0){z-index:1001}.gb_Z:hover:not(.gb_0)\ + \ a{border:1px solid #e5e5e5;-moz-border-radius:2px;border-radius:2px;margin:7px\ + \ 1px}.gb_Z.gb_1:not(.gb_0) a{border:1px solid #e5e5e5;-moz-box-shadow:0 1px\ + \ 2px rgba(0,0,0,0.1);box-shadow:0 1px 2px rgba(0,0,0,0.1)}.gb_Z.gb_1 a{background:#fff;cursor:-moz-grabbing;cursor:-webkit-grabbing;margin:-1px;visibility:visible;z-index:1001}.gb_2{opacity:.5}.gb_Z.gb_1\ + \ a{color:#404040!important;cursor:-moz-grabbing;cursor:-webkit-grabbing;font:13px/27px\ + \ Arial,sans-serif;text-decoration:none!important}.gb_O{color:#404040;display:inline-block;font-size:13px;margin:8px\ + \ 2px;text-align:center;outline:none}.gb_O .gb_3,.gb_O .gb_M{display:inline-block;vertical-align:top;height:64px;width:64px}.gb_4{display:block;line-height:20px;overflow:hidden;white-space:nowrap;width:84px;text-overflow:ellipsis}.gb_Z:hover\ + \ .gb_O{z-index:1}.gb_Z:hover .gb_4{background:rgba(255,255,255,.9);white-space:normal;overflow-wrap:break-word;word-wrap:break-word}.gb_O.gb_0{cursor:default;filter:url(\\\ + \"data:image/svg+xml;utf8,\\\\00003csvg xmlns=\\\\000027http://www.w3.org/2000/svg\\\ + \\000027\\\\00003e\\\\00003cfilter id=\\\\000027g\\\\000027\\\\00003e\\\\\ + 00003cfeColorMatrix values=\\\\0000270.3333 0.3333 0.3333 0 0 0.3333 0.3333\ + \ 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0\\\\000027/\\\\00003e\\\\00003c/filter\\\ + \\00003e\\\\00003c/svg\\\\00003e#g\\\");opacity:.4}.gb_O .gb_3{background-image:url('//ssl.gstatic.com/gb/images/v1_8758f7bb.png');background-size:92px\ + \ 2541px}.gb_N .gb_O .gb_3,.gb_N .gb_5.gb_3{background-image:none;overflow:hidden;position:relative}.gb_N\ + \ .gb_O .gb_3::before,.gb_N .gb_5.gb_3::before{content:url('//ssl.gstatic.com/gb/images/v1_8758f7bb.png');position:absolute}.gb_N\ + \ .gb_M{background-image:none!important;position:relative}.gb_N .gb_M::before{left:0;position:absolute;top:0}@media\ + \ (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_O\ + \ .gb_3{background-image:url('//ssl.gstatic.com/gb/images/v2_0bc49e87.png')}.gb_N\ + \ .gb_O .gb_3::before{content:url('//ssl.gstatic.com/gb/images/v2_0bc49e87.png');-moz-transform:scale(.5);transform:scale(.5);-moz-transform-origin:0\ + \ 0;transform-origin:0 0}.gb_N .gb_O .gb_M::before{-moz-transform:scale(.5);transform:scale(.5);-moz-transform-origin:0\ + \ 0;transform-origin:0 0}}.gb_6 .gb_O:focus,#gb#gb .gb_6 a.gb_O:focus{text-decoration:underline}.gb_Z[aria-grabbed=true].gb_7{visibility:visible}.gb_8,.gb_9{position:relative;top:27px;visibility:hidden}.gb_aa,.gb_ba{left:37px;visibility:hidden}.gb_8{float:left;width:0;height:0;border-top:5px\ + \ solid transparent;border-bottom:5px solid transparent;border-right:5px solid\ + \ #4273db}.gb_9{float:right;width:0;height:0;border-top:5px solid transparent;border-bottom:5px\ + \ solid transparent;border-left:5px solid #4273db}.gb_aa{position:absolute;top:0;width:0;height:0;border-left:5px\ + \ solid transparent;border-right:5px solid transparent;border-bottom:5px solid\ + \ #4273db}.gb_ba{position:absolute;top:59px;width:0;height:0;border-left:5px\ + \ solid transparent;border-right:5px solid transparent;border-top:5px solid\ + \ #4273db}ul.gb_ca li.gb_7:not(:first-child) .gb_8,ul.gb_ca li.gb_7:not(:nth-child(-n+3))\ + \ .gb_aa,ul.gb_ca li.gb_7 .gb_9,ul.gb_ca li.gb_7 .gb_ba,ul.gb_da li.gb_7 .gb_8,ul.gb_da\ + \ li.gb_7 .gb_aa,ul.gb_da li.gb_7:not(:last-child) .gb_9,ul.gb_da li.gb_7:not(:nth-last-child(-n+3))\ + \ .gb_ba{visibility:visible}.gb_Ca{border:none;color:#4285f4;cursor:default;font-weight:bold;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap;-moz-user-select:none}.gb_Ca:hover{background-color:rgba(153,153,153,.2)}.gb_Ca:active{background-color:rgba(153,153,153,.4)}.gb_Ca,.gb_Da,.gb_Ea{display:inline-block;line-height:28px;padding:0\ + \ 12px;-moz-border-radius:2px;border-radius:2px}.gb_Da{background:#f8f8f8;border:1px\ + \ solid #c6c6c6}.gb_Ea{background:#f8f8f8}.gb_Da,#gb a.gb_Da.gb_Da,.gb_Ea{color:#666;cursor:default;text-decoration:none}#gb\ + \ a.gb_Ea.gb_Ea{cursor:default;text-decoration:none}.gb_Ea{border:1px solid\ + \ #4285f4;font-weight:bold;outline:none;background:#4285f4;background:-moz-linear-gradient(top,#4387fd,#4683ea);background:linear-gradient(top,#4387fd,#4683ea);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4387fd,endColorstr=#4683ea,GradientType=0)}#gb\ + \ a.gb_Ea.gb_Ea{color:#fff}.gb_Ea:hover{-moz-box-shadow:0 1px 0 rgba(0,0,0,.15);box-shadow:0\ + \ 1px 0 rgba(0,0,0,.15)}.gb_Ea:active{-moz-box-shadow:inset 0 2px 0 rgba(0,0,0,.15);box-shadow:inset\ + \ 0 2px 0 rgba(0,0,0,.15);background:#3c78dc;background:-moz-linear-gradient(top,#3c7ae4,#3f76d3);background:linear-gradient(top,#3c7ae4,#3f76d3);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3c7ae4,endColorstr=#3f76d3,GradientType=0)}.gb_Nd{display:inline-block;line-height:normal;position:relative;z-index:987}.gb_1a{background-size:32px\ + \ 32px;-moz-border-radius:50%;border-radius:50%;display:block;margin:-1px;overflow:hidden;position:relative;height:32px;width:32px}@media\ + \ (min-resolution:1.25dppx),(-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_1a::before{-moz-transform:scale(.5);transform:scale(.5);-moz-transform-origin:left\ + \ 0;transform-origin:left 0}.gb_lb::before{-moz-transform:scale(.5);transform:scale(.5);-moz-transform-origin:left\ + \ 0;transform-origin:left 0}}.gb_1a:hover,.gb_1a:focus{-moz-box-shadow:0 1px\ + \ 0 rgba(0,0,0,.15);box-shadow:0 1px 0 rgba(0,0,0,.15)}.gb_1a:active{-moz-box-shadow:inset\ + \ 0 2px 0 rgba(0,0,0,.15);box-shadow:inset 0 2px 0 rgba(0,0,0,.15)}.gb_1a:active::after{background:rgba(0,0,0,.1);-moz-border-radius:50%;border-radius:50%;content:'';display:block;height:100%}.gb_2a{cursor:pointer;line-height:30px;min-width:30px;opacity:.75;overflow:hidden;vertical-align:middle;text-overflow:ellipsis}.gb_b.gb_2a{width:auto}.gb_2a:hover,.gb_2a:focus{opacity:.85}.gb_3a\ + \ .gb_2a,.gb_3a .gb_4a{line-height:26px}#gb#gb.gb_3a a.gb_2a,.gb_3a .gb_4a{font-size:11px;height:auto}.gb_5a{border-top:4px\ + \ solid #000;border-left:4px dashed transparent;border-right:4px dashed transparent;display:inline-block;margin-left:6px;opacity:.75;vertical-align:middle}.gb_6a:hover\ + \ .gb_5a{opacity:.85}.gb_X .gb_2a,.gb_X .gb_5a{opacity:1}#gb#gb.gb_X.gb_X\ + \ a.gb_2a,#gb#gb .gb_X.gb_X a.gb_2a{color:#fff}.gb_X.gb_X .gb_5a{border-top-color:#fff;opacity:1}.gb_fa\ + \ .gb_1a:hover,.gb_X .gb_1a:hover,.gb_fa .gb_1a:focus,.gb_X .gb_1a:focus{-moz-box-shadow:0\ + \ 1px 0 rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 0 rgba(0,0,0,.15),0\ + \ 1px 2px rgba(0,0,0,.2)}.gb_7a .gb_8a,.gb_9a .gb_8a{position:absolute;right:1px}.gb_8a.gb_R,.gb_ab.gb_R,.gb_6a.gb_R{flex:0\ + \ 1 auto;flex:0 1 main-size}.gb_bb.gb_W .gb_2a{width:30px!important}.gb_2a~.gb_cb,.gb_2a~.gb_db{left:auto;right:6.5px}.gb_eb{outline:none}.gb_fb,#gb\ + \ a.gb_fb.gb_fb,.gb_gb a,#gb .gb_gb.gb_gb a{color:#36c;text-decoration:none}.gb_fb:active,#gb\ + \ a.gb_fb:active,.gb_fb:hover,#gb a.gb_fb:hover,.gb_gb a:active,#gb .gb_gb\ + \ a:active,.gb_gb a:hover,#gb .gb_gb a:hover{text-decoration:underline}.gb_hb{margin:20px}.gb_ib,.gb_jb{display:inline-block;vertical-align:top}.gb_ib{margin-right:20px;position:relative}.gb_kb{-moz-border-radius:50%;border-radius:50%;overflow:hidden}.gb_lb{background-size:96px\ + \ 96px;border:none;vertical-align:top;height:96px;width:96px}.gb_mb{background:rgba(78,144,254,.7);bottom:0;color:#fff;font-size:9px;font-weight:bold;left:0;line-height:9px;position:absolute;padding:7px\ + \ 0;text-align:center;width:96px}.gb_kb .gb_mb{background:rgba(0,0,0,.54)}.gb_nb{font-weight:bold;margin:-4px\ + \ 0 1px 0}.gb_ob{color:#666}.gb_gb{color:#ccc;margin:6px 0}.gb_gb a{margin:0\ + \ 10px}.gb_gb a:first-child{margin-left:0}.gb_gb a:last-child{margin-right:0}.gb_jb\ + \ .gb_pb{background:#4d90fe;border-color:#3079ed;font-weight:bold;margin:10px\ + \ 0 0 0;color:#fff}#gb .gb_jb a.gb_pb.gb_pb{color:#fff}.gb_jb .gb_pb:hover{background:#357ae8;border-color:#2f5bb7}.gb_qb{background:#f5f5f5;border-top:1px\ + \ solid #ccc;border-color:rgba(0,0,0,.2);padding:10px 0;width:100%;display:table}.gb_qb\ + \ .gb_pb{margin:0 20px}.gb_qb>div{display:table-cell;text-align:right}.gb_qb>div:first-child{text-align:left}.gb_qb\ + \ .gb_rb{display:block;text-align:center}.gb_sb .gb_cb{border-bottom-color:#fef9db}.gb_tb{background:#fef9db;font-size:11px;padding:10px\ + \ 20px;white-space:normal}.gb_tb b,.gb_fb{white-space:nowrap}.gb_ub{background:#f5f5f5;border-top:1px\ + \ solid #ccc;border-top-color:rgba(0,0,0,.2);max-height:230px;overflow:auto}.gb_vb{border-top:1px\ + \ solid #ccc;border-top-color:rgba(0,0,0,.2);display:block;padding:10px 20px}.gb_wb\ + \ .gb_vb:focus .gb_xb{outline:1px dotted #fff}.gb_vb:hover{background:#eee}.gb_vb:first-child,.gb_yb:first-child+.gb_vb{border-top:0}.gb_yb{display:none}.gb_zb{cursor:default}.gb_zb:hover{background:transparent}.gb_Ab{border:none;vertical-align:top;height:48px;width:48px}.gb_xb{display:inline-block;margin:6px\ + \ 0 0 10px}.gb_zb .gb_Ab,.gb_zb .gb_xb{opacity:.4}.gb_Bb{color:#000}.gb_zb\ + \ .gb_Bb{color:#666}.gb_Cb{color:#666}.gb_Db{background:#f5f5f5;border-top:1px\ + \ solid #ccc;border-top-color:rgba(0,0,0,.2);display:block;padding:10px 20px}.gb_Eb{background-position:0\ + \ -1421px;display:inline-block;margin:1px 0;vertical-align:middle;height:25px;width:25px}.gb_N\ + \ .gb_Eb::before{left:0;top:-1421px}.gb_Fb{color:#427fed;display:inline-block;padding:0\ + \ 25px 0 10px;vertical-align:middle;white-space:normal}.gb_Db:hover .gb_Fb{text-decoration:underline}.gb_2e{color:#000;font:13px/27px\ + \ Arial,sans-serif;left:0;min-width:1152px;position:absolute;top:0;-moz-user-select:-moz-none;width:100%}.gb_ce{font:13px/27px\ + \ Arial,sans-serif;position:relative;height:60px;width:100%}.gb_3a .gb_ce{height:28px}#gba{height:60px}#gba.gb_3a{height:28px}#gba.gb_3e{height:90px}#gba.gb_3e.gb_3a{height:58px}.gb_ce>.gb_R{height:60px;line-height:58px;vertical-align:middle}.gb_3a\ + \ .gb_ce>.gb_R{height:28px;line-height:26px}.gb_ce::before{background:#e5e5e5;bottom:0;content:'';display:none;height:1px;left:0;position:absolute;right:0}.gb_ce{background:#f1f1f1}.gb_4e\ + \ .gb_ce{background:#fff}.gb_4e .gb_ce::before,.gb_3a .gb_ce::before{display:none}.gb_fa\ + \ .gb_ce,.gb_X .gb_ce,.gb_3a .gb_ce{background:transparent}.gb_fa .gb_ce::before{background:#e1e1e1;background:rgba(0,0,0,.12)}.gb_X\ + \ .gb_ce::before{background:#333;background:rgba(255,255,255,.2)}.gb_R{display:inline-block;flex:0\ + \ 0 auto;flex:0 0 main-size}.gb_R.gb_5e{float:right;order:1}.gb_6e{white-space:nowrap}.gb_T\ + \ .gb_6e{display:-webkit-flex;display:flex}.gb_6e,.gb_R{margin-left:0!important;margin-right:0!important}.gb_Pb{background-image:url('//ssl.gstatic.com/gb/images/v1_8758f7bb.png');background-size:92px\ + \ 2541px}@media (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_Pb{background-image:url('//ssl.gstatic.com/gb/images/v2_0bc49e87.png')}}.gb_bb:not(.gb_N)\ + \ .gb_1a::before,.gb_bb:not(.gb_N) .gb_lb::before{content:none}.gb_N .gb_Nb\ + \ .gb_Pb::before{left:0;top:-762px}.gb_N.gb_X .gb_Nb .gb_Pb::before{left:0;top:-2439px}.gb_N.gb_fa\ + \ .gb_Nb .gb_Pb::before{left:0;top:-1883px}.gb_N .gb_Qb{background-image:none!important}.gb_N\ + \ .gb_Rb{visibility:visible}.gb_wb .gb_Qc span{background:transparent}.gb_Kb{min-width:127px;overflow:hidden;position:relative;z-index:987}.gb_Lb{position:absolute;padding:0\ + \ 20px 0 15px}.gb_Mb .gb_Lb{right:100%;margin-right:-127px}.gb_Nb{display:inline-block;outline:none;vertical-align:middle}.gb_Ob\ + \ .gb_Nb{position:relative;top:2px}.gb_Nb .gb_Pb,.gb_Qb{display:block}.gb_Rb{border:none;display:block;visibility:hidden}.gb_Nb\ + \ .gb_Pb{background-position:0 -762px;height:33px;width:92px}.gb_Qb{background-repeat:no-repeat}.gb_X\ + \ .gb_Nb .gb_Pb{background-position:0 -2439px}.gb_fa .gb_Nb .gb_Pb{background-position:0\ + \ -1883px;opacity:.54}@-moz-keyframes gb__nb{0%{-moz-transform:scale(0,0);transform:scale(0,0)}20%{-moz-transform:scale(1.4,1.4);transform:scale(1.4,1.4)}50%{-moz-transform:scale(.8,.8);transform:scale(.8,.8)}85%{-moz-transform:scale(1.1,1.1);transform:scale(1.1,1.1)}to{-moz-transform:scale(1.0,1.0);transform:scale(1.0,1.0)}}@keyframes\ + \ gb__nb{0%{-moz-transform:scale(0,0);transform:scale(0,0)}20%{-moz-transform:scale(1.4,1.4);transform:scale(1.4,1.4)}50%{-moz-transform:scale(.8,.8);transform:scale(.8,.8)}85%{-moz-transform:scale(1.1,1.1);transform:scale(1.1,1.1)}to{-moz-transform:scale(1.0,1.0);transform:scale(1.0,1.0)}}.gb_ec{background-position:-35px\ + \ -658px;opacity:.55;height:100%;width:100%}.gb_b:hover .gb_ec,.gb_b:focus\ + \ .gb_ec{opacity:.85}.gb_fc .gb_ec{background-position:-35px -1848px}.gb_gc{background-color:#cb4437;-moz-border-radius:8px;border-radius:8px;font:bold\ + \ 11px Arial;color:#fff;line-height:16px;min-width:14px;padding:0 1px;position:absolute;right:0;text-align:center;text-shadow:0\ + \ 1px 0 rgba(0,0,0,0.1);top:0;visibility:hidden;z-index:990}.gb_hc .gb_gc,.gb_hc\ + \ .gb_ic,.gb_hc .gb_ic.gb_jc{visibility:visible}.gb_ic{padding:0 2px;visibility:hidden}.gb_kc:not(.gb_lc)\ + \ .gb_db,.gb_kc:not(.gb_lc) .gb_cb{left:3px}.gb_gc.gb_mc{-moz-animation:gb__nb\ + \ .6s 1s both ease-in-out;animation:gb__nb .6s 1s both ease-in-out;-moz-perspective-origin:top\ + \ right;perspective-origin:top right;-moz-transform:scale(1,1);transform:scale(1,1);-moz-transform-origin:top\ + \ right;transform-origin:top right}.gb_mc .gb_ic{visibility:visible}.gb_fa\ + \ .gb_b .gb_ec{background-position:0 -311px;opacity:.7}.gb_fa .gb_fc .gb_ec{background-position:0\ + \ -1658px}.gb_fa .gb_b:hover .gb_ec,.gb_fa .gb_b:focus .gb_ec{opacity:.85}.gb_X\ + \ .gb_b .gb_ec{background-position:-35px -623px;opacity:1}.gb_X .gb_fc .gb_ec{background-position:-35px\ + \ -276px}.gb_fa .gb_gc,.gb_X .gb_gc{border:none}.gb_kc .gb_nc{font-size:14px;font-weight:bold;top:0;right:0}.gb_kc\ + \ .gb_b{display:inline-block;vertical-align:middle;-moz-box-sizing:border-box;box-sizing:border-box;height:30px;width:30px}.gb_kc\ + \ .gb_cb{border-bottom-color:#e5e5e5}.gb_oc{background-color:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:bold;line-height:20px;margin:5px;padding:0\ + \ 2px;text-align:center;-moz-box-sizing:border-box;box-sizing:border-box;-moz-border-radius:50%;border-radius:50%;height:20px;width:20px}.gb_oc.gb_pc{background-position:-35px\ + \ -1675px}.gb_oc.gb_qc{background-position:-69px 0}.gb_b:hover .gb_oc,.gb_b:focus\ + \ .gb_oc{background-color:rgba(0,0,0,.85)}#gbsfw.gb_rc{background:#e5e5e5;border-color:#ccc}.gb_fa\ + \ .gb_oc{background-color:rgba(0,0,0,.7)}.gb_X .gb_oc.gb_oc,.gb_X .gb_hc .gb_oc.gb_oc,.gb_X\ + \ .gb_hc .gb_b:hover .gb_oc,.gb_X .gb_hc .gb_b:focus .gb_oc{background-color:#fff;color:#404040}.gb_X\ + \ .gb_oc.gb_pc{background-position:0 -1921px}.gb_X .gb_oc.gb_qc{background-position:-69px\ + \ -869px}.gb_hc .gb_oc.gb_oc{background-color:#db4437;color:#fff}.gb_hc .gb_b:hover\ + \ .gb_oc,.gb_hc .gb_b:focus .gb_oc{background-color:#a52714}.gb_hc .gb_oc.gb_qc{background-position:-69px\ + \ 0}.gb_N .gb_ec::before{left:-35px;top:-658px}.gb_N .gb_fc .gb_ec::before{left:-35px;top:-1848px}.gb_N.gb_fa\ + \ .gb_b .gb_ec::before{left:0;top:-311px}.gb_N.gb_fa .gb_fc .gb_ec::before{left:0;top:-1658px}.gb_N.gb_X\ + \ .gb_b .gb_ec::before{left:-35px;top:-623px}.gb_N.gb_X .gb_fc .gb_ec::before{left:-35px;top:-276px}.gb_wb\ + \ .gb_oc{border:1px solid #fff;color:#fff}.gb_wb.gb_fa .gb_oc{border-color:#000;color:#000}.gb_N\ + \ .gb_oc.gb_pc::before,.gb_wb.gb_N.gb_X .gb_oc.gb_pc::before{left:-35px;top:-1675px}.gb_N\ + \ .gb_oc.gb_qc::before,.gb_wb.gb_N.gb_X .gb_oc.gb_qc::before{left:-69px;top:0}.gb_N.gb_X\ + \ .gb_oc.gb_pc::before,.gb_wb.gb_N.gb_fa .gb_oc.gb_pc::before{left:0;top:-1921px}.gb_N.gb_X\ + \ .gb_oc.gb_qc::before,.gb_wb.gb_N.gb_fa .gb_oc.gb_qc::before{left:-69px;top:-869px}.gb_wc{color:#ffffff;font-size:13px;font-weight:bold;height:25px;line-height:19px;padding-top:5px;padding-left:12px;position:relative;background-color:#4d90fe}.gb_wc\ + \ .gb_xc{color:#ffffff;cursor:default;font-size:22px;font-weight:normal;position:absolute;right:12px;top:5px}.gb_wc\ + \ .gb_yc,.gb_wc .gb_zc{color:#ffffff;display:inline-block;font-size:11px;margin-left:16px;padding:0\ + \ 8px;white-space:nowrap}.gb_Ac{background:none;background-image:-moz-linear-gradient(top,rgba(0,0,0,0.16),rgba(0,0,0,0.2));background-image:linear-gradient(top,rgba(0,0,0,0.16),rgba(0,0,0,0.2));background-image:-moz-linear-gradient(top,rgba(0,0,0,0.16),rgba(0,0,0,0.2));-moz-border-radius:2px;border-radius:2px;border:1px\ + \ solid #dcdcdc;border:1px solid rgba(0,0,0,0.1);cursor:default!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#160000ff,endColorstr=#220000ff);text-decoration:none!important}.gb_Ac:hover{background:none;background-image:-moz-linear-gradient(top,rgba(0,0,0,0.14),rgba(0,0,0,0.2));background-image:linear-gradient(top,rgba(0,0,0,0.14),rgba(0,0,0,0.2));background-image:-moz-linear-gradient(top,rgba(0,0,0,0.14),rgba(0,0,0,0.2));border:1px\ + \ solid rgba(0,0,0,0.2);box-shadow:0 1px 1px rgba(0,0,0,0.1);-moz-box-shadow:0\ + \ 1px 1px rgba(0,0,0,0.1);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#14000000,endColorstr=#22000000)}.gb_Ac:active{box-shadow:inset\ + \ 0 1px 2px rgba(0,0,0,0.3);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.3)}.gb_4c.gb_Od{padding:0}.gb_Od\ + \ .gb_ga{padding:26px 26px 22px 13px;background:#ffffff}.gb_Pd.gb_Od .gb_ga{background:#4d90fe}a.gb_Qd{color:#666666!important;font-size:22px;height:9px;opacity:.8;position:absolute;right:14px;top:4px;text-decoration:none!important;width:9px}.gb_Pd\ + \ a.gb_Qd{color:#c1d1f4!important}a.gb_Qd:hover,a.gb_Qd:active{opacity:1}.gb_Rd{padding:0;width:258px;white-space:normal;display:table}.gb_Sd\ + \ .gb_ga{top:36px;border:0;padding:18px 24px 16px 20px;-moz-box-shadow:4px\ + \ 4px 12px rgba(0,0,0,0.4);box-shadow:4px 4px 12px rgba(0,0,0,0.4)}.gb_Sd\ + \ .gb_Rd{width:356px}.gb_Sd .gb_Ea,.gb_Sd .gb_Td,.gb_Sd .gb_Ud,.gb_Sd .gb_Ca,.gb_Vd{line-height:normal;font-family:Roboto,RobotoDraft,Helvetica,Arial,sans-serif}.gb_Sd\ + \ .gb_Ea,.gb_Sd .gb_Td,.gb_Sd .gb_Ca{font-weight:500}.gb_Sd .gb_Ea,.gb_Sd\ + \ .gb_Ca{border:0;padding:10px 8px}.gb_Od .gb_Ea:active{outline:none;-moz-box-shadow:0\ + \ 4px 5px rgba(0,0,0,.16);box-shadow:0 4px 5px rgba(0,0,0,.16)}.gb_Sd .gb_Td{margin-bottom:8px}.gb_Sd\ + \ .gb_Ud{color:#808080;font-size:14px}.gb_Wd{text-align:right;font-size:14px;padding-bottom:10px}.gb_Wd\ + \ .gb_yc{margin-left:16px}.gb_Vd{background-color:#404040;color:#fff;padding:16px\ + \ 24px 16px 20px;position:absolute;top:36px;width:356px;right:0;-moz-border-radius:2px;border-radius:2px;-moz-box-shadow:4px\ + \ 4px 12px rgba(0,0,0,0.4);box-shadow:4px 4px 12px rgba(0,0,0,0.4)}.gb_Vd\ + \ a,.gb_Vd a:visited{color:#5e97f6;text-decoration:none}.gb_Xd{position:absolute;right:20px;text-transform:uppercase}.gb_Pd\ + \ .gb_Rd{width:200px}.gb_Td{color:#333333;font-size:16px;line-height:20px;margin:0;margin-bottom:16px}.gb_Pd\ + \ .gb_Td{color:#ffffff}.gb_Ud{color:#666666;line-height:17px;margin:0;margin-bottom:5px}.gb_Pd\ + \ .gb_Ud{color:#ffffff}.gb_Zd{position:absolute;background:transparent;top:-999px;z-index:-1;visibility:hidden;margin-top:1px;margin-left:1px}#gb\ + \ .gb_Od{margin:0}.gb_Od .gb_pb{background:#4d90fe;border-color:#3079ed;margin-top:15px}#gb\ + \ .gb_Od a.gb_pb.gb_pb{color:#ffffff}.gb_Od .gb_pb:hover{background:#357ae8;border-color:#2f5bb7}.gb_0d\ + \ .gb_nc .gb_cb{border-bottom-color:#ffffff;display:block}.gb_1d .gb_nc .gb_cb{border-bottom-color:#4d90fe;display:block}.gb_0d\ + \ .gb_nc .gb_db,.gb_1d .gb_nc .gb_db{display:block}.gb_2d,.gb_3d{display:table-cell}.gb_2d{vertical-align:middle}.gb_Sd\ + \ .gb_2d{vertical-align:top}.gb_3d{padding-left:13px;width:100%}.gb_Sd .gb_3d{padding-left:20px}.gb_5d{margin-bottom:32px;font-size:small}.gb_5d\ + \ .gb_6d{margin-right:5px}.gb_5d .gb_7d{color:red}.gb_vc{display:none}.gb_vc.gb_g{display:block}.gb_8d{position:relative;width:650px;z-index:986}#gbq2{padding-top:15px}.gb_T\ + \ .gb_8d{min-width:200px;flex:0 2 auto;flex:0 2 main-size}.gb_V~.gb_8d{min-width:0}.gb_T\ + \ #gbqf{margin-right:0;display:-webkit-flex;display:flex}.gb_T .gbqff{min-width:0;flex:1\ + \ 1 auto;flex:1 1 main-size}.gb_N .gbqfi::before{left:0;top:-415px}.gb_wb\ + \ .gbqfb:focus .gbqfi{outline:1px dotted #fff}#gbq2{display:block}#gbqf{display:block;margin:0;margin-right:60px;white-space:nowrap}.gbqff{border:none;display:inline-block;margin:0;padding:0;vertical-align:top;width:100%}.gbqfqw,#gbqfb,.gbqfwa{vertical-align:top}#gbqfaa,#gbqfab,#gbqfqwb{position:absolute}#gbqfaa{left:0}#gbqfab{right:0}.gbqfqwb,.gbqfqwc{right:0;left:0;height:100%}.gbqfqwb{padding:0\ + \ 8px}#gbqfbw{display:inline-block;vertical-align:top}#gbqfb{border:1px solid\ + \ transparent;border-bottom-left-radius:0;border-top-left-radius:0;height:30px;margin:0;outline:none;padding:0\ + \ 0;width:60px;-moz-box-shadow:none;box-shadow:none;-moz-box-sizing:border-box;box-sizing:border-box;background:#4285f4;background:-moz-linear-gradient(top,#4387fd,#4683ea);background:linear-gradient(top,#4387fd,#4683ea);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4387fd,endColorstr=#4683ea,GradientType=1)}#gbqfb:hover{-moz-box-shadow:0\ + \ 1px 0 rgba(0,0,0,.15);box-shadow:0 1px 0 rgba(0,0,0,.15)}#gbqfb:focus{-moz-box-shadow:inset\ + \ 0 0 0 1px #fff;box-shadow:inset 0 0 0 1px #fff}#gbqfb:hover:focus{-moz-box-shadow:0\ + \ 1px 0 rgba(0,0,0,.15),inset 0 0 0 1px #fff;box-shadow:0 1px 0 rgba(0,0,0,.15),inset\ + \ 0 0 0 1px #fff}#gbqfb:active:active{border:1px solid transparent;-moz-box-shadow:inset\ + \ 0 2px 0 rgba(0,0,0,.15);box-shadow:inset 0 2px 0 rgba(0,0,0,.15);background:#3c78dc;background:-moz-linear-gradient(top,#3c7ae4,#3f76d3);background:linear-gradient(top,#3c7ae4,#3f76d3);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3c7ae4,endColorstr=#3f76d3,GradientType=1)}.gbqfi{background-position:0\ + \ -415px;display:inline-block;margin:-1px;height:30px;width:30px}.gbqfqw{background:#fff;background-clip:padding-box;border:1px\ + \ solid #cdcdcd;border-color:rgba(0,0,0,.15);border-right-width:0;height:30px;-moz-box-sizing:border-box;box-sizing:border-box}#gbfwc\ + \ .gbqfqw{border-right-width:1px}#gbqfqw{position:relative}.gbqfqw.gbqfqw:hover{border-color:#a9a9a9;border-color:rgba(0,0,0,.3)}.gbqfwa{display:inline-block;width:100%}.gbqfwb{width:40%}.gbqfwc{width:60%}.gbqfwb\ + \ .gbqfqw{margin-left:10px}.gbqfqw.gbqfqw:active,.gbqfqw.gbqfqwf.gbqfqwf{border-color:#4285f4}#gbqfq,#gbqfqb,#gbqfqc{background:transparent;border:none;height:20px;margin-top:4px;padding:0;vertical-align:top;width:100%}#gbqfq:focus,#gbqfqb:focus,#gbqfqc:focus{outline:none}.gbqfif,.gbqfsf{color:#222;font:16px\ + \ arial,sans-serif}#gbqfbwa{display:none;text-align:center;height:0}#gbqfbwa\ + \ .gbqfba{margin:16px 8px}#gbqfsa,#gbqfsb{font:bold 11px/27px Arial,sans-serif!important;vertical-align:top}.gb_fa\ + \ .gbqfqw.gbqfqw,.gb_X .gbqfqw.gbqfqw{border-color:rgba(255,255,255,1);-moz-box-shadow:0\ + \ 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 2px rgba(0,0,0,.2)}.gb_fa #gbqfb,.gb_X\ + \ #gbqfb{-moz-box-shadow:0 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 2px rgba(0,0,0,.2)}.gb_fa\ + \ #gbqfb:hover,.gb_X #gbqfb:hover{-moz-box-shadow:0 1px 0 rgba(0,0,0,.15),0\ + \ 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 0 rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.2)}.gb_fa\ + \ #gbqfb:active,.gb_X #gbqfb:active{-moz-box-shadow:inset 0 2px 0 rgba(0,0,0,.15),0\ + \ 1px 2px rgba(0,0,0,.2);box-shadow:inset 0 2px 0 rgba(0,0,0,.15),0 1px 2px\ + \ rgba(0,0,0,.2)}.gbqfb,.gbqfba,.gbqfbb{cursor:default!important;display:inline-block;font-weight:bold;height:29px;line-height:29px;min-width:54px;padding:0\ + \ 8px;text-align:center;text-decoration:none!important;-moz-border-radius:2px;border-radius:2px;-moz-user-select:-moz-none}.gbqfba:focus{border:1px\ + \ solid #4d90fe;outline:none;-moz-box-shadow:inset 0 0 0 1px #fff;box-shadow:inset\ + \ 0 0 0 1px #fff}.gbqfba:hover{border-color:#c6c6c6;color:#222!important;-moz-box-shadow:0\ + \ 1px 0 rgba(0,0,0,.15);box-shadow:0 1px 0 rgba(0,0,0,.15);background:#f8f8f8;background:-moz-linear-gradient(top,#f8f8f8,#f1f1f1);background:linear-gradient(top,#f8f8f8,#f1f1f1);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#f8f8f8,endColorstr=#f1f1f1,GradientType=1)}.gbqfba:hover:focus{-moz-box-shadow:0\ + \ 1px 0 rgba(0,0,0,.15),inset 0 0 0 1px #fff;box-shadow:0 1px 0 rgba(0,0,0,.15),inset\ + \ 0 0 0 1px #fff}.gbqfb::-moz-focus-inner{border:0}.gbqfba::-moz-focus-inner{border:0}.gbqfba{border:1px\ + \ solid #dcdcdc;border-color:rgba(0,0,0,0.1);color:#444!important;font-size:11px;background:#f5f5f5;background:-moz-linear-gradient(top,#f5f5f5,#f1f1f1);background:linear-gradient(top,#f5f5f5,#f1f1f1);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#f5f5f5,endColorstr=#f1f1f1,GradientType=1)}.gbqfba:active{-moz-box-shadow:inset\ + \ 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.gb_9d\ + \ .gb_b{background-position:0 -623px;opacity:.55;height:30px;width:30px}.gb_9d\ + \ .gb_b:hover,.gb_9d .gb_b:focus{opacity:.85}.gb_9d .gb_cb{border-bottom-color:#f5f5f5}#gbsfw.gb_ae{background:#f5f5f5;border-color:#ccc}.gb_X\ + \ .gb_9d .gb_b{background-position:0 -2404px;opacity:1}.gb_fa .gb_9d .gb_b{background-position:0\ + \ -1848px;opacity:.7}.gb_fa .gb_9d .gb_b:hover,.gb_fa .gb_9d .gb_b:focus{opacity:.85}.gb_N\ + \ .gb_9d .gb_b::before{left:0;top:-623px}.gb_N.gb_fa .gb_9d .gb_b::before{left:0;top:-1848px}.gb_N.gb_X\ + \ .gb_9d .gb_b::before{left:0;top:-2404px}.gb_ve{width:480px}.gb_we{background:#e7e7e7;background:rgba(0,0,0,.04);border-bottom-right-radius:0;line-height:30px;position:relative;text-align:center;width:100%}.gb_we:hover{background:#dbdbdb;background:rgba(0,0,0,.08)}.gb_we\ + \ .gb_xe{margin:0 10px}.gb_ye{position:relative;z-index:1}.gb_ze{background:#eee;border-bottom:1px\ + \ solid #e3e3e3;border-left:1px solid #e3e3e3;display:inline-block;line-height:32px;text-align:center;width:160px}.gb_ye\ + \ .gb_ze:first-child{border-left:none}.gb_ye .gb_g{background:#fff;border-bottom:none}.gb_Ae{display:none;text-align:center}.gb_Ae.gb_g{display:block}.gb_Be{color:inherit;display:inline-block;padding:15px;text-decoration:none}.gb_Ce{background-clip:content-box;background-origin:content-box;display:inherit;height:64px;width:64px}.gb_De{display:block;text-align:center}.gb_Ee{border-top:none;top:78px;z-index:1;-moz-border-radius:0\ + \ 0 2px 2px;border-radius:0 0 2px 2px}.gb_Fe{display:inline-block;vertical-align:middle}.gb_He{display:inline-block;vertical-align:middle;background-size:100%;height:20px;width:20px}.gb_Ie{background-image:url('//ssl.gstatic.com/gb/images/a/5a1c013d3d.png')}.gb_Je{background-image:url('//ssl.gstatic.com/gb/images/a/de580e5330.png')}.gb_Ke{background-image:url('//ssl.gstatic.com/gb/images/a/451603daf6.png')}.gb_Fe{margin-left:4px}.gb_Le{margin:5px;width:470px}.gb_Me{border:none;display:block;margin:0\ + \ 5px;outline:none;padding:0 5px;height:30px;width:450px}.gb_Ne{border:none;display:block;margin:0\ + \ 5px;outline:none;padding:0 5px;height:30px;width:450px;border-top:1px solid\ + \ #e3e3e3}.gb_Oe{border-color:#e3e3e3;display:block;font:inherit;margin:0\ + \ 5px;outline:none;padding:5px;text-align:left;height:320px;width:450px}.gb_Pe,.gb_Qe{border:1px\ + \ solid #e3e3e3;-moz-border-radius:2px;border-radius:2px;cursor:pointer;line-height:27px;margin:5px;padding:0\ + \ 8px;width:54px}.gb_Pe{float:left}.gb_Qe{float:right}.gb_bb{min-width:315px;padding-left:30px;padding-right:30px;position:relative;text-align:right;z-index:986;align-items:center;justify-content:flex-end;-moz-user-select:-moz-none}.gb_3a\ + \ .gb_bb{min-width:0}.gb_bb.gb_R{flex:1 1 auto;flex:1 1 main-size}.gb_cc{line-height:normal;position:relative;text-align:left}.gb_cc.gb_R,.gb_Re.gb_R,.gb_4a.gb_R{flex:0\ + \ 1 auto;flex:0 1 main-size}.gb_Se,.gb_Te{display:inline-block;padding:0 0\ + \ 0 15px;position:relative;vertical-align:middle}.gb_Re{line-height:normal;padding-right:15px}.gb_bb\ + \ .gb_Re.gb_U{padding-right:0}.gb_4a{color:#404040;line-height:30px;min-width:30px;overflow:hidden;vertical-align:middle;text-overflow:ellipsis}#gb.gb_3a.gb_3a\ + \ .gb_ne,#gb.gb_3a.gb_3a .gb_cc>.gb_Te .gb_oe{background:none;border:none;color:#36c;cursor:pointer;filter:none;font-size:11px;line-height:26px;padding:0;-moz-box-shadow:none;box-shadow:none}#gb.gb_3a.gb_X\ + \ .gb_ne,#gb.gb_3a.gb_X .gb_cc>.gb_Te .gb_oe{color:#fff}.gb_3a .gb_ne{text-transform:uppercase}.gb_bb.gb_V{padding-left:0;padding-right:29px}.gb_bb.gb_Ue{max-width:400px}.gb_Ve{background-clip:content-box;background-origin:content-box;opacity:.27;padding:22px;height:16px;width:16px}.gb_Ve.gb_R{display:none}.gb_Ve:hover,.gb_Ve:focus{opacity:.55}.gb_We{background-position:-70px\ + \ -623px}.gb_Xe{background-position:-70px -519px;padding-left:30px;padding-right:14px;position:absolute;right:0;top:0;z-index:990}.gb_7a:not(.gb_9a)\ + \ .gb_Xe,.gb_V .gb_We{display:inline-block}.gb_7a .gb_We{padding-left:30px;padding-right:0;width:0}.gb_7a:not(.gb_9a)\ + \ .gb_Ze{display:none}.gb_bb.gb_R.gb_V,.gb_V:not(.gb_9a) .gb_cc{flex:0 0 auto;flex:0\ + \ 0 main-size}.gb_Ve,.gb_V .gb_Re,.gb_9a .gb_cc{overflow:hidden}.gb_7a .gb_Re{padding-right:0}.gb_V\ + \ .gb_cc{padding:1px 1px 1px 0}.gb_7a .gb_cc{width:75px}.gb_bb.gb_0e,.gb_bb.gb_0e\ + \ .gb_We,.gb_bb.gb_0e .gb_We::before,.gb_bb.gb_0e .gb_Re,.gb_bb.gb_0e .gb_cc{-moz-transition:width\ + \ .5s ease-in-out,min-width .5s ease-in-out,max-width .5s ease-in-out,padding\ + \ .5s ease-in-out,left .5s ease-in-out;transition:width .5s ease-in-out,min-width\ + \ .5s ease-in-out,max-width .5s ease-in-out,padding .5s ease-in-out,left .5s\ + \ ease-in-out}.gb_T .gb_bb{min-width:0}.gb_bb.gb_W,.gb_bb.gb_W .gb_cc,.gb_bb.gb_1e,.gb_bb.gb_1e\ + \ .gb_cc{min-width:0!important}.gb_bb.gb_W,.gb_bb.gb_W .gb_R{-moz-box-flex:0\ + \ 0 auto!important;flex:0 0 auto!important}.gb_bb.gb_W .gb_4a{width:30px!important}.gb_N\ + \ .gb_We::before{clip:rect(623px 86px 639px 70px);left:-48px;top:-601px}.gb_N\ + \ .gb_Pb.gb_Xe{position:absolute}.gb_N .gb_Xe::before{clip:rect(519px 86px\ + \ 535px 70px);left:-40px;top:-497px}.gb_N .gb_7a .gb_We::before{left:-40px}@media\ + \ (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_N\ + \ .gb_We::before{clip:rect(1246px 172px 1278px 140px)}.gb_N .gb_Xe::before{clip:rect(1038px\ + \ 172px 1070px 140px)}}.gb_N .gb_Pb,.gb_N .gbii,.gb_N .gbip{background-image:none;overflow:hidden;position:relative}.gb_N\ + \ .gb_Pb::before{content:url('//ssl.gstatic.com/gb/images/v1_8758f7bb.png');position:absolute}@media\ + \ (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_N\ + \ .gb_Pb::before{content:url('//ssl.gstatic.com/gb/images/v2_0bc49e87.png');-moz-transform:scale(.5);transform:scale(.5);-moz-transform-origin:0\ + \ 0;transform-origin:0 0}}.gb_wb a:focus{outline:1px dotted #fff!important}#gb.gb_9e{min-width:980px}#gb.gb_9e\ + \ .gb_8d{min-width:0;position:static;width:0}.gb_9e .gb_ce{background:transparent;border-bottom-color:transparent}.gb_9e\ + \ .gb_ce::before{display:none}.gb_9e.gb_9e .gb_Q{display:inline-block}.gb_9e.gb_bb\ + \ .gb_Re.gb_U{padding-right:15px}.gb_T.gb_9e .gb_S.gb_R{display:-webkit-flex;display:flex}.gb_9e.gb_T\ + \ #gbqf{display:block}.gb_9e #gbq{height:0;position:absolute}.gb_9e.gb_bb{z-index:987}sentinel{}#gbq\ + \ .gbgt-hvr,#gbq .gbgt:focus{background-color:transparent;background-image:none}.gbqfh#gbq1{display:none}.gbxx{display:none\ + \ !important}#gbq{line-height:normal;position:relative;top:0px;white-space:nowrap}#gbq{left:0;width:100%}#gbq2{top:0px;z-index:986}#gbq4{display:inline-block;max-height:29px;overflow:hidden;position:relative}.gbqfh#gbq2{z-index:985}.gbqfh#gbq2{margin:0;margin-left:0\ + \ !important;padding-top:0;position:relative;top:310px}.gbqfh #gbqf{margin:auto;min-width:534px;padding:0\ + \ !important}.gbqfh #gbqfbw{display:none}.gbqfh #gbqfbwa{display:block}.gbqfh\ + \ #gbqf{max-width:572px;min-width:572px}.gbqfh .gbqfqw{border-right-width:1px}\\\ + n.gbii::before{content:url(//ssl.gstatic.com/gb/images/silhouette_27.png);position:absolute}.gbip::before{content:url(//ssl.gstatic.com/gb/images/silhouette_96.png);position:absolute}@media\ + \ (min-resolution:1.25dppx),(-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gbii::before{content:url(//ssl.gstatic.com/gb/images/silhouette_27.png)}.gbip::before{content:url(//ssl.gstatic.com/gb/images/silhouette_96.png)}}\\\ + n.gbii{background-image:url(//ssl.gstatic.com/gb/images/silhouette_27.png)}.gbip{background-image:url(//ssl.gstatic.com/gb/images/silhouette_96.png)}@media\ + \ (min-resolution:1.25dppx),(-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gbii{background-image:url(//ssl.gstatic.com/gb/images/silhouette_27.png)}.gbip{background-image:url(//ssl.gstatic.com/gb/images/silhouette_96.png)}}\\\ + n#gb192 .gb_3::before{left:0px;top:-1451px}
      Usuario lector de pantalla, clic aqu\xED para desact. Google Instant.\ + \
      Noticias
    1. Gmail
    2. Drive
    3. Calendario
    4. Google+
    5. Traductor
    6. Fotos
    7. M\xE1s
    8. Documentos
    9. Libros
    10. Blogger
    11. Contactos
    12. Hangouts
    13. M\xE1s de Google
      \xD7

      \xBFVienes aqu\xED seguido? Convierte\ + \ a Google en tu p\xE1gina de inicio.

      Claro
      Error
      Enviando
      Enviado axxx-xxx-xxxUpdate phone number \ + \

      \ + \
      Google Instant\ + \ no est\xE1 disponible. Ingresa para realizar una b\xFAsqueda.\_M\xE1\ + s informaci\xF3n
      Google Google\ + \ Instant est\xE1 desactivado debido a la velocidad de conexi\xF3n. Presiona\ + \ Aceptar para buscar.
      \\\"Presionar Enter para\ + \ buscar\\\"
      Color
    14. Cualquier color
    15. A todo color
    16. En blanco y negro
    17. Transparentes
    18. Tipo
      Fecha
      Derechos de uso
      M\xE1s herramientas
      {\\\"tw\\\":203, \\\"th\\\":249}
      {\\\"tw\\\":213, \\\"th\\\":237}
      {\\\"cl\\\":21,\\\"cr\\\":21,\\\"tw\\\":287,\ + \ \\\"th\\\":176}
      {\\\"ct\\\":21,\\\"cb\\\":21,\\\"cl\\\":21,\\\ + \"cr\\\":21,\\\"tw\\\":259, \\\"th\\\":194}

      \_
      \_

      Resultados de b\xFAsqueda

      \\\
      1200\_\xD7\_1200 - apple.com
      {\\\"cb\\\":21,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\":21,\\\"\ + id\\\":\\\"DV49ugmyPgIGVM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"jpg\\\",\\\"oh\\\":1200,\\\"ou\\\":\\\"http://images.apple.com/euro/home/n/generic/images/og.jpg?201602090601\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"Apple (Espa\xF1a)\\\",\\\"rid\\\":\\\"T6Md6OcWwrF27M\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/es/\\\",\\\"s\\\":\\\"\\\",\\\"sc\\\ + \":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTo9brcxalOzWUhweYTXf2PEC7Pb_0JL-agMnlYe0rOmVPXl4TXSg\\\"\ + ,\\\"tw\\\":225}
      \\\
      1200\_\xD7\_1200 - apple.com
      {\\\"cb\\\":21,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\":21,\\\"\ + id\\\":\\\"LL-KayQ-mXwalM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"jpg\\\",\\\"oh\\\":1200,\\\"ou\\\":\\\"http://www.apple.com/mx/ipod/home/images/social/og.jpg?201601060706\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"iPod - Apple (MX)\\\",\\\"rid\\\":\\\"BWRPDRg7qEj7GM\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/mx/ipod/\\\",\\\"s\\\":\\\"\\\",\\\"\ + sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcRZTmyZ3XfUHG6p_gpT3051lFd5Jvzk6BtssnCFKhBWGiBS5Aiz\\\",\\\ + \"tw\\\":225}
      \\\
      2000\_\xD7\_2000 - marcianophone.com
      {\\\"cl\\\":9,\\\"cr\\\":12,\\\"id\\\":\\\"DwmtNsGZ03Ox1M:\\\ + \",\\\"isu\\\":\\\"marcianophone.com\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\"\ + :2000,\\\"ou\\\":\\\"http://marcianophone.com/wp-content/uploads/2016/02/Apple_logo_black.svg_.png\\\ + \",\\\"ow\\\":2000,\\\"pt\\\":\\\"Apple lanza una versi\xF3n \\\\u201csecundaria\\\ + \\u201d de iOS 9.2.1 para solucionar ...\\\",\\\"rid\\\":\\\"xBLvzab3LBGgOM\\\ + \",\\\"ru\\\":\\\"https://www.marcianophone.com/archives/78703\\\",\\\"s\\\ + \":\\\"Con este error, Apple simplemente pretende mejorar la seguridad de\ + \ sus dispositivos para que nadie pueda acceder a tus huellas ni claves mediante\ + \ un cambio ...\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcRtgWAjhXTSujaMbdY00xXQWeXYI04RAaa_konBm4X5Hut7Q8zLww\\\"\ + ,\\\"tw\\\":225}
      \\\
      220\_\xD7\_220 - techcrunch.com
      {\\\"cb\\\":9,\\\"cl\\\":15,\\\"cr\\\":15,\\\"ct\\\ + \":9,\\\"id\\\":\\\"J1fpJxAeccKurM:\\\",\\\"isu\\\":\\\"techcrunch.com\\\"\ + ,\\\"ity\\\":\\\"png\\\",\\\"oh\\\":220,\\\"ou\\\":\\\"https://tctechcrunch2011.files.wordpress.com/2014/06/apple_topic.png?w\\\ + \\u003d220\\\",\\\"ow\\\":220,\\\"pt\\\":\\\"Apple | TechCrunch\\\",\\\"rid\\\ + \":\\\"BqOlv-WNmHzf7M\\\",\\\"ru\\\":\\\"http://techcrunch.com/topic/company/apple/\\\ + \",\\\"s\\\":\\\"Apple\\\",\\\"sc\\\":1,\\\"th\\\":176,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSzn62n_Wlcy36VwSK_e3AsBjsY40Ijut5tRNVzPmOmqZHgkPQMcQ\\\"\ + ,\\\"tw\\\":176}
      \\\
      512\_\xD7\_512 - sites.google.com
      {\\\"cb\\\":3,\\\"cl\\\":9,\\\"cr\\\":9,\\\"ct\\\"\ + :3,\\\"id\\\":\\\"UUm8pWjKFq2SiM:\\\",\\\"isu\\\":\\\"sites.google.com\\\"\ + ,\\\"ity\\\":\\\"png\\\",\\\"oh\\\":512,\\\"ou\\\":\\\"http://www.beevoz.com/wp-content/uploads/2015/03/Apple-Logo.png\\\ + \",\\\"ow\\\":512,\\\"pt\\\":\\\"Apple - AppleAndAndroidApps\\\",\\\"rid\\\ + \":\\\"YmCLsQZHUwq9nM\\\",\\\"ru\\\":\\\"https://sites.google.com/site/myappleandandroidapps/home/apple\\\ + \",\\\"s\\\":\\\"App Store es un servicio para el iPhone, el iPod Touche,\ + \ el iPad y Mac OS Ox Snow Leopar o posterior, creado por Apple Inc. Que permite\ + \ a los usuarios ...\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQRIvL1O51kqoqTK9ENxEGy8asczpQ73G_L1dPa3jzdZtHuqxkJOw\\\"\ + ,\\\"tw\\\":225}
      \\\
      436\_\xD7\_276 - support.apple.com
      {\\\"id\\\":\\\"JoGvoZ4pniOIKM:\\\",\\\"isu\\\":\\\ + \"support.apple.com\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":276,\\\"ou\\\"\ + :\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/osx/mac-apple-logo-screen-icon.png\\\ + \",\\\"ow\\\":436,\\\"pt\\\":\\\"Acerca de las pantallas que aparecen al arrancar\ + \ el Mac - Soporte ...\\\",\\\"rid\\\":\\\"mA_Jb9NO-al6hM\\\",\\\"ru\\\":\\\ + \"https://support.apple.com/es-es/HT204156\\\",\\\"s\\\":\\\"Logotipo de Apple\\\ + \",\\\"sc\\\":1,\\\"th\\\":179,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcRKlZWYkvm7q1qsyLSLSeChIzbt6DAe4XIUU4MLITt2AWl4AdmblQ\\\"\ + ,\\\"tw\\\":282}
      \\\
      1499\_\xD7\_739 - axpe-blogs.com
      {\\\"cb\\\":6,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\ + \":3,\\\"id\\\":\\\"LHInqsKlGi_SUM:\\\",\\\"isu\\\":\\\"axpe-blogs.com\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":739,\\\"ou\\\":\\\"http://www.axpe-blogs.com/wp-content/uploads/Apple_logo.jpg\\\ + \",\\\"ow\\\":1499,\\\"pt\\\":\\\"La \\\\u0026#39;iPhone-dependencia\\\\u0026#39;\ + \ de Apple - Blog oficial de Axpe Consulting\\\",\\\"rid\\\":\\\"3vOK_u0DBJi8lM\\\ + \",\\\"ru\\\":\\\"http://www.axpe-blogs.com/tecnologia/la-iphone-dependencia-de-apple/\\\ + \",\\\"s\\\":\\\"Apple ha presentado hoy sus resultados financieros correspondientes\ + \ al cuarto trimestre fiscal de 2015, que finaliz\xF3 el pasado 26 de septiembre.\\\ + \",\\\"th\\\":157,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQafnUIXr287x5jFMI4JsfwW2TcvPIRMPbpn8NVxGDEjM4hEBtwJw\\\"\ + ,\\\"tw\\\":320}
      \\\
      874\_\xD7\_1024 - impactony.com
      {\\\"id\\\":\\\"6GOKElj0paqeLM:\\\",\\\"isu\\\":\\\ + \"impactony.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1024,\\\"ou\\\":\\\"\ + http://www.impactony.com/wp-content/uploads/2014/08/Apple-logo1.jpg\\\",\\\ + \"ow\\\":874,\\\"pt\\\":\\\"Apple | Impacto Latin News \u2122\\\",\\\"rid\\\ + \":\\\"tI1BGO8hSFhT7M\\\",\\\"ru\\\":\\\"http://www.impactony.com/tag/apple/\\\ + \",\\\"s\\\":\\\"Apple-logo\\\",\\\"sc\\\":1,\\\"th\\\":243,\\\"tu\\\":\\\"\ + https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcSXwGE8t1A52kM4YUpUU-rmi5WQwz2JE6VqzYU5jkeb0_VC4fBj\\\ + \",\\\"tw\\\":207}
      \\\
      2290\_\xD7\_2290 - weknowyourdreamz.com
      {\\\"cb\\\":3,\\\"cl\\\":15,\\\"cr\\\":18,\\\"id\\\ + \":\\\"lglsoGJZIAC58M:\\\",\\\"isu\\\":\\\"weknowyourdreamz.com\\\",\\\"ity\\\ + \":\\\"jpg\\\",\\\"oh\\\":2290,\\\"ou\\\":\\\"http://weknowyourdreamz.com/images/apple/apple-06.jpg\\\ + \",\\\"ow\\\":2290,\\\"pt\\\":\\\"Interpretation of a dream in which you saw\ + \ \xABApple\xBB\\\",\\\"rid\\\":\\\"ArhdgBHqRcsKGM\\\",\\\"ru\\\":\\\"http://weknowyourdreamz.com/apple.html\\\ + \",\\\"s\\\":\\\"1 | 2 | 3 | 4 | 5. Dreams Interpretation - Apple\\\",\\\"\ + sc\\\":1,\\\"th\\\":224,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcR9dfnC_DQaO7aQ5lSe9MSnmYd7K67TYfOiWhVvUNrD--lUXdyB\\\",\\\ + \"tw\\\":224}
      \\\
      1920\_\xD7\_1080 - guillermoalegre.es
      {\\\"cb\\\":6,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\ + \":9,\\\"id\\\":\\\"zXLuYWTt7HqqhM:\\\",\\\"isu\\\":\\\"guillermoalegre.es\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1080,\\\"ou\\\":\\\"http://www.guillermoalegre.es/wp-content/uploads/2014/02/apple-wallpaper-logo-1.jpg\\\ + \",\\\"ow\\\":1920,\\\"pt\\\":\\\"Apple Inc. y su ventaja competitiva | Guillermo\ + \ Alegre\\\",\\\"rid\\\":\\\"oMnFa4U1GBb5gM\\\",\\\"ru\\\":\\\"http://www.guillermoalegre.es/apple-inc-y-su-ventaja-competitiva/\\\ + \",\\\"s\\\":\\\"Apple Inc. y su ventaja competitiva\\\",\\\"th\\\":168,\\\ + \"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQ0_twW6whESTN_D5VbMLZcY8B59iWJDfLW6zKXWzfJUuAX9IBlyg\\\ + \",\\\"tw\\\":300}
      \\\
      2272\_\xD7\_1704 - girabsas.com
      {\\\"cb\\\":9,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\ + \":9,\\\"id\\\":\\\"o62v3mff1wC9rM:\\\",\\\"isu\\\":\\\"girabsas.com\\\",\\\ + \"ity\\\":\\\"png\\\",\\\"oh\\\":1704,\\\"ou\\\":\\\"http://www.girabsas.com/files/image/14/14336/553082a17eb08.png\\\ + \",\\\"ow\\\":2272,\\\"pt\\\":\\\"Las m\xE1s dif\xEDciles e ins\xF3litas preguntas\ + \ de Apple para conseguir ...\\\",\\\"rid\\\":\\\"ms4LdUbvtmNgNM\\\",\\\"\ + ru\\\":\\\"http://www.girabsas.com/nota/7790/\\\",\\\"s\\\":\\\"Las m\xE1\ + s dif\xEDciles e ins\xF3litas preguntas de Apple para conseguir trabajo -\ + \ Gira BsAs\\\",\\\"sc\\\":1,\\\"th\\\":194,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSCp0aPsHO_gE5nbHITgeiTa01mSkuyCSmqF80yJRBzEDA5MupcPA\\\"\ + ,\\\"tw\\\":259}
      \\\
      500\_\xD7\_500 - macrumors.com
      {\\\"id\\\":\\\"xSvwXDgGp0WrNM:\\\",\\\"isu\\\":\\\ + \"macrumors.com\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":500,\\\"ou\\\":\\\"\ + http://cdn.macrumors.com/article-new/2015/02/Apple-Logo.png?retina\\\",\\\"\ + ow\\\":500,\\\"pt\\\":\\\"Apple to Open $25 Million Technology Development\ + \ Site in India ...\\\",\\\"rid\\\":\\\"w9CqPdSQ58lbtM\\\",\\\"ru\\\":\\\"\ + http://www.macrumors.com/2016/02/12/apple-india-hyderabad-development/\\\"\ + ,\\\"s\\\":\\\"Apple to Open $25 Million Technology Development Site in India\ + \ [Updated] - Mac Rumors\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTsHIFkFjyZQyBec-QnGUAHkXh886Pqy9esMTBuJ7ilvuYEIpDGAw\\\"\ + ,\\\"tw\\\":225}
      \\\
      3172\_\xD7\_2709 - weknowyourdreamz.com
      {\\\"cb\\\":12,\\\"cl\\\":21,\\\"cr\\\":18,\\\"ct\\\ + \":9,\\\"id\\\":\\\"Mxd6eMffENefZM:\\\",\\\"isu\\\":\\\"weknowyourdreamz.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":2709,\\\"ou\\\":\\\"http://weknowyourdreamz.com/images/apple/apple-05.jpg\\\ + \",\\\"ow\\\":3172,\\\"pt\\\":\\\"Interpretation of a dream in which you saw\ + \ \xABApple\xBB\\\",\\\"rid\\\":\\\"ArhdgBHqRcsKGM\\\",\\\"ru\\\":\\\"http://weknowyourdreamz.com/apple.html\\\ + \",\\\"s\\\":\\\"1 | 2 | 3 | 4 | 5. Dreams Interpretation - Apple\\\",\\\"\ + sc\\\":1,\\\"th\\\":207,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQGzX9EkXBBLwxQTF4jvQeLqGrNu-eJOwjrD20WDoMnv8UqJ9HG\\\",\\\ + \"tw\\\":243}
      \\\
      1200\_\xD7\_630 - apple.com
      {\\\"id\\\":\\\"uopsmMz6OOkjAM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":630,\\\"ou\\\":\\\"http://images.apple.com/euro/tv/d/generic/images/og.jpg??201602090348\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"Apple TV - Apple (ES)\\\",\\\"rid\\\":\\\ + \"qo4gFzd07jZS2M\\\",\\\"ru\\\":\\\"http://www.apple.com/es/tv/\\\",\\\"s\\\ + \":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":163,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQ1YtxJTInDE0djJEnPNCUGuzdEgfyvlzZLlHmWvf1VWW048GYU\\\",\\\ + \"tw\\\":310}
      \\\
      848\_\xD7\_480 - apple.com
      {\\\"cb\\\":6,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\":3,\\\"\ + id\\\":\\\"sISfrWsL97eylM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"jpg\\\",\\\"oh\\\":480,\\\"ou\\\":\\\"http://images.apple.com/es/apple-events/static/apple-events/september-2013/video/poster_large.jpg\\\ + \",\\\"ow\\\":848,\\\"pt\\\":\\\"Apple - Eventos de Apple - Evento especial,\ + \ septiembre de 2013\\\",\\\"rid\\\":\\\"4ByrtT7-eAIMVM\\\",\\\"ru\\\":\\\"\ + http://www.apple.com/es/apple-events/september-2013/\\\",\\\"s\\\":\\\"Evento\ + \ especial de Apple. 10 de septiembre de 2013.\\\",\\\"sc\\\":1,\\\"th\\\"\ + :169,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcTCTDh-wzJSeUM1XRVWVcJuMNBdbZvB_jsljpE5KlRjAZSZCdcb0A\\\ + \",\\\"tw\\\":299}
      \\\
      3968\_\xD7\_4496 - blog.loseit.com
      {\\\"cl\\\":6,\\\"cr\\\":9,\\\"ct\\\":3,\\\"id\\\"\ + :\\\"rTI6opaq4cHh-M:\\\",\\\"isu\\\":\\\"blog.loseit.com\\\",\\\"ity\\\":\\\ + \"jpg\\\",\\\"oh\\\":4496,\\\"ou\\\":\\\"https://loseitapp.files.wordpress.com/2014/09/istock_000014459318_double.jpg\\\ + \",\\\"ow\\\":3968,\\\"pt\\\":\\\"The Apple: A Perfect Fruit for Weight Loss?\ + \ |\\\",\\\"rid\\\":\\\"7-a-U6e0cIeHhM\\\",\\\"ru\\\":\\\"http://blog.loseit.com/the-apple-a-perfect-fruit-for-weight-loss/\\\ + \",\\\"s\\\":\\\"It\\\\u0026#39;s no secret that apples offer a nutrient goldmine.\ + \ Here are three ways that the nutrients in apples keep those doctors away\ + \ and help keep your body ...\\\",\\\"sc\\\":1,\\\"th\\\":239,\\\"tu\\\":\\\ + \"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQvqH1aYdncoxPWHtKwsHkmnTKs1wK_0VrwiCV8JdwME8nFNX1Kvg\\\ + \",\\\"tw\\\":211}
      \\\
      1200\_\xD7\_909 - cellz.com
      {\\\"cl\\\":15,\\\"cr\\\":21,\\\"ct\\\":6,\\\"id\\\":\\\"PFkVTzUtCvQXzM:\\\ + \",\\\"isu\\\":\\\"cellz.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":909,\\\ + \"ou\\\":\\\"http://www.cellz.com/blog/wp-content/uploads/2014/11/3D-Apple-full.jpg\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"Apple | Cellz Blog\\\",\\\"rid\\\":\\\"\ + z68zmbgCvLn5jM\\\",\\\"ru\\\":\\\"http://www.cellz.com/blog/category/apple/\\\ + \",\\\"s\\\":\\\"3D-Apple-full\\\",\\\"sc\\\":1,\\\"th\\\":195,\\\"tu\\\"\ + :\\\"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcTn-wiVhRC5Krp_lxWNqTxbnlu_KbAejdbUNJmOJ-JCaKGnC3p1xQ\\\ + \",\\\"tw\\\":258}
      \\\
      2218\_\xD7\_2216 - quotesgram.com
      {\\\"cb\\\":6,\\\"cl\\\":15,\\\"cr\\\":15,\\\"ct\\\ + \":6,\\\"id\\\":\\\"WpLAV4HbrSK3PM:\\\",\\\"isu\\\":\\\"quotesgram.com\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":2216,\\\"ou\\\":\\\"http://assisted-living.benchmarkseniorliving.com/wp-content/uploads/2012/11/iStock_000017473165Large.jpg\\\ + \",\\\"ow\\\":2218,\\\"pt\\\":\\\"Apple Quotes. QuotesGram\\\",\\\"rid\\\"\ + :\\\"UEXwaprkxpH9WM\\\",\\\"ru\\\":\\\"http://quotesgram.com/apple-quotes/\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":224,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQhKUDrTJVFrLRx7UWNtTKDP7qppvJon8ULIMU2cT61WyxBeiYozQ\\\"\ + ,\\\"tw\\\":225}
      \\\
      894\_\xD7\_893 - weknowyourdreamz.com
      {\\\"cb\\\":9,\\\"cl\\\":15,\\\"cr\\\":15,\\\"ct\\\ + \":3,\\\"id\\\":\\\"Ig4W18BX-UQMyM:\\\",\\\"isu\\\":\\\"weknowyourdreamz.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":893,\\\"ou\\\":\\\"http://weknowyourdreamz.com/images/apple/apple-01.jpg\\\ + \",\\\"ow\\\":894,\\\"pt\\\":\\\"Interpretation of a dream in which you saw\ + \ \xABApple\xBB\\\",\\\"rid\\\":\\\"ArhdgBHqRcsKGM\\\",\\\"ru\\\":\\\"http://weknowyourdreamz.com/apple.html\\\ + \",\\\"s\\\":\\\"Photo Gallery of Dream - Apple\\\",\\\"sc\\\":1,\\\"th\\\"\ + :224,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcRL2A1ILpxvXdO-6edv_L6WMkPXH8lVRBIaRbchg1Go_wXS6AAK\\\ + \",\\\"tw\\\":225}
      \\\
      400\_\xD7\_400 - apple.com
      {\\\"cb\\\":9,\\\"cl\\\":12,\\\"cr\\\":9,\\\"ct\\\":21,\\\"\ + id\\\":\\\"e4fbZpwSReF4EM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"png\\\",\\\"oh\\\":400,\\\"ou\\\":\\\"https://www.apple.com/support/assets/images/home/2015/apple_tv_icon_2x.png\\\ + \",\\\"ow\\\":400,\\\"pt\\\":\\\"Soporte t\xE9cnico de Apple oficial\\\",\\\ + \"rid\\\":\\\"P_uuKPQm6B6M6M\\\",\\\"ru\\\":\\\"https://www.apple.com/es/support/\\\ + \",\\\"s\\\":\\\"Soporte t\xE9cnico Apple TV\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\ + \"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcT3vjh4izEwVDMmuUbwOT7U-_jPRDwKxAgT1OhC4u7iSkfHORTr\\\ + \",\\\"tw\\\":225}
      \\\
      1920\_\xD7\_1080 - infoes.net
      {\\\"cl\\\":9,\\\"cr\\\":18,\\\"id\\\":\\\"cPRrDYIJiZARYM:\\\ + \",\\\"isu\\\":\\\"infoes.net\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1080,\\\ + \"ou\\\":\\\"http://infoes.net/wp-content/uploads/2016/02/logo-apple.jpg\\\ + \",\\\"ow\\\":1920,\\\"pt\\\":\\\"Apple | INFOES\\\",\\\"rid\\\":\\\"FJRXt-ru8QFZzM\\\ + \",\\\"ru\\\":\\\"http://infoes.net/apple/\\\",\\\"s\\\":\\\"Zoom en Leer\ + \ M\xE1s\\\",\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTPsJJwtcqvoi59J7C_w0_lg8tvk1i1UBrE12HaTzyQLKiCJaQAeA\\\"\ + ,\\\"tw\\\":300}
      \\\
      1600\_\xD7\_1613 - dreamatico.com
      {\\\"cb\\\":9,\\\"cl\\\":12,\\\"cr\\\":15,\\\"ct\\\ + \":18,\\\"id\\\":\\\"xA0yo66Zq3LyRM:\\\",\\\"isu\\\":\\\"dreamatico.com\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1613,\\\"ou\\\":\\\"http://dreamatico.com/data_images/apple/apple-6.jpg\\\ + \",\\\"ow\\\":1600,\\\"pt\\\":\\\"The meaning of the dream in which you saw\ + \ \xABApple\xBB\\\",\\\"rid\\\":\\\"rK_GSJwJSTW-2M\\\",\\\"ru\\\":\\\"http://dreamatico.com/apple.html\\\ + \",\\\"s\\\":\\\"Photo Gallery of \xABApple\xBB:\\\",\\\"sc\\\":1,\\\"th\\\ + \":225,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcRSDr5MwSpmZPzbpxQLX91KySEWe8ilY7rf7Zt08Fa_owoxacua\\\ + \",\\\"tw\\\":224}
      \\\
      416\_\xD7\_416 - forbes.com
      {\\\"cb\\\":6,\\\"cl\\\":9,\\\"cr\\\":15,\\\"id\\\":\\\"lZgE0JQs4FnkPM:\\\ + \",\\\"isu\\\":\\\"forbes.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":416,\\\ + \"ou\\\":\\\"http://i.forbesimg.com/media/lists/companies/apple_416x416.jpg\\\ + \",\\\"ow\\\":416,\\\"pt\\\":\\\"Apple on the Forbes Canada\\\\u0026#39;s\ + \ Best Employers List\\\",\\\"rid\\\":\\\"SiS47bI6wAuB-M\\\",\\\"ru\\\":\\\ + \"http://www.forbes.com/companies/apple/\\\",\\\"s\\\":\\\"\\\",\\\"th\\\"\ + :225,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcRWxiHrMmPGGzjxEZ-v8bY1heYy5AVK23HXlMv48wm8ZFxY-fZR\\\ + \",\\\"tw\\\":225}
      \\\
      350\_\xD7\_440 - apple.com
      {\\\"id\\\":\\\"q-6dAcRCQySwiM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":440,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/appletv/witb/appletv-witb-tv-201511?wid\\\ + \\u003d350\\\\u0026hei\\\\u003d440\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453547347338\\\",\\\"ow\\\":350,\\\"pt\\\":\\\"Comprar\ + \ el Apple TV - Apple (MX)\\\",\\\"rid\\\":\\\"6IDJ4bn9VJDNDM\\\",\\\"ru\\\ + \":\\\"http://www.apple.com/mx/shop/buy-tv/apple-tv\\\",\\\"s\\\":\\\"Apple\ + \ TV\\\",\\\"sc\\\":1,\\\"th\\\":252,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQv4iBNvS4LKE5N_uRo9SZiFAkUIf-_t_NGVHWJn0fnYwteRIom\\\",\\\ + \"tw\\\":200}
      \\\
      1920\_\xD7\_1080 - youtube.com
      {\\\"id\\\":\\\"BOpOjlg3u4mo2M:\\\",\\\"isu\\\":\\\ + \"youtube.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1080,\\\"ou\\\":\\\"\ + https://i.ytimg.com/vi/OON2bZdqVzs/maxresdefault.jpg\\\",\\\"ow\\\":1920,\\\ + \"pt\\\":\\\"Apple - YouTube\\\",\\\"rid\\\":\\\"pcOjmCN79JHQsM\\\",\\\"ru\\\ + \":\\\"https://www.youtube.com/user/Apple\\\",\\\"s\\\":\\\"\\\",\\\"sc\\\"\ + :1,\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSVYkDPIeL6E9jPphXHytVNob1yjF7CGCAQmh3JSqCGxIGE6rV6\\\",\\\ + \"tw\\\":300}
      \\\
      600\_\xD7\_315 - apple.com
      {\\\"cb\\\":9,\\\"cl\\\":9,\\\"cr\\\":9,\\\"id\\\":\\\"a-JcEP_-HgnXBM:\\\ + \",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":315,\\\ + \"ou\\\":\\\"http://www.apple.com/euro/ipad-pro/a/generic/images/overview_social.jpg?201602260611\\\ + \",\\\"ow\\\":600,\\\"pt\\\":\\\"iPad Pro - Apple (ES)\\\",\\\"rid\\\":\\\"\ + X4Sbq50L91-veM\\\",\\\"ru\\\":\\\"http://www.apple.com/es/ipad-pro/\\\",\\\ + \"s\\\":\\\"\\\",\\\"th\\\":163,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQCkiUl_bdJtgjCzsyl0LiAqUPkEbWrqyCErfk0JJZZSsnsymb0ig\\\"\ + ,\\\"tw\\\":310}
      \\\
      1600\_\xD7\_1000 - territoriomarketing.es
      {\\\"cl\\\":21,\\\"cr\\\":9,\\\"id\\\":\\\"YozCV0lX8L8YMM:\\\ + \",\\\"isu\\\":\\\"territoriomarketing.es\\\",\\\"ity\\\":\\\"jpg\\\",\\\"\ + oh\\\":1000,\\\"ou\\\":\\\"http://territoriomarketing.es/wp-content/uploads/2014/05/Apple.jpg\\\ + \",\\\"ow\\\":1600,\\\"pt\\\":\\\"Apple al desnudo - Territorio Marketing\\\ + \",\\\"rid\\\":\\\"sronnRmOQH_N1M\\\",\\\"ru\\\":\\\"http://territoriomarketing.es/apple/\\\ + \",\\\"s\\\":\\\"Apple al desnudo\\\",\\\"th\\\":177,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSera-XxQKL4EEhwXC2BCJNyo8xu7wcY9-xiqgtMxuG8OpujhWcyw\\\"\ + ,\\\"tw\\\":284}
      \\\
      765\_\xD7\_573 - dreamatico.com
      {\\\"cb\\\":6,\\\"cl\\\":18,\\\"cr\\\":18,\\\"ct\\\ + \":3,\\\"id\\\":\\\"QdKUhnZ187NRkM:\\\",\\\"isu\\\":\\\"dreamatico.com\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":573,\\\"ou\\\":\\\"http://dreamatico.com/data_images/apple/apple-1.jpg\\\ + \",\\\"ow\\\":765,\\\"pt\\\":\\\"The meaning of the dream in which you saw\ + \ \xABApple\xBB\\\",\\\"rid\\\":\\\"rK_GSJwJSTW-2M\\\",\\\"ru\\\":\\\"http://dreamatico.com/apple.html\\\ + \",\\\"s\\\":\\\"Photo Gallery of \xABApple\xBB:\\\",\\\"sc\\\":1,\\\"th\\\ + \":194,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcQT5HjB4zEtr4LqYwXKgYYpiXt6EfJKjWUcRfJEr25fTxZFaDpmsA\\\ + \",\\\"tw\\\":259}
      \\\
      2880\_\xD7\_2600 - en.wikipedia.org
      {\\\"cb\\\":3,\\\"cl\\\":6,\\\"cr\\\":6,\\\"ct\\\"\ + :6,\\\"id\\\":\\\"hpFDFOBGqc6NDM:\\\",\\\"isu\\\":\\\"en.wikipedia.org\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":2600,\\\"ou\\\":\\\"https://upload.wikimedia.org/wikipedia/commons/0/07/Honeycrisp-Apple.jpg\\\ + \",\\\"ow\\\":2880,\\\"pt\\\":\\\"Honeycrisp - Wikipedia, the free encyclopedia\\\ + \",\\\"rid\\\":\\\"PxTuVIuVeV8iiM\\\",\\\"ru\\\":\\\"https://en.wikipedia.org/wiki/Honeycrisp\\\ + \",\\\"s\\\":\\\"Honeycrisp-Apple.jpg\\\",\\\"sc\\\":1,\\\"th\\\":213,\\\"\ + tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcTm_FG6VDLzbLkW9u3nfqchN6A7-MHcKQleIpcbXSyo2p2u0qU\\\ + \",\\\"tw\\\":236}
      \\\
      300\_\xD7\_300 - marketingdirecto.com
      {\\\"cb\\\":6,\\\"cl\\\":6,\\\"cr\\\":9,\\\"ct\\\"\ + :3,\\\"id\\\":\\\"_geQub_qtPxkqM:\\\",\\\"isu\\\":\\\"marketingdirecto.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":300,\\\"ou\\\":\\\"http://www.marketingdirecto.com/wp-content/uploads/2012/12/apple4.jpg\\\ + \",\\\"ow\\\":300,\\\"pt\\\":\\\"Los momentos clave en la \\\\u0026quot;iRevoluci\xF3\ + n\\\\u0026quot; de Apple - Marketing Directo\\\",\\\"rid\\\":\\\"MqAfEDTVybvR3M\\\ + \",\\\"ru\\\":\\\"http://www.marketingdirecto.com/actualidad/checklists/los-momentos-clave-en-la-irevolucion-de-apple/\\\ + \",\\\"s\\\":\\\"Todo lo que toca se convierte oro pero, \xBFc\xF3mo ha logrado\ + \ Apple convertirse en el rey Midas del universo tecnol\xF3gico? En la llegada\ + \ de Apple al \\\\u0026quot;trono\\\\u0026quot; de la ...\\\",\\\"sc\\\":1,\\\ + \"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\\ + u003dtbn:ANd9GcSaHJawrul1VqmNc1ymMNuAZh5dwiZ1PrbexP9mBm1Qv06jElsYDg\\\",\\\ + \"tw\\\":225}
      \\\
      4481\_\xD7\_3753 - avereymcdowell.blogspot.com
      {\\\"cb\\\":3,\\\"cl\\\":15,\\\"cr\\\":12,\\\"ct\\\ + \":3,\\\"id\\\":\\\"wdQK2J4RZqsytM:\\\",\\\"isu\\\":\\\"avereymcdowell.blogspot.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":3753,\\\"ou\\\":\\\"http://www.public-domain-image.com/free-images/flora-plants/fruits/apple-pictures/red-apple-with-water-droplets.jpg\\\ + \",\\\"ow\\\":4481,\\\"pt\\\":\\\"Mythology and Folklore : Portfolio Week\ + \ 9 Storytelling: George and ...\\\",\\\"rid\\\":\\\"ydIm3jGN1TIKsM\\\",\\\ + \"ru\\\":\\\"http://avereymcdowell.blogspot.com/2015/10/week-9-storytelling-george-and-apple.html\\\ + \",\\\"s\\\":\\\"The Red Apple.\\\",\\\"th\\\":205,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQFYZlmEL8qnSy_2MUoQNx8adPRWfLY5fVYRBv0PMPMvUI5oHO7nw\\\"\ + ,\\\"tw\\\":245}
      \\\
      420\_\xD7\_288 - weknowyourdreamz.com
      {\\\"cb\\\":6,\\\"cl\\\":18,\\\"cr\\\":21,\\\"ct\\\ + \":3,\\\"id\\\":\\\"UapPQ47xiBcZDM:\\\",\\\"isu\\\":\\\"weknowyourdreamz.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":288,\\\"ou\\\":\\\"http://weknowyourdreamz.com/images/apple/apple-02.jpg\\\ + \",\\\"ow\\\":420,\\\"pt\\\":\\\"Interpretation of a dream in which you saw\ + \ \xABApple\xBB\\\",\\\"rid\\\":\\\"ArhdgBHqRcsKGM\\\",\\\"ru\\\":\\\"http://weknowyourdreamz.com/apple.html\\\ + \",\\\"s\\\":\\\"Photo Gallery of Dream - Apple\\\",\\\"sc\\\":1,\\\"th\\\"\ + :186,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcQmUvbQDo8IiMPEW-7MUcGINuFybUM2O0H-l9IYJk7YJR99FSZK7Q\\\ + \",\\\"tw\\\":271}
      \\\
      2000\_\xD7\_1536 - apple.com
      {\\\"cb\\\":18,\\\"cl\\\":15,\\\"cr\\\":15,\\\"ct\\\":18,\\\"\ + id\\\":\\\"NDoJoEaeDgF0BM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"\\\",\\\"oh\\\":1536,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/tv/apple-tv-gallery1-2015?wid\\\ + \\u003d2000\\\\u0026hei\\\\u003d1536\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\ + \\u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453545817238\\\",\\\"ow\\\":2000,\\\"pt\\\":\\\"Comprar\ + \ Apple TV (tercera generaci\xF3n) - Apple (MX)\\\",\\\"rid\\\":\\\"HKFtBFbXk_q1iM\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/mx/shop/buy-tv/apple-tv-3rd-gen\\\"\ + ,\\\"s\\\":\\\"Imagen 1 ...\\\",\\\"sc\\\":1,\\\"th\\\":197,\\\"tu\\\":\\\"\ + https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcSFoUxbL7oIR8Fce1tZuVGjAcixcehb6wItivIThUesi2q1yPMnDA\\\ + \",\\\"tw\\\":256}
      \\\
      300\_\xD7\_201 - macworld.com
      {\\\"id\\\":\\\"rbGHSGCFKlxhIM:\\\",\\\"isu\\\":\\\"macworld.com\\\ + \",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":201,\\\"ou\\\":\\\"http://core3.staticworld.net/images/article/2015/09/apple-logo-news-slide-background-100612702-medium.png\\\ + \",\\\"ow\\\":300,\\\"pt\\\":\\\"News and Insights on the Apple universe\\\ + \",\\\"rid\\\":\\\"E3FsAH7CnVud6M\\\",\\\"ru\\\":\\\"http://www.macworld.com/news\\\ + \",\\\"s\\\":\\\"apple\\\",\\\"sc\\\":1,\\\"th\\\":160,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQ5k4hnFufGdBBkSqHG9-gd4ekk9wFk2jAE915-Ml-LcMtXwjA5sA\\\"\ + ,\\\"tw\\\":240}
      \\\
      299\_\xD7\_877 - apple.com
      {\\\"id\\\":\\\"nwgdmimPIpOIjM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":877,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/s/ce/scene0/left/scene0-left-2x?wid\\\ + \\u003d299\\\\u0026hei\\\\u003d877\\\\u0026fmt\\\\u003dpng-alpha\\\\u0026qlt\\\ + \\u003d95\\\\u0026.v\\\\u003d1453543739065\\\",\\\"ow\\\":299,\\\"pt\\\":\\\ + \"Compra un iPhone 6 o un iPhone 6 Plus - Apple (ES)\\\",\\\"rid\\\":\\\"\ + ZhCjqxYEAMHSfM\\\",\\\"ru\\\":\\\"http://www.apple.com/es/shop/buy-iphone/iphone6\\\ + \",\\\"s\\\":\\\"Comprar un iPhone 6\\\",\\\"sc\\\":1,\\\"th\\\":239,\\\"\ + tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcS9OXTqa3l7h_mK_76NvcXvfJkL2BQOzRmilp6OBfmszmTME26H\\\ + \",\\\"tw\\\":81}
      \\\
      450\_\xD7\_565 - apple.com
      {\\\"cr\\\":3,\\\"ct\\\":3,\\\"id\\\":\\\"05uXkGEnkYe1qM:\\\"\ + ,\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"\\\",\\\"oh\\\":565,\\\"ou\\\ + \":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/i/ph/iphone6s/scene1/iphone6s-scene1?wid\\\ + \\u003d450\\\\u0026hei\\\\u003d565\\\\u0026fmt\\\\u003dpng-alpha\\\\u0026qlt\\\ + \\u003d95\\\\u0026.v\\\\u003d1453545831471\\\",\\\"ow\\\":450,\\\"pt\\\":\\\ + \"Compra un iPhone 6s o un iPhone 6s Plus - Apple (ES)\\\",\\\"rid\\\":\\\"\ + 4umIyG8WQehF3M\\\",\\\"ru\\\":\\\"http://www.apple.com/es/shop/buy-iphone/iphone6s\\\ + \",\\\"s\\\":\\\"Ver galer\xEDa\\\",\\\"sc\\\":1,\\\"th\\\":252,\\\"tu\\\"\ + :\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcQ7O91Hc0YNtfV-ir-uWuL5MQrqrp_NMvzIirq3nJl6QEw1_XH_aw\\\ + \",\\\"tw\\\":200}
      \\\
      1200\_\xD7\_630 - apple.com
      {\\\"id\\\":\\\"iO1Cvx7v_L4pAM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":630,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/tv/apple-tv-hero-select-201510?wid\\\ + \\u003d1200\\\\u0026hei\\\\u003d630\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\ + \\u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453547618625\\\",\\\"ow\\\":1200,\\\"pt\\\":\\\"Apple TV\ + \ (cuarta generaci\xF3n) 32 GB - Apple (MX)\\\",\\\"rid\\\":\\\"YTkVNlcbZshE3M\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/mx/shop/buy-tv/apple-tv/apple-tv-32-gb\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":163,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQR6R6JoEmy-WmZmiC51s5hAZa9dfGpI3vyE91TiNb2Ks6Pjc00Fg\\\"\ + ,\\\"tw\\\":310}
      \\\
      2560\_\xD7\_1600 - pandasecurity.com
      {\\\"id\\\":\\\"dSSeBcLQ-zLTwM:\\\",\\\"isu\\\":\\\ + \"pandasecurity.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1600,\\\"ou\\\"\ + :\\\"http://www.pandasecurity.com/spain/mediacenter/src/uploads/2015/07/apple.jpg\\\ + \",\\\"ow\\\":2560,\\\"pt\\\":\\\"Apple Phishing\\\",\\\"rid\\\":\\\"PNvLYGnhR2zYNM\\\ + \",\\\"ru\\\":\\\"http://www.pandasecurity.com/spain/mediacenter/noticias/phishing-apple/\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":177,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSdE3isiV02ix-7uq8kL0b26KRx9qQvRGPSNfDG1c1o99EOBDB1\\\",\\\ + \"tw\\\":284}
      \\\
      1200\_\xD7\_630 - huffingtonpost.com
      {\\\"cb\\\":3,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\ + \":3,\\\"id\\\":\\\"-q2gF09oo0a3lM:\\\",\\\"isu\\\":\\\"huffingtonpost.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":630,\\\"ou\\\":\\\"http://images.huffingtonpost.com/2015-06-10-1433944614-3985241-og.jpg\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"Apple Music and the Race to Coexist\\\"\ + ,\\\"rid\\\":\\\"Q6tf0w65S_pYOM\\\",\\\"ru\\\":\\\"http://www.huffingtonpost.com/seth-schachner/apple-music-and-the-race-to-coexist_b_7552334.html\\\ + \",\\\"s\\\":\\\"2015-06-10-1433944614-3985241-og.jpg\\\",\\\"sc\\\":1,\\\"\ + th\\\":163,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\\ + u003dtbn:ANd9GcR39JI3cNl5M8j1V8ZMHaNutmpTQUmxe9dGOXoK3VvZxCdEQ28E\\\",\\\"\ + tw\\\":310}
      \\\
      400\_\xD7\_400 - apple.com
      {\\\"cb\\\":6,\\\"cl\\\":6,\\\"cr\\\":6,\\\"ct\\\":15,\\\"id\\\ + \":\\\"Y-8NfJs6McPFaM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"\\\ + \",\\\"oh\\\":400,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/A/PP/APPLECARE/APPLECARE?wid\\\ + \\u003d400\\\\u0026hei\\\\u003d400\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453538181395\\\",\\\"ow\\\":400,\\\"pt\\\":\\\"Apple TV:\ + \ AppleCare Protection Plan - Apple (ES)\\\",\\\"rid\\\":\\\"eo_DL5NGv2aUgM\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/es/shop/product/MF219E/A/apple-tv-applecare-protection-plan\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcRVK3pTaPd-yogfrn_cX7xlOQLq-igU7VYzBg2-3qAoO1A6yBH7Ng\\\"\ + ,\\\"tw\\\":225}
      \\\
      600\_\xD7\_315 - apple.com
      {\\\"id\\\":\\\"QhuJO7vXO8FKNM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":315,\\\"ou\\\":\\\"http://www.apple.com/euro/apple-pencil/a/generic/images/pencil_social.jpg?201602260609\\\ + \",\\\"ow\\\":600,\\\"pt\\\":\\\"iPad Pro - Apple Pencil - Apple (ES)\\\"\ + ,\\\"rid\\\":\\\"38PgssJqz8kz0M\\\",\\\"ru\\\":\\\"http://www.apple.com/es/apple-pencil/\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":163,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcT9DAMsBlSPbZhyN-9hATkO9mYrWKKWvRt9GWb5r4kKAhwsZLVF\\\",\\\ + \"tw\\\":310}
      \\\
      620\_\xD7\_385 - cnnexpansion.com
      {\\\"cb\\\":15,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\ + \":3,\\\"id\\\":\\\"Ybd9yMPBZM-6cM:\\\",\\\"isu\\\":\\\"cnnexpansion.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":385,\\\"ou\\\":\\\"http://static.cnnexpansion.com/media/2014/09/15/apple-logotipo-manzana_5.jpg\\\ + \",\\\"ow\\\":620,\\\"pt\\\":\\\"Por qu\xE9 el logotipo de Apple es una manzana\ + \ mordida? - Especiales ...\\\",\\\"rid\\\":\\\"ln_PP90REXqUKM\\\",\\\"ru\\\ + \":\\\"http://www.cnnexpansion.com/especiales/2014/09/12/por-que-el-logotipo-de-apple-es-una-manzana-mordida\\\ + \",\\\"s\\\":\\\"cnnexpansion\\\",\\\"sc\\\":1,\\\"th\\\":177,\\\"tu\\\":\\\ + \"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcR9BY13NvnmadH1nqCjJU53YQ04p6SGE98RW1-ox28EqUk9XPXu\\\ + \",\\\"tw\\\":285}
      \\\
      700\_\xD7\_900 - support.apple.com
      {\\\"cb\\\":6,\\\"cl\\\":12,\\\"cr\\\":9,\\\"ct\\\"\ + :6,\\\"id\\\":\\\"BIRGK_rmVYbszM:\\\",\\\"isu\\\":\\\"support.apple.com\\\"\ + ,\\\"ity\\\":\\\"png\\\",\\\"oh\\\":900,\\\"ou\\\":\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/appletv/apple-tv-2-3-gen-remote-tech-spec.png\\\ + \",\\\"ow\\\":700,\\\"pt\\\":\\\"C\xF3mo utilizar el control remoto Apple\ + \ Remote con el Apple TV ...\\\",\\\"rid\\\":\\\"rcbUFmpz-qPoAM\\\",\\\"ru\\\ + \":\\\"https://support.apple.com/es-mx/HT200131\\\",\\\"s\\\":\\\"... viene\ + \ con el Apple TV (segunda y tercera generaci\xF3n) Si tienes un Apple TV\ + \ (cuarta generaci\xF3n), obt\xE9n informaci\xF3n sobre c\xF3mo utilizar el\ + \ control remoto ...\\\",\\\"sc\\\":1,\\\"th\\\":255,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcS_61vlXRAWi74wvhw7Pw8hTV9nbFVyJ1lr7y66PT-Ivf_A9ssj\\\",\\\ + \"tw\\\":198}
      \\\
      1200\_\xD7\_1200 - apple.com
      {\\\"cb\\\":18,\\\"cl\\\":6,\\\"cr\\\":6,\\\"ct\\\":15,\\\"\ + id\\\":\\\"4aDMyuuzNR3yxM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"jpg\\\",\\\"oh\\\":1200,\\\"ou\\\":\\\"http://images.apple.com/euro/macbook/a/generic/overview/images/macbook_overview_og.jpg?201601250709\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"MacBook - Apple (ES)\\\",\\\"rid\\\":\\\"\ + sFcb0SUPb2vg4M\\\",\\\"ru\\\":\\\"http://www.apple.com/es/macbook/\\\",\\\"\ + s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcT2xwpnMIBvHSeLu-kFkBZsILEGgI6F6nDXMQVWROhmCH3RUaC9eQ\\\"\ + ,\\\"tw\\\":225}
      \\\
      2000\_\xD7\_1536 - apple.com
      {\\\"cb\\\":21,\\\"cl\\\":15,\\\"cr\\\":15,\\\"ct\\\":21,\\\"\ + id\\\":\\\"hVYnBg1Z63tFcM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"\\\",\\\"oh\\\":1536,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/tv/apple-tv-gallery2-2015?wid\\\ + \\u003d2000\\\\u0026hei\\\\u003d1536\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\ + \\u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453545529871\\\",\\\"ow\\\":2000,\\\"pt\\\":\\\"Comprar\ + \ Apple TV (tercera generaci\xF3n) - Apple (MX)\\\",\\\"rid\\\":\\\"HKFtBFbXk_q1iM\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/mx/shop/buy-tv/apple-tv-3rd-gen\\\"\ + ,\\\"s\\\":\\\"Imagen 1 ...\\\",\\\"sc\\\":1,\\\"th\\\":197,\\\"tu\\\":\\\"\ + https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcSb4dMQ1Ujq3sBBxsyyCBpuUsU6YOnADGc0eTQ91kcBvJbgOhcS\\\ + \",\\\"tw\\\":256}
      \\\
      650\_\xD7\_450 - todoappleblog.com
      {\\\"id\\\":\\\"C5uRjSF9lNAuuM:\\\",\\\"isu\\\":\\\ + \"todoappleblog.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":450,\\\"ou\\\"\ + :\\\"http://www.todoappleblog.com/wp-content/uploads/2016/01/apple-interes-lifi.jpg\\\ + \",\\\"ow\\\":650,\\\"pt\\\":\\\"El c\xF3digo de iOS 9 muestra que Apple tiene\ + \ inter\xE9s en LiFi\\\",\\\"rid\\\":\\\"NKnc4ZoTnMbvnM\\\",\\\"ru\\\":\\\"\ + http://www.todoappleblog.com/ios-9-muestra-que-apple-tiene-interes-en-la-tecnologia-lifi/\\\ + \",\\\"s\\\":\\\"C\xF3digos localizados en iOS 9 demuestran que Apple est\xE1\ + \ trabajando con la tecnolog\xEDa LiFi\\\",\\\"sc\\\":1,\\\"th\\\":187,\\\"\ + tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcSfWHWXlhuaCzy4IamlKiZCbpIcp0PRl1UfvekEbJpilMTv4Epz\\\ + \",\\\"tw\\\":270}
      \\\
      300\_\xD7\_260 - apple.com
      {\\\"cb\\\":21,\\\"cl\\\":18,\\\"cr\\\":18,\\\"ct\\\":18,\\\"\ + id\\\":\\\"S_5-hZ-KcoHZcM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"png\\\",\\\"oh\\\":260,\\\"ou\\\":\\\"https://www.apple.com/support/assets/images/home/2015/promo_twitter_2x.png\\\ + \",\\\"ow\\\":300,\\\"pt\\\":\\\"Official Apple Support\\\",\\\"rid\\\":\\\ + \"OrunAFB7EkjeiM\\\",\\\"ru\\\":\\\"https://www.apple.com/support/\\\",\\\"\ + s\\\":\\\"Apple Support is on Twitter\\\",\\\"sc\\\":1,\\\"th\\\":208,\\\"\ + tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQdkx3HLjjq7DcWsjg4YIRu6Lq6EyB3bxHvS_noIlC97nnDr86TeA\\\ + \",\\\"tw\\\":240}
      \\\
      260\_\xD7\_220 - apple.com
      {\\\"id\\\":\\\"jXjaopmLP_nwqM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":220,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/appletv/2/appletv-2-compare-201509?wid\\\ + \\u003d260\\\\u0026hei\\\\u003d220\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453547643693\\\",\\\"ow\\\":260,\\\"pt\\\":\\\"Comprar\ + \ el Apple TV - Apple (MX)\\\",\\\"rid\\\":\\\"6IDJ4bn9VJDNDM\\\",\\\"ru\\\ + \":\\\"http://www.apple.com/mx/shop/buy-tv/apple-tv\\\",\\\"s\\\":\\\"Apple\ + \ TV (cuarta generaci\xF3n)\\\",\\\"sc\\\":1,\\\"th\\\":176,\\\"tu\\\":\\\"\ + https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQM-iP7Rnn60cvtWVk-nMg-EfW6TZmCWzGVhbdmxGwtPXn_uhI2\\\ + \",\\\"tw\\\":208}
      \\\
      1180\_\xD7\_1690 - support.apple.com
      {\\\"cl\\\":9,\\\"id\\\":\\\"P6YVXEKaU0c6JM:\\\",\\\ + \"isu\\\":\\\"support.apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1690,\\\ + \"ou\\\":\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/iOS/apple-pay-wrap-hero.jpg\\\ + \",\\\"ow\\\":1180,\\\"pt\\\":\\\"Set up and use Apple Pay with your Apple\ + \ Watch - Apple Support\\\",\\\"rid\\\":\\\"CAP1OgaYY1yZ_M\\\",\\\"ru\\\"\ + :\\\"https://support.apple.com/en-us/HT204506\\\",\\\"s\\\":\\\"\\\",\\\"\ + sc\\\":1,\\\"th\\\":269,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQSTkeO-XHf_E2fZbrvpFVsItRju2NIxhVnASyesGFQHpOChggu\\\",\\\ + \"tw\\\":188}
      \\\
      646\_\xD7\_484 - euroresidentes.com
      {\\\"cb\\\":6,\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\ + \":3,\\\"id\\\":\\\"XZEDzuAAIuudtM:\\\",\\\"isu\\\":\\\"euroresidentes.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":484,\\\"ou\\\":\\\"http://2.bp.blogspot.com/-ex1W3uglrGY/UfoyG7DfHlI/AAAAAAAAJ8Y/YBcUNMwLdTc/s1600/logo-apple.jpg\\\ + \",\\\"ow\\\":646,\\\"pt\\\":\\\"Los beneficios de Apple superan expectativas\ + \ - \xC9xito Empresarial\\\",\\\"rid\\\":\\\"51qpPTIw92uKPM\\\",\\\"ru\\\"\ + :\\\"https://www.euroresidentes.com/empresa/exito-empresarial/los-beneficios-de-apple-superan\\\ + \",\\\"s\\\":\\\"Las acciones del fabricante de ordenadores y tel\xE9fonos\ + \ inteligentes Apple subieron casi un 5% en las operaciones electr\xF3nicas\ + \ fuera de hora tras informar de ...\\\",\\\"th\\\":194,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcR0iWriqxss3OEadgLJX80_xoKMpNa5hpHYMArK3x4KU5NrXLJp\\\",\\\ + \"tw\\\":259}
      \\\
      332\_\xD7\_392 - apple.com
      {\\\"cb\\\":9,\\\"cl\\\":9,\\\"cr\\\":12,\\\"ct\\\":18,\\\"\ + id\\\":\\\"NQ3MTkKXRFL-hM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"\\\",\\\"oh\\\":392,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/s/38/s38si/sbwh/s38si-sbwh-sel-201509?wid\\\ + \\u003d332\\\\u0026hei\\\\u003d392\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453545508020\\\",\\\"ow\\\":332,\\\"pt\\\":\\\"Apple Watch\ + \ Sport - Comprar un Apple Watch Sport - Apple (ES)\\\",\\\"rid\\\":\\\"UpP7DXQ0G0XH7M\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/es/shop/buy-watch/apple-watch-sport\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":244,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQd6u4vbLg6gwMvYorTvIfmH5u_pXYoHMyXisOuX9El4jhMEXCx\\\",\\\ + \"tw\\\":207}
      \\\
      4608\_\xD7\_3072 - pexels.com
      {\\\"cb\\\":6,\\\"cl\\\":21,\\\"cr\\\":12,\\\"ct\\\":9,\\\"\ + id\\\":\\\"Q-N-wDymnkiezM:\\\",\\\"isu\\\":\\\"pexels.com\\\",\\\"ity\\\"\ + :\\\"jpg\\\",\\\"oh\\\":3072,\\\"ou\\\":\\\"https://static.pexels.com/photos/8208/pexels-photo.jpg\\\ + \",\\\"ow\\\":4608,\\\"pt\\\":\\\"Free stock photo of healthy, apple, fruit\\\ + \",\\\"rid\\\":\\\"MSxvSQ_ypyb3sM\\\",\\\"ru\\\":\\\"https://www.pexels.com/photo/apple-fruit-healthy-8208/\\\ + \",\\\"s\\\":\\\"Free Download\\\",\\\"th\\\":183,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQZDwktuxT1KTxpa9NHiDD_EWKfzvn04wGT6QvKb8CnDgBZtIXe\\\",\\\ + \"tw\\\":275}
      \\\
      2742\_\xD7\_3504 - pngimg.com
      {\\\"id\\\":\\\"Yw10pHwD_jWUnM:\\\",\\\"isu\\\":\\\"pngimg.com\\\ + \",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":3504,\\\"ou\\\":\\\"http://pngimg.com/upload/apple_PNG38.png\\\ + \",\\\"ow\\\":2742,\\\"pt\\\":\\\"Apples png images, pictures, cliparts\\\"\ + ,\\\"rid\\\":\\\"fLV1t_6ynQtruM\\\",\\\"ru\\\":\\\"http://pngimg.com/img/fruits/apple\\\ + \",\\\"s\\\":\\\"Red apple PNG image\\\",\\\"sc\\\":1,\\\"th\\\":254,\\\"\ + tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcQVaOhC7yCTMLMwee3oguGqTZITA1-XPhvgptnL2Q2wxqs2HK6u7A\\\ + \",\\\"tw\\\":199}
      \\\
      160\_\xD7\_160 - apple.com
      {\\\"cb\\\":9,\\\"cl\\\":3,\\\"cr\\\":9,\\\"ct\\\":9,\\\"id\\\ + \":\\\"DVwi6uVneLCzYM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"\ + png\\\",\\\"oh\\\":160,\\\"ou\\\":\\\"https://www.apple.com/support/assets/images/home/2015/airport_2x.png\\\ + \",\\\"ow\\\":160,\\\"pt\\\":\\\"Soporte t\xE9cnico de Apple oficial\\\",\\\ + \"rid\\\":\\\"P_uuKPQm6B6M6M\\\",\\\"ru\\\":\\\"https://www.apple.com/es/support/\\\ + \",\\\"s\\\":\\\"Soporte t\xE9cnico AirPort + Wi-Fi\\\",\\\"th\\\":128,\\\"\ + tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQ_M9BNLYF15ChBu1Ye8OrX_WyWp3WcCJ-WbbGR9Os6ff_B4dN5\\\ + \",\\\"tw\\\":128}
      \\\
      245\_\xD7\_220 - apple.com
      {\\\"id\\\":\\\"exeog1gTt_I9dM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":220,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/appletv/compare/appletv-compare-201509?wid\\\ + \\u003d245\\\\u0026hei\\\\u003d220\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453547645529\\\",\\\"ow\\\":245,\\\"pt\\\":\\\"Buy Apple\ + \ TV (3rd gen) - Apple (ES)\\\",\\\"rid\\\":\\\"Bo-jGBURQ2fy4M\\\",\\\"ru\\\ + \":\\\"http://www.apple.com/es/shop/buy-tv/apple-tv-3rd-gen\\\",\\\"s\\\"\ + :\\\"Apple TV (3.\xAA generaci\xF3n)\\\",\\\"sc\\\":1,\\\"th\\\":176,\\\"\ + tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcQx6tua0dd007PlJ9VceXDTbYSPQz5QzId7xXotIHO50HYzgxOt\\\ + \",\\\"tw\\\":196}
      \\\
      1024\_\xD7\_683 - applesfera.com
      {\\\"cb\\\":12,\\\"cl\\\":21,\\\"cr\\\":21,\\\"id\\\ + \":\\\"edyg9hU0_4MynM:\\\",\\\"isu\\\":\\\"applesfera.com\\\",\\\"ity\\\"\ + :\\\"jpg\\\",\\\"oh\\\":683,\\\"ou\\\":\\\"http://i.blogs.es/4832ba/apple-music-periodo-prueba-trial-0/original.jpg\\\ + \",\\\"ow\\\":1024,\\\"pt\\\":\\\"Apple lanza la primera beta de Apple Music\ + \ para Android en la Play ...\\\",\\\"rid\\\":\\\"mQzjN7Nnh_1PoM\\\",\\\"\ + ru\\\":\\\"http://www.applesfera.com/itunes/apple-lanza-la-primera-beta-de-apple-music-para-android-en-la-play-store\\\ + \",\\\"s\\\":\\\"Apple lanza la primera beta de Apple Music para Android en\ + \ la Play Store\\\",\\\"th\\\":183,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcThEjCcdpabpb_2RtIuPe1OSaboPQ6j7l-wu8QWVq7V-XiLuNr3\\\",\\\ + \"tw\\\":275}
      \\\
      700\_\xD7\_950 - support.apple.com
      {\\\"cl\\\":6,\\\"cr\\\":6,\\\"id\\\":\\\"l0s4DqszX_5aiM:\\\ + \",\\\"isu\\\":\\\"support.apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\"\ + :950,\\\"ou\\\":\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/appletv/apple-tv-first-second-gen-remotes.jpg\\\ + \",\\\"ow\\\":700,\\\"pt\\\":\\\"Enlazar el control remoto Apple Remote al\ + \ Apple TV (tercera ...\\\",\\\"rid\\\":\\\"NHtOGI5ogTx0NM\\\",\\\"ru\\\"\ + :\\\"https://support.apple.com/es-la/HT201254\\\",\\\"s\\\":\\\"Antes de empezar\\\ + \",\\\"sc\\\":1,\\\"th\\\":262,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQ4YQmLUzYhJkbNKPxhhyQVwMzgv4NpkxfRaw9ahI1iSSVdjXLjtw\\\"\ + ,\\\"tw\\\":193}
      \\\
      620\_\xD7\_349 - cbc.ca
      {\\\"cl\\\":9,\\\"cr\\\":21,\\\"id\\\":\\\"CQPMd-J22UL_QM:\\\ + \",\\\"isu\\\":\\\"cbc.ca\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":349,\\\"\ + ou\\\":\\\"http://i.cbc.ca/1.2749556.1409245330!/httpImage/image.jpg_gen/derivatives/16x9_620/apple-logo-for-apple-event-story.jpg\\\ + \",\\\"ow\\\":620,\\\"pt\\\":\\\"Apple event: New iPhones, Apple TV, smarter\ + \ Siri expected on Sept ...\\\",\\\"rid\\\":\\\"dkYirhx-FttfTM\\\",\\\"ru\\\ + \":\\\"http://www.cbc.ca/news/technology/apple-event-new-iphones-apple-tv-smarter-siri-expected-on-sept-9-1.3218867\\\ + \",\\\"s\\\":\\\"Apple has scheduled an event for Sept. 9 at 10 a.m. PT (1\ + \ p.m.\\\",\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQdaNEVu_-wgHvACvYfYcIKIY3YdrYebJwgm-QSPz-bRpvGM7LhVg\\\"\ + ,\\\"tw\\\":299}
      \\\
      534\_\xD7\_401 - usatoday.com
      {\\\"cb\\\":21,\\\"cl\\\":6,\\\"cr\\\":21,\\\"ct\\\":6,\\\"\ + id\\\":\\\"J3sFHNe6hhxywM:\\\",\\\"isu\\\":\\\"usatoday.com\\\",\\\"ity\\\"\ + :\\\"jpg\\\",\\\"oh\\\":401,\\\"ou\\\":\\\"http://www.gannett-cdn.com/-mm-/9dbe32c1c0832c803dc30f788ea67265050d3876/c\\\ + \\u003d67-0-957-669\\\\u0026r\\\\u003dx404\\\\u0026c\\\\u003d534x401/local/-/media/2015/09/10/USATODAY/USATODAY/635774864480351068-AP-APPLE-75734234.JPG\\\ + \",\\\"ow\\\":534,\\\"pt\\\":\\\"Apple \\\\u0026#39;isn\\\\u0026#39;t reinventing\ + \ the world\\\\u0026#39; anymore\\\",\\\"rid\\\":\\\"gpyIHtxnbeK6BM\\\",\\\ + \"ru\\\":\\\"http://www.usatoday.com/story/money/markets/2015/09/10/apple-reinventing-world-stock-aapl/72005518/\\\ + \",\\\"s\\\":\\\"\\\",\\\"th\\\":194,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSR_2b-pQj5z6bVJkhanNeFTieS8EAbos6-9hVV_XWyV0NGG2E_\\\",\\\ + \"tw\\\":259}
      \\\
      520\_\xD7\_720 - pixabay.com
      {\\\"id\\\":\\\"6UNsD5b_FDEGgM:\\\",\\\"isu\\\":\\\"pixabay.com\\\ + \",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":720,\\\"ou\\\":\\\"https://pixabay.com/static/uploads/photo/2013/07/13/11/34/apple-158419_960_720.png\\\ + \",\\\"ow\\\":520,\\\"pt\\\":\\\"Vector gratis: Apple, Rojo, Frutas, Vitaminas\ + \ - Imagen gratis en ...\\\",\\\"rid\\\":\\\"eI0-muKinnc7wM\\\",\\\"ru\\\"\ + :\\\"https://pixabay.com/es/apple-rojo-frutas-vitaminas-158419/\\\",\\\"s\\\ + \":\\\"Apple, Rojo, Frutas, Vitaminas, Saludable\\\",\\\"sc\\\":1,\\\"th\\\ + \":264,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcRhGQqz3scUAg6kj_OFyXOixcHX830ys1WfjsQPnud6ocYmDnJi\\\ + \",\\\"tw\\\":191}
      \\\
      569\_\xD7\_405 - apple.com
      {\\\"cb\\\":3,\\\"id\\\":\\\"848NC_1EruqsKM:\\\",\\\"isu\\\"\ + :\\\"apple.com\\\",\\\"ity\\\":\\\"\\\",\\\"oh\\\":405,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/m/ac/macbook/box/macbook-box-hw-silver-201504?wid\\\ + \\u003d569\\\\u0026hei\\\\u003d405\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453495470706\\\",\\\"ow\\\":569,\\\"pt\\\":\\\"Compra un\ + \ MacBook - Apple (ES)\\\",\\\"rid\\\":\\\"k5kBXwtH5mh-6M\\\",\\\"ru\\\":\\\ + \"http://www.apple.com/es/shop/buy-mac/macbook\\\",\\\"s\\\":\\\"MacBook\\\ + \",\\\"sc\\\":1,\\\"th\\\":189,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSLmR0jcsSLug11ZE2fiIjZijHyed8ZjLwhhHwjc4s68pqiTRRheg\\\"\ + ,\\\"tw\\\":266}
      \\\
      960\_\xD7\_637 - pixabay.com
      {\\\"cb\\\":3,\\\"cr\\\":6,\\\"ct\\\":9,\\\"id\\\":\\\"3tUCeuPcYNVepM:\\\ + \",\\\"isu\\\":\\\"pixabay.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":637,\\\ + \"ou\\\":\\\"https://pixabay.com/static/uploads/photo/2015/04/05/20/41/apple-708521_960_720.jpg\\\ + \",\\\"ow\\\":960,\\\"pt\\\":\\\"Free photo: Apple, Fruit, Green Apple - Free\ + \ Image on Pixabay - 708521\\\",\\\"rid\\\":\\\"NsglyVzkG1dVsM\\\",\\\"ru\\\ + \":\\\"https://pixabay.com/en/apple-fruit-green-apple-red-apple-708521/\\\"\ + ,\\\"s\\\":\\\"Apple, Fruit, Green Apple, Red Apple\\\",\\\"th\\\":183,\\\"\ + tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcSl__saX29YTF-ccJdr1K2cRP7StAAgyzkyDDM0S3EM4TQr509U\\\ + \",\\\"tw\\\":276}
      \\\
      510\_\xD7\_490 - efresh.com
      {\\\"cl\\\":6,\\\"cr\\\":6,\\\"ct\\\":3,\\\"id\\\":\\\"xZmgFzJbFbRDYM:\\\ + \",\\\"isu\\\":\\\"efresh.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":490,\\\ + \"ou\\\":\\\"http://efresh.com/sites/default/files/Green-Apple_1.jpg\\\",\\\ + \"ow\\\":510,\\\"pt\\\":\\\"Green Apple | eFresh.com\\\",\\\"rid\\\":\\\"\ + A15NPK6ZukZbvM\\\",\\\"ru\\\":\\\"http://efresh.com/product/green-apple-658\\\ + \",\\\"s\\\":\\\"Green Apple\\\",\\\"sc\\\":1,\\\"th\\\":220,\\\"tu\\\":\\\ + \"https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcQrhooCWwIjN35rArY6dypUPpTXWUhz4TvMUtbaXITQQVV719QZ\\\ + \",\\\"tw\\\":229}
      \\\
      1200\_\xD7\_694 - macrumors.com
      {\\\"cl\\\":3,\\\"cr\\\":3,\\\"id\\\":\\\"38E_Rv4J54b4CM:\\\ + \",\\\"isu\\\":\\\"macrumors.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":694,\\\ + \"ou\\\":\\\"http://cdn.macrumors.com/article-new/2014/11/apple_product_red_gift_card.jpg?retina\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"Apple Details Special (RED) Shopping Day\ + \ Gift Card Amounts for ...\\\",\\\"rid\\\":\\\"kDxR8ZZrjL_aGM\\\",\\\"ru\\\ + \":\\\"http://www.macrumors.com/2014/11/24/apple-back-friday-red/\\\",\\\"\ + s\\\":\\\"Apple Details Special (RED) Shopping Day Gift Card Amounts for Black\ + \ Friday - Mac Rumors\\\",\\\"th\\\":171,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSJL7KyhBTeuOqDKYmGkKwAOBllaFpdOUG4wxlct-koiysUChyQ\\\",\\\ + \"tw\\\":295}
      \\\
      420\_\xD7\_900 - support.apple.com
      {\\\"id\\\":\\\"FFXD8byWfM0W_M:\\\",\\\"isu\\\":\\\ + \"support.apple.com\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":900,\\\"ou\\\"\ + :\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/appletv/apple-remote-aluminum-battery-diagram-2.png\\\ + \",\\\"ow\\\":420,\\\"pt\\\":\\\"C\xF3mo reemplazar la bater\xEDa en el control\ + \ remoto Apple Remote ...\\\",\\\"rid\\\":\\\"yycL5E6nf07wKM\\\",\\\"ru\\\"\ + :\\\"https://support.apple.com/es-mx/HT201531\\\",\\\"s\\\":\\\"Extracci\xF3\ + n de la bater\xEDa de tu Apple Remote (aluminio)\\\",\\\"sc\\\":1,\\\"th\\\ + \":204,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcRfzvvhUT1zsWwDHWmKzctLVXxV4Tx5jr_vPfdSeRiLOKO98d1K\\\ + \",\\\"tw\\\":95}
      \\\
      400\_\xD7\_400 - bloomberg.com
      {\\\"cb\\\":6,\\\"cl\\\":3,\\\"cr\\\":6,\\\"ct\\\"\ + :6,\\\"id\\\":\\\"aFO7NKng1nit_M:\\\",\\\"isu\\\":\\\"bloomberg.com\\\",\\\ + \"ity\\\":\\\"gif\\\",\\\"oh\\\":400,\\\"ou\\\":\\\"http://www.bloomberg.com/features/2015-how-apple-built-3d-touch-iphone-6s/img/animation2-final.gif\\\ + \",\\\"ow\\\":400,\\\"pt\\\":\\\"How Apple Built 3D Touch\\\",\\\"rid\\\"\ + :\\\"Z-2vHKrWAxNgNM\\\",\\\"ru\\\":\\\"http://www.bloomberg.com/features/2015-how-apple-built-3d-touch-iphone-6s/\\\ + \",\\\"s\\\":\\\"Some of this technology was first revealed in the Apple Watch,\ + \ which has a feature called Force Touch. But 3D Touch is to Force Touch as\ + \ ocean swimming is ...\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQHXJM2LgWE0dT9lxdlxjQB9l1aL1oR690uorjwQPxmBTwOVwnl\\\",\\\ + \"tw\\\":225}
      \\\
      640\_\xD7\_360 - itespresso.es
      {\\\"id\\\":\\\"78pAlu6ehW3FrM:\\\",\\\"isu\\\":\\\ + \"itespresso.es\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":360,\\\"ou\\\":\\\"\ + http://www.itespresso.es/wp-content/uploads/2014/10/Apple-logo-jpg.jpg\\\"\ + ,\\\"ow\\\":640,\\\"pt\\\":\\\"Apple bloquea todas sus relaciones comerciales\ + \ con Crimea\\\",\\\"rid\\\":\\\"aVl-f3cuTVssUM\\\",\\\"ru\\\":\\\"http://www.itespresso.es/apple-bloquea-todas-sus-relaciones-comerciales-con-crimea-133325.html\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTHJFOm82pdCINeuWTMvgVZfbaF9HL6fYqUGcLjaFG40wPBwbU7\\\",\\\ + \"tw\\\":300}
      \\\
      332\_\xD7\_392 - apple.com
      {\\\"cb\\\":9,\\\"cl\\\":9,\\\"cr\\\":12,\\\"ct\\\":18,\\\"\ + id\\\":\\\"Tfd9lrHePXm9xM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"\\\",\\\"oh\\\":392,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/8748/as-images.apple.com/is/image/AppleInc/aos/published/images/w/38/w38ss/sbwh/w38ss-sbwh-sel-201509?wid\\\ + \\u003d332\\\\u0026hei\\\\u003d392\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453545531685\\\",\\\"ow\\\":332,\\\"pt\\\":\\\"Apple Watch\ + \ - Shop Apple Watch - Apple (MY)\\\",\\\"rid\\\":\\\"rK9WiVjysmYRsM\\\",\\\ + \"ru\\\":\\\"http://www.apple.com/my/shop/buy-watch/apple-watch\\\",\\\"s\\\ + \":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":244,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcT3OE002RasR8ajU_9m8rtIy1BY4i2h09-6ynowFsytR3W7sAUd\\\",\\\ + \"tw\\\":207}
      \\\
      636\_\xD7\_358 - imfnd.com
      {\\\"cl\\\":3,\\\"cr\\\":15,\\\"id\\\":\\\"JDDMsq5KLnlpsM:\\\ + \",\\\"isu\\\":\\\"imfnd.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":358,\\\ + \"ou\\\":\\\"http://www.imfnd.com/wp-content/uploads/2015/05/ceuhkwdqlrne37qwattm.jpg\\\ + \",\\\"ow\\\":636,\\\"pt\\\":\\\"Apple has been ruling the charts as the most\ + \ valuable brand in the ...\\\",\\\"rid\\\":\\\"T4i0yiTRMhjsiM\\\",\\\"ru\\\ + \":\\\"http://www.imfnd.com/apple-has-been-ruling-the-charts-as-the-most-valuable-brand-in-the-world-2015/\\\ + \",\\\"s\\\":\\\"Apple has been ruling the charts as the most valuable brand\ + \ in the world 2015\\\",\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQS9RLjXTaVfFfKmwE_FHX574Kk83BrgKshHhPADlBW_VoMXWrI\\\",\\\ + \"tw\\\":299}
      \\\
      760\_\xD7\_368 - support.apple.com
      {\\\"cl\\\":21,\\\"cr\\\":21,\\\"id\\\":\\\"b_1VYsfpFb7pyM:\\\ + \",\\\"isu\\\":\\\"support.apple.com\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\"\ + :368,\\\"ou\\\":\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/ios/built_in/triple-nav-messages.png\\\ + \",\\\"ow\\\":760,\\\"pt\\\":\\\"Iniciar sesi\xF3n con un ID de Apple distinto\ + \ en tu iPhone, iPad o ...\\\",\\\"rid\\\":\\\"dzRSQ9LQ2nYs0M\\\",\\\"ru\\\ + \":\\\"https://support.apple.com/es-es/HT203983\\\",\\\"s\\\":\\\"Cambiar\ + \ ID de Apple en iPhone para Mensajes\\\",\\\"th\\\":156,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcSwlKo-9-Iomq8WRpqr1M5zNNeDmpNPZgmsxnHexy05fTezLpbz\\\",\\\ + \"tw\\\":323}
      \\\
      700\_\xD7\_450 - support.apple.com
      {\\\"cb\\\":3,\\\"cl\\\":6,\\\"cr\\\":6,\\\"id\\\"\ + :\\\"ufcGQJlP0Z32ZM:\\\",\\\"isu\\\":\\\"support.apple.com\\\",\\\"ity\\\"\ + :\\\"png\\\",\\\"oh\\\":450,\\\"ou\\\":\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/apple-store-giftcard-2col.png\\\ + \",\\\"ow\\\":700,\\\"pt\\\":\\\"Qu\xE9 tipo de tarjeta regalo tengo? - Soporte\ + \ t\xE9cnico de Apple\\\",\\\"rid\\\":\\\"bNwYTayL2-0C9M\\\",\\\"ru\\\":\\\ + \"https://support.apple.com/es-es/HT204199\\\",\\\"s\\\":\\\"Tarjeta regalo\ + \ del Apple Store\\\",\\\"th\\\":180,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcT96G2G0ldPdk9zGJFSq430MVYVOMLs_DFB7uWkzsoAuQSr0wNn\\\",\\\ + \"tw\\\":280}
      \\\
      445\_\xD7\_445 - apple.com
      {\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\":3,\\\"id\\\":\\\"h8FyMWhlPsKiVM:\\\ + \",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"\\\",\\\"oh\\\":445,\\\"\ + ou\\\":\\\"http://store.storeimages.cdn-apple.com/4973/as-images.apple.com/is/image/AppleInc/aos/published/images/M/C8/MC838/MC838?wid\\\ + \\u003d445\\\\u0026hei\\\\u003d445\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453466889710\\\",\\\"ow\\\":445,\\\"pt\\\":\\\"Cables y\ + \ cargadores - Todos los accesorios - Apple (MX)\\\",\\\"rid\\\":\\\"ut9ReUkx1Qd2kM\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/mx/shop/accessories/all-accessories/power-cables\\\ + \",\\\"s\\\":\\\"Cable de HDMI a HDMI de Apple (1.8 m) - Next Gallery Image\\\ + \",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQrYIkZqQMokVnh9r3jmTBWM38DXdzpCykFoqo9oNvLpXP974kDhg\\\"\ + ,\\\"tw\\\":225}
      \\\
      300\_\xD7\_300 - drinkre.com
      {\\\"cb\\\":3,\\\"cl\\\":6,\\\"cr\\\":9,\\\"ct\\\":12,\\\"id\\\ + \":\\\"bpjPAVIgZL6WBM:\\\",\\\"isu\\\":\\\"drinkre.com\\\",\\\"ity\\\":\\\"\ + png\\\",\\\"oh\\\":300,\\\"ou\\\":\\\"http://drinkre.com/wp-content/uploads/2013/12/apple1.png\\\ + \",\\\"ow\\\":300,\\\"pt\\\":\\\"Apple\\\",\\\"rid\\\":\\\"bs5xN09D8SGgmM\\\ + \",\\\"ru\\\":\\\"http://drinkre.com/fruit/apple/\\\",\\\"s\\\":\\\"\\\",\\\ + \"sc\\\":1,\\\"th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQrRJKm561xYo-p1X2EiAJVlqU2mrE7aJhwIlAns7PsCTc5FG6fgQ\\\"\ + ,\\\"tw\\\":225}
      \\\
      840\_\xD7\_1125 - apple.com
      {\\\"id\\\":\\\"bWM1Hea2TFLzmM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":1125,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/i/pa/ipad/pro/ipad-pro-scene2-1?wid\\\ + \\u003d840\\\\u0026hei\\\\u003d1125\\\\u0026fmt\\\\u003dpng-alpha\\\\u0026qlt\\\ + \\u003d95\\\\u0026.v\\\\u003d1453547102660\\\",\\\"ow\\\":840,\\\"pt\\\":\\\ + \"Buy iPad Pro - Apple (UK)\\\",\\\"rid\\\":\\\"IKsdGtmkdiyTOM\\\",\\\"ru\\\ + \":\\\"http://www.apple.com/uk/shop/buy-ipad/ipad-pro\\\",\\\"s\\\":\\\"\\\ + \",\\\"sc\\\":1,\\\"th\\\":260,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcR272NuKmZOuhQkM7Rouwv8lD7oJlHiP1e43dm4JIXux9t2rvGaDg\\\"\ + ,\\\"tw\\\":194}
      \\\
      640\_\xD7\_1310 - support.apple.com
      {\\\"cb\\\":9,\\\"ct\\\":6,\\\"id\\\":\\\"il4WaE35pjJ2_M:\\\ + \",\\\"isu\\\":\\\"support.apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\"\ + :1310,\\\"ou\\\":\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/icloud/iphone6-watch-app-mywatch-apple-watch-mark-as-missing-full.jpg\\\ + \",\\\"ow\\\":640,\\\"pt\\\":\\\"Acerca del bloqueo de activaci\xF3n en el\ + \ Apple Watch - Soporte ...\\\",\\\"rid\\\":\\\"zvJBy456EG0QLM\\\",\\\"ru\\\ + \":\\\"https://support.apple.com/es-es/HT205009\\\",\\\"s\\\":\\\"Desactiva\ + \ las tarjetas de Apple Pay en el Apple Watch.\\\",\\\"th\\\":200,\\\"tu\\\ + \":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQNq5-UeXChRZGXKj6Y3IZwOh8Lue9G-dKyPFNVQIG0wtKznrpU\\\ + \",\\\"tw\\\":97}
      \\\
      300\_\xD7\_260 - apple.com
      {\\\"cb\\\":9,\\\"cl\\\":9,\\\"cr\\\":12,\\\"ct\\\":6,\\\"id\\\ + \":\\\"d0_l2YFn8QPj8M:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"\ + png\\\",\\\"oh\\\":260,\\\"ou\\\":\\\"https://www.apple.com/support/assets/images/home/2015/promo_communities_2x.png\\\ + \",\\\"ow\\\":300,\\\"pt\\\":\\\"Soporte t\xE9cnico de Apple oficial\\\",\\\ + \"rid\\\":\\\"P_uuKPQm6B6M6M\\\",\\\"ru\\\":\\\"https://www.apple.com/es/support/\\\ + \",\\\"s\\\":\\\"Comunidades de soporte t\xE9cnico de Apple\\\",\\\"sc\\\"\ + :1,\\\"th\\\":208,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcRU4ac6LmtbLd_oPkiZPam8z0OH0FgNoUk-fMnXW7BmVnoPuhIr\\\",\\\ + \"tw\\\":240}
      \\\
      849\_\xD7\_481 - apple.com
      {\\\"cl\\\":21,\\\"cr\\\":21,\\\"ct\\\":6,\\\"id\\\":\\\"n9VnN3bUQPDPxM:\\\ + \",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":481,\\\ + \"ou\\\":\\\"http://images.apple.com/es/apple-events/static/apple-events/september-2015/video/poster_large.jpg\\\ + \",\\\"ow\\\":849,\\\"pt\\\":\\\"Eventos de Apple - Evento especial de Apple,\ + \ septiembre de 2015 ...\\\",\\\"rid\\\":\\\"d26PYCdcheVbtM\\\",\\\"ru\\\"\ + :\\\"http://www.apple.com/es/apple-events/september-2015/\\\",\\\"s\\\":\\\ + \"Evento especial de Apple. 9 de septiembre de 2015\\\",\\\"sc\\\":1,\\\"\ + th\\\":169,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\\ + u003dtbn:ANd9GcSANlZRlm7kHQZ4uQPUolxu2u9ywda5qF45RhQJmZHM6xRt6QXmgw\\\",\\\ + \"tw\\\":298}
      \\\
      960\_\xD7\_720 - pixabay.com
      {\\\"cb\\\":12,\\\"cl\\\":18,\\\"cr\\\":21,\\\"ct\\\":3,\\\"\ + id\\\":\\\"5HrXwGkhvJPnCM:\\\",\\\"isu\\\":\\\"pixabay.com\\\",\\\"ity\\\"\ + :\\\"png\\\",\\\"oh\\\":720,\\\"ou\\\":\\\"https://pixabay.com/static/uploads/photo/2015/11/27/13/48/apple-1065563_960_720.png\\\ + \",\\\"ow\\\":960,\\\"pt\\\":\\\"Ilustraci\xF3n gratis: Apple, Frutas, Rojo,\ + \ Los Alimentos - Imagen ...\\\",\\\"rid\\\":\\\"f1RB8v-C0m8SSM\\\",\\\"ru\\\ + \":\\\"https://pixabay.com/es/apple-frutas-rojo-los-alimentos-1065563/\\\"\ + ,\\\"s\\\":\\\"Apple, Frutas, Rojo, Los Alimentos, Transparente\\\",\\\"sc\\\ + \":1,\\\"th\\\":194,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcS5d3b9HP01DbcFdZ2e35r9Kxy1TtpqicMleyRcwfNZwmQCzys0xA\\\"\ + ,\\\"tw\\\":259}
      \\\
      332\_\xD7\_392 - apple.com
      {\\\"cb\\\":6,\\\"cl\\\":9,\\\"cr\\\":9,\\\"ct\\\":15,\\\"id\\\ + \":\\\"Zif0slL9gc5ApM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"\\\ + \",\\\"oh\\\":392,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/s/38/s38si/sbor/s38si-sbor-sel-201509?wid\\\ + \\u003d332\\\\u0026hei\\\\u003d392\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\\\ + u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1453545887556\\\",\\\"ow\\\":332,\\\"pt\\\":\\\"Apple Watch\ + \ Sport - Comprar un Apple Watch Sport - Apple (ES)\\\",\\\"rid\\\":\\\"UpP7DXQ0G0XH7M\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/es/shop/buy-watch/apple-watch-sport\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":244,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcR_mUXbhcrehEG02EbaM9L_PqWuUPt3RNCNnhmfHjeuBAzbIc6m\\\",\\\ + \"tw\\\":207}
      \\\
      1920\_\xD7\_1080 - maclatino.com
      {\\\"cb\\\":6,\\\"cl\\\":21,\\\"cr\\\":6,\\\"ct\\\"\ + :3,\\\"id\\\":\\\"q7hkNDmBPJjLlM:\\\",\\\"isu\\\":\\\"maclatino.com\\\",\\\ + \"ity\\\":\\\"jpg\\\",\\\"oh\\\":1080,\\\"ou\\\":\\\"http://www.maclatino.com/wp-content/ups/2015/10/Apple-inc.jpg\\\ + \",\\\"ow\\\":1920,\\\"pt\\\":\\\"Apple contin\xFAa entre las empresas m\xE1\ + s valiosas del mundo ...\\\",\\\"rid\\\":\\\"tqqaHYVuzxZnzM\\\",\\\"ru\\\"\ + :\\\"http://www.maclatino.com/apple-continua-entre-las-empresas-mas-valiosas-del-mundo/\\\ + \",\\\"s\\\":\\\"Apple ha estado durante a\xF1os en los primeros puestos de\ + \ las empresas m\xE1s valiosas del mundo y a\xFAn contin\xFAa entre las m\xE1\ + s importantes.\\\",\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcT1Mao_tHUIEetJkVsaFIxdLnmn3yKbjK0hmQe3O15OsGqSq3hHvA\\\"\ + ,\\\"tw\\\":300}
      \\\
      780\_\xD7\_458 - support.apple.com
      {\\\"id\\\":\\\"rxtP8VJVRf72QM:\\\",\\\"isu\\\":\\\ + \"support.apple.com\\\",\\\"ity\\\":\\\"gif\\\",\\\"oh\\\":458,\\\"ou\\\"\ + :\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/applemusic/apple-music-suggestion-hero-animation.gif\\\ + \",\\\"ow\\\":780,\\\"pt\\\":\\\"Refine the For You suggestions in Apple Music\ + \ on your iPhone, iPad ...\\\",\\\"rid\\\":\\\"bEkQWAwesO_B4M\\\",\\\"ru\\\ + \":\\\"https://support.apple.com/en-us/HT204842\\\",\\\"s\\\":\\\"When you\ + \ join Apple Music, we ask you to choose genres and artists that you like.\ + \ We use these choices to give you music suggestions from our experts who\ + \ ...\\\",\\\"sc\\\":1,\\\"th\\\":172,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTokJ-L5uyJVEfGed5hxEAkqsKxo07I_F7KV03LEZlq4DW5Tn8\\\",\\\"\ + tw\\\":293}
      \\\
      2000\_\xD7\_1000 - foreveralone.es
      {\\\"cl\\\":6,\\\"cr\\\":6,\\\"id\\\":\\\"RZGq-DDul8dJQM:\\\ + \",\\\"isu\\\":\\\"foreveralone.es\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\"\ + :1000,\\\"ou\\\":\\\"http://i.huffpost.com/gen/3398918/images/o-LAPIZ-APPLE-facebook.jpg\\\ + \",\\\"ow\\\":2000,\\\"pt\\\":\\\"Foreveralone - Y Twitter se cachonde\xF3\ + \ del l\xE1piz de Apple de 100\\\\u20ac\\\",\\\"rid\\\":\\\"lXfPb6M2jjt2FM\\\ + \",\\\"ru\\\":\\\"http://www.foreveralone.es/post/128729429274/y-twitter-se-cachonde%C3%B3-del-l%C3%A1piz-de-apple-de-100\\\ + \",\\\"s\\\":\\\"la gran novedad de Apple era un lapiz de 100\\\\u20ac y como\ + \ no pod\xEDa ser menos se ha montado el cachondeo el Twitter, aqu\xED las\ + \ mejores co\xF1as de la red social al ...\\\",\\\"sc\\\":1,\\\"th\\\":159,\\\ + \"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcTy1V4CX-VOoAeh6jWck29LXbSQ0aC3NNSjzFvy6k_HxsuvfKjl\\\ + \",\\\"tw\\\":318}
      \\\
      1560\_\xD7\_900 - support.apple.com
      {\\\"id\\\":\\\"OrPKMIh7yCix0M:\\\",\\\"isu\\\":\\\ + \"support.apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":900,\\\"ou\\\"\ + :\\\"https://support.apple.com/library/content/dam/edam/applecare/images/en_US/appleid/apple-id-standard-reset-password-email-sent.jpg\\\ + \",\\\"ow\\\":1560,\\\"pt\\\":\\\"If you forgot your Apple ID password - Apple\ + \ Support\\\",\\\"rid\\\":\\\"pmU_bh5yqEkuMM\\\",\\\"ru\\\":\\\"https://support.apple.com/en-us/HT201487\\\ + \",\\\"s\\\":\\\"Use email authentication\\\",\\\"sc\\\":1,\\\"th\\\":170,\\\ + \"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcSShiYkCzf7d7nvMu4oFyERtEDVbN45WUFdsvJvsj4DiausGLN_\\\ + \",\\\"tw\\\":296}
      \\\
      1200\_\xD7\_1200 - apple.com
      {\\\"cb\\\":21,\\\"cl\\\":18,\\\"cr\\\":21,\\\"id\\\":\\\"vK7xTXvoO0cMTM:\\\ + \",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":1200,\\\ + \"ou\\\":\\\"http://images.apple.com/euro/mac/home/n/generic/images/social/macbook_mac_og.jpg?201602220751\\\ + \",\\\"ow\\\":1200,\\\"pt\\\":\\\"Mac - Apple (ES)\\\",\\\"rid\\\":\\\"RyeA513XrFw3SM\\\ + \",\\\"ru\\\":\\\"http://www.apple.com/es/mac/\\\",\\\"s\\\":\\\"\\\",\\\"\ + th\\\":225,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\\\ + u003dtbn:ANd9GcRkiW3rAgl0att-1oLqVq5zCxBFlYKCy1mtuzT_1rr0hZJl8Wrv9Q\\\",\\\ + \"tw\\\":225}
      \\\
      160\_\xD7\_160 - apple.com
      {\\\"cb\\\":21,\\\"cl\\\":15,\\\"cr\\\":15,\\\"ct\\\":21,\\\"\ + id\\\":\\\"lxy0B1NHKslblM:\\\",\\\"isu\\\":\\\"apple.com\\\",\\\"ity\\\":\\\ + \"png\\\",\\\"oh\\\":160,\\\"ou\\\":\\\"https://www.apple.com/support/assets/images/home/2015/apple_pay_icon_2x.png\\\ + \",\\\"ow\\\":160,\\\"pt\\\":\\\"Official Apple Support\\\",\\\"rid\\\":\\\ + \"OrunAFB7EkjeiM\\\",\\\"ru\\\":\\\"https://www.apple.com/support/\\\",\\\"\ + s\\\":\\\"Apple Pay Support\\\",\\\"sc\\\":1,\\\"th\\\":128,\\\"tu\\\":\\\"\ + https://encrypted-tbn0.gstatic.com/images?q\\\\u003dtbn:ANd9GcSzUffJMY0g6XLxURAZ1a5zYUWnyivxKrS4OiTqis8hdd-vf13O\\\ + \",\\\"tw\\\":128}
      \\\
      232\_\xD7\_232 - apple.com
      {\\\"id\\\":\\\"xxBAPjtNbwTSZM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":232,\\\"ou\\\":\\\"https://www.apple.com/support/assets/images/products/programs/hero_programs.jpg\\\ + \",\\\"ow\\\":232,\\\"pt\\\":\\\"Programas de servicio de Apple - Soporte\ + \ t\xE9cnico de Apple\\\",\\\"rid\\\":\\\"rTLJhjdY0-GBDM\\\",\\\"ru\\\":\\\ + \"https://www.apple.com/es/support/programs/\\\",\\\"s\\\":\\\"Pie de p\xE1\ + gina de Apple\\\",\\\"sc\\\":1,\\\"th\\\":185,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcQXNBCYASGiMVwF8OTzxZUCninXV8LKQdvZvGpVqlNfkHNMuIxm\\\",\\\ + \"tw\\\":185}
      \\\
      700\_\xD7\_700 - poderpda.com
      {\\\"cl\\\":21,\\\"cr\\\":21,\\\"id\\\":\\\"32v5Q1FDQ4I16M:\\\ + \",\\\"isu\\\":\\\"poderpda.com\\\",\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":700,\\\ + \"ou\\\":\\\"https://s3.amazonaws.com/poderpda/2015/05/apple-iphone-5c-700x700.jpg\\\ + \",\\\"ow\\\":700,\\\"pt\\\":\\\"Apple no crear\xE1 \\\\u201cbackdoor\\\\\ + u201d para el FBI | PoderPDA\\\",\\\"rid\\\":\\\"1_qQPCHnGml9SM\\\",\\\"ru\\\ + \":\\\"http://www.poderpda.com/plataformas/apple/apple-niega-crear-backdoor/\\\ + \",\\\"s\\\":\\\"apple iphone 5c\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\"\ + :\\\"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcQ_SS3hMDEbrv4CNp5uTDGdGMJxek3rAl70plzKpTZ4zOelHSal\\\ + \",\\\"tw\\\":225}
      \\\
      800\_\xD7\_799 - imore.com
      {\\\"cb\\\":18,\\\"cl\\\":3,\\\"id\\\":\\\"kJWvtPo8cEK0XM:\\\ + \",\\\"isu\\\":\\\"imore.com\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":799,\\\ + \"ou\\\":\\\"http://www.imore.com/sites/imore.com/files/styles/large/public/topic_images/2015/apple-tv-4-topic.png?itok\\\ + \\u003dqAEoCCk0\\\",\\\"ow\\\":800,\\\"pt\\\":\\\"Apple TV \\\\u2014 Everything\ + \ you need to know! | iMore\\\",\\\"rid\\\":\\\"BAWSc_QV6-2IjM\\\",\\\"ru\\\ + \":\\\"http://www.imore.com/apple-tv\\\",\\\"s\\\":\\\"Apple TV\\\",\\\"sc\\\ + \":1,\\\"th\\\":224,\\\"tu\\\":\\\"https://encrypted-tbn3.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTGo2WqDgxl6u9eQ31UJelwZda2_XHQm0l0RqMDyVv-4d0dNJk4Gw\\\"\ + ,\\\"tw\\\":225}
      \\\
      800\_\xD7\_800 - clipartpanda.com
      {\\\"cb\\\":3,\\\"cl\\\":9,\\\"cr\\\":9,\\\"ct\\\"\ + :3,\\\"id\\\":\\\"h-XL1DGOub3FAM:\\\",\\\"isu\\\":\\\"clipartpanda.com\\\"\ + ,\\\"ity\\\":\\\"png\\\",\\\"oh\\\":800,\\\"ou\\\":\\\"http://images.clipartpanda.com/apple-clipart-apple5.png\\\ + \",\\\"ow\\\":800,\\\"pt\\\":\\\"Apple Clipart | Clipart Panda - Free Clipart\ + \ Images\\\",\\\"rid\\\":\\\"4iAesQutRHaySM\\\",\\\"ru\\\":\\\"http://www.clipartpanda.com/categories/apple-clipart\\\ + \",\\\"s\\\":\\\"apple%20clipart\\\",\\\"sc\\\":1,\\\"th\\\":225,\\\"tu\\\"\ + :\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcTirE7dOKgMJjG5s3V6iOxNDGbtjJjFgcJ_rhQultHxpXBsL5CZ\\\ + \",\\\"tw\\\":225}
      \\\
      2000\_\xD7\_2200 - en.wikipedia.org
      {\\\"id\\\":\\\"QU50BCxNTMxUyM:\\\",\\\"isu\\\":\\\ + \"en.wikipedia.org\\\",\\\"ity\\\":\\\"png\\\",\\\"oh\\\":2200,\\\"ou\\\"\ + :\\\"https://upload.wikimedia.org/wikipedia/commons/thumb/8/84/Apple_Computer_Logo_rainbow.svg/2000px-Apple_Computer_Logo_rainbow.svg.png\\\ + \",\\\"ow\\\":2000,\\\"pt\\\":\\\"History of Apple Inc. - Wikipedia, the free\ + \ encyclopedia\\\",\\\"rid\\\":\\\"XlwI6ynf58KZqM\\\",\\\"ru\\\":\\\"https://en.wikipedia.org/wiki/History_of_Apple_Inc.\\\ + \",\\\"s\\\":\\\"Created by Rob Janoff in 1977, the Apple logo with the rainbow\ + \ scheme was used from April of that year [10] until August 26, 1999.\\\"\ + ,\\\"sc\\\":1,\\\"th\\\":235,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcR78mBAvw3kSOj5QCGkdR275kT1R5yML0rDPOocQRDa7rDHj10o5w\\\"\ + ,\\\"tw\\\":214}
      \\\
      1102\_\xD7\_1289 - taringa.net
      {\\\"cl\\\":3,\\\"cr\\\":3,\\\"ct\\\":3,\\\"id\\\"\ + :\\\"Flp61hOT67GVrM:\\\",\\\"isu\\\":\\\"taringa.net\\\",\\\"ity\\\":\\\"\ + jpg\\\",\\\"oh\\\":1289,\\\"ou\\\":\\\"http://virket.com/wp-content/uploads/Manzana.jpg\\\ + \",\\\"ow\\\":1102,\\\"pt\\\":\\\"Los 10 fracasos de Apple - Taringa!\\\"\ + ,\\\"rid\\\":\\\"u88VDr8ef_5vIM\\\",\\\"ru\\\":\\\"http://www.taringa.net/post/economia-negocios/17553341/Los-10-fracasos-de-Apple.html\\\ + \",\\\"s\\\":\\\"Los 10 fracasos de Apple\\\",\\\"sc\\\":1,\\\"th\\\":243,\\\ + \"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcS4UeHfpR9_wJCqbZIkpHPsqHa7WzXiF8_Pv8u52fNaF9qZgkkdXA\\\ + \",\\\"tw\\\":208}
      \\\
      277\_\xD7\_784 - apple.com
      {\\\"id\\\":\\\"dptmCRz8ZaBozM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":784,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/s/ce/scene0/right/scene0-right-2x?wid\\\ + \\u003d277\\\\u0026hei\\\\u003d784\\\\u0026fmt\\\\u003dpng-alpha\\\\u0026qlt\\\ + \\u003d95\\\\u0026.v\\\\u003d1453543709552\\\",\\\"ow\\\":277,\\\"pt\\\":\\\ + \"Compra un iPhone 6 o un iPhone 6 Plus - Apple (ES)\\\",\\\"rid\\\":\\\"\ + ZhCjqxYEAMHSfM\\\",\\\"ru\\\":\\\"http://www.apple.com/es/shop/buy-iphone/iphone6\\\ + \",\\\"s\\\":\\\"\\\",\\\"sc\\\":1,\\\"th\\\":235,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcS0tvjg_ErVfnyvxnw-yePYXQ0i_u-OAUHg8gUBOWA25sMP8Ujq3w\\\"\ + ,\\\"tw\\\":83}
      \\\
      660\_\xD7\_410 - appleinsider.com
      {\\\"cb\\\":3,\\\"cl\\\":3,\\\"cr\\\":3,\\\"ct\\\"\ + :3,\\\"id\\\":\\\"r1B3wsjEJIg13M:\\\",\\\"isu\\\":\\\"appleinsider.com\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":410,\\\"ou\\\":\\\"http://photos.appleinsidercdn.com/gallery/14203-9543-Screen-Shot-2015-09-09-at-63530-PM-l.jpg\\\ + \",\\\"ow\\\":660,\\\"pt\\\":\\\"Hands On: Apple TV 2015 with tvOS apps, Remote\ + \ featuring touch ...\\\",\\\"rid\\\":\\\"qDV-u_J4ARIk5M\\\",\\\"ru\\\":\\\ + \"http://appleinsider.com/articles/15/09/12/hands-on-apple-tv-2015-with-tvos-apps-remote-featuring-touch-motion-siri\\\ + \",\\\"s\\\":\\\"It\\\\u0026#39;s not clear if Apple will actually support\ + \ 3D-TV output from the new Apple TV, but 1.4 does suggest that as a possibility,\ + \ even if only in a future ...\\\",\\\"sc\\\":1,\\\"th\\\":177,\\\"tu\\\"\ + :\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcQEaxtKUK-OYLzvJrpDGNninqvYZ7qrwKV76Ud9tYHTLOihEDPJ\\\ + \",\\\"tw\\\":285}
      \\\
      600\_\xD7\_450 - techtimes.com
      {\\\"cb\\\":6,\\\"cl\\\":9,\\\"cr\\\":3,\\\"ct\\\"\ + :3,\\\"id\\\":\\\"9d-cCahhPJ_8KM:\\\",\\\"isu\\\":\\\"techtimes.com\\\",\\\ + \"ity\\\":\\\"jpg\\\",\\\"oh\\\":450,\\\"ou\\\":\\\"http://images.techtimes.com/data/images/full/191029/iphone-7.jpg?w\\\ + \\u003d600\\\",\\\"ow\\\":600,\\\"pt\\\":\\\"What To Expect From Apple In\ + \ 2016: iPhone 7, Apple Watch 2 ...\\\",\\\"rid\\\":\\\"Z99CeMHdtW1QFM\\\"\ + ,\\\"ru\\\":\\\"http://www.techtimes.com/articles/121536/20160106/what-to-expect-from-apple-in-2016-iphone-7-apple-watch-2-macbook-air-and-more.htm\\\ + \",\\\"s\\\":\\\"A rumor circulating on the Internet suggests that Apple might\ + \ ditch the classic 3.5mm headphone jack from the iPhone 7 in favor of Lightning\ + \ ports or ...\\\",\\\"th\\\":194,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcTPNKvOxk-O4Fo5mzYTkZWZeI-H3I1rgI4PAaXfpg50yZwQ985KMw\\\"\ + ,\\\"tw\\\":259}
      \\\
      701\_\xD7\_377 - apple.com
      {\\\"id\\\":\\\"78w6FTfuX5e8oM:\\\",\\\"isu\\\":\\\"apple.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":377,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4662/as-images.apple.com/is/image/AppleInc/aos/published/images/l/p/lp/reuse/lp-reuse-versa-201503a?wid\\\ + \\u003d701\\\\u0026hei\\\\u003d377\\\\u0026fmt\\\\u003dpng-alpha\\\\u0026qlt\\\ + \\u003d95\\\\u0026.v\\\\u003d1453517230273\\\",\\\"ow\\\":701,\\\"pt\\\":\\\ + \"Programa de Reciclaje y Reutilizaci\xF3n de Apple para iPhone, iPad ...\\\ + \",\\\"rid\\\":\\\"SdGRAJDxFn5fXM\\\",\\\"ru\\\":\\\"http://www.apple.com/es/shop/browse/reuse_and_recycle\\\ + \",\\\"s\\\":\\\"Seg\xFAn el tipo de dispositivo, tendr\xE1s que llevarlo\ + \ a un Apple Store o tramitarlo por Internet.\\\",\\\"sc\\\":1,\\\"th\\\"\ + :165,\\\"tu\\\":\\\"https://encrypted-tbn2.gstatic.com/images?q\\\\u003dtbn:ANd9GcQKveSq-75OF0-oofnDoXwq2AamHUx2ATDfCZll9pUDUxG2gJIv6A\\\ + \",\\\"tw\\\":306}
      \\\
      650\_\xD7\_430 - maclovers.universiablogs.net
      {\\\"cb\\\":3,\\\"cl\\\":21,\\\"cr\\\":21,\\\"id\\\ + \":\\\"d2h5X5Nsu8InmM:\\\",\\\"isu\\\":\\\"maclovers.universiablogs.net\\\"\ + ,\\\"ity\\\":\\\"jpg\\\",\\\"oh\\\":430,\\\"ou\\\":\\\"http://maclovers.universiablogs.net/files/apple_beats2_6501.jpg\\\ + \",\\\"ow\\\":650,\\\"pt\\\":\\\"Apple se hace con Beats | MACLOVERS Aprendiendo\ + \ Mac\\\",\\\"rid\\\":\\\"fm9x4_clLsLXaM\\\",\\\"ru\\\":\\\"http://maclovers.universiablogs.net/2014/05/29/apple-se-hace-finalmente-con-beats/\\\ + \",\\\"s\\\":\\\"De hecho, los de la manzana se interesaron por esta compa\xF1\ + \xEDa para potenciar el negocio de iTunes gracias a su servicio de Streaming;\ + \ tomando la delantera a ...\\\",\\\"sc\\\":1,\\\"th\\\":183,\\\"tu\\\":\\\ + \"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcSf67tmBSG1HQXvmRj29qHjo60vqHV-6isLY6Mcka31gM5JUzSI3g\\\ + \",\\\"tw\\\":276}
      \\\
      770\_\xD7\_433 - infobae.com
      {\\\"cb\\\":3,\\\"cl\\\":3,\\\"cr\\\":3,\\\"ct\\\":3,\\\"id\\\ + \":\\\"23XZ4s06o6htBM:\\\",\\\"isu\\\":\\\"infobae.com\\\",\\\"ity\\\":\\\"\ + jpg\\\",\\\"oh\\\":433,\\\"ou\\\":\\\"http://cdn01.ib.infobae.com/adjuntos/162/imagenes/014/198/0014198501.jpg\\\ + \",\\\"ow\\\":770,\\\"pt\\\":\\\"Apple prepara varios cambios para el iPhone\ + \ 7 | Apple, iPhone ...\\\",\\\"rid\\\":\\\"I6DorJ_g87SmWM\\\",\\\"ru\\\"\ + :\\\"http://www.infobae.com/2016/02/03/1787552-apple-prepara-varios-cambios-el-iphone-7\\\ + \",\\\"s\\\":\\\"El dise\xF1o del iPhone 7 basado en los nuevos rumores\\\"\ + ,\\\"sc\\\":1,\\\"th\\\":168,\\\"tu\\\":\\\"https://encrypted-tbn1.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcRuByE98p0ZO66OIVaYggPHHkEbQWLaYJT-q2h6TdKWxx-PVsugjw\\\"\ + ,\\\"tw\\\":300}
      \\\
      1020\_\xD7\_681 - theverge.com
      {\\\"cl\\\":12,\\\"cr\\\":12,\\\"ct\\\":21,\\\"id\\\ + \":\\\"BIWXxSUcFAajWM:\\\",\\\"isu\\\":\\\"theverge.com\\\",\\\"ity\\\":\\\ + \"jpg\\\",\\\"oh\\\":681,\\\"ou\\\":\\\"https://cdn1.vox-cdn.com/thumbor/TT51CKCf623RakUIha5Qw0q3sek\\\ + \\u003d/1020x0/cdn0.vox-cdn.com/uploads/chorus_asset/file/4044620/apple-iphone-6s-live-_0824.0.jpg\\\ + \",\\\"ow\\\":1020,\\\"pt\\\":\\\"This is the stylus for Apple\\\\u0026#39;s\ + \ new iPad Pro | The Verge\\\",\\\"rid\\\":\\\"23jXcA0_UFNQZM\\\",\\\"ru\\\ + \":\\\"http://www.theverge.com/2015/9/9/9295901/apple-ipad-stylus-pen-announced-price-specs-date\\\ + \",\\\"s\\\":\\\"Apple iphone 6s live 0824.0\\\",\\\"th\\\":183,\\\"tu\\\"\ + :\\\"https://encrypted-tbn3.gstatic.com/images?q\\\\u003dtbn:ANd9GcTTVcwavQ3jGI_FF0Lqrbh7JP8tIoeR-X2n_mVwdeKs8VL8tgbQBA\\\ + \",\\\"tw\\\":275}
      \\\"Resultado
      2000\_\xD7\_1536 - ishopcentroamerica.com
      {\\\"cb\\\":15,\\\"cl\\\":18,\\\"cr\\\":21,\\\"ct\\\ + \":15,\\\"id\\\":\\\"VNIZ3ku7M6Q0EM:\\\",\\\"isu\\\":\\\"ishopcentroamerica.com\\\ + \",\\\"ity\\\":\\\"\\\",\\\"oh\\\":1536,\\\"ou\\\":\\\"http://store.storeimages.cdn-apple.com/4528/as-images.apple.com/is/image/AppleInc/aos/published/images/a/pp/apple/tv/apple-tv-gallery4-2015?wid\\\ + \\u003d2000\\\\u0026hei\\\\u003d1536\\\\u0026fmt\\\\u003djpeg\\\\u0026qlt\\\ + \\u003d95\\\\u0026op_sharpen\\\\u003d0\\\\u0026resMode\\\\u003dbicub\\\\u0026op_usm\\\ + \\u003d0.5,0.5,0,0\\\\u0026iccEmbed\\\\u003d0\\\\u0026layer\\\\u003dcomp\\\ + \\u0026.v\\\\u003d1441801116406%E2%80%9C\\\",\\\"ow\\\":2000,\\\"pt\\\":\\\ + \"Apple TV 3G | iShop Costa Rica Distribuidor Apple Autorizado.\\\",\\\"rid\\\ + \":\\\"0NpLjao8bA9cZM\\\",\\\"ru\\\":\\\"http://www.ishopcentroamerica.com/shop/apple-tv/apple-tv/\\\ + \",\\\"s\\\":\\\"Apple TV 3G\\\",\\\"sc\\\":1,\\\"th\\\":197,\\\"tu\\\":\\\ + \"https://encrypted-tbn1.gstatic.com/images?q\\\\u003dtbn:ANd9GcQ59HF9WQKLmPW83sf3XX8G8VDU37OQm7gza1jo4w0bAExEdyLz\\\ + \",\\\"tw\\\":256}
      \\\"Resultado
      310\_\xD7\_233 - economictimes.indiatimes.com
      {\\\"cb\\\":9,\\\"cr\\\":21,\\\"id\\\":\\\"1s-AZ8rVSTzUQM:\\\ + \",\\\"isu\\\":\\\"economictimes.indiatimes.com\\\",\\\"ity\\\":\\\"jpg\\\"\ + ,\\\"oh\\\":233,\\\"ou\\\":\\\"http://economictimes.indiatimes.com/thumb/msid-50660935,width-310,resizemode-4/apple-posts-record-india-sales-in-tough-october-december-quarter.jpg\\\ + \",\\\"ow\\\":310,\\\"pt\\\":\\\"Apple posts record India sales in tough October-December\ + \ quarter ...\\\",\\\"rid\\\":\\\"D9KChmpKb5GJjM\\\",\\\"ru\\\":\\\"http://economictimes.indiatimes.com/small-biz/apple-posts-record-india-sales-in-tough-october-december-quarter/articleshow/50660904.cms\\\ + \",\\\"s\\\":\\\"Apple\\\\u0026#39;s sales in India rose to a record in the\ + \ October-December period thanks to\\\",\\\"th\\\":186,\\\"tu\\\":\\\"https://encrypted-tbn0.gstatic.com/images?q\\\ + \\u003dtbn:ANd9GcR7RAQEXwKZjpRgUAbk0DRLOJmS7FNNWLzsHhdnyzB2EI4yw5v0\\\",\\\ + \"tw\\\":248}
      Im\xE1genes relacionadas:
      Ver m\xE1s
      Las im\xE1genes pueden estar sujetas a derechos de autor.Enviar comentarios
      Im\xE1genes relacionadas:
      Ver m\xE1s
      Las im\xE1genes pueden estar sujetas a derechos de autor.Enviar comentarios
      Im\xE1genes relacionadas:
      Ver m\xE1s
      Las im\xE1genes pueden estar sujetas a derechos de autor.Enviar comentarios
      Im\xE1genes relacionadas:
      Ver m\xE1s
      Las im\xE1genes pueden estar sujetas a derechos de autor.Enviar comentarios
        \ + \
        \ + \
        \ + \
        \"}"} + headers: + connection: [close] + content-length: ['675034'] + content-type: [application/json; charset=UTF-8] + date: ['Wed, 23 Mar 2016 01:46:25 GMT'] + server: [httpd.js] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: [application/json] + Connection: [keep-alive] + Content-type: [application/json;charset="UTF-8"] + DELETE: [!!python/unicode '/hub/session/f0baf8c0-8109-8745-b27e-205763034e37'] + User-Agent: [Python http auth] + method: DELETE + uri: http://127.0.0.1:64104/hub/session/f0baf8c0-8109-8745-b27e-205763034e37 + response: + body: {string: !!python/unicode '{"name":"quit","sessionId":"f0baf8c0-8109-8745-b27e-205763034e37","status":0,"value":""}'} + headers: + connection: [close] + content-length: ['88'] + content-type: [application/json; charset=UTF-8] + date: ['Wed, 23 Mar 2016 01:46:25 GMT'] + server: [httpd.js] + status: {code: 200, message: OK} +version: 1 diff --git a/requirements.py b/requirements.py new file mode 100644 index 0000000..8a439fe --- /dev/null +++ b/requirements.py @@ -0,0 +1,230 @@ +import os +import re +import logging +import warnings +from pkg_resources import Requirement as Req + +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + + +__version__ = '0.1.0' + +logging.basicConfig(level=logging.WARNING) + +VCS = ['git', 'hg', 'svn', 'bzr'] + + +class Requirement(object): + """ + This class is inspired from + https://github.com/davidfischer/requirements-parser/blob/master/requirements/requirement.py#L30 + License: BSD + """ + + def __init__(self, line): + self.line = line + self.is_editable = False + self.is_local_file = False + self.is_specifier = False + self.vcs = None + self.name = None + self.uri = None + self.full_uri = None + self.path = None + self.revision = None + self.scheme = None + self.login = None + self.extras = [] + self.specs = [] + + def __repr__(self): + return ''.format(self.line) + + @classmethod + def parse(cls, line, editable=False): + """ + Parses a Requirement from an "editable" requirement which is either + a local project path or a VCS project URI. + + See: pip/req.py:from_editable() + + :param line: an "editable" requirement + :returns: a Requirement instance for the given line + :raises: ValueError on an invalid requirement + """ + if editable: + req = cls('-e {0}'.format(line)) + req.is_editable = True + else: + req = cls(line) + + url = urlparse(line) + req.uri = None + if url.scheme: + req.scheme = url.scheme + req.uri = url.scheme + '://' + url.netloc + url.path + fragment = url.fragment.split(' ')[0].strip() + req.name = fragment.split('egg=')[-1] or None + req.path = url.path + if fragment: + req.uri += '#{}'.format(fragment) + if url.username or url.password: + username = url.username or '' + password = url.password or '' + req.login = username + ':' + password + if '@' in url.path: + req.revision = url.path.split('@')[-1] + + for vcs in VCS: + if req.uri.startswith(vcs): + req.vcs = vcs + if req.scheme.startswith('file://'): + req.is_local_file = True + + if not req.vcs and not req.is_local_file and 'egg=' not in line: + # This is a requirement specifier. + # Delegate to pkg_resources and hope for the best + req.is_specifier = True + pkg_req = Req.parse(line) + req.name = pkg_req.unsafe_name + req.extras = list(pkg_req.extras) + req.specs = pkg_req.specs + if req.specs: + req.specs = sorted(req.specs) + + return req + + +class Requirements: + + def __init__( + self, + requirements="requirements.txt", + tests_requirements="tests_requirements.txt"): + self.requirements_path = requirements + self.tests_requirements_path = tests_requirements + + def format_specifiers(self, requirement): + return ', '.join( + ['{} {}'.format(s[0], s[1]) for s in requirement.specs]) + + @property + def install_requires(self): + dependencies = [] + for requirement in self.parse(self.requirements_path): + if not requirement.is_editable and not requirement.uri \ + and not requirement.vcs: + full_name = requirement.name + specifiers = self.format_specifiers(requirement) + if specifiers: + full_name = "{} {}".format(full_name, specifiers) + dependencies.append(full_name) + for requirement in self.get_dependency_links(): + print(":: (base:install_requires) {}".format(requirement.name)) + dependencies.append(requirement.name) + return dependencies + + @property + def tests_require(self): + dependencies = [] + for requirement in self.parse(self.tests_requirements_path): + if not requirement.is_editable and not requirement.uri \ + and not requirement.vcs: + full_name = requirement.name + specifiers = self.format_specifiers(requirement) + if specifiers: + full_name = "{} {}".format(full_name, specifiers) + print(":: (tests:tests_require) {}".format(full_name)) + dependencies.append(full_name) + return dependencies + + @property + def dependency_links(self): + dependencies = [] + for requirement in self.parse(self.requirements_path): + if requirement.uri or requirement.vcs or requirement.path: + print(":: (base:dependency_links) {}".format( + requirement.uri)) + dependencies.append(requirement.uri) + return dependencies + + @property + def dependencies(self): + install_requires = self.install_requires + dependency_links = self.dependency_links + tests_require = self.tests_require + if dependency_links: + print( + "\n" + "!! Some dependencies are linked to repository or local path.") + print( + "!! You'll need to run pip with following option: " + "`--process-dependency-links`" + "\n") + return { + 'install_requires': install_requires, + 'dependency_links': dependency_links, + 'tests_require': tests_require} + + def get_dependency_links(self): + dependencies = [] + for requirement in self.parse(self.requirements_path): + if requirement.uri or requirement.vcs or requirement.path: + dependencies.append(requirement) + return dependencies + + def parse(self, path=None): + path = path or self.requirements_path + path = os.path.abspath(path) + base_directory = os.path.dirname(path) + + if not os.path.exists(path): + warnings.warn( + 'Requirements file: {} does not exists.'.format(path)) + return + + with open(path) as requirements: + for index, line in enumerate(requirements.readlines()): + index += 1 + line = line.strip() + if not line: + logging.debug('Empty line (line {} from {})'.format( + index, path)) + continue + elif line.startswith('#'): + logging.debug( + 'Comments line (line {} from {})'.format(index, path)) + elif line.startswith('-f') or \ + line.startswith('--find-links') or \ + line.startswith('-i') or \ + line.startswith('--index-url') or \ + line.startswith('--extra-index-url') or \ + line.startswith('--no-index'): + warnings.warn('Private repos not supported. Skipping.') + continue + elif line.startswith('-Z') or line.startswith( + '--always-unzip'): + warnings.warn('Unused option --always-unzip. Skipping.') + continue + elif line.startswith('-r') or line.startswith('--requirement'): + logging.debug( + 'Pining to another requirements file ' + '(line {} from {})'.format(index, path)) + for _line in self.parse(path=os.path.join( + base_directory, line.split()[1])): + yield _line + elif line.startswith('-e') or line.startswith('--editable'): + # Editable installs are either a local project path + # or a VCS project URI + yield Requirement.parse( + re.sub(r'^(-e|--editable=?)\s*', '', line), + editable=True) + else: + logging.debug('Found "{}" (line {} from {})'.format( + line, index, path)) + yield Requirement.parse(line, editable=False) + +r = Requirements() diff --git a/setup.cfg b/setup.cfg index 07c9070..a072f69 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,3 @@ verbosity=1 detailed-errors=1 with-coverage=1 cover-package=google -debug=nose.loader -pdb=1 -pdb-failures=1 \ No newline at end of file diff --git a/setup.py b/setup.py index 6de523e..a9604b3 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,8 @@ except ImportError: from distutils.core import setup +# from requirements import r + with open("requirements.txt") as f: requirements = [req.strip() for req in f.readlines()] @@ -41,5 +43,6 @@ ], setup_requires=['nose>=1.0'], test_suite='nose.collector', - tests_require=test_requirements + tests_require=test_requirements, + # **r.requirements ) diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..3e5c8ca --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,2 @@ +vcrpy +nose diff --git a/test_standard_search.yaml b/test_standard_search.yaml new file mode 100644 index 0000000..578b821 --- /dev/null +++ b/test_standard_search.yaml @@ -0,0 +1,732 @@ +interactions: +- request: + body: null + headers: + Connection: [close] + Host: [!!python/unicode 'www.google.com'] + !!python/unicode 'User-Agent': [!!python/unicode 'Mozilla/5.001 (windows; U; + NT4.0; en-US; rv:1.0) Gecko/25250101'] + method: GET + uri: http://www.google.com/search?q=github&start=0&num=10&nl=en + response: + body: {string: !!python/unicode "\n302 Moved\n\ +

        302 Moved

        \nThe document has moved\nhere.\r\n\r\n"} + headers: + cache-control: [private] + connection: [close] + content-length: ['302'] + content-type: [text/html; charset=UTF-8] + date: ['Wed, 23 Mar 2016 02:07:31 GMT'] + location: ['http://www.google.com.ar/search?q=github&start=0&num=10&gws_rd=cr&ei=Y_rxVtP3GIenwATdt6kw'] + p3p: ['CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en + for more info."'] + server: [gws] + set-cookie: ['NID=77=MKPmowcdOv61Yajhblditp4lg0Vm72pUkZEg-YO0z4u57y2ys7MkKNqgNaVsBv6RCn3BBhOlJ52fXjnosE92mVsmzDPWMWndfNq_P2-n-wDsCfNl3jfCcJke5H8t6E3t; + expires=Thu, 22-Sep-2016 02:07:31 GMT; path=/; domain=.google.com; HttpOnly'] + x-frame-options: [SAMEORIGIN] + x-xss-protection: [1; mode=block] + status: {code: 302, message: Found} +- request: + body: null + headers: + Connection: [close] + Host: [www.google.com.ar] + !!python/unicode 'User-Agent': [!!python/unicode 'Mozilla/5.001 (windows; U; + NT4.0; en-US; rv:1.0) Gecko/25250101'] + method: GET + uri: http://www.google.com.ar/search?q=github&start=0&num=10&gws_rd=cr&ei=Y_rxVtP3GIenwATdt6kw + response: + body: {string: "github\ + \ - Buscar con Google

        Account Options

          Iniciar sesi\xF3n

         

        Search Options

        • Cualquier idioma
        • P\xE1ginas en espa\xF1ol
      1. Cerca de 127.000.000\ + \ resultados
        Online project\ + \ hosting using Git. Includes source-code browser, in-line editing,
        \n\ + wikis, and ticketing. Free for public open-source code. Commercial closed\ + \ source
        \n ...

        GitHub Pages

        GitHub Pages ... Hosted directly from your GitHub repository ...

        Blog

        Engineering -\ + \ Posts - New Features - New Hires - Meetups

        GitHub Help

        Help documentation for GitHub.com, GitHub Enterprise, GitHub ...

        Explore

        Explore GitHub ... Want to get the explore page delivered to your ...

        GitHub Desktop

        GitHub Desktop is a seamless way to contribute to projects on ...

        Gist

        ...\ + \ to load this preview, sorry. Add file. Create public gist. Create ...

        GitHub - Wikipedia, la enciclopedia libre

        https://es.wikipedia.org/wiki/GitHub\u200E
        GitHub\ + \ es una forja (plataforma de desarrollo colaborativo) para alojar proyectos\ + \
        \nutilizando el sistema de control de versiones Git. Utiliza el framework\ + \ Ruby on ...

        GitHub - Wikipedia, the free encyclopedia

        https://en.wikipedia.org/wiki/GitHub\u200E
        GitHub\ + \ is a web-based Git repository hosting service. It offers all of the distributed\ + \
        \nrevision control and source code management (SCM) functionality of\ + \ Git as ...

        Documentaci\xF3n - Conociendo GitHub \u2014 Conociendo GitHub\ + \ 0.1 ...

        https://conociendogithub.readthedocs.org/
        Build the future\ + \ of communications at Twilio SIGNAL conference 2016.
        \nConociendo GitHub.\ + \ Docs \xBB; Documentaci\xF3n - Conociendo GitHub; Edit on
        \nGitHub ...

        GitHub (@github) | Twitter

        https://twitter.com/github?lang=es\u200E
        3175 tweets \u2022\ + \ 38 photos/videos \u2022 807K followers. Check out the latest Tweets
        \n\ + from GitHub (@github)

        GitHub | Slack

        https://slack.com/apps/A0F7YS2SX-github\u200E
        GitHub\ + \ offers online source code hosting for Git projects, with powerful
        \n\ + collaboration, code review, and issue tracking. This integration will post\ + \ commits,
        \npull ...

        GitHub - YouTube
        https://www.youtube.com/user/github\u200E
        How people build\ + \ software. GitHub is how people build software. More than 10
        \n\ + million people use GitHub to discover, fork, and contribute to over\ + \ 25 million p...

        Siguiente
        12345678910
        B\xFAsqueda avanzadaAyuda\ + \ de b\xFAsqueda Enviar comentarios

        "} + headers: + accept-ranges: [none] + cache-control: ['private, max-age=0'] + connection: [close] + content-type: [text/html; charset=UTF-8] + date: ['Wed, 23 Mar 2016 02:07:31 GMT'] + expires: ['-1'] + p3p: ['CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en + for more info."'] + server: [gws] + set-cookie: ['NID=77=spatKaoqzDiNX-fmmvcPHiaLQ2yiWH6OlUDNAHh9w-0ONo0MS8pI8LzhKYDMx13aQUkK-a91Ywqq7Kfix6n3zabTPmOBl4ylays4HBZT11NPAMRGOLYkAvb_eZw_64LD; + expires=Thu, 22-Sep-2016 02:07:31 GMT; path=/; domain=.google.com.ar; HttpOnly'] + vary: [Accept-Encoding] + x-frame-options: [SAMEORIGIN] + x-xss-protection: [1; mode=block] + status: {code: 200, message: OK} +version: 1 From c4fb8d63f2aa70b13537fe7c19e0803ba43350b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agusti=CC=81n=20Benassi?= Date: Tue, 22 Mar 2016 23:30:47 -0300 Subject: [PATCH 076/106] Hide shopping method because is not working anymore. --- google/google.py | 7 +- google/modules/shopping_search.py | 3 +- google/tests/test_google.py | 30 +- .../vcr_cassetes/test_convert_currency.yaml | 285 +++++++++++ .../vcr_cassetes/test_exchange_rate.yaml | 285 +++++++++++ .../vcr_cassetes/test_shopping_search.yaml | 462 ++++++++++++++++++ .../vcr_cassetes/test_standard_search.yaml | 162 +++--- 7 files changed, 1131 insertions(+), 103 deletions(-) create mode 100644 google/tests/vcr_cassetes/test_convert_currency.yaml create mode 100644 google/tests/vcr_cassetes/test_exchange_rate.yaml create mode 100644 google/tests/vcr_cassetes/test_shopping_search.yaml rename test_standard_search.yaml => google/tests/vcr_cassetes/test_standard_search.yaml (92%) diff --git a/google/google.py b/google/google.py index 18f8827..3a8f98d 100644 --- a/google/google.py +++ b/google/google.py @@ -4,7 +4,7 @@ from modules import currency from modules import calculator from modules import standard_search -from modules import shopping_search +# from modules import shopping_search __author__ = "Anthony Casagrande , " + \ "Agustin Benassi " @@ -18,7 +18,10 @@ convert_currency = currency.convert exchange_rate = currency.exchange_rate calculate = calculator.calculate -shopping = shopping_search.shopping + +# TODO: This method is not working anymore! There is a new GET +# link for this kind of search +# shopping = shopping_search.shopping if __name__ == "__main__": import doctest diff --git a/google/modules/shopping_search.py b/google/modules/shopping_search.py index 492d63f..dca28a6 100644 --- a/google/modules/shopping_search.py +++ b/google/modules/shopping_search.py @@ -33,7 +33,8 @@ def shopping(query, pages=1): j = 0 soup = BeautifulSoup(html) - products = soup.findAll("li", "g") + products = soup.findAll("div", "g") + print "yoooo", products for prod in products: res = ShoppingResult() diff --git a/google/tests/test_google.py b/google/tests/test_google.py index 7ccadab..0edbd50 100644 --- a/google/tests/test_google.py +++ b/google/tests/test_google.py @@ -71,44 +71,36 @@ def test_calculator(self, html_f): calc = google.calculate("157.3kg in grams") self.assertEqual(calc.value, 157300) - @load_html_file("html_files") - def test_exchange_rate(self, html_f): + # @load_html_file("html_files") + @vcr.use_cassette(get_dir_vcr("test_exchange_rate.yaml")) + def test_exchange_rate(self): """Test method to get an exchange rate in google.""" - # replace method to get html from a test html file - google.currency.get_html = \ - Mock(return_value=html_f.read().decode('utf8')) - usd_to_eur = google.exchange_rate("USD", "EUR") self.assertGreater(usd_to_eur, 0.0) - @load_html_file("html_files") - def test_convert_currency(self, html_f): + # @load_html_file("html_files") + @vcr.use_cassette(get_dir_vcr("test_convert_currency.yaml")) + def test_convert_currency(self): """Test method to convert currency in google.""" - # replace method to get html from a test html file - google.currency.get_html = \ - Mock(return_value=html_f.read().decode('utf8')) - euros = google.convert_currency(5.0, "USD", "EUR") self.assertGreater(euros, 0.0) # @load_html_file("html_files") - @vcr.use_cassette("test_standard_search.yaml") + @vcr.use_cassette(get_dir_vcr("test_standard_search.yaml")) def test_standard_search(self): """Test method to search in google.""" search = google.search("github") self.assertNotEqual(len(search), 0) - @load_html_file("html_files") - def test_shopping_search(self, html_f): + # @load_html_file("html_files") + @vcr.use_cassette(get_dir_vcr("test_shopping_search.yaml")) + @unittest.skip("skip") + def test_shopping_search(self): """Test method for google shopping.""" - # replace method to get html from a test html file - google.shopping_search.get_html = \ - Mock(return_value=html_f.read().decode('utf8')) - shop = google.shopping("Disgaea 4") self.assertNotEqual(len(shop), 0) diff --git a/google/tests/vcr_cassetes/test_convert_currency.yaml b/google/tests/vcr_cassetes/test_convert_currency.yaml new file mode 100644 index 0000000..37041c2 --- /dev/null +++ b/google/tests/vcr_cassetes/test_convert_currency.yaml @@ -0,0 +1,285 @@ +interactions: +- request: + body: null + headers: + Connection: [close] + Host: [!!python/unicode 'www.google.com'] + !!python/unicode 'User-Agent': [!!python/unicode 'Mozilla/5.001 (windows; U; + NT4.0; en-US; rv:1.0) Gecko/25250101'] + method: GET + uri: https://www.google.com/finance/converter?a=5.0&from=USD&to=EUR + response: + body: {string: "\n\n\nCurrency Converter - Google Finance\n\n\n\n\n
        \n\ +
        \n
        \n\ + \n
        \n\ +
        \n\n
        \n
        to
        \n
        \n\n\n\nAlbanian Lek (ALL)\n\n\n\nArgentine Peso (ARS)\n\n\n\ + \n\n\n\n\n\ + \n\n\n\ + \n\n\n\nBitcoin (\u0E3F)\n\n\n\ + \n\n\n\n\ + \n\n\n\nChinese Yuan (CN\xA5)\n\n\n\ + \n\n\n\nDjiboutian Franc (DJF)\n\n\n\ + \n\n\n\ + \n\n\n\ + \n\n\n\nGeorgian Lari (GEL)\n\n\n\ + \n\n\n\ + \n\n\n\nHaitian Gourde (HTG)\n\n\n\ + \n\n\n\nIranian Rial (IRR)\n\n\n\ + \n\n\n\nKyrgystani Som (KGS)\n\n\n\ + \n\n\n\n\ + \n\n\n\ + \n\n\n\n\ + \n\n\n\ + \n\n\n\ + \n\n\n\ + \n\n\n\n\ + \n\n\n\n\ + \n\n\n\nNew Zealand Dollar (NZ$)\n\n\n\ + \n\n\n\nPakistani Rupee (PKR)\n\n\n\ + \n\n\n\ + \n\n\n\ + \n\n\n\nSingapore Dollar (SGD)\n\n\n\ + \n\n\n\n\n\ + \n\n\n\ + \n\n\n\n\ + \n\n\n\n\ + \n\n\n\ + \n\n\n\n\ + \n\n\n\n\ + \n\n\n\nSouth African Rand (ZAR)\n\n\n\n\ + \n
        \n \n
        5 USD = 4.4585 EUR\n\n
        \n\ + \n
        \n\ + \n
        \n\n"} + headers: + accept-ranges: [none] + cache-control: ['private, max-age=0'] + connection: [close] + content-type: [text/html; charset=UTF-8] + date: ['Wed, 23 Mar 2016 02:19:11 GMT'] + expires: ['Wed, 23 Mar 2016 02:19:11 GMT'] + server: [GSE] + set-cookie: ['SC=RV=:ED=us; expires=Thu, 22-Sep-2016 02:19:11 GMT; path=/finance; + domain=.google.com'] + vary: [Accept-Encoding] + x-content-type-options: [nosniff] + x-ua-compatible: [IE=EmulateIE7] + x-xss-protection: [1; mode=block] + status: {code: 200, message: OK} +version: 1 diff --git a/google/tests/vcr_cassetes/test_exchange_rate.yaml b/google/tests/vcr_cassetes/test_exchange_rate.yaml new file mode 100644 index 0000000..034a29b --- /dev/null +++ b/google/tests/vcr_cassetes/test_exchange_rate.yaml @@ -0,0 +1,285 @@ +interactions: +- request: + body: null + headers: + Connection: [close] + Host: [!!python/unicode 'www.google.com'] + !!python/unicode 'User-Agent': [!!python/unicode 'Mozilla/5.001 (windows; U; + NT4.0; en-US; rv:1.0) Gecko/25250101'] + method: GET + uri: https://www.google.com/finance/converter?a=1&from=USD&to=EUR + response: + body: {string: "\n\n\nCurrency Converter - Google Finance\n\n\n\n\n
        \n\ +
        \n
        \n\ + \n
        \n\ +
        \n\n
        \n
        to
        \n
        \n\n\n\nAlbanian Lek (ALL)\n\n\n\nArgentine Peso (ARS)\n\n\n\ + \n\n\n\n\n\ + \n\n\n\ + \n\n\n\nBitcoin (\u0E3F)\n\n\n\ + \n\n\n\n\ + \n\n\n\nChinese Yuan (CN\xA5)\n\n\n\ + \n\n\n\nDjiboutian Franc (DJF)\n\n\n\ + \n\n\n\ + \n\n\n\ + \n\n\n\nGeorgian Lari (GEL)\n\n\n\ + \n\n\n\ + \n\n\n\nHaitian Gourde (HTG)\n\n\n\ + \n\n\n\nIranian Rial (IRR)\n\n\n\ + \n\n\n\nKyrgystani Som (KGS)\n\n\n\ + \n\n\n\n\ + \n\n\n\ + \n\n\n\n\ + \n\n\n\ + \n\n\n\ + \n\n\n\ + \n\n\n\n\ + \n\n\n\n\ + \n\n\n\nNew Zealand Dollar (NZ$)\n\n\n\ + \n\n\n\nPakistani Rupee (PKR)\n\n\n\ + \n\n\n\ + \n\n\n\ + \n\n\n\nSingapore Dollar (SGD)\n\n\n\ + \n\n\n\n\n\ + \n\n\n\ + \n\n\n\n\ + \n\n\n\n\ + \n\n\n\ + \n\n\n\n\ + \n\n\n\n\ + \n\n\n\nSouth African Rand (ZAR)\n\n\n\n\ + \n
        \n \n
        1 USD = 0.8917 EUR\n\n
        \n\ + \n
        \n\ + \n
        \n\n"} + headers: + accept-ranges: [none] + cache-control: ['private, max-age=0'] + connection: [close] + content-type: [text/html; charset=UTF-8] + date: ['Wed, 23 Mar 2016 02:19:11 GMT'] + expires: ['Wed, 23 Mar 2016 02:19:11 GMT'] + server: [GSE] + set-cookie: ['SC=RV=:ED=us; expires=Thu, 22-Sep-2016 02:19:11 GMT; path=/finance; + domain=.google.com'] + vary: [Accept-Encoding] + x-content-type-options: [nosniff] + x-ua-compatible: [IE=EmulateIE7] + x-xss-protection: [1; mode=block] + status: {code: 200, message: OK} +version: 1 diff --git a/google/tests/vcr_cassetes/test_shopping_search.yaml b/google/tests/vcr_cassetes/test_shopping_search.yaml new file mode 100644 index 0000000..52b5ef2 --- /dev/null +++ b/google/tests/vcr_cassetes/test_shopping_search.yaml @@ -0,0 +1,462 @@ +interactions: +- request: + body: null + headers: + Connection: [close] + Host: [!!python/unicode 'www.google.com'] + !!python/unicode 'User-Agent': [!!python/unicode 'Mozilla/5.001 (windows; U; + NT4.0; en-US; rv:1.0) Gecko/25250101'] + method: GET + uri: http://www.google.com/search?hl=en&q=Disgaea+4&tbm=shop&start=0&num=10 + response: + body: {string: !!python/unicode "\n302 Moved\n\ +

        302 Moved

        \nThe document has moved\nhere.\r\n\r\n"} + headers: + cache-control: [private] + connection: [close] + content-length: ['330'] + content-type: [text/html; charset=UTF-8] + date: ['Wed, 23 Mar 2016 02:20:00 GMT'] + location: ['http://www.google.com.ar/search?hl=en&q=Disgaea+4&tbm=shop&start=0&num=10&gws_rd=cr&ei=UP3xVrrrFoylwgSnm4rQCA'] + p3p: ['CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en + for more info."'] + server: [gws] + set-cookie: ['NID=77=t6vhSA2fkfZZgyUu6RXBB7A3oNtuXn6vED0oJa6MmM1-XLUtF4Rdq9f7po-oJ9UK6Em2r4YNy9cHzoj7lpUT5t1agZlxNdltg6fv04q_U2EhCFF43Ob44eQED9Vo4CIT; + expires=Thu, 22-Sep-2016 02:20:00 GMT; path=/; domain=.google.com; HttpOnly'] + x-frame-options: [SAMEORIGIN] + x-xss-protection: [1; mode=block] + status: {code: 302, message: Found} +- request: + body: null + headers: + Connection: [close] + Host: [www.google.com.ar] + !!python/unicode 'User-Agent': [!!python/unicode 'Mozilla/5.001 (windows; U; + NT4.0; en-US; rv:1.0) Gecko/25250101'] + method: GET + uri: http://www.google.com.ar/search?hl=en&q=Disgaea+4&tbm=shop&start=0&num=10&gws_rd=cr&ei=UP3xVrrrFoylwgSnm4rQCA + response: + body: {string: !!python/unicode 'Disgaea + 4 - Google Search

         

        Your search - Disgaea 4 + - did not match any documents.

        Suggestions:

        • Make sure that all words are spelled correctly.
        • Try + different keywords.
        • Try more general keywords.
        • Try fewer keywords.

        '} + headers: + accept-ranges: [none] + cache-control: ['private, max-age=0'] + connection: [close] + content-type: [text/html; charset=UTF-8] + date: ['Wed, 23 Mar 2016 02:20:00 GMT'] + expires: ['-1'] + p3p: ['CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en + for more info."'] + server: [gws] + set-cookie: ['NID=77=uUzxfQZ91yrl1UW6iQb2JTS8CDi8otx8msfTAsi7GisngMa5p5inzRoCv3GCFAJQcNh-61OF3gNZB1qFkYXwYc9My9UzKnSPnbu5wnVuAzqvY9SSUwcWbfmUSXri1UA6; + expires=Thu, 22-Sep-2016 02:20:00 GMT; path=/; domain=.google.com.ar; HttpOnly'] + vary: [Accept-Encoding] + x-frame-options: [SAMEORIGIN] + x-xss-protection: [1; mode=block] + status: {code: 200, message: OK} +version: 1 diff --git a/test_standard_search.yaml b/google/tests/vcr_cassetes/test_standard_search.yaml similarity index 92% rename from test_standard_search.yaml rename to google/tests/vcr_cassetes/test_standard_search.yaml index 578b821..a3aaeda 100644 --- a/test_standard_search.yaml +++ b/google/tests/vcr_cassetes/test_standard_search.yaml @@ -11,20 +11,20 @@ interactions: response: body: {string: !!python/unicode "\n302 Moved\n\ -

        302 Moved

        \nThe document has moved\n302 Moved\nThe document has moved\nhere.\r\n\r\n"} headers: cache-control: [private] connection: [close] content-length: ['302'] content-type: [text/html; charset=UTF-8] - date: ['Wed, 23 Mar 2016 02:07:31 GMT'] - location: ['http://www.google.com.ar/search?q=github&start=0&num=10&gws_rd=cr&ei=Y_rxVtP3GIenwATdt6kw'] + date: ['Wed, 23 Mar 2016 02:19:12 GMT'] + location: ['http://www.google.com.ar/search?q=github&start=0&num=10&gws_rd=cr&ei=IP3xVpRqh8DABJfCoNgK'] p3p: ['CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info."'] server: [gws] - set-cookie: ['NID=77=MKPmowcdOv61Yajhblditp4lg0Vm72pUkZEg-YO0z4u57y2ys7MkKNqgNaVsBv6RCn3BBhOlJ52fXjnosE92mVsmzDPWMWndfNq_P2-n-wDsCfNl3jfCcJke5H8t6E3t; - expires=Thu, 22-Sep-2016 02:07:31 GMT; path=/; domain=.google.com; HttpOnly'] + set-cookie: ['NID=77=HgFZ_LQFk_-QQHu9bg8-tuGB8nCiuFrp1LmyJmYnfhN6VZeaVGWPv-Ggs_R6_dhIN8o0s7stsJhnPintOeHCc9i9xxCIvecYRRYfQP05a8-6Hska3iRv6BXrpgCZXi12; + expires=Thu, 22-Sep-2016 02:19:12 GMT; path=/; domain=.google.com; HttpOnly'] x-frame-options: [SAMEORIGIN] x-xss-protection: [1; mode=block] status: {code: 302, message: Found} @@ -36,15 +36,15 @@ interactions: !!python/unicode 'User-Agent': [!!python/unicode 'Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101'] method: GET - uri: http://www.google.com.ar/search?q=github&start=0&num=10&gws_rd=cr&ei=Y_rxVtP3GIenwATdt6kw + uri: http://www.google.com.ar/search?q=github&start=0&num=10&gws_rd=cr&ei=IP3xVpRqh8DABJfCoNgK response: body: {string: "