parsers.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. from collections import namedtuple
  2. from ua_parser import user_agent_parser
  3. from .compat import string_types
  4. MOBILE_DEVICE_FAMILIES = (
  5. 'iPhone',
  6. 'iPod',
  7. 'Generic Smartphone',
  8. 'Generic Feature Phone',
  9. 'PlayStation Vita',
  10. 'iOS-Device'
  11. )
  12. PC_OS_FAMILIES = (
  13. 'Windows 95',
  14. 'Windows 98',
  15. 'Windows ME',
  16. 'Solaris',
  17. )
  18. MOBILE_OS_FAMILIES = (
  19. 'Windows Phone',
  20. 'Windows Phone OS', # Earlier versions of ua-parser returns Windows Phone OS
  21. 'Symbian OS',
  22. 'Bada',
  23. 'Windows CE',
  24. 'Windows Mobile',
  25. 'Maemo',
  26. )
  27. MOBILE_BROWSER_FAMILIES = (
  28. 'Opera Mobile',
  29. 'Opera Mini',
  30. )
  31. TABLET_DEVICE_FAMILIES = (
  32. 'iPad',
  33. 'BlackBerry Playbook',
  34. 'Blackberry Playbook', # Earlier versions of ua-parser returns "Blackberry" instead of "BlackBerry"
  35. 'Kindle',
  36. 'Kindle Fire',
  37. 'Kindle Fire HD',
  38. 'Galaxy Tab',
  39. 'Xoom',
  40. 'Dell Streak',
  41. )
  42. TOUCH_CAPABLE_OS_FAMILIES = (
  43. 'iOS',
  44. 'Android',
  45. 'Windows Phone',
  46. 'Windows Phone OS',
  47. 'Windows RT',
  48. 'Windows CE',
  49. 'Windows Mobile',
  50. 'Firefox OS',
  51. 'MeeGo',
  52. )
  53. TOUCH_CAPABLE_DEVICE_FAMILIES = (
  54. 'BlackBerry Playbook',
  55. 'Blackberry Playbook',
  56. 'Kindle Fire',
  57. )
  58. EMAIL_PROGRAM_FAMILIES = {
  59. 'Outlook',
  60. 'Windows Live Mail',
  61. 'AirMail',
  62. 'Apple Mail',
  63. 'Outlook',
  64. 'Thunderbird',
  65. 'Lightning',
  66. 'ThunderBrowse',
  67. 'Windows Live Mail',
  68. 'The Bat!',
  69. 'Lotus Notes',
  70. 'IBM Notes',
  71. 'Barca',
  72. 'MailBar',
  73. 'kmail2',
  74. 'YahooMobileMail'
  75. }
  76. def verify_attribute(attribute):
  77. if isinstance(attribute, string_types) and attribute.isdigit():
  78. return int(attribute)
  79. return attribute
  80. def parse_version(major=None, minor=None, patch=None, patch_minor=None):
  81. # Returns version number tuple, attributes will be integer if they're numbers
  82. major = verify_attribute(major)
  83. minor = verify_attribute(minor)
  84. patch = verify_attribute(patch)
  85. patch_minor = verify_attribute(patch_minor)
  86. return tuple(
  87. filter(lambda x: x is not None, (major, minor, patch, patch_minor))
  88. )
  89. Browser = namedtuple('Browser', ['family', 'version', 'version_string'])
  90. def parse_browser(family, major=None, minor=None, patch=None, patch_minor=None):
  91. # Returns a browser object
  92. version = parse_version(major, minor, patch)
  93. version_string = '.'.join([str(v) for v in version])
  94. return Browser(family, version, version_string)
  95. OperatingSystem = namedtuple('OperatingSystem', ['family', 'version', 'version_string'])
  96. def parse_operating_system(family, major=None, minor=None, patch=None, patch_minor=None):
  97. version = parse_version(major, minor, patch)
  98. version_string = '.'.join([str(v) for v in version])
  99. return OperatingSystem(family, version, version_string)
  100. Device = namedtuple('Device', ['family', 'brand', 'model'])
  101. def parse_device(family, brand, model):
  102. return Device(family, brand, model)
  103. class UserAgent(object):
  104. def __init__(self, user_agent_string):
  105. ua_dict = user_agent_parser.Parse(user_agent_string)
  106. self.ua_string = user_agent_string
  107. self.os = parse_operating_system(**ua_dict['os'])
  108. self.browser = parse_browser(**ua_dict['user_agent'])
  109. self.device = parse_device(**ua_dict['device'])
  110. def __str__(self):
  111. device = self.is_pc and "PC" or self.device.family
  112. os = ("%s %s" % (self.os.family, self.os.version_string)).strip()
  113. browser = ("%s %s" % (self.browser.family, self.browser.version_string)).strip()
  114. return " / ".join([device, os, browser])
  115. def __unicode__(self):
  116. return unicode(str(self))
  117. def _is_android_tablet(self):
  118. # Newer Android tablets don't have "Mobile" in their user agent string,
  119. # older ones like Galaxy Tab still have "Mobile" though they're not
  120. if ('Mobile Safari' not in self.ua_string and
  121. self.browser.family != "Firefox Mobile"):
  122. return True
  123. return False
  124. def _is_blackberry_touch_capable_device(self):
  125. # A helper to determine whether a BB phone has touch capabilities
  126. # Blackberry Bold Touch series begins with 99XX
  127. if 'Blackberry 99' in self.device.family:
  128. return True
  129. if 'Blackberry 95' in self.device.family: # BB Storm devices
  130. return True
  131. if 'Blackberry 95' in self.device.family: # BB Torch devices
  132. return True
  133. return False
  134. @property
  135. def is_tablet(self):
  136. if self.device.family in TABLET_DEVICE_FAMILIES:
  137. return True
  138. if (self.os.family == 'Android' and self._is_android_tablet()):
  139. return True
  140. if self.os.family.startswith('Windows RT'):
  141. return True
  142. if self.os.family == 'Firefox OS' and 'Mobile' not in self.browser.family:
  143. return True
  144. return False
  145. @property
  146. def is_mobile(self):
  147. # First check for mobile device and mobile browser families
  148. if self.device.family in MOBILE_DEVICE_FAMILIES:
  149. return True
  150. if self.browser.family in MOBILE_BROWSER_FAMILIES:
  151. return True
  152. # Device is considered Mobile OS is Android and not tablet
  153. # This is not fool proof but would have to suffice for now
  154. if ((self.os.family == 'Android' or self.os.family == 'Firefox OS')
  155. and not self.is_tablet):
  156. return True
  157. if self.os.family == 'BlackBerry OS' and self.device.family != 'Blackberry Playbook':
  158. return True
  159. if self.os.family in MOBILE_OS_FAMILIES:
  160. return True
  161. # TODO: remove after https://github.com/tobie/ua-parser/issues/126 is closed
  162. if 'J2ME' in self.ua_string or 'MIDP' in self.ua_string:
  163. return True
  164. # This is here mainly to detect Google's Mobile Spider
  165. if 'iPhone;' in self.ua_string:
  166. return True
  167. if 'Googlebot-Mobile' in self.ua_string:
  168. return True
  169. # Mobile Spiders should be identified as mobile
  170. if self.device.family == 'Spider' and 'Mobile' in self.browser.family:
  171. return True
  172. # Nokia mobile
  173. if 'NokiaBrowser' in self.ua_string and 'Mobile' in self.ua_string:
  174. return True
  175. return False
  176. @property
  177. def is_touch_capable(self):
  178. # TODO: detect touch capable Nokia devices
  179. if self.os.family in TOUCH_CAPABLE_OS_FAMILIES:
  180. return True
  181. if self.device.family in TOUCH_CAPABLE_DEVICE_FAMILIES:
  182. return True
  183. if self.os.family.startswith('Windows 8') and 'Touch' in self.ua_string:
  184. return True
  185. if 'BlackBerry' in self.os.family and self._is_blackberry_touch_capable_device():
  186. return True
  187. return False
  188. @property
  189. def is_pc(self):
  190. # Returns True for "PC" devices (Windows, Mac and Linux)
  191. if 'Windows NT' in self.ua_string or self.os.family in PC_OS_FAMILIES:
  192. return True
  193. # TODO: remove after https://github.com/tobie/ua-parser/issues/127 is closed
  194. if self.os.family == 'Mac OS X' and 'Silk' not in self.ua_string:
  195. return True
  196. # Maemo has 'Linux' and 'X11' in UA, but it is not for PC
  197. if 'Maemo' in self.ua_string:
  198. return False
  199. if 'Chrome OS' in self.os.family:
  200. return True
  201. if 'Linux' in self.ua_string and 'X11' in self.ua_string:
  202. return True
  203. return False
  204. @property
  205. def is_bot(self):
  206. return True if self.device.family == 'Spider' else False
  207. @property
  208. def is_email_client(self):
  209. if self.browser.family in EMAIL_PROGRAM_FAMILIES:
  210. return True
  211. return False
  212. def parse(user_agent_string):
  213. return UserAgent(user_agent_string)