user_agent_parser.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. # Copyright 2009 Google Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the 'License')
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an 'AS IS' BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Python implementation of the UA parser."""
  15. from __future__ import absolute_import
  16. import os
  17. import re
  18. __author__ = 'Lindsey Simon <elsigh@gmail.com>'
  19. class UserAgentParser(object):
  20. def __init__(self, pattern, family_replacement=None, v1_replacement=None, v2_replacement=None):
  21. """Initialize UserAgentParser.
  22. Args:
  23. pattern: a regular expression string
  24. family_replacement: a string to override the matched family (optional)
  25. v1_replacement: a string to override the matched v1 (optional)
  26. v2_replacement: a string to override the matched v2 (optional)
  27. """
  28. self.pattern = pattern
  29. self.user_agent_re = re.compile(self.pattern)
  30. self.family_replacement = family_replacement
  31. self.v1_replacement = v1_replacement
  32. self.v2_replacement = v2_replacement
  33. def MatchSpans(self, user_agent_string):
  34. match_spans = []
  35. match = self.user_agent_re.search(user_agent_string)
  36. if match:
  37. match_spans = [match.span(group_index)
  38. for group_index in range(1, match.lastindex + 1)]
  39. return match_spans
  40. def Parse(self, user_agent_string):
  41. family, v1, v2, v3 = None, None, None, None
  42. match = self.user_agent_re.search(user_agent_string)
  43. if match:
  44. if self.family_replacement:
  45. if re.search(r'\$1', self.family_replacement):
  46. family = re.sub(r'\$1', match.group(1), self.family_replacement)
  47. else:
  48. family = self.family_replacement
  49. else:
  50. family = match.group(1)
  51. if self.v1_replacement:
  52. v1 = self.v1_replacement
  53. elif match.lastindex and match.lastindex >= 2:
  54. v1 = match.group(2)
  55. if self.v2_replacement:
  56. v2 = self.v2_replacement
  57. elif match.lastindex and match.lastindex >= 3:
  58. v2 = match.group(3)
  59. if match.lastindex and match.lastindex >= 4:
  60. v3 = match.group(4)
  61. return family, v1, v2, v3
  62. class OSParser(object):
  63. def __init__(self, pattern, os_replacement=None,
  64. os_v1_replacement=None, os_v2_replacement=None,
  65. os_v3_replacement=None, os_v4_replacement=None):
  66. """Initialize UserAgentParser.
  67. Args:
  68. pattern: a regular expression string
  69. os_replacement: a string to override the matched os (optional)
  70. os_v1_replacement: a string to override the matched v1 (optional)
  71. os_v2_replacement: a string to override the matched v2 (optional)
  72. os_v3_replacement: a string to override the matched v3 (optional)
  73. os_v4_replacement: a string to override the matched v4 (optional)
  74. """
  75. self.pattern = pattern
  76. self.user_agent_re = re.compile(self.pattern)
  77. self.os_replacement = os_replacement
  78. self.os_v1_replacement = os_v1_replacement
  79. self.os_v2_replacement = os_v2_replacement
  80. self.os_v3_replacement = os_v3_replacement
  81. self.os_v4_replacement = os_v4_replacement
  82. def MatchSpans(self, user_agent_string):
  83. match_spans = []
  84. match = self.user_agent_re.search(user_agent_string)
  85. if match:
  86. match_spans = [match.span(group_index)
  87. for group_index in range(1, match.lastindex + 1)]
  88. return match_spans
  89. def Parse(self, user_agent_string):
  90. os, os_v1, os_v2, os_v3, os_v4 = None, None, None, None, None
  91. match = self.user_agent_re.search(user_agent_string)
  92. if match:
  93. if self.os_replacement:
  94. if re.search(r'\$1', self.os_replacement):
  95. os = re.sub(r'\$1', match.group(1), self.os_replacement)
  96. else:
  97. os = self.os_replacement
  98. elif match.lastindex:
  99. os = match.group(1)
  100. if self.os_v1_replacement:
  101. if re.search(r'\$1', self.os_v1_replacement):
  102. os_v1 = re.sub(r'\$1', match.group(1), self.os_v1_replacement)
  103. else:
  104. os_v1 = self.os_v1_replacement
  105. elif match.lastindex and match.lastindex >= 2:
  106. os_v1 = match.group(2)
  107. if self.os_v2_replacement:
  108. os_v2 = self.os_v2_replacement
  109. elif match.lastindex and match.lastindex >= 3:
  110. os_v2 = match.group(3)
  111. if self.os_v3_replacement:
  112. os_v3 = self.os_v3_replacement
  113. elif match.lastindex and match.lastindex >= 4:
  114. os_v3 = match.group(4)
  115. if self.os_v4_replacement:
  116. os_v4 = self.os_v4_replacement
  117. elif match.lastindex and match.lastindex >= 5:
  118. os_v4 = match.group(5)
  119. return os, os_v1, os_v2, os_v3, os_v4
  120. class DeviceParser(object):
  121. def __init__(self, pattern, regex_flag=None, device_replacement=None, brand_replacement=None,
  122. model_replacement=None):
  123. """Initialize UserAgentParser.
  124. Args:
  125. pattern: a regular expression string
  126. device_replacement: a string to override the matched device (optional)
  127. """
  128. self.pattern = pattern
  129. if regex_flag == 'i':
  130. self.user_agent_re = re.compile(self.pattern, re.IGNORECASE)
  131. else:
  132. self.user_agent_re = re.compile(self.pattern)
  133. self.device_replacement = device_replacement
  134. self.brand_replacement = brand_replacement
  135. self.model_replacement = model_replacement
  136. def MatchSpans(self, user_agent_string):
  137. match_spans = []
  138. match = self.user_agent_re.search(user_agent_string)
  139. if match:
  140. match_spans = [match.span(group_index)
  141. for group_index in range(1, match.lastindex + 1)]
  142. return match_spans
  143. def MultiReplace(self, string, match):
  144. def _repl(m):
  145. index = int(m.group(1)) - 1
  146. group = match.groups()
  147. if index < len(group):
  148. return group[index]
  149. return ''
  150. _string = re.sub(r'\$(\d)', _repl, string)
  151. _string = re.sub(r'^\s+|\s+$', '', _string)
  152. if _string == '':
  153. return None
  154. return _string
  155. def Parse(self, user_agent_string):
  156. device, brand, model = None, None, None
  157. match = self.user_agent_re.search(user_agent_string)
  158. if match:
  159. if self.device_replacement:
  160. device = self.MultiReplace(self.device_replacement, match)
  161. else:
  162. device = match.group(1)
  163. if self.brand_replacement:
  164. brand = self.MultiReplace(self.brand_replacement, match)
  165. if self.model_replacement:
  166. model = self.MultiReplace(self.model_replacement, match)
  167. elif len(match.groups()) > 0:
  168. model = match.group(1)
  169. return device, brand, model
  170. MAX_CACHE_SIZE = 20
  171. _parse_cache = {}
  172. def Parse(user_agent_string, **jsParseBits):
  173. """ Parse all the things
  174. Args:
  175. user_agent_string: the full user agent string
  176. jsParseBits: javascript override bits
  177. Returns:
  178. A dictionary containing all parsed bits
  179. """
  180. jsParseBits = jsParseBits or {}
  181. key = (user_agent_string, repr(jsParseBits))
  182. cached = _parse_cache.get(key)
  183. if cached is not None:
  184. return cached
  185. if len(_parse_cache) > MAX_CACHE_SIZE:
  186. _parse_cache.clear()
  187. v = {
  188. 'user_agent': ParseUserAgent(user_agent_string, **jsParseBits),
  189. 'os': ParseOS(user_agent_string, **jsParseBits),
  190. 'device': ParseDevice(user_agent_string, **jsParseBits),
  191. 'string': user_agent_string
  192. }
  193. _parse_cache[key] = v
  194. return v
  195. def ParseUserAgent(user_agent_string, **jsParseBits):
  196. """ Parses the user-agent string for user agent (browser) info.
  197. Args:
  198. user_agent_string: The full user-agent string.
  199. jsParseBits: javascript override bits.
  200. Returns:
  201. A dictionary containing parsed bits.
  202. """
  203. if 'js_user_agent_family' in jsParseBits and jsParseBits['js_user_agent_family'] != '':
  204. family = jsParseBits['js_user_agent_family']
  205. if 'js_user_agent_v1' in jsParseBits:
  206. v1 = jsParseBits['js_user_agent_v1'] or None
  207. if 'js_user_agent_v2' in jsParseBits:
  208. v2 = jsParseBits['js_user_agent_v2'] or None
  209. if 'js_user_agent_v3' in jsParseBits:
  210. v3 = jsParseBits['js_user_agent_v3'] or None
  211. else:
  212. for uaParser in USER_AGENT_PARSERS:
  213. family, v1, v2, v3 = uaParser.Parse(user_agent_string)
  214. if family:
  215. break
  216. # Override for Chrome Frame IFF Chrome is enabled.
  217. if 'js_user_agent_string' in jsParseBits:
  218. js_user_agent_string = jsParseBits['js_user_agent_string']
  219. if (
  220. js_user_agent_string and js_user_agent_string.find('Chrome/') > -1 and
  221. user_agent_string.find('chromeframe') > -1
  222. ):
  223. jsOverride = {}
  224. jsOverride = ParseUserAgent(js_user_agent_string)
  225. family = 'Chrome Frame (%s %s)' % (family, v1)
  226. v1 = jsOverride['major']
  227. v2 = jsOverride['minor']
  228. v3 = jsOverride['patch']
  229. family = family or 'Other'
  230. return {
  231. 'family': family,
  232. 'major': v1,
  233. 'minor': v2,
  234. 'patch': v3
  235. }
  236. def ParseOS(user_agent_string, **jsParseBits):
  237. """ Parses the user-agent string for operating system info
  238. Args:
  239. user_agent_string: The full user-agent string.
  240. jsParseBits: javascript override bits.
  241. Returns:
  242. A dictionary containing parsed bits.
  243. """
  244. for osParser in OS_PARSERS:
  245. os, os_v1, os_v2, os_v3, os_v4 = osParser.Parse(user_agent_string)
  246. if os:
  247. break
  248. os = os or 'Other'
  249. return {
  250. 'family': os,
  251. 'major': os_v1,
  252. 'minor': os_v2,
  253. 'patch': os_v3,
  254. 'patch_minor': os_v4
  255. }
  256. def ParseDevice(user_agent_string):
  257. """ Parses the user-agent string for device info.
  258. Args:
  259. user_agent_string: The full user-agent string.
  260. ua_family: The parsed user agent family name.
  261. Returns:
  262. A dictionary containing parsed bits.
  263. """
  264. for deviceParser in DEVICE_PARSERS:
  265. device, brand, model = deviceParser.Parse(user_agent_string)
  266. if device:
  267. break
  268. if device is None:
  269. device = 'Other'
  270. return {
  271. 'family': device,
  272. 'brand': brand,
  273. 'model': model
  274. }
  275. def PrettyUserAgent(family, v1=None, v2=None, v3=None):
  276. """Pretty user agent string."""
  277. if v3:
  278. if v3[0].isdigit():
  279. return '%s %s.%s.%s' % (family, v1, v2, v3)
  280. else:
  281. return '%s %s.%s%s' % (family, v1, v2, v3)
  282. elif v2:
  283. return '%s %s.%s' % (family, v1, v2)
  284. elif v1:
  285. return '%s %s' % (family, v1)
  286. return family
  287. def PrettyOS(os, os_v1=None, os_v2=None, os_v3=None, os_v4=None):
  288. """Pretty os string."""
  289. if os_v4:
  290. return '%s %s.%s.%s.%s' % (os, os_v1, os_v2, os_v3, os_v4)
  291. if os_v3:
  292. if os_v3[0].isdigit():
  293. return '%s %s.%s.%s' % (os, os_v1, os_v2, os_v3)
  294. else:
  295. return '%s %s.%s%s' % (os, os_v1, os_v2, os_v3)
  296. elif os_v2:
  297. return '%s %s.%s' % (os, os_v1, os_v2)
  298. elif os_v1:
  299. return '%s %s' % (os, os_v1)
  300. return os
  301. def ParseWithJSOverrides(user_agent_string,
  302. js_user_agent_string=None,
  303. js_user_agent_family=None,
  304. js_user_agent_v1=None,
  305. js_user_agent_v2=None,
  306. js_user_agent_v3=None):
  307. """ backwards compatible. use one of the other Parse methods instead! """
  308. # Override via JS properties.
  309. if js_user_agent_family is not None and js_user_agent_family != '':
  310. family = js_user_agent_family
  311. v1 = None
  312. v2 = None
  313. v3 = None
  314. if js_user_agent_v1 is not None:
  315. v1 = js_user_agent_v1
  316. if js_user_agent_v2 is not None:
  317. v2 = js_user_agent_v2
  318. if js_user_agent_v3 is not None:
  319. v3 = js_user_agent_v3
  320. else:
  321. for parser in USER_AGENT_PARSERS:
  322. family, v1, v2, v3 = parser.Parse(user_agent_string)
  323. if family:
  324. break
  325. # Override for Chrome Frame IFF Chrome is enabled.
  326. if (
  327. js_user_agent_string and js_user_agent_string.find('Chrome/') > -1 and
  328. user_agent_string.find('chromeframe') > -1
  329. ):
  330. family = 'Chrome Frame (%s %s)' % (family, v1)
  331. ua_dict = ParseUserAgent(js_user_agent_string)
  332. v1 = ua_dict['major']
  333. v2 = ua_dict['minor']
  334. v3 = ua_dict['patch']
  335. return family or 'Other', v1, v2, v3
  336. def Pretty(family, v1=None, v2=None, v3=None):
  337. """ backwards compatible. use PrettyUserAgent instead! """
  338. if v3:
  339. if v3[0].isdigit():
  340. return '%s %s.%s.%s' % (family, v1, v2, v3)
  341. else:
  342. return '%s %s.%s%s' % (family, v1, v2, v3)
  343. elif v2:
  344. return '%s %s.%s' % (family, v1, v2)
  345. elif v1:
  346. return '%s %s' % (family, v1)
  347. return family
  348. def GetFilters(user_agent_string, js_user_agent_string=None,
  349. js_user_agent_family=None,
  350. js_user_agent_v1=None,
  351. js_user_agent_v2=None,
  352. js_user_agent_v3=None):
  353. """Return the optional arguments that should be saved and used to query.
  354. js_user_agent_string is always returned if it is present. We really only need
  355. it for Chrome Frame. However, I added it in the generally case to find other
  356. cases when it is different. When the recording of js_user_agent_string was
  357. added, we created new records for all new user agents.
  358. Since we only added js_document_mode for the IE 9 preview case, it did not
  359. cause new user agent records the way js_user_agent_string did.
  360. js_document_mode has since been removed in favor of individual property
  361. overrides.
  362. Args:
  363. user_agent_string: The full user-agent string.
  364. js_user_agent_string: JavaScript ua string from client-side
  365. js_user_agent_family: This is an override for the family name to deal
  366. with the fact that IE platform preview (for instance) cannot be
  367. distinguished by user_agent_string, but only in javascript.
  368. js_user_agent_v1: v1 override - see above.
  369. js_user_agent_v2: v1 override - see above.
  370. js_user_agent_v3: v1 override - see above.
  371. Returns:
  372. {js_user_agent_string: '[...]', js_family_name: '[...]', etc...}
  373. """
  374. filters = {}
  375. filterdict = {
  376. 'js_user_agent_string': js_user_agent_string,
  377. 'js_user_agent_family': js_user_agent_family,
  378. 'js_user_agent_v1': js_user_agent_v1,
  379. 'js_user_agent_v2': js_user_agent_v2,
  380. 'js_user_agent_v3': js_user_agent_v3
  381. }
  382. for key, value in filterdict.items():
  383. if value is not None and value != '':
  384. filters[key] = value
  385. return filters
  386. # Build the list of user agent parsers from YAML
  387. UA_PARSER_YAML = os.environ.get("UA_PARSER_YAML")
  388. if UA_PARSER_YAML:
  389. # This will raise an ImportError if missing, obviously since it's no
  390. # longer a requirement
  391. import yaml
  392. try:
  393. # Try and use libyaml bindings if available since faster
  394. from yaml import CSafeLoader as SafeLoader
  395. except ImportError:
  396. from yaml import SafeLoader
  397. with open(UA_PARSER_YAML) as fp:
  398. regexes = yaml.load(fp, Loader=SafeLoader)
  399. USER_AGENT_PARSERS = []
  400. for _ua_parser in regexes['user_agent_parsers']:
  401. _regex = _ua_parser['regex']
  402. _family_replacement = None
  403. if 'family_replacement' in _ua_parser:
  404. _family_replacement = _ua_parser['family_replacement']
  405. _v1_replacement = None
  406. if 'v1_replacement' in _ua_parser:
  407. _v1_replacement = _ua_parser['v1_replacement']
  408. _v2_replacement = None
  409. if 'v2_replacement' in _ua_parser:
  410. _v2_replacement = _ua_parser['v2_replacement']
  411. USER_AGENT_PARSERS.append(UserAgentParser(_regex,
  412. _family_replacement,
  413. _v1_replacement,
  414. _v2_replacement))
  415. OS_PARSERS = []
  416. for _os_parser in regexes['os_parsers']:
  417. _regex = _os_parser['regex']
  418. _os_replacement = None
  419. if 'os_replacement' in _os_parser:
  420. _os_replacement = _os_parser['os_replacement']
  421. _os_v1_replacement = None
  422. if 'os_v1_replacement' in _os_parser:
  423. _os_v1_replacement = _os_parser['os_v1_replacement']
  424. _os_v2_replacement = None
  425. if 'os_v2_replacement' in _os_parser:
  426. _os_v2_replacement = _os_parser['os_v2_replacement']
  427. _os_v3_replacement = None
  428. if 'os_v3_replacement' in _os_parser:
  429. _os_v3_replacement = _os_parser['os_v3_replacement']
  430. _os_v4_replacement = None
  431. if 'os_v4_replacement' in _os_parser:
  432. _os_v4_replacement = _os_parser['os_v4_replacement']
  433. OS_PARSERS.append(OSParser(_regex,
  434. _os_replacement,
  435. _os_v1_replacement,
  436. _os_v2_replacement,
  437. _os_v3_replacement,
  438. _os_v4_replacement))
  439. DEVICE_PARSERS = []
  440. for _device_parser in regexes['device_parsers']:
  441. _regex = _device_parser['regex']
  442. _regex_flag = None
  443. if 'regex_flag' in _device_parser:
  444. _regex_flag = _device_parser['regex_flag']
  445. _device_replacement = None
  446. if 'device_replacement' in _device_parser:
  447. _device_replacement = _device_parser['device_replacement']
  448. _brand_replacement = None
  449. if 'brand_replacement' in _device_parser:
  450. _brand_replacement = _device_parser['brand_replacement']
  451. _model_replacement = None
  452. if 'model_replacement' in _device_parser:
  453. _model_replacement = _device_parser['model_replacement']
  454. DEVICE_PARSERS.append(DeviceParser(_regex,
  455. _regex_flag,
  456. _device_replacement,
  457. _brand_replacement,
  458. _model_replacement))
  459. # Clean our our temporary vars explicitly
  460. # so they can't be reused or imported
  461. del regexes
  462. del yaml
  463. del SafeLoader
  464. else:
  465. # Just load our pre-compiled versions
  466. from ._regexes import USER_AGENT_PARSERS, DEVICE_PARSERS, OS_PARSERS