def json_loads_single(s): '''处理不标准JSON结构化数据''' try: return json.loads(s.replace("'", '"').replace('\t', '')) except (ValueError, UnicodeDecodeError): logger.error(traceback.format_exc()) return None
def on_link_task_added(info, error=None): if error or not info: logger.error('CloudPage.do_add_link_task: %s, %s' % (info, error)) self.app.toast(_('Failed to parse download link')) return if info.get('error_code', -1) != 0: logger.error('CloudPage.do_add_link_task: %s, %s' % (info, error)) if 'task_id' in info or info['error_code'] == 0: self.reload() elif info['error_code'] == -19: vcode = info['vcode'] vcode_dialog = VCodeDialog(self, self.app, info) response = vcode_dialog.run() vcode_input = vcode_dialog.get_vcode() vcode_dialog.destroy() if response != Gtk.ResponseType.OK: return gutil.async_call(pcs.cloud_add_link_task, self.app.cookie, self.app.tokens, source_url, save_path, vcode, vcode_input, callback=on_link_task_added) else: self.app.toast(_('Error: {0}').format(info['error_msg']))
def on_check_login(info, error=None): if not info or error: logger.error('SigninDialog.on_check_login: %s, %s' % (info, error)) self.signin_failed(_('Failed to check login, please try again')) else: ubi_cookie, status = info cookie.load_list(ubi_cookie) nonlocal codeString nonlocal verifycode codeString = status['data']['codeString'] vcodetype = status['data']['vcodetype'] if codeString: dialog = SigninVcodeDialog(self, username, cookie, tokens, codeString, vcodetype) response = dialog.run() verifycode = dialog.get_vcode() codeString = dialog.codeString dialog.destroy() if not verifycode or len(verifycode) != 4: self.signin_failed(_('Please input verification code!')) return else: gutil.async_call(auth.get_public_key, cookie, tokens, callback=on_get_public_key) else: gutil.async_call(auth.get_public_key, cookie, tokens, callback=on_get_public_key)
def open_video_link(red_url, error=None): '''得到视频最后地址后, 调用播放器直接播放''' if error or not red_url: logger.error('IconWindow.launch_app_with_app_info: %s, %s' % (red_url, error)) return gutil.async_call(app_info.launch_uris, [red_url, ], None)
def on_list_task(info, error=None): self.loading_spin.stop() self.loading_spin.hide() if not info: self.app.toast(_('Network error, info is empty')) if error or not info: logger.error('CloudPage.load: %s, %s' % (info, error)) return tasks = info['task_info'] for task in tasks: self.liststore.append([ task['task_id'], task['task_name'], task['save_path'], task['source_url'], 0, 0, int(task['status']), 0, '0', gutil.escape(task['save_path']) ]) self.scan_tasks() nonlocal start start = start + len(tasks) if info['total'] > start: gutil.async_call(pcs.cloud_list_task, self.app.cookie, self.app.tokens, start, callback=on_list_task)
def urlopen_without_redirect(url, headers={}, data=None, retries=RETRIES): """请求一个URL, 并返回一个Response对象. 不处理重定向. 使用这个函数可以返回URL重定向(Error 301/302)后的地址, 也可以重到URL中请 求的文件的大小, 或者Header中的其它认证信息. """ headers_merged = default_headers.copy() for key in headers.keys(): headers_merged[key] = headers[key] parse_result = urllib.parse.urlparse(url) for i in range(retries): try: conn = http.client.HTTPConnection(parse_result.netloc) if data: conn.request("POST", url, body=data, headers=headers_merged) else: conn.request("GET", url, body=data, headers=headers_merged) return conn.getresponse() except OSError: logger.error(traceback.format_exc()) except: logger.error(traceback.format_exc()) # return None return None
def load_profile(profile_name): '''读取特定帐户的配置信息 有时, dbus会出现连接错误, 这里会进行重试. 但如果超过最大尝试次数, 就 会失效, 此时, profile['password'] 是一个空字符串, 所以在下一步, 应该去 检查一下password是否有效, 如果无效, 应该提醒用户. ''' path = os.path.join(Config.CONF_DIR, profile_name) if not os.path.exists(path): return DEFAULT_PROFILE with open(path) as fh: profile = json.load(fh) for key in DEFAULT_PROFILE: if key not in profile: profile[key] = DEFAULT_PROFILE[key] global keyring_available if keyring_available: for i in range(RETRIES): try: profile['password'] = keyring.get_password( Config.DBUS_APP_NAME, profile['username']) break except (keyring.errors.InitError, dbus.exceptions.DBusException): logger.error(traceback.format_exc()) else: keyring_available = False if not profile['password']: profile['password'] = '' return profile
def get_share_page(url): '''获取共享页面的文件信息''' req = net.urlopen(url) if req: content = req.data.decode() match = re.findall('applicationConfig,(.+)\]\);', content) share_files = {} if not match: match = re.findall('viewShareData=(.+");FileUtils.spublic', content) if not match: logger.error('get_share_page(): %s, %s' % (url, match)) return None list_ = json.loads(json.loads(match[0])) else: list_ = json.loads(json.loads(match[0])) if isinstance(list_, dict): share_files['list'] = [list_, ] else: share_files['list'] = list_ id_match = re.findall('FileUtils\.share_id="(\d+)"', content) uk_match = re.findall('/share/home\?uk=(\d+)" target=', content) sign_match = re.findall('FileUtils\.share_sign="([^"]+)"', content) if id_match and uk_match and sign_match: share_files['share_id'] = id_match[0] share_files['uk'] = uk_match[0] share_files['sign'] = sign_match[0] return share_files return None
def post_multipart(url, headers, fields, files, retries=RETRIES): content_type, body = encode_multipart_formdata(fields, files) schema = urllib.parse.urlparse(url) headers_merged = default_headers.copy() for key in headers.keys(): headers_merged[key] = headers[key] headers_merged["Content-Type"] = content_type headers_merged["Content-length"] = str(len(body)) for i in range(retries): try: h = http.client.HTTPConnection(schema.netloc) h.request("POST", url, body=body, headers=headers_merged) req = h.getresponse() encoding = req.getheader("Content-encoding") req.data = req.read() if encoding == "gzip": req.data = gzip.decompress(req.data) elif encoding == "deflate": req.data = zlib.decompress(req.data, -zlib.MAX_WBITS) return req except OSError: logger.error(traceback.format_exc()) except: logger.error(traceback.format_exc()) # return None return None
def urlopen(url, headers={}, data=None, retries=RETRIES, timeout=TIMEOUT): """打开一个http连接, 并返回Request. headers 是一个dict. 默认提供了一些项目, 比如User-Agent, Referer等, 就 不需要重复加入了. 这个函数只能用于http请求, 不可以用于下载大文件. 如果服务器支持gzip压缩的话, 就会使用gzip对数据进行压缩, 然后在本地自动 解压. req.data 里面放着的是最终的http数据内容, 通常都是UTF-8编码的文本. """ headers_merged = default_headers.copy() for key in headers.keys(): headers_merged[key] = headers[key] opener = urllib.request.build_opener(ForbiddenHandler) opener.addheaders = [(k, v) for k, v in headers_merged.items()] for i in range(retries): try: req = opener.open(url, data=data, timeout=timeout) encoding = req.headers.get("Content-encoding") req.data = req.read() if encoding == "gzip": req.data = gzip.decompress(req.data) elif encoding == "deflate": req.data = zlib.decompress(req.data, -zlib.MAX_WBITS) return req except OSError: logger.error(traceback.format_exc()) except: logger.error(traceback.format_exc()) return None
def on_do_extract(info, error=None): if error or not info[0] or info[0]['error_code'] != 0: logger.error('IconWindow.on_do_extract: %s, %s' % (info, error)) self.app.toast(_('Error occurred while extracting')) return self.app.toast(_('Extracted successfully'))
def on_before_extract(info, error=None): '''检查压缩包能否可以解压''' if error or not info[0]: logger.error('IconWindow.on_before_extract: %s, %s' % (info, error)) self.app.toast(_('Unknown error')) return elif 'time' in info[0]: logger.debug('IconWindow.on_before_extract: extracting %s, %s' % (info[1], info[0])) self.app.toast(_('Extracting')) return elif info[0]['errno'] == 31180: self.app.toast(_('Password needed')) return elif info[0]['errno'] == 31182: self.app.toast(_('Broken file')) return elif info[0]['errno'] == 31183: self.app.toast(_('File size limit exceeded (500MB)')) return elif info[0]['errno'] == 31184: self.app.toast(_('Unsupported file')) return elif info[0]['errno'] != 0: logger.error('IconWindow.on_before_extract: %s, %s' % (info, error)) self.app.toast(_('Unable to unzip file')) return else: file_info, path = info do_extract(path)
def urlopen_simple(url, retries=RETRIES, timeout=TIMEOUT): for i in range(retries): try: return urllib.request.urlopen(url, timeout=timeout) except OSError: logger.error(traceback.format_exc()) return None
def get_download_link(cookie, tokens, path): '''在下载之前, 要先获取最终的下载链接. path - 一个文件的绝对路径. @return red_url, red_url 是重定向后的URL, 如果获取失败, 就返回原来的dlink; ''' metas = get_metas(cookie, tokens, path) if (not metas or metas.get('errno', -1) != 0 or 'info' not in metas or len(metas['info']) != 1): logger.error('pcs.get_download_link(): %s' % metas) return None dlink = metas['info'][0]['dlink'] url = '{0}&cflg={1}'.format(dlink, cookie.get('cflag').value) req = net.urlopen_without_redirect(url, headers={ 'Cookie': cookie.sub_output( 'BAIDUID', 'BDUSS', 'cflag'), 'Accept': const.ACCEPT_HTML, }) if not req: return url else: return req.getheader('Location', url)
def urlopen_without_redirect(url, headers={}, data=None, retries=RETRIES): '''请求一个URL, 并返回一个Response对象. 不处理重定向. 使用这个函数可以返回URL重定向(Error 301/302)后的地址, 也可以重到URL中请 求的文件的大小, 或者Header中的其它认证信息. ''' headers_merged = default_headers.copy() for key in headers.keys(): headers_merged[key] = headers[key] parse_result = urllib.parse.urlparse(url) for i in range(retries): try: conn = http.client.HTTPConnection(parse_result.netloc) if data: conn.request('POST', url, body=data, headers=headers_merged) else: conn.request('GET', url, body=data, headers=headers_merged) return conn.getresponse() except OSError: logger.error(traceback.format_exc()) except: logger.error(traceback.format_exc()) # return None return None
def on_get_share_uk(info, error=None): if error or not info: logger.error('SharePage.reload: %s, %s' % (error, info)) self.app.toast(_('Invalid link: {0}!'.format(self.curr_url))) self.has_next = False self.url_entry.props.secondary_icon_name = REFRESH_ICON return else: need_pwd = info[0] if info[1]: self.surl = info[1] if len(info) == 4 and info[2] and info[3]: self.uk, self.shareid = info[2:4] # 输入密码: if need_pwd: pwd_dialog = PwdDialog(self.app) response = pwd_dialog.run() if response == Gtk.ResponseType.OK: pwd = pwd_dialog.get_pwd() else: return pwd_dialog.destroy() gutil.async_call(pcs.verify_share_password, self.app.cookie, self.uk, self.shareid, self.surl, pwd, callback=on_verify_password) else: self.load_url()
def check_vcode(info, error=None): if error or not info: logger.error('CloudPage.check_vcode: %s, %s' % (info, error)) return if info.get('error_code', -1) != 0: logger.error('CloudPage.check_vcode: %s, %s' % (info, error)) if 'task_id' in info or info['error_code'] == 0: self.reload() elif info['error_code'] == -19: vcode_dialog = VCodeDialog(self, self.app, info) response = vcode_dialog.run() vcode_input = vcode_dialog.get_vcode() vcode_dialog.destroy() if response != Gtk.ResponseType.OK: return gutil.async_call(pcs.cloud_add_bt_task, self.app.cookie, self.app.tokens, source_url, save_path, selected_idx, file_sha1, info['vcode'], vcode_input, callback=check_vcode) else: self.app.toast(_('Error: {0}').format(info['error_msg']))
def post_multipart(url, headers, fields, files, retries=RETRIES): content_type, body = encode_multipart_formdata(fields, files) schema = urllib.parse.urlparse(url) headers_merged = default_headers.copy() for key in headers.keys(): headers_merged[key] = headers[key] headers_merged['Content-Type'] = content_type headers_merged['Content-length'] = str(len(body)) for i in range(retries): try: h = http.client.HTTPConnection(schema.netloc) h.request('POST', url, body=body, headers=headers_merged) req = h.getresponse() encoding = req.getheader('Content-encoding') req.data = req.read() if encoding == 'gzip': req.data = gzip.decompress(req.data) elif encoding == 'deflate': req.data = zlib.decompress(req.data, -zlib.MAX_WBITS) return req except OSError: logger.error(traceback.format_exc()) return None
def copy_link_to_clipboard(url, error=None): if error or not url: logger.error('IconWindow.on_copy_link_activated: %s, %s' % (url, error)) self.app.toast(_('Failed to copy link')) return self.app.update_clipboard(url)
def on_share(info, error=None): if error or not info or info['errno'] != 0: logger.error('IconWindow.on_share_activated: %s, %s' % (info, error)) self.app.toast(_('Failed to share selected files')) return self.app.update_clipboard(info['shorturl'])
def update_task_status(info, error=None): if error or not info: logger.error('CloudPage.scan_tasks: %s, %s' % (info, error)) return tasks = info['task_info'] for row in self.liststore: if not row or row[TASKID_COL] not in tasks: continue task = tasks[row[TASKID_COL]] row[SIZE_COL] = int(task['file_size']) row[FINISHED_COL] = int(task['finished_size']) row[STATUS_COL] = int(task['status']) if row[SIZE_COL]: row[PERCENT_COL] = int( row[FINISHED_COL] / row[SIZE_COL] * 100) for status_ in (STATUS_FINISHED, STATUS_DOWNLOADING, STATUS_FAILED): if row[STATUS_COL] in Status[status_]: if status_ == STATUS_DOWNLOADING: row[STATUSNAME_COL] = '{0}%'.format(row[PERCENT_COL]) else: row[STATUSNAME_COL] = StatusNames[status_] break size = util.get_human_size(row[SIZE_COL])[0] finished_size = util.get_human_size(row[FINISHED_COL])[0] if row[SIZE_COL] == row[FINISHED_COL]: row[HUMANSIZE_COL] = size else: row[HUMANSIZE_COL] = '{0}/{1}'.format(finished_size, size)
def do_update_avatar(info, error=None): if error or not info: logger.error("Failed to get user avatar: %s, %s" % (info, error)) else: uk, uname, img_path = info self.img_avatar.set_from_file(img_path) self.img_avatar.props.tooltip_text = "\n".join([self.profile["username"], uname])
def urlopen(url, headers={}, data=None, retries=RETRIES, timeout=TIMEOUT): '''打开一个http连接, 并返回Request. headers 是一个dict. 默认提供了一些项目, 比如User-Agent, Referer等, 就 不需要重复加入了. 这个函数只能用于http请求, 不可以用于下载大文件. 如果服务器支持gzip压缩的话, 就会使用gzip对数据进行压缩, 然后在本地自动 解压. req.data 里面放着的是最终的http数据内容, 通常都是UTF-8编码的文本. ''' headers_merged = default_headers.copy() for key in headers.keys(): headers_merged[key] = headers[key] opener = urllib.request.build_opener(ForbiddenHandler) opener.addheaders = [(k, v) for k, v in headers_merged.items()] for i in range(retries): try: req = opener.open(url, data=data, timeout=timeout) encoding = req.headers.get('Content-encoding') req.data = req.read() if encoding == 'gzip': req.data = gzip.decompress(req.data) elif encoding == 'deflate': req.data = zlib.decompress(req.data, -zlib.MAX_WBITS) return req except OSError: logger.error(traceback.format_exc()) return None
def on_verify_password(pwd_cookie, error=None): if error or not pwd_cookie: self.app.toast(_("Error: password error, please try again")) logger.error("SharePage.verify_password: %s, %s" % (pwd_cookie, error)) else: self.app.cookie.load_list(pwd_cookie) self.load_url()
def on_check_login(info, error=None): if not info or error: logger.error('SigninDialog.on_check_login: %s, %s' % (info, error)) self.signin_failed( _('Failed to check login, please try again')) else: ubi_cookie, status = info cookie.load_list(ubi_cookie) nonlocal codeString nonlocal verifycode codeString = status['data']['codeString'] vcodetype = status['data']['vcodetype'] if codeString: dialog = SigninVcodeDialog(self, username, cookie, tokens, codeString, vcodetype) response = dialog.run() verifycode = dialog.get_vcode() codeString = dialog.codeString dialog.destroy() if not verifycode or len(verifycode) != 4: self.signin_failed( _('Please input verification code!')) return else: gutil.async_call(auth.get_public_key, cookie, tokens, callback=on_get_public_key) else: gutil.async_call(auth.get_public_key, cookie, tokens, callback=on_get_public_key)
def _refresh_vcode(info, error=None): if not info or error: logger.error("SigninVcode.refresh_vcode: %s, %s." % (info, error)) return logger.debug("refresh vcode: %s" % info) self.codeString = info["data"]["verifyStr"] gutil.async_call(auth.get_signin_vcode, self.cookie, self.codeString, callback=self.update_img)
def refresh_signin_vcode(cookie, tokens, vcodetype): '''刷新验证码. vcodetype - 在调用check_login()时返回的vcodetype. ''' url = ''.join([ const.PASSPORT_BASE, 'v2/?reggetcodestr', '&token=', tokens['token'], '&tpl=pp&apiver=v3', '&tt=', util.timestamp(), '&fr=ligin', '&vcodetype=', encoder.encode_uri(vcodetype), ]) headers={ 'Cookie': cookie.header_output(), 'Referer': const.REFERER, } logger.debug('refresh vcode url: %s' % url) req = net.urlopen(url, headers=headers) if req: try: data = req.data.decode('gbk') logger.debug('refresh vcode: %s' % data) return json.loads(data) except ValueError: logger.error(traceback.format_exc()) return None
def on_verify_password(pwd_cookie, error=None): if error or not pwd_cookie: self.app.toast(_('Error: password error, please try again')) logger.error('SharePage.verify_password: %s, %s' % (pwd_cookie, error)) else: self.app.cookie.load_list(pwd_cookie) self.load_url()
def on_get_UBI(ubi_cookie, error=None): if error or not ubi_cookie: logger.error("SigninDialog.on_getUBI: %s, %s" % (ubi_cookie, error)) self.signin_failed(_("Failed to get UBI, please try again.")) else: cookie.load_list(ubi_cookie) self.signin_button.set_label(_("Check login")) gutil.async_call(auth.check_login, cookie, tokens, username, callback=on_check_login)
def on_delete_files(info, error=None): if error or not info or info['errno'] != 0: self.app.toast(_('Failed to delete files!')) logger.error('IconWindow.on_trash_activated: %s %s' % (info, error)) return else: self.parent.reload()
def on_get_bdstoken(bdstoken, error=None): if error or not bdstoken: logger.error("SigninDialog.on_get_bdstoken: %s, %s" % (bdstoken, error)) self.signin_failed(_("Failed to get bdstoken!")) else: nonlocal tokens tokens["bdstoken"] = bdstoken self.update_profile(username, password, cookie, tokens, dump=True)
def on_get_BAIDUID(uid_cookie, error=None): if error or not uid_cookie: logger.error("SigninDialog.on_get_BAIDUID: %s, %s" % (uid_cookie, error)) self.signin_failed(_("Failed to get BAIDUID cookie, please try again.")) else: cookie.load_list(uid_cookie) self.signin_button.set_label(_("Get TOKEN...")) gutil.async_call(auth.get_token, cookie, callback=on_get_token)
def on_share(info, error=None): if error or not info or info["errno"] != 0: logger.error("IconWindow.on_share_activated: %s, %s" % (info, error)) self.app.toast(_("Failed to share selected files")) return self.app.update_clipboard(info["shorturl"]) self.app.blink_page(self.app.my_share_page) self.app.my_share_page.load()
def on_share(info, error=None): if error or not info or info['errno'] != 0: logger.error('CreatePublicShareDialog.on_share: %s, %s' % (info, error)) self.app.toast(_('Failed to share selected files')) return self.app.update_clipboard(info['shorturl']) self.response(Gtk.ResponseType.OK)
def _refresh_vcode(info, error=None): if not info or error: logger.error('SigninVcode.refresh_vcode: %s, %s.' % (info, error)) return logger.debug('refresh vcode: %s' % info) self.codeString = info['data']['verifyStr'] gutil.async_call(auth.get_signin_vcode, self.cookie, self.codeString, callback=self.update_img)
def on_transfer_files(info, error=None): if error or not info: self.app.toast(_("Failed to copy selected files!")) logger.error("SharePage.on_cloud_button_clicked: %s %s" % (info, error)) elif info["errno"] != 0: self.app.toast(_("Failed to copy selected files! {0}").format(ErrorMsg.o.get(info["errno"]))) logger.error("SharePage.on_cloud_button_clicked: %s %s" % (info, error)) else: self.app.blink_page(self.app.home_page)
def on_share(info, error=None): if error or not info[0] or info[0]['errno'] != 0: logger.error('CreatePrivateShareDialog.on_share: %s, %s' % (info, error)) self.app.toast(_('Failed to share selected files')) return file_info, passwd = info self.app.update_clipboard("{0} {1}".format(file_info['shorturl'], passwd)) self.response(Gtk.ResponseType.OK)
def on_get_bdstoken(bdstoken, error=None): if error or not bdstoken: logger.error('SigninDialog.on_get_bdstoken: %s, %s' % (bdstoken, error)) self.signin_failed(_('Failed to get bdstoken!')) else: nonlocal tokens tokens['bdstoken'] = bdstoken self.update_profile(username, password, cookie, tokens, dump=True)
def on_share(info, error=None): print('on share:', info, error) if error or not info or info[0]['errno'] != 0: logger.error('IconWindow.on_share_activated: %s, %s' % (info, error)) self.app.toast(_('Failed to share selected files')) return #self.app.update_clipboard(info['shorturl']) file_info, passwd = info print('info:', file_info, passwd)
def on_get_UBI(ubi_cookie, error=None): if error or not ubi_cookie: logger.error('SigninDialog.on_getUBI: %s, %s' % (ubi_cookie, error)) self.signin_failed(_('Failed to get UBI, please try again.')) else: cookie.load_list(ubi_cookie) self.signin_button.set_label(_('Check login')) gutil.async_call(auth.check_login, cookie, tokens, username, callback=on_check_login)
def on_create_superfile(pcs_file, error=None): if error or not pcs_file: self.app.toast(_('Failed to upload, please try again')) logger.error('UploadPage.do_worker_merge_files: %s, %s' % (pcs_file, error)) do_worker_error(fid) return else: self.remove_slice_db(fid) do_worker_uploaded(fid)
def on_get_BAIDUID(uid_cookie, error=None): if error or not uid_cookie: logger.error('SigninDialog.on_get_BAIDUID: %s, %s' % (uid_cookie, error)) self.signin_failed( _('Failed to get BAIDUID cookie, please try again.')) else: cookie.load_list(uid_cookie) self.signin_button.set_label(_('Get TOKEN...')) gutil.async_call(auth.get_token, cookie, callback=on_get_token)
def update_image(filepath, tree_iter): try: pix = GdkPixbuf.Pixbuf.new_from_file_at_size(filepath, icon_size, icon_size) tree_path = liststore.get_path(tree_iter) if tree_path is None: return liststore[tree_path][col] = pix except GLib.GError: logger.error(traceback.format_exc())
def update_image(filepath, tree_iter): try: pix = GdkPixbuf.Pixbuf.new_from_file_at_size( filepath, icon_size, icon_size) tree_path = liststore.get_path(tree_iter) if tree_path is None: return liststore[tree_path][col] = pix except GLib.GError: logger.error(traceback.format_exc())
def update_img(self, request, error=None): if error or not request: # TODO: add a refresh button logger.error('VCodeDialog.update_img: %s, %s' % (request, error)) return vcode_path = os.path.join( Config.get_tmp_path(self.app.profile['username']), 'bcloud-download-vcode.jpg') with open(vcode_path, 'wb') as fh: fh.write(request.data) self.img.set_from_file(vcode_path)
def do_call(): result = None error = None try: result = func(*args) except Exception: error = traceback.format_exc() logger.error(error) if callback: GLib.idle_add(callback, result, error)
def on_load(self, info, error=None): self.loading_spin.stop() self.loading_spin.hide() if not info: self.app.toast(_('Network error')) elif info.get('errno', -1) != 0: self.app.toast(info.get('error_msg', _('Network error'))) if error or not info or info.get('errno', -1) != 0: logger.error('HomePage.on_load: %s, %s' % (info, error)) return self.icon_window.load(info['list'])
def xdg_open(uri): '''使用桌面环境中默认的程序打开指定的URI 当然, 除了URI格式之外, 也可以是路径名, 文件名, 比如: xdg_open('/etc/issue') 推荐使用Gio.app_info_xx() 来启动一般程序, 而用xdg_open() 来打开目录. ''' try: subprocess.call(['xdg-open', uri, ]) except FileNotFoundError: logger.error(traceback.format_exc())