CMDB资产管理系统开发【day25】:windows客户端开发
1、目录结构
PS Y:\MadkingClient> tree /f 卷 netgame 的文件夹 PATH 列表 卷序列号为 ACE3-896E Y:. ├─bin │ NedStark.py │ __init__.py │ ├─conf │ │ settings.py │ │ __init__.py │ │ │ └─__pycache__ │ settings.cpython-35.pyc │ __init__.cpython-35.pyc │ ├─core │ │ api_token.py │ │ HouseStark.py │ │ info_collection.py │ │ __init__.py │ │ │ └─__pycache__ │ api_token.cpython-35.pyc │ HouseStark.cpython-35.pyc │ info_collection.cpython-35.pyc │ __init__.cpython-35.pyc │ ├─logs │ run_log │ __init__.py │ ├─plugins │ │ plugin_api.py │ │ __init__.py │ │ │ ├─linux │ │ │ MegaCli │ │ │ sysinfo.py │ │ │ __init__.py │ │ │ │ │ └─__pycache__ │ │ sysinfo.cpython-35.pyc │ │ __init__.cpython-35.pyc │ │ │ ├─windows │ │ │ sysinfo.py │ │ │ │ │ └─__pycache__ │ │ sysinfo.cpython-35.pyc │ │ │ └─__pycache__ │ plugin_api.cpython-35.pyc │ __init__.cpython-35.pyc │ └─var .asset_id PS Y:\MadkingClient>
2、安装收集windows里硬件信息的模块
安装pywin32-221
下载地址:
https://jaist.dl.sourceforge.net/project/pywin32/pywin32/Build%20221/pywin32-221.win-amd64-py3.5.exe
安装一路下一步就可以了
安装WMI
下载地址:
https://files.pythonhosted.org/packages/f6/6b/3c15ef280e2a6244ff0635f763b86fdc113654afc1192fcea8a0109f47f8/WMI-1.4.9.win32.exe
安装一路回车就可以了
python WMI模块的使用实例
https://blog.csdn.net/zmj_88888888/article/details/8700950
turer单词写错
3、如何把数据传输都服务器?
你不知道管理员什么时候审批?
让管理员知道你把数据发给他了,
1、肯定不能存在表里, 2、存到服务器内存不行,客户端一重启就不行了, 3、写到文件里可以 4、存到数据库的临时表里面
我有一条资产要发送给服务器我把数据不能存到数据库里那我存到那里呀?
答:新资产审批区,等管理员审批完了写到数据库里
客户第二次回报的数据怎样和第一次存到数据库里面的数据如何关联?
1、可以用SN号作为关联
这个是最简单的
2、可以同过自增id
之前,我在汽车之家就没有把sn当做资产唯一值
数据更新流程
1、第一次数据存到待批准区
2、当管理员资产一批准就会把资产Id返回给客户端
3、以后客户端更新就带着服务器端给的资产ID
服务器如何把资产id返回给客户端?
1、服务端无法返回因为是一个web浏览器
2、所以他只有等只有等客户端第二次链接的时候给他一个资产ID
4、NedStark入口
#_*_coding:utf-8_*_
import os,sys,platform #for linux if platform.system() == "Windows": BASE_DIR = '\\'.join(os.path.abspath(os.path.dirname(__file__)).split('\\')[:-1]) print BASE_DIR else: BASE_DIR = '/'.join(os.path.abspath(os.path.dirname(__file__)).split('/')[:-1]) sys.path.append(BASE_DIR) from core import HouseStark if __name__ == '__main__': HouseStark.ArgvHandler(sys.argv)
#为什么叫nedstack入口文件,分析参数,手机参数,私有方法
5、HouseStark注释
#_*_coding:utf-8_*_ import info_collection from conf import settings import urllib,urllib2,sys,os,json,datetime import api_token class ArgvHandler(object): def __init__(self,argv_list): self.argvs = argv_list self.parse_argv() def parse_argv(self): if len(self.argvs) >1: if hasattr(self,self.argvs[1]): func = getattr(self,self.argvs[1]) func() else: self.help_msg() else: self.help_msg() #帮助菜单.有参数就执行,没参数打印帮助 def help_msg(self): msg = ''' collect_data 收集硬件信息 run_forever 永远运行 get_asset_id 获取资产ID report_asset 收集硬件信息并汇报 ''' print(msg) def collect_data(self): """收集硬件信息""" obj = info_collection.InfoCollection() asset_data = obj.collect() #加上括号不是类就是方法这里显然是方法,因为我给它赋了一个obj #print asset_data # def run_forever(self): pass def __attach_token(self,url_str): '''generate md5 by token_id and username,and attach it on the url request''' user = settings.Params['auth']['user'] token_id = settings.Params['auth']['token'] md5_token,timestamp = api_token.get_token(user,token_id) url_arg_str = "user=%s×tamp=%s&token=%s" %(user,timestamp,md5_token) if "?" in url_str:#already has arg new_url = url_str + "&" + url_arg_str else: new_url = url_str + "?" + url_arg_str return new_url #print(url_arg_str) def __submit_data(self,action_type,data,method): ''' send data to server param action_type:url param data:具体要发送的数据 param method :get/post return: ''' if action_type in settings.Params['urls']: if type(settings.Params['port']) is int: url = "http://%s:%s%s" %(settings.Params['server'],settings.Params['port'],settings.Params['urls'][action_type]) #有端口 else: url = "http://%s%s" %(settings.Params['server'],settings.Params['urls'][action_type]) #没有端口 url = self.__attach_token(url) #端口验证 print('Connecting [%s], it may take a minute' % url) if method == "get": args = "" for k,v in data.items(): args += "&%s=%s" %(k,v) args = args[1:] url_with_args = "%s?%s" %(url,args) try: req = urllib2.Request(url_with_args) req_data = urllib2.urlopen(req,timeout=settings.Params['request_timeout']) callback = req_data.read() print("-->server response:",callback) return callback except urllib2.URLError as e: sys.exit("\033[31;1m%s\033[0m"%e) elif method == "post": try: data_encode = urllib.urlencode(data) req = urllib2.Request(url=url,data=data_encode) res_data = urllib2.urlopen(req,timeout=settings.Params['request_timeout']) callback = res_data.read() callback = json.loads(callback) print("\033[31;1m[%s]:[%s]\033[0m response:\n%s" %(method,url,callback)) return callback except Exception as e: sys.exit("\033[31;1m%s\033[0m"%e) else: raise KeyError #def __get_asset_id_by_sn(self,sn): # return self.__submit_data("get_asset_id_by_sn",{"sn":sn},"get") def load_asset_id(self,sn=None): asset_id_file = settings.Params['asset_id'] has_asset_id = False if os.path.isfile(asset_id_file): asset_id = open(asset_id_file).read().strip() if asset_id.isdigit(): return asset_id else: has_asset_id = False else: has_asset_id = False def __update_asset_id(self,new_asset_id): asset_id_file = settings.Params['asset_id'] f = open(asset_id_file,"wb") f.write(str(new_asset_id)) f.close() def report_asset(self): obj = info_collection.InfoCollection() asset_data = obj.collect() """ asset_id是干什么的? def log_record """ asset_id = self.load_asset_id(asset_data["sn"]) """ 为什么要oad_asset_id,拿到文件名 第一次回报肯定没有,但是我不知道你是第几次,所以只能用这个资产id来判断 """ if asset_id: #reported to server before asset_data["asset_id"] = asset_id post_url = "asset_report" else:#first time report to server '''report to another url,this will put the asset into approval waiting zone, when the asset is approved ,this request returns asset's ID''' asset_data["asset_id"] = None post_url = "asset_report_with_no_id" """ 首先要判断文件存在不,如果存在就判断是不是一个整数,要取资产ID post_url是干什么的? 为了不影响全局我单独写一个URL,name是什么,是一个变量l """ data = {"asset_data": json.dumps(asset_data)} response = self.__submit_data(post_url,data,method="post") if "asset_id" in response: self.__update_asset_id(response["asset_id"]) self.log_record(response) """ asset_id是干什么的? def log_record """ def log_record(self,log,action_type=None): f = open(settings.Params["log_file"],"ab") if log is str: pass if type(log) is dict: if "info" in log: for msg in log["info"]: log_format = "%s\tINFO\t%s\n" %(datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S"),msg) #print msg f.write(log_format) if "error" in log: for msg in log["error"]: log_format = "%s\tERROR\t%s\n" %(datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S"),msg) f.write(log_format) if "warning" in log: for msg in log["warning"]: log_format = "%s\tWARNING\t%s\n" %(datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S"),msg) f.write(log_format) f.close()
urllib.request read()
Windows PowerShell 版权所有 (C) 2009 Microsoft Corporation。保留所有权利。 PS C:\Users\Administrator> python Python 3.5.3 (v3.5.3:1880cb95a742, Jan 16 2017, 16:02:32) [MSC v.1900 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import urllib.request >>> urllib.request.urlopen("http://www.baidu.com") <http.client.HTTPResponse object at 0x0000000002C38B00> >>> rep = urllib.request.urlopen("http://www.baidu.com") >>> rep.read() b'<!DOCTYPE html>\n<!--STATUS OK-->\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\ n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\ n\r\n\r\n\r\n\r\n\r\n\t\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\ r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\t\r\n \r\n\t\t\t \r\n\t\r\n\t\t\t \r\n\t\r\n\t\t\t \r\n\ t\r\n\t\t\t \r\n\t\t\t \r\n\r\n\t\r\n \r\n\t\t\t \r\n\t\r\n\t\t\t \r\n\t\r\n\t\t\t \r\n\t\r\n\t\t\t \r\n\t\t\t \r\n\r\n\r\n\r\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\r\n\n<html>\n<head>\n \ n <meta http-equiv="content-type" content="text/html;charset=utf-8">\n <meta http-equiv="X-UA-Compatible" content= "IE=Edge">\n\t<meta content="always" name="referrer">\n <meta name="theme-color" content="#2932e1">\n <link rel="s hortcut icon" href="/favicon.ico" type="image/x-icon" />\n <link rel="search" type="application/opensearchdescription +xml" href="/content-search.xml" title="\xe7\x99\xbe\xe5\xba\xa6\xe6\x90\x9c\xe7\xb4\xa2" />\n <link rel="icon" sizes ="any" mask href="//www.baidu.com/img/baidu_85beaf5496f291521eb75ba38eacbd87.svg">\n\t\n\t\n\t<link rel="dns-prefetch" h ref="//s1.bdstatic.com"/>\n\t<link rel="dns-prefetch" href="//t1.baidu.com"/>\n\t<link rel="dns-prefetch" href="//t2.bai du.com"/>\n\t<link rel="dns-prefetch" href="//t3.baidu.com"/>\n\t<link rel="dns-prefetch" href="//t10.baidu.com"/>\n\t<l ink rel="dns-prefetch" href="//t11.baidu.com"/>\n\t<link rel="dns-prefetch" href="//t12.baidu.com"/>\n\t<link rel="dns-p ...省略 to_empty=!0,window.__switch_add_mask=!0;var s="http://s1.bdstatic.com/r/www/cache/static/global/js/all_async_search_eef4 222.js",n="/script";document.write("<script src=\'"+s+"\'><"+n+">"),bds.comm.newindex&&$(window).on("index_off",function (){$(\'<div class="c-tips-container" id="c-tips-container"></div>\').insertAfter("#wrapper"),window.__sample_dynamic_tab &&$("#s_tab").remove()\n}),bds.comm&&bds.comm.ishome&&Cookie.get("H_PS_PSSID")&&(bds.comm.indexSid=Cookie.get("H_PS_PSSI D"))}();</script>\r\n\r\n\r\n\r\n<script>\r\nif(bds.comm.supportis){\r\n window.__restart_confirm_timeout=true;\r\n window.__confirm_timeout=8000;\r\n window.__disable_is_guide=true;\r\n window.__disable_swap_to_empty=true;\r\n} \r\ninitPreload({\r\n \'isui\':true,\r\n \'index_form\':"#form",\r\n \'index_kw\':"#kw",\r\n \'result_form\' :"#form",\r\n \'result_kw\':"#kw"\r\n});\r\n</script>\r\n\r\n<script>\r\nif(navigator.cookieEnabled){\r\n\tdocument.c ookie="NOJS=;expires=Sat, 01 Jan 2000 00:00:00 GMT";\r\n}\r\n</script>\r\n\r\n\n\n</body>\n</html>\n\r\n\r\n\r\n\n\r\n' >>>
url拼接截图
6、info_collection注释
#_*_coding:utf-8_*_ from plugins import plugin_api import json,platform,sys class InfoCollection(object): '''手机信息''' def __init__(self): pass def get_platform(self): os_platform = platform.system() ''' 获取平台是linux还是window >>> import platform >>> platform.system() 'Windows' >>> [root@adminset ~]# python Python 2.7.5 (default, Apr 11 2018, 07:36:10) [GCC 4.8.5 20150623 (Red Hat 4.8.5-28)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import platform >>> platform.system() 'Linux' >>> ''' return os_platform def collect(self): os_platform = self.get_platform() ''' 通过类反射看有没有相应的平台 ''' try: func = getattr(self,os_platform) info_data = func() ''' 获取的数据返回给collect平台了 ''' formatted_data = self.build_report_data(info_data) return formatted_data except AttributeError as e: sys.exit("Error:MadKing doens't support os [%s]! " % os_platform) def Linux(self): sys_info = plugin_api.LinuxSysInfo() return sys_info def Windows(self): sys_info = plugin_api.WindowsSysInfo() print(sys_info) #f = file('data_tmp.txt','wb') #f.write(json.dumps(sys_info)) #f.close() return sys_info def build_report_data(self,data): #add token info in here before send return data
7、api_token注释
#_*_coding:utf-8_*_ import hashlib,time def get_token(username,token_id): timestamp = int(time.time()) md5_format_str = "%s\n%s\n%s" %(username,timestamp,token_id) obj = hashlib.md5() obj.update(md5_format_str) print "token format:[%s]" % md5_format_str print "token :[%s]" % obj.hexdigest() return obj.hexdigest()[10:17], timestamp if __name__ =='__main__': print get_token('alex','test')
8、settings注释
#_*_coding:utf8_*_ import os BaseDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) Params = { "server": "192.168.1.38", "port":9000, 'request_timeout':30, "urls":{ "asset_report_with_no_id":"/asset/report/asset_with_no_asset_id/", #新资产批准区 "asset_report":"/asset/report/",#正式资产表 }, 'asset_id': '%s/var/.asset_id' % BaseDir, ''' 你从服务器拿到资产ID,你只能存到文件里,你不肯呢个在本地建一个数据库吧! 存到var下,为什么是一个隐藏文件? liunx的所有的进程号存在文件里,而且是隐藏的,所以我也存文件 ''' 'log_file': '%s/logs/run_log' % BaseDir, 'auth':{ 'user':'lijie3721@126.com', 'token': 'abc' }, }
9、plugin_api注释
#_*_coding:utf-8_*_ from linux import sysinfo def LinuxSysInfo(): #print __file__ return sysinfo.collect() def WindowsSysInfo(): from windows import sysinfo as win_sysinfo return win_sysinfo.collect() ''' window的导入模块为什么写在下面,,这个是需要单独安装, 因为写在文件头,安装Linux的时候没有这个包会报错 '''
10、sysinfo注释
#_*_coding:utf-8_*_ __author__ = 'Alex Li' import platform import win32com import wmi import os def collect(): data = { 'os_type': platform.system(), 'os_release':"%s %s %s "%( platform.release() ,platform.architecture()[0],platform.version()), 'os_distribution': 'Microsoft', 'asset_type':'server' } #data.update(cpuinfo()) win32obj = Win32Info() data.update(win32obj.get_cpu_info()) data.update(win32obj.get_ram_info()) data.update(win32obj.get_server_info()) data.update(win32obj.get_disk_info()) data.update(win32obj.get_nic_info()) #for k,v in data.items(): # print k,v return data class Win32Info(object): def __init__(self): self.wmi_obj = wmi.WMI() self.wmi_service_obj = win32com.client.Dispatch("WbemScripting.SWbemLocator") self.wmi_service_connector =self.wmi_service_obj.ConnectServer(".","root\cimv2") def get_cpu_info(self): data = {} cpu_lists = self.wmi_obj.Win32_Processor() cpu_core_count = 0 for cpu in cpu_lists: cpu_core_count += cpu.NumberOfCores cpu_model = cpu.Name data["cpu_count"] = len(cpu_lists) data["cpu_model"] = cpu_model data["cpu_core_count"] =cpu_core_count return data def get_ram_info(self): data = [] ram_collections = self.wmi_service_connector.ExecQuery("Select * from Win32_PhysicalMemory") for item in ram_collections: item_data = {} #print item mb = int(1024 * 1024) ram_size = int(item.Capacity) / mb item_data = { "slot":item.DeviceLocator.strip(), "capacity":ram_size, "model":item.Caption, "manufactory":item.Manufacturer, "sn":item.SerialNumber, } data.append(item_data) #for i in data: # print i return {"ram":data} def get_server_info(self): computer_info = self.wmi_obj.Win32_ComputerSystem()[0] system_info = self.wmi_obj.Win32_OperatingSystem()[0] data = {} data['manufactory'] = computer_info.Manufacturer data['model'] = computer_info.Model data['wake_up_type'] = computer_info.WakeUpType data['sn'] = system_info.SerialNumber #print data return data def get_disk_info(self): data = [] for disk in self.wmi_obj.Win32_DiskDrive(): #print disk.Model,disk.Size,disk.DeviceID,disk.Name,disk.Index,disk.SerialNumber,disk.SystemName,disk.Description item_data = {} iface_choices = ["SAS","SCSI","SATA","SSD"] for iface in iface_choices: if iface in disk.Model: item_data['iface_type'] = iface break else: item_data['iface_type'] = 'unknown' item_data['slot'] = disk.Index item_data['sn'] = disk.SerialNumber item_data['model'] = disk.Model item_data['manufactory'] = disk.Manufacturer item_data['capacity'] = int(disk.Size ) / (1024*1024*1024) data.append(item_data) return {'physical_disk_driver':data} def get_nic_info(self): data = [] for nic in self.wmi_obj.Win32_NetworkAdapterConfiguration(): if nic.MACAddress is not None: item_data = {} item_data['macaddress'] = nic.MACAddress item_data['model'] = nic.Caption item_data['name'] = nic.Index if nic.IPAddress is not None: item_data['ipaddress'] = nic.IPAddress[0] item_data['netmask'] = nic.IPSubnet else: item_data['ipaddress'] = '' item_data['netmask'] = '' bonding = 0 #print nic.MACAddress ,nic.IPAddress,nic.ServiceName,nic.Caption,nic.IPSubnet #print item_data data.append(item_data) return {'nic':data} if __name__=="__main__": collect()
获取cpu信息
PS Y:\MadkingClient> python Python 3.5.3 (v3.5.3:1880cb95a742, Jan 16 2017, 16:02:32) [MSC v.1900 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import platform >>> import win32com >>> import wmi >>> import os >>> wmi_obj = wmi.WMI() >>> wmi_service_obj = win32com.client.Dispatch("WbemScripting.SWbemLocator") >>> wmi_service_connector = wmi_service_obj.ConnectServer(".","root\cimv2") >>> wmi_obj.Win32_Processor() [<_wmi_object: b'\\\\XK104\\root\\cimv2:Win32_Processor.DeviceID="CPU0"'>] >>> cpu_list = wmi_obj.Win32_Processor() >>> dir(cpu_list[0]) ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getat tribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__re duce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_associated_classes' , '_cached_associated_classes', '_cached_methods', '_cached_properties', '_getAttributeNames', '_get_keys', '_instance_o f', '_keys', '_methods', '_properties', 'associated_classes', 'associators', 'derivation', 'id', 'keys', 'methods', 'ole _object', 'path', 'properties', 'property_map', 'put', 'qualifiers', 'references', 'set', 'wmi_property'] >>> cpu = cpu_list[0] >>> cpu.id 'winmgmts:{authenticationlevel=pktprivacy,impersonationlevel=impersonate}!\\\\xk104\\root\\cimv2:win32_processor.devicei d="cpu0"' >>> cpu.Name 'Intel(R) Core(TM) i5-4570 CPU @ 3.20GHz' >>> cpu.NumberOfCores 4
获取内存信息
PS C:\Users\Administrator> python Python 3.5.3 (v3.5.3:1880cb95a742, Jan 16 2017, 16:02:32) [MSC v.1900 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import platform >>> import win32com >>> import wmi >>> import os >>> wmi_service_obj = win32com.client.Dispatch("WbemScripting.SWbemLocator") >>> wmi_service_connector = wmi_service_obj.ConnectServer(".","root\cimv2") >>> ram_collections = wmi_service_connector.ExecQuery("Select * from Win32_PhysicalMemory") >>> ram_collections <COMObject <unknown>> >>> for i in ram_collections: ... print(i.Capacity,i.Caption,i.Manufacturer,i.SerialNumber,i.DeviceLocator) ... 8589934592 Physical Memory Kingston 16474864 ChannelB-DIMM1 >>>
遇到的坑:
1、Python中expected an indented block 缩进的问题
2、Manufacturer单词写错
收集资产截图
11、新资产待审批区表结构注释
class NewAssetApprovalZone(models.Model): """新资产待审批区""" sn = models.CharField(u'资产SN号', max_length=128, unique=True) asset_type_choices = ( ('server', u'服务器'), ('switch', u'交换机'), ('router', u'路由器'), ('firewall', u'防火墙'), ('storage', u'存储设备'), ('NLB', u'NetScaler'), ('wireless', u'无线AP'), ('software', u'软件资产'), ('others', u'其它类'), ) asset_type = models.CharField(choices=asset_type_choices, max_length=64, blank=True, null=True) manufactory = models.CharField(max_length=64, blank=True, null=True) model = models.CharField(max_length=128, blank=True, null=True) ram_size = models.IntegerField(blank=True, null=True) cpu_model = models.CharField(max_length=128, blank=True, null=True) cpu_count = models.IntegerField(blank=True, null=True) cpu_core_count = models.IntegerField(blank=True, null=True) os_distribution = models.CharField(max_length=64, blank=True, null=True) os_type = models.CharField(max_length=64, blank=True, null=True) os_release = models.CharField(max_length=64, blank=True, null=True) """ 客户端过来的数据会临时存到临时表里 上面的字段都不重要,重要的就是下面的data,data里面会存上面?存所有的资产信息 """ data = models.TextField(u'资产数据') date = models.DateTimeField(u'汇报日期', auto_now_add=True) approved = models.BooleanField(u'已批准', default=False) approved_by = models.ForeignKey('UserProfile', verbose_name=u'批准人', blank=True, null=True) approved_date = models.DateTimeField(u'批准日期', blank=True, null=True) def __str__(self): return self.sn class Meta: verbose_name = '新上线待批准资产' verbose_name_plural = "新上线待批准资产"
作者:罗阿红
出处:http://www.cnblogs.com/luoahong/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。