├── uaDevice ├── __init__.py ├── version.py ├── utils.py ├── ua_parser.py ├── os_detector.py ├── browser_detector.py └── device_detector.py ├── .travis.yml ├── .gitignore ├── setup.py ├── README.md └── test └── test.py /uaDevice/__init__.py: -------------------------------------------------------------------------------- 1 | from uaDevice.ua_parser import parseUA -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 2.7 4 | 5 | script: 6 | - "python test/test.py" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | .idea 4 | logs 5 | log 6 | output 7 | package-lock.json 8 | .DS_Store 9 | views 10 | tmp 11 | public 12 | python/*.txt 13 | *.pyc 14 | test1 15 | data10000.txt 16 | not_sumbit_test 17 | dist 18 | uaDevice.egg-info 19 | test_old.py 20 | *.txt 21 | old -------------------------------------------------------------------------------- /uaDevice/version.py: -------------------------------------------------------------------------------- 1 | class Version: 2 | """版本信息类""" 3 | def __init__(self, v=None): 4 | """ 5 | 初始化版本信息 6 | v: 字典类型,包含 value(原始版本号)、alias(别名)、details(详细信息) 等字段 7 | """ 8 | v = v or {} 9 | self.original = v.get('value', '') # 原始版本号 10 | self.alias = v.get('alias', '') # 版本别名 11 | self.details = v.get('details', None) # 详细信息 12 | self.minor = -1 # 次版本号 13 | self.type = '' # 版本类型 14 | 15 | def __str__(self): 16 | """字符串表示""" 17 | return self.original 18 | 19 | def is_equal(self, v): 20 | """判断版本是否相等""" 21 | if not v: 22 | return False 23 | return self.original == v 24 | 25 | def is_greater_than(self, v): 26 | """判断是否大于指定版本""" 27 | if not v: 28 | return False 29 | try: 30 | current = [int(x) for x in self.original.split('.')] 31 | compare = [int(x) for x in str(v).split('.')] 32 | return current > compare 33 | except (ValueError, AttributeError): 34 | return False 35 | 36 | def is_less_than(self, v): 37 | """判断是否小于指定版本""" 38 | if not v: 39 | return False 40 | try: 41 | current = [int(x) for x in self.original.split('.')] 42 | compare = [int(x) for x in str(v).split('.')] 43 | return current < compare 44 | except (ValueError, AttributeError): 45 | return False -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding:utf-8 -*- 3 | 4 | ############################################# 5 | # File Name: setup.py 6 | # Author: kaivean 7 | # Mail: kaivean@outlook.com 8 | # 1: modify following version 9 | # 2. build and upload 10 | # usage test: rm -fr dist && python3 setup.py sdist && twine upload --repository-url https://test.pypi.org/legacy/ dist/uaDevice-*.tar.gz 11 | # usage: rm -fr dist && python3 setup.py sdist && twine upload dist/uaDevice-*.tar.gz 12 | ############################################# 13 | 14 | import os 15 | from setuptools import setup, find_packages 16 | ROOT = os.path.dirname(os.path.realpath(__file__)) 17 | 18 | import sys 19 | import re 20 | import os 21 | import json 22 | 23 | if sys.version_info < (3, 0): 24 | desc = open(os.path.join(ROOT, 'README.md')).read() 25 | else: 26 | desc = open(os.path.join(ROOT, 'README.md'), encoding='UTF-8').read() 27 | 28 | setup( 29 | name = "uaDevice", 30 | version = "2.0.1", 31 | keywords = ("ua", "user-agent", "User Agent", "parser", "device", "os", "browser", "engine", "data analysis", "china", "中国", "国内"), 32 | description = "User Agent parser, More accurate", 33 | long_description = desc, 34 | long_description_content_type="text/markdown", 35 | license = "MIT Licence", 36 | 37 | url = "https://github.com/kaivean/python-ua-device", 38 | author = "kaivean", 39 | author_email = "kaivean@outlook.com", 40 | 41 | packages = find_packages(exclude=['*.test', '*.test.*', 'test.*', 'test', 'test.py']), 42 | include_package_data = True, 43 | platforms = "any", 44 | install_requires = [] 45 | ) 46 | -------------------------------------------------------------------------------- /uaDevice/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | # 正则表达式缓存 4 | regObj = {} 5 | 6 | def optimizedSearch(regStr, matchStr, flags=0): 7 | """ 8 | 优化的正则搜索函数 9 | regStr: 正则表达式字符串 10 | matchStr: 要匹配的字符串 11 | flags: 正则标志 12 | """ 13 | if regStr not in regObj: 14 | regObj[regStr] = re.compile(regStr, flags) 15 | return regObj[regStr].search(matchStr) 16 | 17 | def optimizedSub(regStr, replaceStr, matchStr, count=0, flags=0): 18 | """ 19 | 优化的正则替换函数 20 | regStr: 正则表达式字符串 21 | replaceStr: 替换字符串 22 | matchStr: 要匹配的字符串 23 | count: 替换次数 24 | flags: 正则标志 25 | """ 26 | if regStr not in regObj: 27 | regObj[regStr] = re.compile(regStr, flags) 28 | return regObj[regStr].sub(replaceStr, matchStr, count=count) 29 | 30 | # 匹配结果缓存 31 | tmpData = { 32 | 'ua': '', 33 | 'match': None 34 | } 35 | 36 | def getMatch(string=None, reg=None, i=False): 37 | """ 38 | 获取正则匹配结果 39 | string: 要匹配的字符串 40 | reg: 正则表达式 41 | i: 是否忽略大小写 42 | """ 43 | if string is None: 44 | return tmpData['match'] 45 | flags = re.I if i else 0 46 | tmpData['match'] = optimizedSearch(reg, string, flags) 47 | return tmpData['match'] is not None 48 | 49 | def cleanupModel(s=''): 50 | """ 51 | 清理设备型号字符串 52 | s: 设备型号字符串 53 | """ 54 | if not s: 55 | return s 56 | 57 | # 基本清理 58 | s = optimizedSub(r'_TD$|_CMCC$', '', s, 1) 59 | s = optimizedSub(r'_', ' ', s) 60 | s = optimizedSub(r'^\s+|\s+$', '', s) 61 | s = optimizedSub(r'\/[^/]+$|\/[^/]+ Android\/.*', '', s, 1) 62 | 63 | # 移除常见前缀 64 | s = optimizedSub(r'^(Android on |Android for |ICS AOSP on )', '', s, 1) 65 | 66 | # 处理品牌前缀 67 | s = optimizedSub(r'^Huawei[ -]', 'Huawei ', s, 1, re.I) 68 | s = optimizedSub(r'^SAMSUNG[ -]', '', s, 1, re.I) 69 | s = optimizedSub(r'^Lenovo[ -]', 'Lenovo ', s, 1) 70 | s = optimizedSub(r'^(LG)[ _\/]', r'\1-', s, 1) 71 | s = optimizedSub(r'^(HTC)[-\/]', r'\1 ', s, 1) 72 | s = optimizedSub(r'^(Motorola[\s|-]|Moto|MOT-)', '', s, 1) 73 | 74 | # 移除运营商和URL 75 | s = optimizedSub(r'-?(orange(-ls)?|vodafone|bouygues)$', '', s, 1, re.I) 76 | s = optimizedSub(r'http:\/\/.+$', '', s, 1, re.I) 77 | 78 | return s.strip() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ua-device 2.0 2 | 解析user-agent的python包,可以获取到系统、浏览器内核、浏览器、设备信息。 3 | 4 | ## 重要提示:2.0版本重大变更 5 | ⚠️ 2.0版本进行了完全重构,与1.x版本不兼容。主要变更: 6 | 7 | 1. **简化代码结构** 8 | - 删除了大量历史设备和过时浏览器的识别规则 9 | - 重构了核心解析逻辑,提升执行效率 10 | - 代码结构更清晰,便于维护和扩展 11 | 12 | 2. **功能优化** 13 | - 优化了浏览器内核识别逻辑,准确区分Blink和WebKit 14 | - 聚焦于主流设备和浏览器的识别 15 | - 移除了低使用率的特殊规则 16 | 17 | 3. **性能提升** 18 | - 减少正则表达式的使用 19 | - 优化了匹配算法 20 | - 显著提升解析速度 21 | 22 | 如果您需要继续使用旧版本的完整功能,请安装1.x版本: 23 | 24 | ```bash 25 | pip install "uaDevice<2.0" 26 | ``` 27 | 28 | ## 特点 29 | * 相比国内外的流行的python包,该模块解析国内复杂的ua信息更加精确 30 | * 已支持鸿蒙系统解析 31 | * 准确识别Blink/WebKit等主流浏览器内核 32 | * 专注于现代浏览器和设备的识别 33 | 34 | ## 主要功能 35 | 36 | ### 1. 浏览器引擎识别 37 | * Blink (Chrome、新版Edge等) 38 | * WebKit (Safari等) 39 | * Gecko (Firefox) 40 | * Trident (IE) 41 | * Presto (旧版Opera) 42 | 43 | ### 2. 浏览器识别 44 | * 主流浏览器:Chrome、Firefox、Safari、Edge等 45 | * 国内浏览器:QQ浏览器、UC浏览器、搜狗浏览器等 46 | * 手机厂商浏览器:MIUI浏览器、华为浏览器、OPPO浏览器等 47 | * 其他常用浏览器:微信内置浏览器、百度App等 48 | 49 | ### 3. 系统识别 50 | * 桌面系统:Windows、macOS、Linux等 51 | * 移动系统:iOS、Android、Harmony等 52 | 53 | ### 4. 设备识别 54 | * 设备类型:手机、平板、桌面设备等 55 | * 设备品牌:Apple、Samsung、Huawei等 56 | * 具体机型识别 57 | 58 | ## 解析成功率 59 | 供参考(以10000个真实请求ua测试): 60 | * 浏览器:98% 61 | * 系统: 99% 62 | * 内核: 98% 63 | * 设备类型: 100% 64 | * 设备型号:86% 65 | * 厂商信息:93% 66 | 67 | ## 安装 68 | 69 | ```bash 70 | pip install -U uaDevice 71 | ``` 72 | 73 | ## 使用 74 | 75 | ```python 76 | import uaDevice 77 | 78 | # Chrome浏览器示例 79 | ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' 80 | info = uaDevice.parseUA(ua) 81 | 82 | # 输出结果示例 83 | print(f"系统: {info['os']['name']} {info['os']['version']['original']}") # Windows 10.0 84 | print(f"浏览器: {info['browser']['name']} {info['browser']['version']['original']}") # Chrome 91.0.4472.124 85 | print(f"内核: {info['engine']['name']}") # Blink 86 | print(f"设备类型: {info['device']['type']}") # desktop 87 | print(f"设备型号: {info['device']['model']}") # PC 88 | print(f"制造商: {info['device']['manufacturer']}") # Unknown 89 | 90 | # 安卓移动端示例 91 | ua = 'Mozilla/5.0 (Linux; Android 13; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36' 92 | info = uaDevice.parseUA(ua) 93 | 94 | # 输出结果示例 95 | print(f"系统: {info['os']['name']} {info['os']['version']['original']}") # Android 13 96 | print(f"浏览器: {info['browser']['name']} {info['browser']['version']['original']}") # Chrome 114.0.0.0 97 | print(f"内核: {info['engine']['name']}") # Blink 98 | print(f"设备类型: {info['device']['type']}") # mobile 99 | print(f"设备型号: {info['device']['model']}") # SM-S918B 100 | print(f"制造商: {info['device']['manufacturer']}") # Samsung 101 | ``` 102 | 103 | ## 贡献 104 | 随着新设备新app等等的上市,ua信息会越来越复杂,该项目需要不断迭代,欢迎提交PR或Issue来完善不支持的ua解析规则。 105 | -------------------------------------------------------------------------------- /uaDevice/ua_parser.py: -------------------------------------------------------------------------------- 1 | from version import Version 2 | from browser_detector import BrowserDetector 3 | from os_detector import OSDetector 4 | from device_detector import DeviceDetector 5 | 6 | class UAParser: 7 | """UA 解析主类""" 8 | def __init__(self, ua='', options=None): 9 | self.options = options or {} 10 | 11 | # 初始化检测器 12 | self.browser_detector = BrowserDetector() 13 | self.os_detector = OSDetector() 14 | self.device_detector = DeviceDetector() 15 | 16 | # 如果提供了 UA,立即解析 17 | if ua: 18 | self.parse(ua) 19 | 20 | def parse(self, ua): 21 | """解析 UA 字符串""" 22 | if not ua: 23 | return self 24 | 25 | # 按顺序进行检测 26 | self.browser_detector.detect(ua) 27 | self.os_detector.detect(ua) 28 | self.device_detector.detect(ua) 29 | 30 | return self 31 | 32 | def get_result(self): 33 | """获取解析结果""" 34 | return { 35 | 'browser': { 36 | 'name': self.browser_detector.browser['name'], 37 | 'version': { 38 | 'original': str(self.browser_detector.browser['version']) if self.browser_detector.browser['version'] else '', 39 | 'alias': self.browser_detector.browser['version'].alias if self.browser_detector.browser['version'] else '' 40 | }, 41 | 'mode': self.browser_detector.browser['mode'], 42 | 'channel': self.browser_detector.browser['channel'] 43 | }, 44 | 'engine': { 45 | 'name': self.browser_detector.engine['name'], 46 | 'version': { 47 | 'original': str(self.browser_detector.engine['version']) if self.browser_detector.engine['version'] else '', 48 | 'alias': self.browser_detector.engine['version'].alias if self.browser_detector.engine['version'] else '' 49 | } 50 | }, 51 | 'os': { 52 | 'name': self.os_detector.os['name'], 53 | 'version': { 54 | 'original': str(self.os_detector.os['version']) if self.os_detector.os['version'] else '', 55 | 'alias': self.os_detector.os['version'].alias if self.os_detector.os['version'] else '' 56 | } 57 | }, 58 | 'device': { 59 | 'type': self.device_detector.device['type'], 60 | 'manufacturer': self.device_detector.device['manufacturer'], 61 | 'model': self.device_detector.device['model'] 62 | } 63 | } 64 | 65 | def parseUA(ua, options=None): 66 | """便捷解析函数""" 67 | parser = UAParser(ua, options) 68 | return parser.get_result() 69 | 70 | def format_result(result): 71 | """格式化结果为字符串""" 72 | device_info = [] 73 | if result['device']['manufacturer']: 74 | device_info.append(result['device']['manufacturer']) 75 | if result['device']['model']: 76 | device_info.append(result['device']['model']) 77 | 78 | os_info = result['os']['name'] 79 | if result['os']['version']['original']: 80 | os_info += ' ' + result['os']['version']['original'] 81 | 82 | browser_info = result['browser']['name'] 83 | if result['browser']['version']['original']: 84 | browser_info += ' ' + result['browser']['version']['original'] 85 | 86 | parts = [] 87 | if device_info: 88 | parts.append(' '.join(device_info)) 89 | if os_info: 90 | parts.append(os_info) 91 | if browser_info: 92 | parts.append(browser_info) 93 | 94 | return ' / '.join(parts) 95 | 96 | # 示例用法 97 | if __name__ == '__main__': 98 | test_ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1" 99 | result = parseUA(test_ua) 100 | print(format_result(result)) 101 | # 输出示例: Apple iPhone / iOS 14.4 / Safari 14.0.3 -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | import sys 3 | import re 4 | import os 5 | import time 6 | import json 7 | from importlib import reload 8 | 9 | reload(sys) 10 | if sys.version_info < (3, 0): 11 | sys.setdefaultencoding('utf-8') 12 | 13 | sys.path.append('uaDevice') 14 | import ua_parser 15 | 16 | if __name__ == '__main__': 17 | # 获取命令行参数作为文件名,默认为 data.txt 18 | filename = sys.argv[1] if len(sys.argv) > 1 else 'data.txt' 19 | # 拼接完整路径 20 | filepath = os.path.join('test', filename) 21 | f = open(filepath) 22 | # f = open('test/data10000.txt') 23 | data = f.read() 24 | 25 | uas = data.split('\n') 26 | 27 | stat = { 28 | 'osName': [0, [], []], 29 | 'osVersion': [0, [], []], 30 | 'browserName': [0, [], []], 31 | 'browserVersion': [0, [], []], 32 | 'engineName': [0, [], []], 33 | 'engineVersion': [0, [], []], 34 | 'deviceType': [0, [], []], 35 | 'deviceModel': [0, [], []], 36 | 'deviceManufacturer': [0, [], []] 37 | } 38 | 39 | total = len(uas) 40 | start = time.time() * 1000 41 | for i in range(total): 42 | ua = uas[i] 43 | # if i + 1 != 1955: 44 | # continue 45 | # print 'ua: ', ua, len(ua) 46 | 47 | s = time.time() * 1000 48 | info = ua_parser.parseUA(ua) 49 | output = '\t'.join([ 50 | info['os']['name'], 51 | info['os']['version']['original'], 52 | info['browser']['name'], 53 | info['browser']['version']['original'], 54 | info['engine']['name'], 55 | info['engine']['version']['original'], 56 | info['device']['type'], 57 | info['device']['model'], 58 | info['device']['manufacturer'] 59 | ]) 60 | print(output) 61 | 62 | if not info['device']['manufacturer']: 63 | print(ua) 64 | 65 | stat['osName'][2].append(info['os']['name']) 66 | if not info['os']['name']: 67 | stat['osName'][0] += 1 68 | stat['osName'][1].append(info['os']['name']) 69 | 70 | stat['osVersion'][2].append(info['os']['version']['original']) 71 | if not info['os']['version']['original']: 72 | stat['osVersion'][0] += 1 73 | stat['osVersion'][1].append(info['os']['version']['original']) 74 | 75 | stat['browserName'][2].append(info['browser']['name']) 76 | if not info['browser']['name']: 77 | stat['browserName'][0] += 1 78 | stat['browserName'][1].append(info['browser']['name']) 79 | 80 | stat['browserVersion'][2].append(info['browser']['version']['original']) 81 | if not info['browser']['version']['original']: 82 | stat['browserVersion'][0] += 1 83 | stat['browserVersion'][1].append(info['browser']['version']['original']) 84 | 85 | stat['engineName'][2].append(info['engine']['name']) 86 | if not info['engine']['name']: 87 | stat['engineName'][0] += 1 88 | stat['engineName'][1].append(info['engine']['name']) 89 | 90 | stat['engineVersion'][2].append(info['engine']['version']['original']) 91 | if not info['engine']['version']['original']: 92 | stat['engineVersion'][0] += 1 93 | stat['engineVersion'][1].append(info['engine']['version']['original']) 94 | 95 | stat['deviceType'][2].append(info['device']['type']) 96 | if not info['device']['type']: 97 | stat['deviceType'][0] += 1 98 | stat['deviceType'][1].append(info['device']['type']) 99 | 100 | stat['deviceModel'][2].append(info['device']['model']) 101 | if not info['device']['model']: 102 | stat['deviceModel'][0] += 1 103 | stat['deviceModel'][1].append(info['device']['model']) 104 | 105 | stat['deviceManufacturer'][2].append(info['device']['manufacturer']) 106 | if not info['device']['manufacturer']: 107 | stat['deviceManufacturer'][0] += 1 108 | stat['deviceManufacturer'][1].append(info['device']['manufacturer']) 109 | 110 | 111 | for key in stat.keys(): 112 | sys.stderr.write("%s: 成功数:%s, 成功率: %s%% \n" % (key, str(total - stat[key][0]), str(float(total - stat[key][0])/total*100))) 113 | 114 | sys.stderr.write("Total ua number: %s ,Total time %s %sms \n" % (str(total), str(i), str(time.time() * 1000 - start))) -------------------------------------------------------------------------------- /uaDevice/os_detector.py: -------------------------------------------------------------------------------- 1 | from version import Version 2 | from utils import optimizedSearch, getMatch 3 | 4 | class OSDetector: 5 | """操作系统检测类""" 6 | def __init__(self): 7 | self.os = { 8 | 'name': '', 9 | 'version': None 10 | } 11 | 12 | # Windows 版本映射 13 | self._windows_versions = { 14 | '10.0.2': '11', 15 | '10.0': '10', 16 | '6.3': '8.1', 17 | '6.2': '8', 18 | '6.1': '7', 19 | '6.0': 'Vista', 20 | '5.2': 'Server 2003/XP x64', 21 | '5.1': 'XP', 22 | '5.0': '2000', 23 | '4.9': 'ME', 24 | '4.1': '98', 25 | '4.0': '95' 26 | } 27 | 28 | def detect(self, ua): 29 | """检测操作系统类型和版本""" 30 | # Harmony 31 | if optimizedSearch(r'OpenHarmony', ua): 32 | self.os['name'] = 'Harmony' 33 | match = optimizedSearch(r'OpenHarmony\s?([0-9.]+)', ua) 34 | if match: 35 | self.os['version'] = Version({'value': match.group(1)}) 36 | return True 37 | 38 | # iOS 39 | elif optimizedSearch(r'iPhone|iPad|iPod', ua): 40 | self.os['name'] = 'iOS' 41 | match = optimizedSearch(r'OS ([\d_]+) like Mac OS X', ua) 42 | if match: 43 | self.os['version'] = Version({ 44 | 'value': match.group(1).replace('_', '.') 45 | }) 46 | return True 47 | 48 | # Android 49 | elif 'Android' in ua: 50 | self.os['name'] = 'Android' 51 | match = optimizedSearch(r'Android[\s\/]+([\d\.]+)', ua) 52 | if match: 53 | self.os['version'] = Version({'value': match.group(1)}) 54 | return True 55 | 56 | # Windows 57 | elif 'Windows' in ua: 58 | self.os['name'] = 'Windows' 59 | # Windows NT 版本 60 | match = optimizedSearch(r'Windows NT ([\d\.]+)', ua) 61 | if match: 62 | nt_version = match.group(1) 63 | if nt_version in self._windows_versions: 64 | version = self._windows_versions[nt_version] 65 | self.os['version'] = Version({ 66 | 'value': version, 67 | 'alias': version 68 | }) 69 | # Windows Phone 70 | elif 'Windows Phone' in ua: 71 | match = optimizedSearch(r'Windows Phone (?:OS )?([\d\.]+)', ua) 72 | if match: 73 | self.os['version'] = Version({'value': match.group(1)}) 74 | return True 75 | 76 | # Mac OS X 77 | elif 'Mac OS X' in ua: 78 | self.os['name'] = 'Mac OS X' 79 | match = optimizedSearch(r'Mac OS X ([\d_\.]+)', ua) 80 | if match: 81 | self.os['version'] = Version({ 82 | 'value': match.group(1).replace('_', '.') 83 | }) 84 | return True 85 | 86 | # Linux 发行版 87 | elif 'Linux' in ua: 88 | # Ubuntu 89 | if 'Ubuntu' in ua: 90 | self.os['name'] = 'Ubuntu' 91 | match = optimizedSearch(r'Ubuntu[\/\s]([\d\.]+)', ua) 92 | if match: 93 | self.os['version'] = Version({'value': match.group(1)}) 94 | return True 95 | 96 | # CentOS 97 | elif 'CentOS' in ua: 98 | self.os['name'] = 'CentOS' 99 | match = optimizedSearch(r'CentOS[\/\s]+([\d\.]+)', ua) 100 | if match: 101 | self.os['version'] = Version({'value': match.group(1)}) 102 | return True 103 | 104 | # Debian 105 | elif 'Debian' in ua: 106 | self.os['name'] = 'Debian' 107 | match = optimizedSearch(r'Debian[\/\s]+([\d\.]+)', ua) 108 | if match: 109 | self.os['version'] = Version({'value': match.group(1)}) 110 | return True 111 | 112 | # Red Hat 113 | elif 'Red Hat' in ua: 114 | self.os['name'] = 'Red Hat' 115 | match = optimizedSearch(r'Red Hat[\/\s]+([\d\.]+)', ua) 116 | if match: 117 | self.os['version'] = Version({'value': match.group(1)}) 118 | return True 119 | 120 | # Fedora 121 | elif 'Fedora' in ua: 122 | self.os['name'] = 'Fedora' 123 | match = optimizedSearch(r'Fedora[\/\s]+([\d\.]+)', ua) 124 | if match: 125 | self.os['version'] = Version({'value': match.group(1)}) 126 | return True 127 | 128 | # 通用 Linux 129 | self.os['name'] = 'Linux' 130 | return True 131 | 132 | return False -------------------------------------------------------------------------------- /uaDevice/browser_detector.py: -------------------------------------------------------------------------------- 1 | from version import Version 2 | from utils import optimizedSearch 3 | 4 | class BrowserDetector: 5 | def __init__(self): 6 | self.browser = { 7 | 'name': '', 8 | 'version': None, 9 | 'mode': '', 10 | 'details': 0, 11 | 'stock': True, 12 | 'hidden': False, 13 | 'channel': '' 14 | } 15 | 16 | self.engine = { 17 | 'name': '', 18 | 'version': None 19 | } 20 | 21 | def _detect_engine(self, ua): 22 | """检测浏览器引擎""" 23 | # Gecko 24 | if 'Gecko' in ua and 'like Gecko' not in ua: 25 | self.engine['name'] = 'Gecko' 26 | match = optimizedSearch(r'rv:([\d\.]+)', ua) 27 | if match: 28 | self.engine['version'] = Version({'value': match.group(1)}) 29 | 30 | # Blink (需要放在WebKit检测之前) 31 | if ('Chrome' in ua or 'Edg/' in ua) and 'AppleWebKit' in ua: 32 | self.engine['name'] = 'Blink' 33 | match = optimizedSearch(r'AppleWebKit\/([\d\.]+)', ua) 34 | if match: 35 | self.engine['version'] = Version({'value': match.group(1)}) 36 | 37 | # WebKit 38 | elif 'AppleWebKit' in ua: 39 | self.engine['name'] = 'WebKit' 40 | match = optimizedSearch(r'AppleWebKit\/([\d\.]+)', ua) 41 | if match: 42 | self.engine['version'] = Version({'value': match.group(1)}) 43 | 44 | # Presto 45 | elif 'Presto' in ua: 46 | self.engine['name'] = 'Presto' 47 | match = optimizedSearch(r'Presto\/([\d\.]+)', ua) 48 | if match: 49 | self.engine['version'] = Version({'value': match.group(1)}) 50 | 51 | # Trident 52 | elif 'Trident' in ua: 53 | self.engine['name'] = 'Trident' 54 | match = optimizedSearch(r'Trident\/([\d\.]+)', ua) 55 | if match: 56 | self.engine['version'] = Version({'value': match.group(1)}) 57 | # IE 特殊处理 58 | elif 'MSIE' in ua: 59 | match = optimizedSearch(r'MSIE ([\d\.]+)', ua) 60 | if match: 61 | version = float(match.group(1)) 62 | if version >= 7: 63 | self.engine['version'] = Version({'value': str(version - 4.0)}) 64 | 65 | def detect(self, ua): 66 | """检测浏览器类型和版本""" 67 | # 先检测引擎 68 | self._detect_engine(ua) 69 | 70 | # WeChat 71 | if 'MicroMessenger' in ua: 72 | self.browser['name'] = 'WeChat' 73 | match = optimizedSearch(r'MicroMessenger\/([\d\.]+)', ua) 74 | if match: 75 | self.browser['version'] = Version({'value': match.group(1)}) 76 | self.browser['stock'] = False 77 | return True 78 | 79 | # QQ Browser 80 | elif 'MQQBrowser' in ua or 'QQBrowser' in ua: 81 | self.browser['name'] = 'QQBrowser' 82 | match = optimizedSearch(r'(?:MQQBrowser|QQBrowser)\/([\d\.]+)', ua) 83 | if match: 84 | self.browser['version'] = Version({'value': match.group(1)}) 85 | self.browser['stock'] = False 86 | return True 87 | 88 | # UC Browser 89 | elif 'UCBrowser' in ua: 90 | self.browser['name'] = 'UCBrowser' 91 | match = optimizedSearch(r'UCBrowser\/([\d\.]+)', ua) 92 | if match: 93 | self.browser['version'] = Version({'value': match.group(1)}) 94 | self.browser['stock'] = False 95 | return True 96 | 97 | # Sogou Browser 98 | elif 'SE' in ua and 'MetaSr' in ua or 'SogouMobileBrowser' in ua: 99 | self.browser['name'] = 'SogouBrowser' 100 | match = optimizedSearch(r'SogouMobileBrowser\/([\d\.]+)', ua) 101 | if match: 102 | self.browser['version'] = Version({'value': match.group(1)}) 103 | self.browser['stock'] = False 104 | return True 105 | 106 | # Baidu App 107 | elif 'baiduboxapp' in ua: 108 | self.browser['name'] = 'BaiduApp' 109 | match = optimizedSearch(r'baiduboxapp\/([\d\.]+)', ua) 110 | if match: 111 | self.browser['version'] = Version({'value': match.group(1)}) 112 | self.browser['stock'] = False 113 | return True 114 | 115 | # Baidu Browser 116 | elif 'BIDUBrowser' in ua or 'baidubrowser' in ua: 117 | self.browser['name'] = 'BaiduBrowser' 118 | match = optimizedSearch(r'(?:BIDUBrowser|baidubrowser)[\s\/]([\d\.]+)', ua) 119 | if match: 120 | self.browser['version'] = Version({'value': match.group(1)}) 121 | self.browser['stock'] = False 122 | return True 123 | 124 | # 360 Browser 125 | elif '360SE' in ua or '360EE' in ua or 'QihooBrowser' in ua: 126 | self.browser['name'] = '360Browser' 127 | match = optimizedSearch(r'QihooBrowser\/([\d\.]+)', ua) 128 | if match: 129 | self.browser['version'] = Version({'value': match.group(1)}) 130 | self.browser['stock'] = False 131 | return True 132 | 133 | # Liebao Browser 134 | elif 'LBBROWSER' in ua or 'LieBaoFast' in ua: 135 | self.browser['name'] = 'LiebaoBrowser' 136 | self.browser['stock'] = False 137 | return True 138 | 139 | # 2345 Browser 140 | elif '2345Explorer' in ua or '2345Browser' in ua: 141 | self.browser['name'] = '2345Browser' 142 | match = optimizedSearch(r'2345(?:Explorer|Browser)\/([\d\.]+)', ua) 143 | if match: 144 | self.browser['version'] = Version({'value': match.group(1)}) 145 | self.browser['stock'] = False 146 | return True 147 | 148 | # Maxthon 149 | elif 'Maxthon' in ua or 'MXiOS' in ua: 150 | self.browser['name'] = 'Maxthon' 151 | match = optimizedSearch(r'(?:Maxthon|MXiOS)[\s\/]([\d\.]+)', ua) 152 | if match: 153 | self.browser['version'] = Version({'value': match.group(1)}) 154 | self.browser['stock'] = False 155 | return True 156 | 157 | # MIUI Browser 158 | elif 'MiuiBrowser' in ua: 159 | self.browser['name'] = 'MIUIBrowser' # 'MIUI Browser' 160 | match = optimizedSearch(r'MiuiBrowser\/([\d\.]+)', ua) 161 | if match: 162 | self.browser['version'] = Version({'value': match.group(1)}) 163 | self.browser['stock'] = False 164 | return True 165 | 166 | # Samsung Browser 167 | elif 'SamsungBrowser' in ua: 168 | self.browser['name'] = 'SamsungBrowser' 169 | match = optimizedSearch(r'SamsungBrowser\/([\d\.]+)', ua) 170 | if match: 171 | self.browser['version'] = Version({'value': match.group(1)}) 172 | self.browser['stock'] = False 173 | return True 174 | 175 | # Huawei Browser 176 | elif 'HuaweiBrowser' in ua: 177 | self.browser['name'] = 'HuaweiBrowser' 178 | match = optimizedSearch(r'HuaweiBrowser\/([\d\.]+)', ua) 179 | if match: 180 | self.browser['version'] = Version({'value': match.group(1)}) 181 | self.browser['stock'] = False 182 | return True 183 | 184 | # VIVO Browser 185 | elif 'VivoBrowser' in ua: 186 | self.browser['name'] = 'VIVOBrowser' 187 | match = optimizedSearch(r'VivoBrowser\/([\d\.]+)', ua) 188 | if match: 189 | self.browser['version'] = Version({'value': match.group(1)}) 190 | self.browser['stock'] = False 191 | return True 192 | 193 | # OPPO Browser 194 | elif 'HeyTapBrowser' in ua or 'OppoBrowser' in ua: 195 | self.browser['name'] = 'OPPOBrowser' 196 | match = optimizedSearch(r'(?:HeyTapBrowser|OppoBrowser)\/([\d\.]+)', ua) 197 | if match: 198 | self.browser['version'] = Version({'value': match.group(1)}) 199 | self.browser['stock'] = False 200 | return True 201 | 202 | # Lenovo Browser 203 | elif 'SLBrowser' in ua or 'LeBrowser' in ua: 204 | self.browser['name'] = 'LenovoBrowser' 205 | match = optimizedSearch(r'(?:SLBrowser|LeBrowser)[\s\/]([\d\.]+)', ua) 206 | if match: 207 | self.browser['version'] = Version({'value': match.group(1)}) 208 | self.browser['stock'] = False 209 | return True 210 | 211 | # Chrome 212 | elif 'Chrome' in ua and 'Chromium' not in ua: 213 | self.browser['name'] = 'Chrome' 214 | match = optimizedSearch(r'Chrome\/([\d\.]+)', ua) 215 | if match: 216 | self.browser['version'] = Version({'value': match.group(1)}) 217 | self.browser['stock'] = False 218 | return True 219 | 220 | # Edge 221 | elif 'Edg' in ua: 222 | self.browser['name'] = 'Edge' 223 | match = optimizedSearch(r'Edg\/([\d\.]+)', ua) 224 | if match: 225 | self.browser['version'] = Version({'value': match.group(1)}) 226 | self.browser['stock'] = False 227 | return True 228 | 229 | # Firefox 230 | elif 'Firefox' in ua: 231 | self.browser['name'] = 'Firefox' 232 | match = optimizedSearch(r'Firefox\/([\d\.]+)', ua) 233 | if match: 234 | self.browser['version'] = Version({'value': match.group(1)}) 235 | self.browser['stock'] = False 236 | return True 237 | 238 | # Opera 239 | elif 'OPR' in ua or 'Opera' in ua: 240 | self.browser['name'] = 'Opera' 241 | match = optimizedSearch(r'(?:OPR|Opera)[\/]([\d\.]+)', ua) 242 | if match: 243 | self.browser['version'] = Version({'value': match.group(1)}) 244 | self.browser['stock'] = False 245 | return True 246 | 247 | # Internet Explorer 248 | elif 'MSIE' in ua or 'Trident' in ua: 249 | self.browser['name'] = 'InternetExplorer' 250 | if 'MSIE' in ua: 251 | match = optimizedSearch(r'MSIE ([\d\.]+)', ua) 252 | else: 253 | match = optimizedSearch(r'rv:([\d\.]+)', ua) 254 | if match: 255 | self.browser['version'] = Version({'value': match.group(1)}) 256 | self.browser['stock'] = True 257 | return True 258 | 259 | # Safari 260 | # 如果是Android设备,不识别为Safari 261 | elif 'Safari' in ua and 'Chrome' not in ua and 'Chromium' not in ua and 'Android' not in ua: 262 | self.browser['name'] = 'Safari' 263 | match = optimizedSearch(r'Version\/([\d\.]+)', ua) 264 | if match: 265 | self.browser['version'] = Version({'value': match.group(1)}) 266 | self.browser['stock'] = True 267 | return True 268 | 269 | # Android Browser 270 | elif 'Android' in ua and 'Chrome' not in ua: 271 | self.browser['name'] = 'AndroidBrowser' 272 | match = optimizedSearch(r'Android[\s\/]([\d\.]+)', ua) 273 | if match: 274 | self.browser['version'] = Version({'value': match.group(1)}) 275 | self.browser['stock'] = True 276 | return True 277 | 278 | return False -------------------------------------------------------------------------------- /uaDevice/device_detector.py: -------------------------------------------------------------------------------- 1 | from version import Version 2 | from utils import optimizedSearch, getMatch, cleanupModel 3 | 4 | class DeviceDetector: 5 | """设备检测类""" 6 | def __init__(self): 7 | self.device = { 8 | 'type': 'desktop', 9 | 'identified': False, 10 | 'manufacturer': '', 11 | 'model': '' 12 | } 13 | 14 | def detect(self, ua): 15 | """检测设备品牌和型号""" 16 | if not ua: 17 | return False 18 | 19 | # 设置设备类型 20 | if 'Mobile' in ua or 'Android' in ua: 21 | self.device['type'] = 'mobile' 22 | if 'Mobile' not in ua and 'Android' in ua: 23 | self.device['type'] = 'tablet' 24 | 25 | # 如果不是移动设备或平板,直接返回 26 | if self.device['type'] not in ['mobile', 'tablet']: 27 | return False 28 | 29 | # Apple 设备 30 | if 'iPhone' in ua or 'iPad' in ua or 'iPod' in ua: 31 | self.device['manufacturer'] = 'Apple' 32 | if 'iPad' in ua: 33 | self.device['model'] = 'iPad' 34 | elif 'iPod' in ua: 35 | self.device['model'] = 'iPod' 36 | elif 'iPhone' in ua: 37 | self.device['model'] = 'iPhone' 38 | self.device['identified'] = True 39 | return True 40 | 41 | # Honor (需要在华为之前检测,因为有些老的UA中Honor和HUAWEI同时存在) 42 | elif 'HONOR' in ua or 'HonorBot' in ua or 'Build/HONOR' in ua: 43 | self.device['manufacturer'] = 'Honor' 44 | self.device['model'] = 'Honor' 45 | return True 46 | 47 | # Huawei (在Honor之后检测) 48 | # NX型号归属说明: 49 | # - NXT-AL10, NXT-TL00, NXT-CL00, NXT-DL00 等属于华为Mate 8 50 | # - NX619J, NX606J 等属于努比亚 51 | elif ('HUAWEI' in ua or 'HuaweiBrowser' in ua or 'Build/HUAWEI' in ua or 52 | optimizedSearch(r';\s*(?:NXT-[A-Z]{2}[0-9]{2}|SP[0-9]{3}|[A-Z]{3}-[A-Z]{2}[0-9]{2})\s+', ua) or 53 | 'HMSCore' in ua): 54 | self.device['manufacturer'] = 'Huawei' 55 | self.device['model'] = '' 56 | 57 | # 首先尝试匹配三字母-两字母数字格式 (如 NOH-AN00) 58 | match = optimizedSearch(r';\s*([A-Z]{3}-[A-Z]{2}[0-9]{2})', ua) 59 | if match: 60 | self.device['model'] = match.group(1) 61 | # 然后尝试匹配Build信息中的型号 62 | elif optimizedSearch(r'(?:Build/HUAWEI)?([\w-]+)\s+Build/', ua): 63 | match = optimizedSearch(r'(?:Build/HUAWEI)?([\w-]+)\s+Build/', ua) 64 | self.device['model'] = match.group(1) 65 | # 如果没找到,再尝试之前的匹配方式 66 | elif optimizedSearch(r'HUAWEI[\s_-]([\w-]+)', ua): 67 | match = optimizedSearch(r'HUAWEI[\s_-]([\w-]+)', ua) 68 | self.device['model'] = match.group(1) 69 | # 如果还没找到,尝试匹配SP系列 70 | elif optimizedSearch(r';\s*(SP[0-9]{3})', ua): 71 | match = optimizedSearch(r';\s*(SP[0-9]{3})', ua) 72 | self.device['model'] = match.group(1) 73 | return True 74 | 75 | # Meizu (在小米之前检测,避免MZ开头型号被误判为小米) 76 | elif 'MZ-' in ua or 'MZBrowser' in ua or 'MEIZU' in ua: 77 | self.device['manufacturer'] = 'Meizu' 78 | self.device['model'] = '' 79 | 80 | # 匹配 MZ- 开头的型号 81 | match = optimizedSearch(r'MZ-([\w-]+)\s+Build\/', ua) 82 | if match: 83 | self.device['model'] = match.group(1) 84 | # 匹配其他可能的型号格式 85 | elif optimizedSearch(r'MEIZU[\s-]([\w-]+)', ua): 86 | match = optimizedSearch(r'MEIZU[\s-]([\w-]+)', ua) 87 | self.device['model'] = match.group(1) 88 | return True 89 | 90 | # Nubia (在华为之后检测) 91 | # NX型号说明: 92 | # - NX619J, NX606J, NX563J, NX513J 等属于努比亚 93 | # - 通常格式为 NX + 3位数字 + 字母 94 | elif 'nubia' in ua or optimizedSearch(r';\s*NX[0-9]{3}[A-Z]\s+Build\/', ua): 95 | self.device['manufacturer'] = 'Nubia' 96 | self.device['model'] = '' 97 | 98 | # 匹配 Build 前的型号 99 | match = optimizedSearch(r';\s*([\w-]+)\s+Build\/', ua) 100 | if match: 101 | self.device['model'] = match.group(1) 102 | # 匹配 nubia 后的型号 103 | elif optimizedSearch(r'nubia\s+([\w-]+)', ua): 104 | match = optimizedSearch(r'nubia\s+([\w-]+)', ua) 105 | self.device['model'] = match.group(1) 106 | return True 107 | 108 | # OPPO 109 | elif 'OPPO' in ua or 'OppoBrowser' in ua or 'HeyTapBrowser' in ua or optimizedSearch(r';\s*(?:OPPO|P[A-Z]{2})[A-Z0-9]+\s+Build\/', ua): 110 | self.device['manufacturer'] = 'OPPO' 111 | self.device['model'] = '' 112 | 113 | # 先尝试匹配Build前的型号 - 使用更通用的格式 114 | match = optimizedSearch(r';\s*((?:OPPO|P[A-Z]{2})[A-Z0-9]+)\s+Build\/', ua) 115 | if match: 116 | self.device['model'] = match.group(1) 117 | # 如果没找到,尝试其他可能的格式 118 | elif optimizedSearch(r';\s*OPPO\s*([A-Za-z][0-9]\w*)', ua): 119 | match = optimizedSearch(r';\s*OPPO\s*([A-Za-z][0-9]\w*)', ua) 120 | self.device['model'] = match.group(1) 121 | 122 | self.device['identified'] = True 123 | return True 124 | 125 | # vivo 126 | elif 'vivo' in ua.lower() or 'VivoBrowser' in ua or optimizedSearch(r';\s*V\d{4}[A-Z]{1,2}\b', ua): 127 | self.device['manufacturer'] = 'vivo' 128 | self.device['model'] = '' 129 | 130 | # 匹配vivo型号 (如 V2352A) 131 | match = optimizedSearch(r';\s*(V[0-9]{4}[A-Z]{1,2})\b', ua) 132 | if match: 133 | self.device['model'] = match.group(1) 134 | # 备用匹配模式 135 | elif optimizedSearch(r'vivo\s*([A-Za-z][0-9][A-Za-z0-9]+)', ua): 136 | match = optimizedSearch(r'vivo\s*([A-Za-z][0-9][A-Za-z0-9]+)', ua) 137 | self.device['model'] = match.group(1) 138 | 139 | self.device['identified'] = True 140 | return True 141 | 142 | # Samsung 143 | elif getMatch(ua, r'SAMSUNG|Galaxy|GT-|SM-|SCH-|SGH-', True): 144 | match = getMatch() 145 | self.device['manufacturer'] = 'Samsung' 146 | if getMatch(ua, r';\s*(Galaxy [^/;]*|GT-[a-zA-Z0-9]+|SM-[a-zA-Z0-9]+|SCH-[a-zA-Z0-9]+|SGH-[a-zA-Z0-9]+)\s+Build\/', True): 147 | match = getMatch() 148 | self.device['model'] = match.group(1) 149 | self.device['identified'] = True 150 | return True 151 | 152 | # OnePlus 153 | elif getMatch(ua, r'OnePlus|ONEPLUS', True): 154 | match = getMatch() 155 | self.device['manufacturer'] = 'OnePlus' 156 | if getMatch(ua, r'ONEPLUS[ _]([a-zA-Z0-9]+)', True): 157 | match = getMatch() 158 | self.device['model'] = match.group(1) 159 | self.device['identified'] = True 160 | return True 161 | 162 | # Realme 163 | if getMatch(ua, r'Realme|RMX[0-9]+', True): 164 | match = getMatch() 165 | self.device['manufacturer'] = 'Realme' 166 | if getMatch(ua, r'(RMX[0-9]+)', True): 167 | match = getMatch() 168 | self.device['model'] = match.group(1) 169 | self.device['identified'] = True 170 | return True 171 | 172 | # Xiaomi & Redmi 173 | if getMatch(ua, r'(Xiaomi|Redmi|MI|HM|MIX|Mi9|Mi 9|Mi10|Mi 10|M2|M3|M4|M5|M6|M7|M8)', True): 174 | match = getMatch() 175 | self.device['manufacturer'] = 'Xiaomi' 176 | if match.group(1).startswith('Redmi'): 177 | self.device['model'] = match.group(1) 178 | elif match.group(1).startswith(('MI', 'Mi')): 179 | self.device['model'] = match.group(1) 180 | self.device['identified'] = True 181 | return True 182 | 183 | # 金立 184 | elif 'GIONEE' in ua or 'GN' in ua or optimizedSearch(r';\s*F[\d]{3}\s+Build\/', ua): 185 | self.device['manufacturer'] = 'Gionee' 186 | self.device['model'] = '' 187 | 188 | # 匹配 Build 前的型号 (F103, M7等) 189 | match = optimizedSearch(r';\s*((?:F|M|S|GN)[\w-]+)\s+Build\/', ua) 190 | if match: 191 | self.device['model'] = match.group(1) 192 | # 匹配 GIONEE 后的型号 193 | elif optimizedSearch(r'GIONEE[\s-]([\w-]+)', ua): 194 | match = optimizedSearch(r'GIONEE[\s-]([\w-]+)', ua) 195 | self.device['model'] = match.group(1) 196 | return True 197 | 198 | # Google (需要在其他Android设备之前检测) 199 | elif 'Pixel' in ua or 'Google' in ua or optimizedSearch(r';\s*(?:Pixel|Nexus)\s+\d', ua): 200 | self.device['manufacturer'] = 'Google' 201 | self.device['model'] = '' 202 | 203 | # Pixel 系列 204 | match = optimizedSearch(r'Pixel\s+(?:\d[a-zA-Z]?|XL|Fold|[a-zA-Z]+)(?:\s+5G)?', ua) 205 | if match: 206 | self.device['model'] = match.group(0) 207 | return True 208 | 209 | # Nexus 系列 (Google 品牌的 Nexus) 210 | match = optimizedSearch(r'Nexus\s+(One|[4-9]|6P|5X|Player|10)(?:\s+Build|[;\)])', ua) 211 | if match: 212 | self.device['model'] = 'Nexus ' + match.group(1) 213 | return True 214 | 215 | # 其他 Google 设备 216 | match = optimizedSearch(r'Google\s+([^;\/\)]+)', ua) 217 | if match: 218 | self.device['model'] = match.group(1).strip() 219 | return True 220 | 221 | return True 222 | 223 | # Xiaomi 224 | elif 'XiaoMi' in ua or 'MI ' in ua or 'MiuiBrowser' in ua or 'MIUI' in ua or optimizedSearch(r';\s*(?:MI|Mi|Redmi|HM|2[0-9])[A-Z0-9]+', ua): 225 | self.device['manufacturer'] = 'Xiaomi' 226 | self.device['model'] = '' 227 | 228 | # 匹配数字型号 (如 2211133C) 229 | match = optimizedSearch(r';\s*(2[0-9][0-9][0-9][0-9][0-9][0-9][0-9][A-Z0-9]+)\s+Build', ua) 230 | if match: 231 | self.device['model'] = match.group(1) 232 | # 匹配传统型号 233 | elif optimizedSearch(r';\s*(?:MI|Mi|Redmi|HM)[ -]([A-Za-z0-9 ]+)\s+Build', ua): 234 | match = optimizedSearch(r';\s*(?:MI|Mi|Redmi|HM)[ -]([A-Za-z0-9 ]+)\s+Build', ua) 235 | self.device['model'] = match.group(1).strip() 236 | 237 | self.device['identified'] = True 238 | return True 239 | 240 | # 其他品牌的通用检测 241 | other_brands = [ 242 | ('Motorola', r'Motorola|Moto[ _]([a-zA-Z0-9]+)'), 243 | ('LG', r'LG[ _-]([a-zA-Z0-9]+)'), 244 | ('Sony', r'Sony([a-zA-Z0-9]+)'), 245 | ('HTC', r'HTC[ _-]([a-zA-Z0-9]+)'), 246 | ('Nokia', r'Nokia[ _]([a-zA-Z0-9\-]+)'), 247 | ('Lenovo', r'Lenovo[ _-]([a-zA-Z0-9]+)'), 248 | ('ZTE', r'ZTE[ _]([a-zA-Z0-9\-]+)'), 249 | ('TCL', r'TCL[ _]([a-zA-Z0-9]+)'), 250 | ('Coolpad', r'Coolpad[ _]([a-zA-Z0-9]+)'), 251 | ('ASUS', r'ASUS[ _]([a-zA-Z0-9]+)'), 252 | ('BlackBerry', r'BlackBerry[ _]([a-zA-Z0-9]+)') 253 | ] 254 | 255 | for brand, pattern in other_brands: 256 | if getMatch(ua, pattern, True): 257 | match = getMatch() 258 | self.device['manufacturer'] = brand 259 | if match.group(1): 260 | self.device['model'] = match.group(1) 261 | self.device['identified'] = True 262 | return True 263 | 264 | # 清理设备型号 265 | if self.device['model']: 266 | self.device['model'] = cleanupModel(self.device['model']) 267 | 268 | return self.device['identified'] --------------------------------------------------------------------------------