分类

类型:
不限 游戏开发 计算机程序开发 Android开发 网站开发 笔记总结 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
原创:
不限
年份:
不限 2018 2019

技术文章列表

  • 把修改/更新过的项目重新提交至github上

    更新项目提交至github只需要几条命令即可:

    在本地的git仓库将你修改过的项目复制到下面(覆盖掉之前上传的)
    右击选择Git Bash Here打开命令行
    输入下面四行命令即可

    git statusgit add . (别忘了add后面+空格+.)git commit -m “备注”git push origin master

    最后刷新下就OK了。
    1 留言 2019-05-13 16:34:48 奖励2点积分
  • 5G将渗透哪些领域

    5G爆发前夕将渗透哪些领域?5G 已经被吹捧为许多细分市场的新的赋能者,它将给移动电话、自动驾驶、虚拟现实(VR)和物联网等行业带来机会。但是,这个新的无线通信标准将在何时和将以何种方式影响这些细分市场,以及它将对半导体设计产生什么样的影响,目前尚有很多问题和不确定性。
    基于 5G 承诺的通信速度的大幅度提高和延迟的大幅度降低,系统供应商必须在将数据处理放在本地或在云端之间作出决定。这将对半导体体系结构产生重大影响,包括处理器和内存的片上吞吐量、I/O 速度、功率预算,甚至电池大小等等。此外,这些决定还将受到 5G 基础设施接入和通信频率的影响。
    不过,这些事情可能要等到数年后才会发生。一开始,大部分的 5G 应用将在低于 6GHz 的范围内,也就是和 4.5G 相当。最大的受益者将是移动电话行业,它将在一段时间内仍然是 5G 技术的最大消费者。移动电话的标准和存储容量将会因为 5G 技术的不断引入而持续进化,它们将依然是 5G 技术的发展的重要资金来源。
    下一个阶段的发展是在毫米波技术引进之时。这时候最重要的变化将开始发生。一般的经验法则是,任何一项新技术想要成功,必须提供10倍的增益,要么是性能提高、或功率降低、成本降低、面积更小或者多种增益的组合。只有在那个时候,5G 技术才会真正地大放光彩。
    “5G 将在连接性能方面提供显著的改进,其目标是比目前 4G 的连接性能提高1000倍”。Steven Woo 说,他是 Rambus 实验室负责系统和解决方案的副总裁、杰出的发明家。 “除了带宽方面的改进,5G 还承诺了降低延迟和更好的覆盖范围。”
    在毫米波技术的应用初期,设计成本和硅晶元出货面积将会显著上升。电力消耗将成为主要的问题,这取决于基础设施和它能承受的负荷压力,因为它们需要承担信号发送和信号到达时的计算工作。
    物联网一个真正能从 5G 中受益的领域是边缘计算,其中功耗是一个限制因素。“我们希望 5G 的引入能使物联网边缘设备的功耗更低,因为平均来说 5G 拉近了它们与接入点的距离。” Cadence 的产品营销总监 Neil Robinson 说:“这意味着与 4G 通信所需要的更长距离相比,5G 通信所需要的功耗更低”。
    这样就打开了一扇大门,让我们有能力处理比目前更加复杂的处理流程和通信方案。Woo 说:“5G 将提供更高的带宽,这意味着越来越多的终端(endpoint)将会更容把它们的数据传输到相邻位置,在那里这些数据得以在本地处理。”这意味着只有少量的,级别更高的数据/信息,才需要传输到云端处理。
    但这种方案也会很快变得复杂。“5G 所需的高带宽和多天线策略意味着,从本质上讲,它们需要更高的能耗。”西门子仿真部门市场营销高级总监 Jean-Marie Brunet 指出。“然而,人们普遍认为物联网的大部分将是机对机(M2M)通信。而机对机通信模式比人工启动的物联网更容易预测,因此机对机实例的低功耗算法应该更高效,这种说法是有争议的。”
    实际上的好处将随着不同的应用和区域有所不同。“归根结底,5G 将使每比特功耗降低10倍到100倍,同时带来相当于10倍的带宽增幅,这些变化将在元器件的寿命方面产生净增益,并同时提高10倍的功效。因使用模式的不同这些好处会有所不同。”
    这是否足以影响架构?物联网边缘设备已开始将推理运算本地化,以避免与向云端传输大量原始数据而带来的带宽影响。
    “更高的带宽和更低的延迟将使在云端进行推理运算变得更容易,如果我们需要这样做的话。” Cadence 的 Robinson 说。“但是,隐私、安全、延迟和功耗问题可能会让这种方式变得不合适。”
    Cadence 的 Tensilica IP 产品管理和营销高级总监 Lazaar Louis 也回应了这些担忧。“对隐私和安全问题的担忧将会使推理运算继续留在边缘设备进行,”他说。“将传感器收集到的信息传送到云端会消耗能量,因此在边缘设备进行推理运算,可以节省在边缘设备上消耗的能量。”
    Rambus 实验室的 Woo 同意这一说法。“更高的带宽可能不能排除在边缘设备上进行推理运算的必要性,但它们将允许更大数量的物联网设备相互连接,使基础设施能够跟上不断增长的物联网设备的需求,并使其捕获和传输的数据量不断增长。”
    VR(虚拟现实)和AR(增强现实)虚拟现实(VR)碰到了难题。如果没有更高的数据速率,设备供应商将很难消除晕眩不适感,这大大地限制了 VR 的使用。毫米波技术可以帮助解决这个问题。虽然毫米波信号不能通过墙壁,只能在相对较短的距离内工作,但 VR 耳机和控制器的距离通常只有几英尺远。
    “与以前运行同一款游戏的 4G 产品相比,高分辨率的 8K 游戏流媒体肯定会更快地将电池的电量耗光。” Brunet 承认 “但是这并不是 5G 造成的,而是因为更高级的 CPU 和显示设备。在毫米波频率下,对于空中下载(OTA)的功耗需求,收发器的功耗将会以加速度级地增加。”
    自动驾驶自动驾驶需要将许多技术结合在一起,5G 就是其中之一。“更低的延迟为具有自主驾驶能力的联网车辆带来了好处,在这种情况下,响应时间至关重要,尤其是在高速公路的行驶速度下。”Woo说道。“5G 覆盖范围的改进,加上增强的本地处理能力,将允许在数据产生的终端设备附近对数据进行聚合和处理,从而减少数据传输。减少长距离的数据传输对于 5G 来说是一个重要的好处,因为它可以同时改进了延迟和功耗。”
    汽车很可能成为通讯枢纽。“汽车将是微型发射器,” Robinson 说。“和现在的 802.11p(V2V)和4G/OnStar(V2X)面临的情况类似,5G 也面临着来自电视网络的阻挠,他们希望使用已经分配给 V2V(车辆对车辆)的相同频段。他们声称,自动驾驶不需要 V2V,在路上使用 V2X(车辆对所有)就可以来了解其他车辆的存在/意图。”
    这个观点得到其他人的同意。“车辆到所有(V2X)将主导通信,车辆将成为拥有很多发射器的移动网络,在许多情况下,每个功能域都有多个发射器。” Brunet 说:“V2X 将是实现安全的关键因素,因为激光雷达或雷达根本看不到拐角处,而 5G 可以依据拐角处的反射将这种实现变得可能。”
    到底是从其他车辆还是从路边的信息中得到这些信息,现在尚没有明确的答案。“汔车能获得的信息越多,它们就越能在自动驾驶体验方面做出更好的决定。” Cadence 公司的 Louis 指出。 “更先进的自动驾驶将得益于和其他车辆/基础设施的 V2X 通信,例如路线规划和车道变更辅助。”
    新的通信能力也可能带给我们今天还没有想到的好处。“联网车辆只是众多可能受益的设备之一。” Woo 说。“更高的带宽和更高的覆盖率,再加上更强的本地处理能力,将帮助车辆与周围环境以及本地地图数据进行通信,从而在将来实现道路导航。今天的联网汽车已经捕获了大量的数据,其中一些信息被传送到云端。预计汽车将继续发展而成为信息枢纽,因为它们可以作为乘客设备的连接点(就像手机可以作为智能手表和健身设备等外围设备的连接点一样),并与其他车辆进行点对点通信,以交流像车道变化之类的预期行动,以及交通状况和危险信息。”
    实施注意事项今天大多数 5G 的实现都还只是原型,而且并不是所有的问题都已经得到解决。例如,5G 可以提供每秒10到20千兆位的传输速度,但数字系统必须具备相应的工作速度才能受益于 5G 的高速度。如果为了降低成本而仍然使用旧的工艺节点生成数字信息,这可能使得能使用的工艺节点变得有限,或者我们需要更先过的封装解决方案。
    “这是摩尔定律的一个结合,它减慢了速度,同时增加了芯片的复杂性和所需要的工艺的复杂性。。” Rambus 的产品管理高级主管 Frank Ferro 表示:“你不需要做一个大型的 ASIC,你必须问一下分解它是否更划算。做两个更小的 ASIC 或重新使用你已经进行了大量投资的混合信号器件是否更便宜?如果你已经在高速工艺技术上进行了投资,你想继续扩大规模吗?或者,您可以使用现有的技术,加上接口技术,而不必每次更改工艺节点时都开发 SerDes 呢?“
    同样,不同的产品领域可能会得出不同的结论。“我不知道 5G 节点的期望值。” Robinson 说道。“这将归结为每个产品或公司的成本
    2 留言 2019-04-22 10:31:20 奖励15点积分
  • python爬虫--爬取网站中的多个网页

    爬取7k7k小游戏的URL
    # -*- coding: utf-8 -*-"""Created on Sun Mar 24 10:04:58 2019@author: pry"""import requestsfrom bs4 import BeautifulSoupimport osimport reimport urllibfrom lxml import etreedef parse_page(): t = 1 headers = { 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3642.0 Safari/537.36' } for i in range(1,5): url_i = 'http://www.7k7k.com/flash_fl/461_' + str(i) + '.htm' response_i = requests.get(url_i, headers = headers) selector = etree.HTML(response_i.text, parser=etree.HTMLParser(encoding = 'utf-8')) print(url_i) content = selector.xpath('//a/@href') for i in content: if i[0] == "j": continue if i[0] == "/": i = url_i + i with open('7k7k_urls.txt','a+') as file: file.write(i) file.write("\n") file.close() print(i) t = t + 1 print(t) print('ok')if __name__ == '__main__': parse_page()
    2 留言 2019-04-25 08:45:45 奖励5点积分
  • Android Studio使用POI读取及修改Word文档(.docx格式)

    一、说明上一篇文章(Android Studio使用POI读取及修改Word文档(.doc格式))使用poi对.doc格式的word文档进行了读取和更改,但很多情况下还需要在word文档中插入图片,这时就需要对.docx格式的word进行操作了。
    二、实现过程2.1 制作文书文书在源代码中可以直接看到,简单说明一下:文书有普通字段、表格、特定位置的图片,又在页眉页脚中加了普通字段和表格,基本满足对于word操作的所有情况。

    2.2 导包还是上篇中poi-3.9压缩资源包中的jar包,对.docx格式文档的操作用到XWPFDocument方法,使用到所有ooxml相关的jar包。

    2.3 build配置这次的build配置有点特殊,特别拿出来截图一下。就像上一篇说的一样,apache的很多配置在安卓是跑不通的,这次导包后,你会遇到方法过多,文件重复,基于jdk1.6以上版本的变异保存等一系列问题,可以按照下面的方法处理。当然不同的android studio版本可能也会有不同的处理方法,可以百度一下。

    权限还是储存权限,直接上代码吧,注解的也很详细。
    2.4 实现源码public class MainActivity extends AppCompatActivity { //创建生成的文件地址 private static final String newPath = "/storage/emulated/0/test.docx"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 0); } Button go = (Button) findViewById(R.id.go); go.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { initData(); } }); } private void initData() { Map<String, Object> map = new HashMap<String, Object>(); map.put("$TITLE$", "标题");//$TITLE$只是个标识符,你也可以用${},[]等等 map.put("$TXT1$", "表格第一行"); map.put("$CONTENT1$", "第一行内容"); map.put("$TXT2$", "表格第二行"); map.put("$CONTENT2$", "第二行内容"); map.put("$CONTENT3$", "页脚中的内容"); map.put("$TXT4$", "页脚表格第一行"); map.put("$CONTENT4$", "页脚第一行内容"); map.put("$TXT5$", "页脚表格第二行"); map.put("$CONTENT5$", "页脚第二行内容"); try { //读取示例文书 InputStream is = getAssets().open("test.docx"); //自定义的XWPFDocument,解决官方版图片不显示问题 CustomXWPFDocument document = new CustomXWPFDocument(is); //读取段落(一般段落,页眉页脚没办法读取) List<XWPFParagraph> listParagraphs = document.getParagraphs(); processParagraphs(listParagraphs, map); //读取页脚 List<XWPFFooter> footerList = document.getFooterList(); processParagraph(footerList, map); //处理表格 Iterator<XWPFTable> it = document.getTablesIterator(); while (it.hasNext()) {//循环操作表格 XWPFTable table = it.next(); List<XWPFTableRow> rows = table.getRows(); for (XWPFTableRow row : rows) {//取得表格的行 List<XWPFTableCell> cells = row.getTableCells(); for (XWPFTableCell cell : cells) {//取得单元格 if ("$IMG$".equals(cell.getText())) { //直接插入图片会在文档的最底端,所以要插在固定位置,要把图片放在表格里 //所以使用判断单元格,并清除单元格放置图片的方式来实现图片定位 cell.removeParagraph(0); XWPFParagraph pargraph = cell.addParagraph(); document.addPictureData(getAssets().open("1.png"), XWPFDocument.PICTURE_TYPE_PNG); document.createPicture(document.getAllPictures().size() - 1, 120, 120, pargraph); } List<XWPFParagraph> paragraphListTable = cell.getParagraphs(); processParagraphs(paragraphListTable, map); } } } FileOutputStream fopts = new FileOutputStream(newPath); document.write(fopts); if (fopts != null) { fopts.close(); } } catch (Exception e) { e.printStackTrace(); } } //处理页脚中的段落,其实就是用方法读取了下页脚中的内容,然后也会当做一般段落处理 private void processParagraph(List<XWPFFooter> footerList, Map<String, Object> map) { if (footerList != null && footerList.size() > 0) { for (XWPFFooter footer : footerList) { //读取一般段落 List<XWPFParagraph> paragraphs = footer.getParagraphs(); processParagraphs(paragraphs, map); //处理表格 List<XWPFTable> tables = footer.getTables(); for (int i = 0; i < tables.size(); i++) { XWPFTable xwpfTable = tables.get(i); List<XWPFTableRow> rows = xwpfTable.getRows(); for (XWPFTableRow row : rows) {//取得表格的行 List<XWPFTableCell> cells = row.getTableCells(); for (XWPFTableCell cell : cells) {//取得单元格 List<XWPFParagraph> paragraphListTable = cell.getParagraphs(); processParagraphs(paragraphListTable, map); } } } } } } //处理段落 public static void processParagraphs(List<XWPFParagraph> paragraphList, Map<String, Object> param) { if (paragraphList != null && paragraphList.size() > 0) { for (XWPFParagraph paragraph : paragraphList) { List<XWPFRun> runs = paragraph.getRuns(); for (XWPFRun run : runs) { String text = run.getText(0); if (text != null) { boolean isSetText = false; for (Map.Entry<String, Object> entry : param.entrySet()) { String key = entry.getKey(); if (text.indexOf(key) != -1) { isSetText = true; Object value = entry.getValue(); if (value instanceof String) {//文本替换 text = text.replace(key, value.toString()); } } } if (isSetText) { run.setText(text, 0); } } } } } }}
    2.5 效果展示
    本文转载自:https://blog.csdn.net/qq_21972583/article/details/82740281
    2 留言 2019-10-08 13:02:23 奖励16点积分
  • Android Studio使用POI读取及修改Word文档(.doc格式)

    一、前言如果你可爱的项目经理要求安卓端的你来操作word实现各种功能,不要犹豫,直接动之以情晓之以理,因为这本来就是java的poi,安卓虽然源自java,但对于java的很多东西是不支持的,已有的各种jar包也不方便更改,各种报错会搞的你脑阔疼。所以编辑word文档这种事让后台来做要比安卓来做简单的多,但如果实在避免不了,接着,给你代码。
    二、说明本篇不支持word2007版,只支持2003版,也就是只支持.doc格式,不支持.docx格式(想要支持.docx格式请参考“Android Studio使用POI读取及修改Word文档(.docx格式)”)。.doc格式的word文档是不支持图片插入的,因为.doc格式和.docx格式有很大的区别,用到的jar包和方法也不同,如果需要插入图片,请查看下篇对.docx格式word文档的处理。
    三、实现过程3.1 制作文书制作.doc格式的文档,然后导入项目:

    文章展示的文档内容如下:

    3.2 导包用到java poi-3.9中的两个包,完整压缩包点这里下载。然后复制用来编辑.doc文档的两个依赖包,导入libs目录。

    如果提示报错,是因为jar中有重复文件,请看build配置。

    加入外部存储读写权限,以后还需要自行申请,代码中有体现:
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    3.3 实现代码读写过程直接上代码了,一看就懂:
    Button go;//生成文件的所在的地址private static final String newPath = "/storage/emulated/0/hwpfdocument.doc";@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); go = (Button) findViewById(R.id.go); go.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { readWord(); } }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 0); }}private void readWord() { try { //从assets读取我们的Word模板 InputStream is = getAssets().open("hwpfdocument.doc"); //创建生成的文件路径 File newFile = new File(newPath); //需要修改的字段放入map中 Map<String, String> map = new HashMap<String, String>(); map.put("${TITLE}", "标题"); map.put("${TXT}", "表格第一行"); map.put("${CONTENT}", "内容"); writeDoc(is, newFile, map); } catch (IOException e) { e.printStackTrace(); }}private void writeDoc(InputStream is, File newFile, Map<String, String> map) { try { //使用poi的HWPFDocument方法 HWPFDocument hdt = new HWPFDocument(is); //Range读取word文本内容 Range range = hdt.getOverallRange(); //replaceText替换文本内容 for (Map.Entry<String, String> entry : map.entrySet()) { range.replaceText(entry.getKey(), entry.getValue()); } ByteArrayOutputStream ostream = new ByteArrayOutputStream(); FileOutputStream out = new FileOutputStream(newFile, true); hdt.write(ostream); //输出字节流 out.write(ostream.toByteArray()); out.close(); ostream.close(); Toast.makeText(this, "文书已生成", Toast.LENGTH_SHORT).show(); } catch (IOException e) { e.printStackTrace(); }}
    3.4 效果展示
    附录关于Apache POI - HWPF和XWPF的说明可以参考这里。
    关于HWPF的API可以参考这里。左上角选择org.apache.poi.hwpf,然后左下角选择hwpfdocument即可。
    本文转载自:https://blog.csdn.net/qq_21972583/article/details/82385940
    1 留言 2019-10-06 11:36:29 奖励15点积分
  • 基于Skin++库实现的换肤功能

    背景之前自己经常使用MFC来开发一些界面程序,这些程序大都是自己练手用的。但,也会有极个别是帮别人开发,给别人使用。当你辛苦做出来的作品拿出去给别人用的时候,你总想让自己的作品给人留下深刻印象,无论是从功能,还是用程序界面上。
    对于,我们使用 VC6.0 或者 VS2008、VS2010等以上版本开发出来的界面程序,界面通常都是千篇一律,非常质朴的。所以,自己就像用最简单的方式去修改下界面,因为界面虽说重要,但也并不是说不可或缺的,所以自己不想花费太多的精力在界面上。
    后来,经过搜索查找,还真让我找到一个不错的、方便易用的界面修改方法,即使用 Skin++界面库 来实现。Skin++ 属于第二代的外挂式的界面库,提供了Skin Builder 工具将所有控件的资源全部设计成为一个独立的文件,便于在应用程序外部独立地进行增删改操作,采用Hook与子类化技术来实现应用程序的自动换肤。
    现在,本文就基于 Skin++界面库 实现给 VC 或 VS 开发的界面程序更换皮肤。把实现的过程整理成文档,分享给大家。
    实现过程程序要使用到的 Skin++库 文件包括:SkinPPWTL.h、SkinPPWTL.lib、skinppwtl.dll 以及 48个 .ssk 的皮肤文件。
    首先,我们需要把 Skin++ 库文件加载到程序里,做法如下:

    把 SkinPPWTL.h 头文件以及 SkinPPWTL.lib 库文件拷贝到我们的项目工程目录下面,然后在程序声明头文件并加载库文件:
    #include <afxcmn.h>#include <Windows.h>#include "skin\\SkinPPWTL.h"#pragma comment(lib, "skin\\SkinPPWTL.lib")
    然后,我们编译下程序,看看有没有错误提示。若出现类似这样的错误提示: “… _CRT_SECURE_NO_WARNINGS …”,则在 项目属性 —> C/C++ —> 预处理器 —> 预处理器定义 中添加 “__CRT_SECURE_NO_WARNINGS” 这个预定义即可。

    经过上面两步操作,就可以正确地把 Skin++ 库文件加载到程序中了。
    接下来,我们就直接调用 skinppLoadSkin 函数加载 .ssk 格式的皮肤库就可以了。但是要注意调用 skinppLoadSkin 函数的地方,一定要在界面实现出来之前的初始化操作里就开始调用,不能再界面显示出来后调用,否则会出问题。对于 MFC 程序和 Windows应用程序,它们调用 skinppLoadSkin 函数加载界面库文件的地方可以是:

    对于 MFC 程序,可以是在 CxxxAPP::InitInstance() 初始化函数的开头调用,也可以是在 CxxxDlg::OnInitDialog() 主窗口类初始化函数里调用。
    对于 Windows 应用程序,可以是在 WinMain 函数的开头调用,也可以是在窗口消息处理过程函数中的 WM_INITDIALOG 消息中加载。

    调用 skinppLoadSkin 函数加载界面库文件的代码如下所示:
    // 加载皮肤::skinppLoadSkin("skins\\XP-Home.ssk");
    要更换皮肤,只需要更改上面加载的 .ssk 库文件就好,本文有 48 个 .ssk 文件,所以,可以实现 48 种换肤。
    最后,我们编译链接生成可执行文件,在运行程序之前,只需要按照上面库文件 .ssk 的路径放置库文件,同时还需要把 skinppwtl.dll 动态链接库放在和可执行程序同一目录下,就可以正常运行程序了。
    程序测试按照上面的方法,我们分别创建了一个 MFC 程序和 Windows应用程序 来进行测试,程序成功更换皮肤。


    总结对于 Skin++界面库使用比较简单,主要在窗口初始化的时候,就对界面库初始化,并加载界面库就好,剩下的,不需要我们去理会,我们只需要正常开发我们的程序功能就好。
    3 留言 2018-11-29 09:23:21 奖励10点积分
  • 基于python构建搜索引擎系列——(四)检索模型 精华

    构建好倒排索引之后,就可以开始检索了。
    检索模型有很多,比如向量空间模型、概率模型、语言模型等。其中最有名的、检索效果最好的是基于概率的BM25模型。
    给定一个查询Q和一篇文档d,d对Q的BM25得分公式为:

    公式中变量含义如下:

    qtf:查询中的词频
    tf:文档中的词频
    ld:文档长度
    avg_l:平均文档长度
    N:文档数量
    df:文档频率
    b,k1,k3:可调参数

    这个公式看起来很复杂,我们把它分解一下,其实很容易理解。第一个公式是外部公式,一个查询Q可能包含多个词项,比如“苹果手机”就包含“苹果”和“手机”两个词项,我们需要分别计算“苹果”和“手机”对某个文档d的贡献分数w(t,d),然后将他们加起来就是整个文档d相对于查询Q的得分。
    第二个公式就是计算某个词项t在文档d中的得分,它包括三个部分。第一个部分是词项t在查询Q中的得分,比如查询“中国人说中国话”中“中国”出现了两次,此时qtf=2,说明这个查询希望找到的文档和“中国”更相关,“中国”的权重应该更大,但是通常情况下,查询Q都很短,而且不太可能包含相同的词项,所以这个因子是一个常数,我们在实现的时候可以忽略。
    第二部分类似于TFIDF模型中的TF项。也就是说某个词项t在文档d中出现次数越多,则t越重要,但是文档长度越长,tf也倾向于变大,所以使用文档长度除以平均长度ld/avg_l起到某种归一化的效果,k1和b是可调参数。
    第三部分类似于TFIDF模型中的IDF项。也就是说虽然“的”、“地”、“得”等停用词在某文档d中出现的次数很多,但是他们在很多文档中都出现过,所以这些词对d的贡献分并不高,接近于0;反而那些很稀有的词如”糖尿病“能够很好的区分不同文档,这些词对文档的贡献分应该较高。
    所以根据BM25公式,我们可以很快计算出不同文档t对查询Q的得分情况,然后按得分高低排序给出结果。
    下面是给定一个查询句子sentence,根据BM25公式给出文档排名的函数:
    def result_by_BM25(self, sentence): seg_list = jieba.lcut(sentence, cut_all=False) n, cleaned_dict = self.clean_list(seg_list) BM25_scores = {} for term in cleaned_dict.keys(): r = self.fetch_from_db(term) if r is None: continue df = r[1] w = math.log2((self.N - df + 0.5) / (df + 0.5)) docs = r[2].split('\n') for doc in docs: docid, date_time, tf, ld = doc.split('\t') docid = int(docid) tf = int(tf) ld = int(ld) s = (self.K1 * tf * w) / (tf + self.K1 * (1 - self.B + self.B * ld / self.AVG_L)) if docid in BM25_scores: BM25_scores[docid] = BM25_scores[docid] + s else: BM25_scores[docid] = s BM25_scores = sorted(BM25_scores.items(), key = operator.itemgetter(1)) BM25_scores.reverse() if len(BM25_scores) == 0: return 0, [] else: return 1, BM25_scores
    首先将句子分词得到所有查询词项,然后从数据库中取出词项对应的倒排记录表,对记录表中的所有文档,计算其BM25得分,最后按得分高低排序作为查询结果。
    类似的,我们还可以对所有文档按时间先后顺序排序,越新鲜的新闻排名越高;还可以按新闻的热度排序,越热门的新闻排名越高。
    关于热度公式,我们认为一方面要兼顾相关度,另一方面也要考虑时间因素,所以是BM25打分和时间打分的一个综合。
    比较有名的热度公式有两个,一个是Hacker News的,另一个是Reddit的,他们的公式分别为:


    可以看出,他们都是将新闻/评论的一个原始得分和时间组合起来,只是一个用除法,一个用加法。所以我们也依葫芦画瓢,”自创“了一个简单的热度公式:

    用BM25得分加上新闻时间和当前时间的差值的倒数,k1k1和k2k2也是可调参数。
    按时间排序和按热度排序的函数和按BM25打分排序的函数类似,这里就不贴出来了,详细情况可以看我的项目News_IR_Demo。
    至此,搜索引擎的搜索功能已经实现了,你可以试着修改./web/search_engine.py的第167行的关键词,看看搜索结果是否和你预想的排序是一样的。不过由于我们的数据量只有1000个新闻,并不能涵盖所有关键词,更多的测试可以留给大家线下完成。
    本文转载自:http://bitjoy.net/2016/01/07/introduction-to-building-a-search-engine-4
    2 留言 2019-06-01 15:29:45 奖励14点积分
  • windows下静态使用QxOrm框架并使用泛型编程 (二)

    这篇开始讲实际编程并且抽象化,让代码书写更少。
    为了模块划分我在Demo文件夹下新增了一个SQLModule文件夹,在此文件夹下又新增QxHandler,QxMapped,QxObject三个文件夹,QxObject是用来存储数据对象类的一个文件夹,QxMapped是存储键值映射的文件夹,QxHandler是操作数据库的实际句柄类文件夹。
    QxObject类
    User.h
    #ifndef USER_H#define USER_H#include "common.h"#include <QxOrm.h>#include <QJsonObject>class User{ QX_REGISTER_FRIEND_CLASS(User) //用于将类注册到QxOrm的宏定义public: User(); long getId(); QString getName(); int getAge();public: QJsonObject toJsonObject(); bool analyzeJson(QByteArray &json); void analyzeJson(QJsonObject &json);private: long m_lId QString m_sName; int m_nAge;};//QX_REGISTER_PRIMARY_KEY(User, int) //主键不是整数类型的时候使用QX_REGISTER_HPP_IMPORT_DLL(User, qx::trait::no_base_class_defined, DATABASE_VERSION) //用于将类注册到QxOrm的宏定义 第一个参数为类名 第二个为默认值 第三个为数据库版本#endif // USER_HUser.cpp
    #include "User.h"QX_REGISTER_CPP_IMPORT_DLL(User) //用于将类注册到QxOrm的宏定义namespace qx //用于将类注册到QxOrm的方法 { template <> void register_class(QxClass<User> & t) { qx::IxDataMember * pData = NULL; Q_UNUSED(pata); qx::IxSqlRelation * pRelation = NULL; Q_UNUSED(pRelation); qx::IxFunction * pFct = NULL; Q_UNUSED(pFct) qx::IxValidator * pValidator = NULL; Q_UNUSED(pValidator); // Register pData =t.id(& User::m_lId, "id",DATABASE_VERSION); pData =t.data(& User::m_nAge, "name",DATABASE_VERSION); pData =t.data(& User::m_nAge, "age",DATABASE_VERSION); qx::QxValidatorX<User> * pAllValidator = t.getAllValidator(); Q_UNUSED(pAllValidator); }}User::User() :m_lId(0) ,m_sName("") ,m_nAge(0){}long User::getId(){ return m_lId;}QString User::getName(){ return m_sName;}int User::getAge(){ return m_nAge}JsonObject User::toJsonObject(){ QJsonObject subObject; subObject.insert("Name",m_sName); subObject.insert("Age",m_nAge); return subObject;}bool User::analyzeJson(QByteArray &json){ bool success=false; QJsonParseError error; QJsonDocument document=QJsonDocument::fromJson(json,&error); if (!document.isNull() && (error.error == QJsonParseError::NoError)) { QJsonObject rootObject =documnt.object(); this->analyzeJson(rootObject); success=true; } else { success=false; qDebug()<<error.error; qDebug("LockoutFun analyze falied"); } return success;}void User::analyzeJson(QJsonObject &json){ QJsonObject rootObject = json.value("User").toObject(); m_sName=rootObject.value(QString("Name")).toString(); m_nAge=rootObject.value(QString("Age")).toInt();}QxMapped
    IMapped 映射抽象类
    #ifndef IMAPPED_H#define IMAPPED_H/*** @author tianmin* @brief The IMapped class 抽象出来的映射类* @date 2019-07-22*/#include <QStringList>#include <QSting>#include <QMapclass IMapped{public //字符map初始化 virtual void initMapped() = 0; //获取QString列表 virtual QStringList getListMapped(int en) =0; //获取QString 字段 virtual QString getMapped(int en) = 0; virtual ~IMapped(){}};#endif // IMAPPED_HUserMapped 际使用的类
    UserMapped .h
    #ifndef USERMAPPED_H#define USERMAPPED_H#include "IMapped.h"namespace USER {enum USER_MAPPED{ EN_ID = 0x00000001, EN_NAME = 0x00000002, EN_AGE = 0x00000004, EN_ALL = 0xFFFFFFFF};}class UserMapped:public IMapped{public: UserMapped(); virtual ~UserMapped();public: virtual void initMapped() virtual QStringList getListMapped(int en); virtual QString getMapped(int en);private: QMap<int,QString> m_map;};#endif // USERMAPPED_HUserMapped.cpp
    #include "UserMapped.h"UserMapped::UserMapped(){}UserMapped::~UserMapped(){}void UserMapped::initMapped(){ m_map.clear(); m_map.insert(USER::EN_ID, "id"); m_map.insert(USER::EN_NAME ,"name"); m_map.insert(USER::EN_AGE ,"age");}QStringList UserMapped::getListMapped(int en){ QStringList temp; QString str; QMap<int,QString>::iterator i; or (i = m_map.begin(); i != m_map.end(); ++i) { if(en&(i.key())) { str=i.value(); temp.append(str); str.clear(); } } return temp;}QString UserMapped::getMapped(int en){ QString str QMap<int,QString>::iterator i; for (i = m_map.begin(); i != m_map.end(); ++i) { if(en==i.key()) { str =i.value(); return str; } } return QString();}QxHandler
    IHandler 抽象化的类模板
    这里只抽象了部分方法 还有save 和事务以及关系的方法未抽象完成
    #ifndef IHANDLER_H#define IHANDLER_H#include <QxOrm.h>#include <QMutexLocker>#include <QMutex>#include <QtSql/QSqlDatabase>#include <QtSql/QSqlError>/*** @brief The ISqlInterface class 数据库操作的抽象方法类 并且模板化减少代码的繁杂*/class ISqlInterface{public: ISqlInterface(){} virtual ~ISqlInterface(){} //数据库连接初始化protected: virtual void initSqlconnect()=0; //建表 virtual bool createTable()=0; //断开连接 virtual void disconnect()=0;};template<class T,class T2,class T3>class IHandler{public: IHandler(){} virtual ~IHandler(){} Virtual bool createTable(QSqlDatabase &m_SqlDatabase) { QSqlError error= qx::dao::create_table<T3>(&m_SqlDatabase); return !error.isValid(); } virtual bool insert(T &t,QMutex &m_Mutex,QSqlDatabase &m_SqlDatabase) { QMutexLocker locker(&m_Mutex); if(!m_SqlDatabase.isOpen()) return false; if(qx::dao::exist(t,&m_SqlDatabase).getValue()!=false) return false; QSqlError error= qx::dao::insert(t,&m_SqlDatabase); return !error.isValid(); } virtual bool deleteObject(T &t,QMutex &m_Mutex,QSqlDatabase &m_SqlDatabase,bool isDestroy) { QMutexLocker locker(&m_Mutex); if(!m_SqlDatabase.isOpen()) return false; QSqlError error; if(qx::dao::exist(t,&m_SqlDatabase).getValue()==false) return false; if(isDestroy==false) { error= qx::dao::delete_by_id(t,&m_SqlDatabase); } else { error= qx::dao::destroy_by_id(t,&m_SqlDatabase); } return !error.isValid(); } virtual bool update(T &t,QMutex &m_Mutex,QSqlDatabase &m_SqlDatabase,QStringList &list) { QMutexLocker locker(&m_Mutex); if(!m_SqlDatabas.isOpen()) return false; if(qx::dao::exist(t,&m_SqlDatabase).getValue()==false) return false; QSqlError error= qx::dao::update(t,&m_SqlDatabase,list); return !error.isValid(); } virtual bool select(T &t,QMutex &m_Mutex,QSqlDatabase &m_SqlDatabase,QStringList &list) QMutexLocker locker(&m_Mutex); if(!m_SqlDatabase.isOpen()) return false; QSqlError error; if(qx::dao::exist(t,&m_SqlDatabase).getValue()==false) return false; error= qx::dao::fetch_by_id(t,&m_SqlDatabase,list); return !error.isValid(); } virtual bool selectByQuery(T2 &t,QMutex &m_Mutex,QSqlDatabase &m_SqlDatabase,qx::QxSqlQuery &query,QStringList &list) { QMutexLocker locker(&m_Mutx); if(!m_SqlDatabase.isOpen()) return false; QSqlError error=qx::dao::fetch_by_query(query,t,&m_SqlDatabase,list); return !error.isValid(); }};#endif // IHANDLER_HUserHandler 实际操作句柄类
    UserHandler.h
    #ifndef USERHANDLER_H#define USERHANDLER_H#include <QxOrm.h>#include <QString>#include <QMutexLocker>#include <QMutex>#include <QtSql/QSqlDatabase>#include <QtSql/QSqlError>#include "IHandler.h"#include "SQLModule/QxMapped/UserMapped.h"#include "SQLModule/QxObject/User.h"namespace USER{const QString DATABASE_TYPE="QSQLITE";const QString CONNECT_NAME="USER_CONNECTED";const QString DATABASENAME="C:/Users/we/Desktop/workTools/demo/qxorm.db";const QString HOSTNAME="localhost";const QString USERNAME="root";const QString PASSWORD="";}using namespace USER;class User; //OBJECT 类typedef QSharedPointer<User> Shared_User; //User类智能指针typedef QList<Shared_User> List_User; //User类数组typedef qx::QxCollection<int,Shared_User> Collection_User; //User容器class UserHandler:public IHandler<Shared_User,Collection_User,User> ,public ISqlInterface{public: UserHandler(); virtual ~UserHandler(); /** * @brief insert 插入数据至数据库 * @param t 插入的单条数据 不需要指定ID值 自增 * @return 0失败 1成功 */ bool insert(Shared_User &t); /** * @brief deleteObject 从数据库中删除指定数据 * @param t 删除的单条数据 需要指定ID值 * @param isDestroy 是否软删除 * @return 0失败 1成功 */ bool deleteObject(Shared_User &t,bool isDestroy=false); /** * @brief update 根据ID值更新数据 * @param t 数据 * @param en 更新字段的映射值 * @return 0失败 1成功 */ bool update(Shared_User &t,int en=EN_ALL); /** * @brief select 根据ID值查询数据 * @param t 数据 * @param en 映射值 * @return 0失败 1成功 */ bool select(Shared_User &t,int en=EN_ALL); /** * @brief selectByQuery 根据搜寻条件查找 * @param t 数据集合 * @param query 搜寻语句 * @param en 数据库列映射值 * @return 0失败 1成功 */ bool selectByQuery(Collection_User &t,qx::QxSqlQuery &query,int en=EN_ALL);protected: virtual void initSqlconnect(); virtual bool createTable(); virtual void disconnect();private: UserMapped m_Mapped; QMutex m_Mutex; QSqlDatabase m_SqlDatabase;};#endif // USERHANDLER_HUserHandler.cpp
    #include "UserHandler.h"UserHandler::UserHandler(){ initSqlconnect(); createTable();}UserHandler::~UserHandler(){ disconnect();}void UserHandler::initSqlconnect(){ QMutexLocker locker(&m_Mutex); if(QSqlDatabase::contains(CONNECT_NAME)) m_SqlDatabase = QSqlDatabase::database(CONNECT_NAME); else m_SqlDatabase= QSqlDatabase::addDatabase(DATABASE_TYPE,CONNECT_NAME); m_SqlDatabase.setDatabaseName(DATABASENAME); m_SqlDatabase.setHostName(HOSTNAME); m_SqlDatabase.setUserName(USERNAME); m_SqlDatabase.setPassword(PASSWORD); m_SqlDatabase.open();}bool UserHandler::createTable(){ return IHandler<Shared_User,Collection_User,User>::createTable(m_SqlDatabase);}void UserHandler::disconnect(){ QMutexLocker locker(&m_Mutex); if(m_SqlDatabase.isOpen()) m_SqlDatabase.close(); QSqlDatabase::removeDatabase(CONNECT_NAME);}bool UserHandler::insert(Shared_User &t){ return IHandler<Shared_User,Collection_User,User>::insert(t,m_Mutex,m_SqlDatabase);}bool UserHandler::deleteObject(Shared_User &t,bool isDestroy){ return IHandler<Shared_User,Collection_User,User>::deleteObject(t,m_Mutex,m_SqlDatabase,isDestroy);}bool UserHandler::update(Shared_User &t, int en){ QStringList list= m_Mapped.getListMapped(en); return IHandler<Shared_User,Collection_User,User>::update(t,m_Mutex,m_SqlDatabase,list);}bool UserHandler::select(Shared_User &t, int en){ QStringList list= m_Mapped.getListMapped(en); return IHandler<Shared_User,Collection_User,User>::select(t,m_Mutex,m_SqlDatabase,list);}bool UserHandler::selectByQuery(Collection_User &t,qx::QxSqlQuery &query,int en){ QStringList list= m_Mapped.getListMapped(en); return IHandler<Shared_User,Collection_User,User>::selectByQuery(t,m_Mutex,m_SqlDatabase,query,list);}
    1 留言 2019-07-23 14:38:57 奖励6点积分
  • PC微信逆向分析のWeTool内部探秘 精华

    作者:zmrbak(赵庆明老师)
    前言先不说微信在社交领域的霸主地位,我们仅从腾讯公司所透露的在研发微信过程中踩过的无数的坑,以及公开的与微信相关的填坑的源码中,我们可以感受到,单从技术上讲,微信是一款非常伟大的产品。然而,伟大的产品,往往会被痴迷于技术的人送进实验室,运用各种可能的工具将其大卸八块,以参悟其“伟大”之所在!。
    WeTool,一款免费的微信社群管理工具,正是一群痴迷于技术的人对于微信这个伟大的产品的研究而得到的成果。在微商界,这个软件真可谓是鼎鼎大名、如雷贯耳。如果你还不知晓这个软件,那么你肯定不是微商界的人。如果你想对你的微信群进行管理,而又不想花钱,也许这个软件就是你最佳的选择。当然,免费软件的套路都是一样的,WeTool“有意地”不满足你的一些特殊需求,如果真的很想要的话,当然是要付费的,那就购买“企业版”吧。
    但是,对于一个对技术有强烈兴趣的人来说,研究WeTool与研究PC微信一样有趣,在这里,我把它们两个一起送进实验室,一窥其中的奥秘!
    微信中的WeTool由于腾讯干预,目前WeTool免费版本已不再公开提供下载。但之前的旧版本仍然可以自动升级到最新版。如果你想获得WeTool这个软件,我想,你应该知道该怎么做了吧。如果你还是不知道,很抱歉,这篇文章对你来说太深奥了。那么我对你的建议是:关掉这个网页吧。
    WeTool在启动的时候,会检查当前计算机上是否安装了版本匹配的PC微信。倘若找不到,或者版本不匹配,WeTool会引导你到它的官网去下载一个版本匹配的PC微信(可能比较旧,但能用)。下载完毕后,还需要你手动去安装一下。
    在WeTool启动的时候,还会检查微信的登录状态,如果微信还未完成登录,WeTool会等待微信登录之后,再开启自己的管理界面。
    这里的问题是:WeTool是如何得知微信是否已经登录了呢?
    在这里,我们使用PCHunter来检查一下微信(WeChat.exe)的进程模块。我们可以看到,在微信的进程中加载了一个特殊的DLL文件(WeHelp.dll),而它的父目录是一个特殊的字符串:“2.6.8.65”,恰好与我们当前运行的微信版本一致。再上一层的目录,“WeToolCore”,很明显,这里的文件是WeTool的一部分。

    恰恰是这个DLL文件帮助WeTool完成了与微信之间的各种互动。也就是说,WeTool通过WeHelp.dll这个文件,可以感知到微信的各种活动,当然也包括微信是否已经登录等等…
    窥探WeTool如果在不经意之间关闭了WeTool,你会发现,你的微信也被关闭了。这又是为什么呢?
    如果你曾经用OD调试过软件,你会发现当你的OD被关闭的时候,被OD所调试的那个软件也被关闭掉了。因此,我们猜想,WeTool对于微信来说,应该使用的是类似于OD之于其他软件相同的原理,那就是“调试”。
    在WeTool管理你的微信的时候,你也会发现,这时候微信无法被OD所附加。其实,还是“调试”。当一个软件已经处于某个调试器的“调试”之下,为了防止出错,调试器会拒绝对这个已处于被调试中的软件的再次调试。这进一步印证了WeTool对于微信的“调试”的事实。
    然而就是这么一个“小小的”设置,就击碎不少“小白”想调试WeTool美梦。
    既然我们找到了WeTool对于微信的关键,那就是文件“WeHelp.dll”。那么,我们就把这个文件请入我们的实验室,让我们把它一点一点地拆开,细细探寻其中的一点一滴的奥秘。
    拆解WeTool在动手拆解之前,我们还是先了解一下WeTool到底向我们的计算机上安装了些什么东东。顺着桌面上的“WeTool 免费版”,我们找到了WeTool安装的目录,安装目录之下22个文件夹和84个文件。当然,让我们比较感兴趣的就是“WeChatVersion”这个文件夹,因为它的名字与微信(WeChat)太让人能联想到一起了。

    双击“WeChatVersion”,我们看到如下结果。恰好是以微信曾经的一个个版本号命名的文件夹。我们猜想,这个文件夹一定与这个版本的微信之间存在中某种联系。目前,我们可以得到最新的微信版本是2.6.8.68(此版本为更新版;从腾讯官网可下载到的版本仅为2.6.8.65),而这里恰好有一个以该版本号命名的文件夹“2.6.8.65”。

    我们双击打开“2.6.8.65”这个文件夹。文章前面所提到的“WeHelp.dll”文件赫然在目。点开其他类似微信版本号的文件夹,同样,每个文件夹中都有这两个文件。唯一的区别就是文件的大小不一样。
    由于我们使用的微信版本是2.6.8.65,那么我们就针对2.6.8.65文件夹下的这个“WeHelp.dll”进行研究。通过二进制对比,我们发现该文件夹下的“WeHelp.dll”文件与微信中加载的“WeHelp.dll” 文件为同一个文件。

    由此,我们得出结论:WeTool为不同版本的微信分别提供了不同的WeHelp.dll文件,在WeTool启动的时候,把WeChatVersion中对应与当前版本微信号的文件夹复制到当前Windows登录用户的应用程序数据文件夹中,然后再将里面的“WeHelp.dll”加载到微信进程的内存中。
    WeHelp解析WeTool为“WeHelp.dll”设置了一道阻止“动态调试”的障碍,这足以让所有的动态调试器,在没有特殊处理前,对它根本无法下手。
    如果能绕道而行,那何必强攻呢?于是我们请出静态分析的利器——IDA PRO 32。注意,这里务必使用32位版本的,因为只有在32位版本中,才可以把汇编代码转换成C语言的伪代码。相比于汇编代码来说,C代码就直观的多了。
    打开IDA,点击按钮“GO”,然后把WeHelp.dll拖入其中,接下来就是十几秒的解析,解析完毕后,界面如下:

    从IDA解析的结果中,让我们很惊奇的是,在“WeHelp.dll”中居然未发现什么加壳啊、加密啊、混淆啊等等这些对于程序版权保护的技术。也许是WeTool太自信了吧!毕竟WeTool是事实上的业界老大,其地位无人可以撼动。
    对于和微信之间交互的这部分功能来说,其实对于一个刚入门的、比较勤奋的逆向新手,只需经过半年到一年时间的练手,这部分功能也是可以完成。对于WeTool来说,其真正的核心价值不在这里,而在于其“正向”的管理逻辑,以及自己后台的Web服务器。在它的管理界面,各种功能实现里逻辑错综复杂,如果你想逆向的话,还不如重写算了,况且它都已经免费给你用了,还有必要逆向吗!!当然,WeTool后台的服务器,你根本就碰不到。
    从IDA解析的结果中,可以看到WeHelp中各个函数、方法,毫无遮拦地完全展示在眼前。而在右侧的窗口中,按下F5,瞬间汇编代码变成了C语言的伪代码。

    对于一个稍稍有一些Window API编程经验的人来说,这些全部都是似曾相识的C代码,只需简单地猜一猜,就能看明白写的是啥。如果还是不懂的话,那就打开Visual Studio,对照着看吧。这里是DllMain,也就是DLL的入口函数。我们还是来创建一个C++的动态链接库(dll)的项目,来对照着看吧:

    fdwReason=1,恰好,DLL_PROCESS_ATTACH=1。一旦DLL被加载,则马上执行DllMain这个函数中的DLL_PROCESS_ATTACH分支。也就是说,当“WeHelp.dll”这个文件被微信加载到进程之后,马上执行一下DllMain函数,DLL_PROCESS_ATTACH分支里面的这两个函数就会马上执行。

    鼠标双击第一个函数(sub_10003040),到里面去看看这个函数里面有啥,如下图,它的返回值来自于一个Windows Api(桃红色字体)——“RegisterWindowMessageW”,查看MSDN后,发现,原来是注册Windows消息。
    这不是我们最想要的,按ESC键,返回。
    鼠标双击下一个函数(sub_100031B0),页面变成这个啦。很明显,在注册一个窗口类。对于一个窗口来说,最重要的就是它的回调函数,因为要在回调函数中,完成对窗口的所有事件处理。这里,lpfnWndProc= sub_10003630,就很明显了,这就是回调函数。

    双击sub_10003630这个函数,窗口切换为如下内容。除了第一条语句的if之外,剩下的if…else if…else if是那么的引人注目。每一个比较判断之后,都调用了一个函数。而判断的依据是传入的参数“lParam”要与一个dword的值比较。
    我们猜测,这些函数大概是WeHelp和微信之间交互相关的函数吧。当然,这只是猜测,我们还要进一步验证才行。

    sub_10003630这个函数,是窗口的回调函数,我们要重点关注。那么,我们先给它改个名字吧。在函数名上点右键,选中“Rename global item”,我们取个名字叫“Fn_WndProc”吧。于是页面就变成了这样:

    虽然在IDA中,“WeHelp.dll”中的函数(方法)全部显示出来了,但是也有40多个呢,我们找个简单一点的来试试。CWeHelp::Logout(void),这个函数没有参数,那么我们就选这个吧。在左侧函数窗口中双击CWeHelp::Logout(void),右侧窗口换成了这个函数的C语言伪代码(如果你显示的还是汇编,请点击汇编代码后按F5)。

    在前面,我们看到,在回调函数中,lParam要与一个dword值进行比较。在这个函数中,我们发现,这里为lParam赋了一个dword类型的值。为了方便记忆,我们把这个dowrd值改个名字吧,因为是Logout函数中用到的数字,那么就叫做“D_Logout”吧。

    接下来,我们要看看”还有谁”在用这个数值。在我们修改后的“D_Logout”上点右键,选择“Jump to xref…”。原来这个数值只有两个地方使用,一个就是当前的“Logout”函数,而另一个却是在”Fn_WndProc”中,那不就是前面的那个回调函数嘛!选中“Fn_WndProc”这一行,点击OK!

    又一次看到了熟悉的if…else if…,还有和“D_Logout”进行比较的分支,而这个分支里面只调用了一个函数sub_10005940,而且不带参数。

    双击函数“sub_10005940”后,发现这个函数很简单。核心语句只有两条,首先调用了sub_100030F0函数,然后得到一个返回值。接下来,为这个返回值加上一个数值“0x3F2BF0”,再转换成一个函数指针,再给一个0作为参数,再调用这个函数指针。最后返回结果。

    我们这里要关注,来自于“sub_100030F0”函数的返回值result到底是什么?
    同样,双击这个函数(sub_100030F0),进去看看呗!原来,调用了一个Windows API函数(GetModuleHandleW),查看MSDN后,发现原来这个函数的功能就是取微信的基址(WeChatWin.dll)。

    那就简单多了!是不是说,如果我们在微信中执行“((int (__stdcall *)(_DWORD))(weChatWinBaseAddress + 0x3F2BF0))(0);”这么一句代码,就可以实现Logout功能呢?
    当然,这只是猜测,我们还需要进一步验证。
    猜测验证打开原来创建的C++的动态链接库项目,把 case分支DLL_PROCESS_ATTACH换成如下内容:
    case DLL_PROCESS_ATTACH: { DWORD weChatWinBaseAddress = (int)GetModuleHandleW(L"WeChatWin.dll"); ((int(__stdcall*)(DWORD))(weChatWinBaseAddress + 0x3F2BF0))(0); }
    注意:把从IDA中拷贝过来的代码中 “_DWORD”中的下划线去掉,就可以编译通过了。

    启动微信,登录微信(这时候,手机微信中会显示“Windows微信已登录”)。
    使用OD附加微信(确保WeTool已经退出,否则OD附加不成功)。
    在OD汇编代码窗口点右键,选择”StrongOD\InjectDll\Remote Thread”,选中刚才Visual Studio中编译成功的那个dll文件。

    一秒钟!OK,神奇的事情发生了:微信提示,你已退出微信!
    同时,手机微信上原来显示的 “Windows微信已登录”,也消失了。
    从这里我们可以确定,微信“真的”是退出了,而不是崩掉了。

    总结其实逆向研究,并不只是靠苦力,更重要的是强烈的好奇心和发散的思维。也许一个瞬间,换一下思维模式,瞬间一切都开朗了。一个人的力量是有限的,融入一个圈子,去借鉴别人的成功经验,同时贡献自己成功经验,你会发现,逆向研究其实是一件非常有趣的事情。当然,我研究的后编写源码都是免费公开的,你可以到GitHub(https://github.com/zmrbak/PcWeChatHooK)上下载,也欢迎和我们一起学习和研究。
    后记2019年3月17日前,本人对WeTool进行过一些探索,始终没有取得进展。时隔两个月之后,突然有所发现,于是在2019年5月29日将我的探索成果录制成一个个视频,分享给和我一起研究和探索微信的好朋友。结果,在他们中激起了强烈的反响,有不少的朋友的研究进度有了一个飞跃性的发展,还有几个朋友短短几日之间,就远远超越了我的研究进度。看到这样的场景,让我的内心充满欢喜。当然,还有不少朋友的评价,更是让我开心,现摘录如下:











    源码分享:https://github.com/zmrbak/PcWeChatHooK
    2 留言 2019-09-14 15:14:33 奖励35点积分
  • 【Cocos Creator 实战教程(1)】——人机对战五子棋(节点事件相关) 精华

    一、涉及知识点
    场景切换按钮事件监听节点事件监听节点数组循环中闭包的应用动态更换sprite图片定时器预制资源
    二、步骤2.1 准备工作首先,我们要新建一个空白工程,并在资源管理器中新建几个文件夹

    在这些文件夹中,我们用来存放不同的资源,其中

    Scene用来存放场景,我们可以把场景看作一个关卡,当关卡切换时,场景就切换了
    Script用来存放脚本文件
    Texture用来存放显示的资源,例如音频,图片
    Prefab用来存放预制资源,接下来我会详细的介绍

    接下来,我们在Scene文件夹中新建一个场景(右击文件夹->新建->Scene),命名为Menu,接着导入背景图片(直接拖拽即可)。最后调整图片大小使图片铺满背景,效果如图。

    2.2 按钮监听与场景切换接下来我们来学习此次实战的第一个难点,按钮监听与场景切换。
    首先,创建一个Button节点,并删除label,放在人机博弈的按钮上,并在属性上调成透明样式。

    接下来,新建一个Game场景,并添加一个棋盘节点,并把锚点设为(0,0)。

    这里,讲一下锚点的作用。
    anchor point 究竟是怎么回事? 之所以造成不容易理解的是因为我们平时看待一个图片是以图片的中心点这一个维度来决定图片的位置的。而在cocos2d中决定一个图片的位置是由两个维度一个是 position 另外一个是anchor point。只要我们搞清楚他们的关系,自然就迎刃而解。默认情况下,anchor point在图片的中心位置(0.5, 0.5),取值在0到1之间的好处就是,锚点不会和具体物体的大小耦合,也即不用关注物件大小,而应取其对应比率,如果把锚点改成(0,0),则进行放置位置时,以图片左下角作为起始点。也就是说,把position设置成(x,y)时,画到屏幕上需要知道:到底图片上的哪个点放在屏幕的(x,y)上,而anchor point就是这个放置的点,anchor point是可以超过图片边界的,比如下例中的(-1,-1),表示从超出图片左下角一个宽和一个高的地方放置到屏幕的(0,0)位置(向右上偏移10个点才开始到图片的左下角,可以认为向右上偏移了10个点的空白区域)
    他们的关系是这样的(假设actualPosition.x,actualPosition.y是真实图片位置的中点):actualPosition.x = position.x + width*(0.5 - anchor_point.x);acturalPosition.y = position.y + height*(0.5 - anchor_point.y)actualPosition 是sprite实际上在屏幕显示的位置, poistion是 程序设置的, achor_point也是程序设置的。
    然后,我们需要新建一个脚本,Menu.js,并添加开始游戏方法。
    cc.Class({ extends: cc.Component, startGame:function(){ cc.director.loadScene('Game');//这里便是运用导演类进行场景切换的代码 }});
    这里提示以下,编辑脚本是需要下载插件的,我选择了VScode,还是很好用的。
    最后我们将其添加为Menu场景的Canvas的组件(添加组件->脚本组件->menu),并在BtnP2C节点上添加按钮监听响应。


    这样,按钮监听就完成了。现在我们在Menu场景里点击一下人机按钮就会跳转到游戏场景了。
    2.3 预制资源预制资源一般是在场景里面创建独立的子界面或子窗口,即预制资源是存放在资源中,并不是节点中,例如本节课中的棋子。
    现在我们就来学习一下如何制作预制资源。

    再将black节点改名为Chess拖入下面Prefab文件夹使其成为预制资源。
    这其中,SpriteFrame 是核心渲染组件 Sprite 所使用的资源,设置或替换 Sprite 组件中的 spriteFrame 属性,就可以切换显示的图像,将其去掉防止图片被预加载,即棋子只是一个有大小的节点不显示图片也就没有颜色。
    2.4 结束场景直接在Game场景上制作结束场景。

    2.5 游戏脚本制作人机对战算法参考了这里https://blog.csdn.net/onezeros/article/details/5542379
    具体步骤便是初始化棋盘上225个棋子节点,并为每个节点添加事件,点击后动态显示棋子图片。
    代码如下:
    cc.Class({ extends: cc.Component, properties: { overSprite:{ default:null, type:cc.Sprite, }, overLabel:{ default:null, type:cc.Label }, chessPrefab:{//棋子的预制资源 default:null, type:cc.Prefab }, chessList:{//棋子节点的集合,用一维数组表示二维位置 default: [], type: [cc.node] }, whiteSpriteFrame:{//白棋的图片 default:null, type:cc.SpriteFrame }, blackSpriteFrame:{//黑棋的图片 default:null, type:cc.SpriteFrame }, touchChess:{//每一回合落下的棋子 default:null, type:cc.Node, visible:false//属性窗口不显示 }, gameState:'white', fiveGroup:[],//五元组 fiveGroupScore:[]//五元组分数 }, //重新开始 startGame(){ cc.director.loadScene("Game"); }, //返回菜单 toMenu(){ cc.director.loadScene("Menu"); }, onLoad: function () { this.overSprite.node.x = 10000;//让结束画面位于屏幕外 var self = this; //初始化棋盘上225个棋子节点,并为每个节点添加事件 for(var y = 0;y<15;y++){ for(var x = 0;x < 15;x++){ var newNode = cc.instantiate(this.chessPrefab);//复制Chess预制资源 this.node.addChild(newNode); newNode.setPosition(cc.p(x*40+20,y*40+20));//根据棋盘和棋子大小计算使每个棋子节点位于指定位置 newNode.tag = y*15+x;//根据每个节点的tag就可以算出其二维坐标 newNode.on(cc.Node.EventType.TOUCH_END,function(event){ self.touchChess = this; if(self.gameState === 'black' && this.getComponent(cc.Sprite).spriteFrame === null){ this.getComponent(cc.Sprite).spriteFrame = self.blackSpriteFrame;//下子后添加棋子图片使棋子显示 self.judgeOver(); if(self.gameState == 'white'){ self.scheduleOnce(function(){self.ai()},1);//延迟一秒电脑下棋 } } }); this.chessList.push(newNode); } } //开局白棋(电脑)在棋盘中央下一子 this.chessList[112].getComponent(cc.Sprite).spriteFrame = this.whiteSpriteFrame; this.gameState = 'black'; //添加五元数组 //横向 for(var y=0;y<15;y++){ for(var x=0;x<11;x++){ this.fiveGroup.push([y*15+x,y*15+x+1,y*15+x+2,y*15+x+3,y*15+x+4]); } } //纵向 for(var x=0;x<15;x++){ for(var y=0;y<11;y++){ this.fiveGroup.push([y*15+x,(y+1)*15+x,(y+2)*15+x,(y+3)*15+x,(y+4)*15+x]); } } //右上斜向 for(var b=-10;b<=10;b++){ for(var x=0;x<11;x++){ if(b+x<0||b+x>10){ continue; }else{ this.fiveGroup.push([(b+x)*15+x,(b+x+1)*15+x+1,(b+x+2)*15+x+2,(b+x+3)*15+x+3,(b+x+4)*15+x+4]); } } } //右下斜向 for(var b=4;b<=24;b++){ for(var y=0;y<11;y++){ if(b-y<4||b-y>14){ continue; }else{ this.fiveGroup.push([y*15+b-y,(y+1)*15+b-y-1,(y+2)*15+b-y-2,(y+3)*15+b-y-3,(y+4)*15+b-y-4]); } } } }, //电脑下棋逻辑 ai:function(){ //评分 for(var i=0;i<this.fiveGroup.length;i++){ var b=0;//五元组里黑棋的个数 var w=0;//五元组里白棋的个数 for(var j=0;j<5;j++){ this.getComponent(cc.Sprite).spriteFrame if(this.chessList[this.fiveGroup[i][j]].getComponent(cc.Sprite).spriteFrame == this.blackSpriteFrame){ b++; }else if(this.chessList[this.fiveGroup[i][j]].getComponent(cc.Sprite).spriteFrame == this.whiteSpriteFrame){ w++; } } if(b+w==0){ this.fiveGroupScore[i] = 7; }else if(b>0&&w>0){ this.fiveGroupScore[i] = 0; }else if(b==0&&w==1){ this.fiveGroupScore[i] = 35; }else if(b==0&&w==2){ this.fiveGroupScore[i] = 800; }else if(b==0&&w==3){ this.fiveGroupScore[i] = 15000; }else if(b==0&&w==4){ this.fiveGroupScore[i] = 800000; }else if(w==0&&b==1){ this.fiveGroupScore[i] = 15; }else if(w==0&&b==2){ this.fiveGroupScore[i] = 400; }else if(w==0&&b==3){ this.fiveGroupScore[i] = 1800; }else if(w==0&&b==4){ this.fiveGroupScore[i] = 100000; } } //找最高分的五元组 var hScore=0; var mPosition=0; for(var i=0;i<this.fiveGroupScore.length;i++){ if(this.fiveGroupScore[i]>hScore){ hScore = this.fiveGroupScore[i]; mPosition = (function(x){//js闭包 return x; })(i); } } //在最高分的五元组里找到最优下子位置 var flag1 = false;//无子 var flag2 = false;//有子 var nPosition = 0; for(var i=0;i<5;i++){ if(!flag1&&this.chessList[this.fiveGroup[mPosition][i]].getComponent(cc.Sprite).spriteFrame == null){ nPosition = (function(x){return x})(i); } if(!flag2&&this.chessList[this.fiveGroup[mPosition][i]].getComponent(cc.Sprite).spriteFrame != null){ flag1 = true; flag2 = true; } if(flag2&&this.chessList[this.fiveGroup[mPosition][i]].getComponent(cc.Sprite).spriteFrame == null){ nPosition = (function(x){return x})(i); break; } } //在最最优位置下子 this.chessList[this.fiveGroup[mPosition][nPosition]].getComponent(cc.Sprite).spriteFrame = this.whiteSpriteFrame; this.touchChess = this.chessList[this.fiveGroup[mPosition][nPosition]]; this.judgeOver(); }, judgeOver:function(){ var x0 = this.touchChess.tag % 15; var y0 = parseInt(this.touchChess.tag / 15); //判断横向 var fiveCount = 0; for(var x = 0;x < 15;x++){ if((this.chessList[y0*15+x].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){ fiveCount++; if(fiveCount==5){ if(this.gameState === 'black'){ this.overLabel.string = "你赢了"; this.overSprite.node.x = 0; }else{ this.overLabel.string = "你输了"; this.overSprite.node.x = 0; } this.gameState = 'over'; return; } }else{ fiveCount=0; } } //判断纵向 fiveCount = 0; for(var y = 0;y < 15;y++){ if((this.chessList[y*15+x0].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){ fiveCount++; if(fiveCount==5){ if(this.gameState === 'black'){ this.overLabel.string = "你赢了"; this.overSprite.node.x = 0; }else{ this.overLabel.string = "你输了"; this.overSprite.node.x = 0; } this.gameState = 'over'; return; } }else{ fiveCount=0; } } //判断右上斜向 var f = y0 - x0; fiveCount = 0; for(var x = 0;x < 15;x++){ if(f+x < 0 || f+x > 14){ continue; } if((this.chessList[(f+x)*15+x].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){ fiveCount++; if(fiveCount==5){ if(this.gameState === 'black'){ this.overLabel.string = "你赢了"; this.overSprite.node.x = 0; }else{ this.overLabel.string = "你输了"; this.overSprite.node.x = 0; } this.gameState = 'over'; return; } }else{ fiveCount=0; } } //判断右下斜向 f = y0 + x0; fiveCount = 0; for(var x = 0;x < 15;x++){ if(f-x < 0 || f-x > 14){ continue; } if((this.chessList[(f-x)*15+x].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){ fiveCount++; if(fiveCount==5){ if(this.gameState === 'black'){ this.overLabel.string = "你赢了"; this.overSprite.node.x = 0; }else{ this.overLabel.string = "你输了"; this.overSprite.node.x = 0; } this.gameState = 'over'; return; } }else{ fiveCount=0; } } //没有输赢交换下子顺序 if(this.gameState === 'black'){ this.gameState = 'white'; }else{ this.gameState = 'black'; } }});
    这里便用到了节点监听,节点数组,定时器和动态更换sprite图片。
    新建Game脚本添加到ChessBoard节点下

    3 注意课程到这里就结束了,本课程部分资源来源于网络。
    第一次写技术分享,如果大家有什么好的建议和问题请在下方留言区留言。
    7 留言 2018-11-21 20:32:10 奖励20点积分
  • 深度学习 20、CNN

    前文链接:https://write-bug.com/article/2575.html
    卷积神经网络CNN上节我们介绍了DNN的基本网络结构和演示,其网络结构为:

    DNN BP神经网络实行全连接特性,参数权值过多,需求大量样本,计算困难,所以CNN利用局部连接、权值共享来实现减少权值的尝试,即局部感知野和权值共享。擅长图像分类问题,识别二维图形,现在也可以用于文本处理。

    CNN的隐藏层分为卷积层conv、和池化层pool,一般CNN结构(LeNet)依次为:

    INPUT->[[CONV -> RELU]N -> POOL?]M -> [FC -> RELU]*K->FC经典LeNet-5结构:最早用于数字识别的CNN


    卷积层Convolitions:
    用卷积层实现对图片局部的特征提取,窗口模式卷积核形式提取特征,映射到高维平面。
    两个关键性操作:

    局部关联:每个神经元看为一个滤波器filter
    窗口滑动:filter对局部数据计算

    局部感受野:
    1000 * 1000 图像 1000 * 1000神经元全连接: 1000 * 1000 * 1000 * 1000 = 10^12 个参数
    局部感受野:每个神经元只需要知道10 * 10区域,训练参数降到100M

    权值共享:
    所有神经元用同一套权重值,100M降到100,这样得到一个feature map。100种权重值100个map,一共需要训练100*100 = 10k参数

    卷积的计算:

    窗口滑动,我们可以想象一个窗口(卷积核feature_map),设置固定的feature_size,里面是固定的权值,这个窗口在同一张图片上从左到右从上到下游走,每次固定步长,从而得到更小size的矩阵。

    这里有两个神经元的卷积层,步长stride设置为2,边缘灰色部分为填充值zero-padding,防止窗口移到边缘时不够滑动遍历所有像素,而这里的神经元个数就是卷积层深度depth。
    在卷积层中每个神经元连接数据窗的权重是固定的,每个神经元只关注一个特性。神经元就是图像处理中的滤波器,比如边缘检测专用的Sobel滤波器,即卷积层的每个滤波器都会有自己所关注一个图像特征,比如垂直边缘,水平边缘,颜色,纹理等等,这些所有神经元加起来就好比就是整张图像的特征提取器集合。

    需要估算的权重个数减少: AlexNet 1亿 => 3.5w
    一组固定的权重和不同窗口内数据做内积: 卷积卷积理解:后面输出结果的前提是依赖前面相应权值积累,比如连续吃三天药身体才好,那么这三天的相关性是不同的权值

    激活函数把卷积层输出结果做非线性映射。CNN采用的激励函数一般为ReLU(The Rectified Linear Unit/修正线性单元),它的特点是收敛快,求梯度简单,但较脆弱,图像如下。

    激励层的实践经验:

    不要用sigmoid!不要用sigmoid!不要用sigmoid!首先试RELU,因为快,但要小心点如果2失效,请用Leaky ReLU或者Maxout某些情况下tanh倒是有不错的结果,但是很少
    池化层pooling:对特征进一步缩放
    池化层夹在连续的卷积层中间,用于压缩数据和参数的量,减小过拟合,压缩图像。
    特点:

    特征不变性:图像缩小还可以认出大概
    特征降维:去除冗余信息,防止过拟合


    池化层用的方法有Max pooling 和 average pooling,而实际用的较多的是Max pooling。
    这里就说一下Max pooling,其实思想非常简单。

    对于每个2*2的窗口选出最大的数作为输出矩阵的相应元素的值,比如输入矩阵第一个2*2窗口中最大的数是6,那么输出矩阵的第一个元素就是6,如此类推。
    平移不变性

    旋转不变性

    缩放不变性

    全连接层FC
    和BP神经网络一样,两层之间互相全连接,即把压缩后的维度打平为一组向量,再通过dnn的方法进行分类处理。
    Droupout
    正则化方法,CNN中解决过拟合问题,同时通用与其他网络。

    随机在全连接层,剪掉一些神经元,防止过拟合。
    卷积网络在本质上是一种输入到输出的映射,它能够学习大量的输入与输出之间的映射关系,而不需要任何输入和输出之间的精确的数学表达式,只要用已知的模式对卷积网络加以训练,网络就具有输入输出对之间的映射能力。
    CNN一个非常重要的特点就是头重脚轻(越往输入权值越小,越往输出权值越多),呈现出一个倒三角的形态,这就很好地避免了BP神经网络中反向传播的时候梯度损失得太快。
    卷积神经网络CNN主要用来识别位移、缩放及其他形式扭曲不变性的二维图形。由于CNN的特征检测层通过训练数据进行学习,所以在使用CNN时,避免了显式的特征抽取,而隐式地从训练数据中进行学习;再者由于同一特征映射面上的神经元权值相同,所以网络可以并行学习,这也是卷积网络相对于神经元彼此相连网络的一大优势。卷积神经网络以其局部权值共享的特殊结构在语音识别和图像处理方面有着独特的优越性,其布局更接近于实际的生物神经网络,权值共享降低了网络的复杂性,特别是多维输入向量的图像可以直接输入网络这一特点避免了特征提取和分类过程中数据重建的复杂度。
    卷积神经网络之训练算法:

    同一般机器学习算法,先定义Loss function,衡量和实际结果之间差距
    找到最小化损失函数的W和b, CNN中用的算法是SGD(随机梯度下降)

    卷积神经网络之优缺:

    优点

    共享卷积核,对高维数据处理无压力无需手动选取特征,训练好权重,即得特征分类效果好
    缺点

    需要调参,需要大样本量,训练最好要GPU物理含义不明确(也就说,我们并不知道没个卷积层到底提取到的是什么特征,而且神经网络本身就是一种难以解释的“黑箱模型”)

    卷积神经网络之典型CNN:

    LeNet,这是最早用于数字识别的CNN
    AlexNet, 2012 ILSVRC比赛远超第2名的CNN,比LeNet更深,用多层小卷积层叠加替换单大卷积层。
    ZF Net, 2013 ILSVRC比赛冠军
    GoogLeNet, 2014 ILSVRC比赛冠军
    VGGNet, 2014 ILSVRC比赛中的模型,图像识别略差于GoogLeNet,但是在很多图像转化学习问题(比如object detection)上效果奇好pytorch网络结构:

    class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 20, 5, 1) self.conv2 = nn.Conv2d(20, 50, 5, 1) self.fc1 = nn.Linear(4*4*50, 500) self.fc2 = nn.Linear(500, 10) def forward(self, x): print("x size: ", x.size()) x = F.relu(self.conv1(x)) print("conv1 size: ", x.size()) x = F.max_pool2d(x, 2, 2) print("pool size: ", x.size()) x = F.relu(self.conv2(x)) print("conv2 size: ", x.size()) x = F.max_pool2d(x, 2, 2) print("pool size: ", x.size()) x = x.view(-1, 4*4*50) print("view size: ", x.size()) x = F.relu(self.fc1(x)) print("fc1 size: ", x.size()) x = self.fc2(x) print("fc2 size: ", x.size()) sys.exit() return F.log_softmax(x, dim=1)
    1 留言 2019-06-11 11:42:43 奖励13点积分
  • 内核双机调试——追踪并分析内核API函数的调用 精华

    在64位Win10主机上调试32位Win7虚拟机内核,查看Win7内核中的函数调用关系,从而分析API函数实现的具体原理和流程。
    一、环境配置前提,对Win7虚拟机设置了调试的COM端口:\\.\pipe\com_1,波特率设置为115200,那么接下来就可以使用WinDbg来进行调试了。具体步骤如下所示:
    首先,在Win10上“以管理员身份”运行64位的WinDbg,点击File—>Kernel Debug—>COM,波特率设置为115200,端口与上面对应\\.\pipe\com_1,并勾选pipe、Reconnect,点击确定即可。

    如果我们不动它,WinDbg命令窗口将会一直显示“Waiting to reconnect…”的信息!这时,我们需要点击下工具栏上“Break”按钮,让Win7系统断下来,这样我们才可以用WinDbg进行调试,输入控制指令。


    接着,设置符号路径:File—>Symbol File Path—>输入:
    srv*c:\symbols*https://msdl.microsoft.com/download/symbols确定即可,这样WinDbg就会自动根据链接下载符号表到本地上。

    等待一会儿,即可下载完毕,方可输入指令。
    二、使用WingDbg追踪API调用流程接下来,我们以分析内核API函数NtQueryDirectoryFile为例,介绍WinDbg调试软件的使用方法。
    输入指令:uf nt!NtQueryDirectoryFile

    大概可以看出,NtQueryDirectoryFile函数的实现主要是调用了nt!BuildQueryDirectoryIrp函数构造查询文件的IRP以及nt!IopSynchronousServiceTail发送IRP来实现的。为了验证我们的想法,我们继续对这两个函数进行查看。
    输入指令:uf nt!BuildQueryDirectoryIrp

    从它函数实现调用了nt!IoAllocateIrp函数可知,我们的猜想是正确的。
    输入指令:uf nt!IopSynchronousServiceTail

    从它函数实现调用了nt!IofCallDriver函数可知,我们的猜想是正确的。
    nt!IofCallDriver函数定义如下所示:
    NTSTATUS IofCallDriver( PDEVICE_OBJECT DeviceObject, __drv_aliasesMem PIRP Irp);
    该函数的功能就是将IRP发送到指定的设备对象中处理,第1个参数就是处理IRP的设备对象。
    所以,接下来,我们继续用WinDbg分析上述nt!IopSynchronousServiceTail函数将IRP发给了哪个驱动设备进行处理的。
    输入指令:bu nt!NtQueryDirectoryFile
    输入指令:bl;查看所有断点
    输入指令:g;继续往下执行
    下断点,只要系统执行到nt!NtQueryDirectoryFile这个函数就会停下来。

    输入指令:u @eip
    输入指令:r;查看寄存器

    由于是对nt! NtQueryDirectoryFile这个函数下断点,所以现在停下来,指令指针eip指向的就是nt! NtQueryDirectoryFile函数的入口地址。此时,uf @eip就是反汇编nt! NtQueryDirectoryFile函数的内容。
    输入指令:bp 84043fdc
    输入指令:g

    84043fdc就是nt!IopSynchronousServiceTail函数的入口地址,断点会自动在此处断下。我们一步步下断点,逼近最终我们需要下断点的nt!IofCallDriver函数,确保是由nt!NtQueryDirectoryFile函数内容实现调用的nt!IofCallDriver函数。
    输入指令:uf @eip

    输入指令:bp 83e4cd19
    输入指令:g

    83e4cd19是nt!IofCallDriver函数的入口地址,WinDbg断点断下后,就会自动停在此处。
    输入指令:u @eip
    输入指令:r

    IofCallDriver函数是FASTCALL类型的调用约定,所以第1个参数的值存储在寄存器ecx中,即873f3508。所以,我们继续查看下该设备对象的结构数据。
    输入指令:dt nt!_DEVICE_OBJECT @ecx

    这时,我们边可以获取到驱动对象的地址DriverObject(0x86f5e670 _DRIVER_OBJECT),继续查看驱动对象的数据内容。
    输入指令:dt nt!_DRIVER_OBJECT 0x86f5e670

    由DriverName中我们可以看出,nt!NtQueryDirectoryFile是将IRP请求包发送给FltMgf驱动程序来处理的。
    3 留言 2019-09-01 09:25:57 奖励20点积分
  • 深度学习 19、DNN

    前文链接:https://write-bug.com/article/2540.html
    深度学习在前几节介绍的模型都属于单点学习模型,比如朴素贝叶斯的模型为概率,lr与softmax的模型为w,b权重,聚类kmeans的模型为中心向量点,还有后面的决策树的树模型等等都属于单点学习模型,都有各自的用途,而这里的深度学习来说,属于机器学习的一部分,可用不同的网络结构来解决各个领域的问题,模型为深度学习网络。
    通过前几节的学习,不知道大家有没有对单点学习的模型有感触,所谓模型是什么?
    我们从已有的例子(训练集)中发现输入x与输出y的关系,这个过程是学习(即通过有限的例子发现输入与输出之间的关系),而我们使用的function就是我们的模型,通过模型预测我们从未见过的未知信息得到输出y,通过激活函数(常见:relu,sigmoid,tanh,swish等)对输出y做非线性变换,压缩值域,而输出y与真实label的差距就是误差error,通过损失函数再求取参数偏导,梯度下降降低误差,从而优化模型,我们前面见过的损失函数是mse和交叉熵

    而DNN的模型就是网络结构,在说如何选取更好的网络结构之前,我们先介绍下神经网络:

    人类大脑的神经网络如上图具象出来就是无数个神经元,而神经元分为几个结构:

    树突:接收信号源
    神经元:核心计算单元
    轴突:传输数据到下一个神经元

    通过一个个神经元的传输,生成结果,比如说条件反射:缩手
    而在我们模型中,这个网络怎么表示呢?

    感知机:最最简单的神经网络,只有一个神经元


    树突:样本特征a1
    轴突:权重w1

    树突与轴突,也就是样本特征和权重相乘,有些类似lr的wx,共同决策了神经元的输入

    神经元:对信号进一步处理,比如lr中wx的∑,并加入偏置b,默认为1
    传递函数(激活函数):对f(s)做压缩非线性变换

    假设一个二分类问题:

    输入:
    树突P1:颜色
    树突P2:形状
    香蕉(黄色,弯形):(-1,-1)
    苹果(红色,圆形):(1,1)

    相当于坐标轴的两个点,我们需要一个分类面把两个点分割开。

    P1颜色的取值为1,-1
    P2形状的取值为1,-1
    初始化w1=w2=1,b=0

    则有:
    s=p1w1+p2w2=2>0s=p2 2w2 2+p2 2w2 2=-2 < 0这个初始化的参数可以区分,但是初始化有随机性,换一组参数就不能区分了
    所以感知机的训练方法,目的就是训练权重和偏置
    w=w+epb=b+ee(误差)=t-a=期望-实际
    如,假设w1=1,w2=-1,b=0

    苹果s=0
    期望为1,e=1-0=1

    修正:
    w1=1+1*1=2w2=-1+1*1=0b=0+1=1s=3>0为苹果但是,y=wx+b只能解决线性可分的问题,对于异或问题,也就是四点对立面,需要一个曲线做分割,而这就意味着需要多层神经元和信号源共同决策分割面,也就形成了复杂的神经网络,而目标:同样是那么多边的权重。

    通过不同层间的结果作为下一层的输入,再对输入中间结果进一步处理,以此类推形成足够的深度,对于大部分问题来说都可以提到一个提升效果的作用。
    感知机只有从输出层具有激活函数,即功能神经元,这里的隐层和输出层都是具有激活函数的功能神经元。
    层与层全连接,同层神经元不连接,不跨层连接的网络又称为多层前馈神经网络。
    前馈:网络拓扑结构上不存在环和回路
    我们通过pytorch实现演示:
    二分类问题:
    假数据准备:
    # make fake data#正态分布随机产生n_data = torch.ones(100, 2)x0 = torch.normal(2*n_data, 1) # class0 x data (tensor), shape=(100, 2)y0 = torch.zeros(100) # class0 y data (tensor), shape=(100, 1)x1 = torch.normal(-2*n_data, 1) # class1 x data (tensor), shape=(100, 2)y1 = torch.ones(100) # class1 y data (tensor), shape=(100, 1)#拼接数据x = torch.cat((x0, x1), 0).type(torch.FloatTensor) # shape (200, 2) FloatTensor = 32-bit floatingy = torch.cat((y0, y1), ).type(torch.LongTensor) # shape (200,) LongTensor = 64-bit integer# tensor类型 :张量:高维特征,不可更新#variable类型:可变参数,更新w,b从而更新xy# torch can only train on Variable, so convert them to Variablex, y = Variable(x), Variable(y)设计构建神经网络:class net 继承torch.nn.model
    200\*2, 2\*10, 10\*2参数:n_fea=2,n_hidden=10,n_output=2
    输入层2
    初始化hidden层10
    self.hidden=torch.nn.linear(n_fea,n_hidden)输出层2
    self.out=torch.nn.linear(n_hidden,n_output)class Net(torch.nn.Module): def __init__(self, n_feature, n_hidden, n_output): super(Net, self).__init__() self.hidden = torch.nn.Linear(n_feature, n_hidden) # hidden layer self.out = torch.nn.Linear(n_hidden, n_output) # output layer#激活函数,在隐藏层后加relu非线性变换 def forward(self, x): x = F.relu(self.hidden(x)) # activation function for hidden layer x = self.out(x) return x #输出的预测值xnet = Net(n_feature=2, n_hidden=10, n_output=2) # define the networkprint(net)参数梯度下降sgd:
    optimizer = torch.optim.sgd(net.parameters(),lr=0.02)损失函数:交叉熵:负对数似然函数,并计算softmax
    loss_func = torch.nn.CrossEntropyLoss()参数初始化:
    optimizer.zero_grad()反向传播:
    loss.backward()计算梯度:
    optimizer.step()for t in range(100): out = net(x) # input x and predict based on x loss = loss_func(out, y) # must be (1. nn output, 2. target), the target label is NOT one-hotted optimizer.zero_grad() # clear gradients for next train loss.backward() # backpropagation, compute gradients optimizer.step() # apply gradients if t % 2 == 0: # plot and show learning process plt.cla() prediction = torch.max(F.softmax(out), 1)[1] pred_y = prediction.data.numpy().squeeze() target_y = y.data.numpy() plt.scatter(x.data.numpy()[:, 0], x.data.numpy()[:, 1], c=pred_y, s=100, lw=0, cmap='RdYlGn') accuracy = sum(pred_y == target_y)/200. plt.text(1.5, -4, 'Accuracy=%.2f' % accuracy, fontdict={'size': 20, 'color': 'red'}) plt.pause(0.1)plt.ioff()plt.show()交叉熵问题:
    二次代价:

    a 是 神经元的输出,其中 a = σ(z), z = wx + b
    C求导:w,b求偏导:
    受到激活函数影响

    交叉熵函数:


    不用对激活函数导数影响,不管是对w求偏导还是对b求偏导,都和e有关,而和激活函数的导数无关,比如说sigmoid函数,只有中间下降最快,两边的下降逐渐缓慢,最后斜率逼近于直线
    反向传播算法:errorBP
    可应用于大部分类型神经网络
    一般说BP算法时,多指多层前馈神经网络
    上面的数据为了演示,造的假数据形成200*2维的矩阵,一列label
    那么如何对接我们的真实数据呢?

    在我们前几节的数据中大都是这种形式,label feature_id:score
    定义一个数组
    output =[None]*3 维护3列tensor数据:
    max_fid = 123num_epochs = 10data_path = '/root/7_codes/pytorch_test/data/a8a'all_sample_size = 0sample_buffer = []with open(data_path, 'r') as fd: for line in fd: all_sample_size += 1 sample_buffer.append(line.strip())output = [None] *3#idids_buffer = torch.LongTensor(all_sample_size, max_fid)ids_buffer.fill_(1)output[0] = ids_buffer#weightweights_buffer = torch.zeros(all_sample_size, max_fid)output[1] = weights_buffer#labellabel_buffer = torch.LongTensor(all_sample_size)output[2] = label_bufferrandom.shuffle(sample_buffer)sample_id = 0for line in sample_buffer: it = line.strip().split(' ') label = int(it[0]) f_id = [] f_w = [] for f_s in it[1:]: f, s = f_s.strip().split(":") f_id.append(int(f)) f_w.append(float(s)) #解析数据后填入数组 f_id_len = len(f_id)#适当拓展数组大小 output[0][sample_id, 0:f_id_len] = torch.LongTensor(f_id) output[1][sample_id, 0:f_id_len] = torch.FloatTensor(f_w) output[2][sample_id] = label sample_id += 1fid, fweight, label = tuple(output)fid, fweight, label = map(Variable, [fid, fweight, label])接下来是设计网络结构:

    def emb_sum(embeds, weights): emb_sum = (embeds * weights.unsqueeze(2).expand_as(embeds)).sum(1) return emb_sum#输入结构为22696条数据*124维特征*32维特征向量:可以将其想象成立方体的三维条边#想要把id的特征向量和对应的score相乘并相加,需要把score也就是权重weight拓展成和id同样的维度,即22696 *124 *32 ,而sum(1)就是把124维相加成1维,最后形成22696×32维的平面矩阵,即每个样本后有个32维的向量存在,相当于把feature_id向量化class Net(torch.nn.Module): def __init__(self, n_embed, n_feature, n_hidden, n_output): super(Net, self).__init__() self.embedding = nn.Embedding(max_fid + 1, n_feature) #124特征*32隐层向量 self.hidden = torch.nn.Linear(n_feature, n_hidden) # hidden layer self.out = torch.nn.Linear(n_hidden, n_output) # output layer def forward(self, fea_ids, fea_weights): embeds = self.embedding(fea_ids) embeds = emb_sum(embeds, fea_weights)#对特征进一步处理,融合id与weight_score进行相容 embeds = nn.functional.tanh(embeds)#非线性变换 hidden = self.hidden(embeds) output = self.out(hidden) return outputnet = Net(n_embed = 32, n_feature= 32, n_hidden=10, n_output=2) # define the networkprint(net)以上,我们对接了真实的数据类型,那这个demo有没有什么问题呢?
    1.是否支持大规模数据,进行快速训练?mini-batch
    我们之前学BGD、SGD、MGD梯度下降的训练方法,在上面就运用了sgd的方法,不管是BGD还是SGD都是对所有样本一次性遍历一次,如果想提升,大致相当于MGD的方法:
    把所有样本分批处理,每批次有多少个样本(batch),循环所有样本循环多少轮(epoch)。
    每训练一个batch产生一条log,参数可保存,之前是每epoch输出一次log,这种方法训练快,性能虽然赶不上全局跑出来的准确率,但仍然一直逼近,小步快跑,每次加载到内存的数据较小,性能方面就高。
    num_epochs=10batch_size =1000data_path = './data/a8a'max_fid = 123def read_and_shuffle(filepath,shuffle=True): lines=[] with open(filepath,’r’) as fd: for linr in fd: lines.append(line.strip()) if shuffle: random.shuffle(lines) return linesclass DataLoader(object): def __init__(self,data_path,batch_size,shuffle = True): self.batch_size = batch_size self.shuffle = shuffle if os.path.isdir(data_path): self.files = [ join_path(data_path,i) for I in os.listdir(data_path)] else: self.files = [data_path] def parse(self,line_ite): for line in line_ite: it = line.strip().split(‘ ’) label = int(it[0]) f_id = [] f_w = [] for f_s in it[1:]: f,s = f_s.strip().split(‘:’) f_id.append(int(f)) f_w.append(float(s)) f_id_len = len(f_id) output = [] output.append(f_id) output.append(f_w) output.append(f_id_len) output.append(label) yield output def to_tensor(self,batch_data,sample_size): output =[None]*3 ids_buffer = torch.LongTensor(sample_size,max_fid) ids_buffer.fill_(1) output[0]=ids_buffer weights_buffer = torch.zeros(sample_size,max_fid) output[1]=weights_buffer label_buffer = torch.LongTensor(sample_size) output[2] = label_buffer for sample_id,sample in enumerate(batch_data): f_id,f_weight,f_id_len,label = sample output[0][sample_id,0:f_id_len] = torch.LongTensor(f_id) output[1][sample_id,0:f_id_len] = torch.FloatTensor(f_w) output[2][sample_id] = label return tuple(output) def batch(self): count = 0 batch_buffer =[] for f in self.files: for parse_res in self.parse(read_and_shuffle(f,self.shuffle)): count+=1 batch_buffer.append(parse_res) if count ==self.batch_size: t_size = len(batch_buffer) batch_tensor = self.to_tensor(batch_buffer,t_size) yield (batch_tensor,t_size) count = 0 batch_buffer = [] if batch_buffer: t_size = len(batch_buffer) batch_tensor = self.to_tensor(batch_buffer,t_size) yield (batch_tensor,t_size)for epoch in range(1,num_epochs+1): for (batch_id,data_tuple) in enumrate(dataloader.batch(),1): data = data_tuple[0] all_sample_size = data_tuple[1] fid,fweight,label =data fid,fweight,label=map(Variable,[fid,fweight,label] optimizer = torch.optim.SGD(net.parameters(), lr=0.02) loss_func = torch.nn.CrossEntropyLoss() # the target label is NOT an one-hotted out = net(fid, fweight) optimizer.zero_grad() # clear gradients for next train loss = loss_func(out, label) loss.backward() # backpropagation, compute gradients optimizer.step() # apply gradients if batch_id % 2 == 0:#每两批次打印一次 # plot and show learning process prediction = torch.max(F.softmax(out), 1)[1] pred_y = prediction.data.numpy().squeeze() target_y = label.data.numpy() accuracy = sum(pred_y == target_y)/float(all_sample_size) print "epoch: %s, batch_id: %s, acc: %s" % (epoch, batch_id, accuracy)2.怎么使用模型?
    现在模型可以训练自己数据了,效果也提升了,那如何把模型的参数保存下来?
    print "epoch: %s, batch_id: %s, acc: %s" % (epoch, batch_id, accuracy) checkpoint = { 'model': net.state_dict(),#网络静态参数 'epoch': epoch, 'batch': batch_id, } model_name = 'epoch_%s_batch_id_%s_acc_%s.chkpt' % (epoch, batch_id, accuracy) model_path = join_path('./model', model_name) torch.save(checkpoint, model_path)拆分上面大文件出各个参数:

    import osimport sysimport torchimport torch.nn as nnimport randomfrom torch.autograd import Variablefrom os.path import join as join_pathimport torch.nn.functional as Fmodel_path = sys.argv[1]checkpoint = torch.load(model_path)model_dict = checkpoint['model']for k, v in model_dict.items(): print k model_sub_file_name = k model_sub_file_path = join_path('./dump', model_sub_file_name) f = open(model_sub_file_path, 'w') value = model_dict[k].cpu().numpy() value.dump(f) f.close()模型怎么用?加载、预测
    import sysimport numpy as npimport mathembedding_weight = np.load('dump/embedding.weight')hidden_weight = np.load('./dump/hidden.weight')hidden_bias = np.load('./dump/hidden.bias')out_weight = np.load('./dump/out.weight')out_bias = np.load('./dump/out.bias')emb_dim = embedding_weight.shape[1]def calc_score(fids, fweights): embedding = np.zeros(emb_dim) for id, weight in zip(fids, fweights): embedding += weight * embedding_weight[id] embedding_tanh = np.tanh(embedding)#对应相乘,第二维相加成10维向量 hidden = np.sum(np.multiply(hidden_weight, embedding_tanh), axis=1) + hidden_bias out = np.sum(np.multiply(out_weight, hidden), axis=1) + out_bias return outdef softmax(x): exp_x = np.exp(x) softmax_x = exp_x / np.sum(exp_x) return softmax_xfor line in sys.stdin: ss = line.strip().split(' ') label = ss[0].strip() fids = [] fweights = [] for f_s in ss[1:]: f, s = f_s.strip().split(':') fids.append(int(f)) fweights.append(float(s)) pred_label = softmax(calc_score(fids, fweights))[1] print label, pred_label3.怎么评估?auc
    cat auc.raw | sort -t$'\t' -k2g | awk -F'\t' '($1==-1){++x;a+=y}($1==1){++y}END{print 1.0 - a/(x*y)}'acc=0.827auc=0.842569acc=0.745auc=0.494206轮数、acc都影响着auc,数字仅供参考
    总结以上,是以二分类为例,从头演示了一遍神经网络,大家可再找一些0-9手写图片分类任务体验一下,这里总结下,模型拥有参数,所以涉及参数初始化,有了参数后涉及正向传递,分为拟合和泛化两部分建立好模型后,学习过程就是确定参数的过程,使用的是梯度下降法,定义一个误差函数loss function 来衡量模型预测值与真实值(label)之间的差距,通过不断降低差距来使模型预测值逼近真实值,更新参数的反向传递是利用对应的误差值来求得梯度(batch、epoch、learning rate 、shuffle),评估标准也根据任务类型和具体要求可以使用更多的指标。
    回归任务常用均方差MSE作为误差函数。评估可用均方差表示。
    分类任务常用交叉熵(cross entropy)和softmax配合使用。评估可用准确率表示。
    回归任务:

    借用知乎yjango末尾的一句话:
    但最重要的并非训练集的准确率,而是模型从未见过的测试集的准确率(泛化能力),正如高考,真正的目的是为了解出从未见过的高考题,而不是已经做过的练习题,学习的困难之处正是要用有限的样本训练出可以符合无限同类样本规律的模型。是有限同无限的对抗,你是否想过,为什么看了那么多的道理、听了那么多人生讲座,依然过不哈这一生,一个原因就在于演讲者向你展示的例子都是训练集的样本,他所归纳的方法可以很轻松的完美拟合这些样本,但未必对你将面临的新问题同样奏效,人的一生都在学习,都在通过观察有限的例子找出问题和答案的规律,中医是,玄学是,科学同样是,但科学,是当中最为可靠的一种。
    1 留言 2019-06-05 13:46:30 奖励18点积分
  • windows下静态使用QxOrm框架并使用泛型编程 (一)

    开发环境
    Qt 5.6 for Desktop
    QxOrm版本为1.46 下载链接https://github.com/QxOrm/QxOrm

    下载完成以后QxOrm文件夹里会有简单的说明文档doc/index.html 不过有些方法是比较过时的,于是我测试了许久才测试好的框架代码。
    下载完成后打开QtCreator,选择新建项目选择 其他项目/子目录项目 类似下图的建立

    然后进入该项目的文件夹路径即200-300.pro所在路径 将QxOrm整个文件夹复制进来
    接着在200-300.pro文件中改为
    TEMPLATE = subdirsSUBDIRS += \ QxOrm这样就可以看到QxOrm项目成功加入到整体项目中了。而程序需要主项目作为运行所以我们右键200-300新建一个Application/Qt Widgets Application 我这边就叫Demo好了 。接着右键Demo添加库/外部库 库文件选择QxOrm/lib/libQxOrm.a 链接选择静态然后点下一步完成
    注意:需要在Demo.pro文件中加入QT += sql 这句话是将数据库加入到项目中去。
    目前环境就算是搭好了。下一篇继续讲如何使用。
    1 留言 2019-07-23 14:37:42 奖励8点积分
  • 【Cocos Creator实战教程(4)】——炸弹人(TiledMap相关) 精华

    1. 相关知识点
    虚拟手柄(新)
    地图制作
    碰撞检测
    动画制作

    2. 步骤2.1 制作地图2.1.1.新建19x19的地图,Tile大小32x32,导入图块资源2.1.2 建立三个图层(ground,hide,main)和一个对象层(objects)ground是背景层,用绿色的草坪图块填充满

    hide是道具层,挑选道具图块(包括出口的门)放在几个位置

    main是障碍物层,还原经典炸弹人的地图布局,每隔一个位置放一个钢块,用土块覆盖道具层的道具,在其他位置再放几个土块

    objects层有两个对象(player,enemy)标记玩家和敌人的位置信息
    最终效果如图

    2.1.3 将除了草坪图块的其他图块添加type属性,属性值分别为,soil(土块),steel(钢块),door(门),prop1(道具1)…
    2.1.4 保存地图文件和图集文件放在一起2.2 绑定地图2.2.1 新建工程Bomberman,将所需资源全部导入,项目结构如下(有些是后来添加的)
    2.2.2 将地图拖入场景(自动生成map和layer节点),将锚点改为(0,0),下面出现的player等节点也都是(0,0)为锚点2.2.3 新建脚本game.js添加为map节点组件,声明全局变量,在属性面板中拖入相关层节点
    2.3 虚拟手柄2.3.1 我们准备了上下左右炸弹五个按钮的原始状态和按下状态共十张图片素材,在场景编辑器中看着顺眼的地方摆放这五个按钮(将label删除),并将按钮的相应状态的按钮图片添加到右侧属性面板中2.3.2 为每个按钮添加点击事件,(map->game->btnBomb, btnUp, btnDown, btnLeft, btnRight)
    2.4 炸弹动画2.4.1 将炸弹图片集中的任意一个拖入场景中,将其宽高改为32x32,再拖回资源管理器中制作一个炸弹的prefab bomb,为其添加Animation组件2.4.2 新建炸弹爆炸动画clip explode,拖入bomb的属性面板2.4.3 在动画播放完成的最后一帧加入一个帧事件,响应事件方法名为exploded
    2.5 主角和敌人2.5.1 添加player和enemy为map节点的两个子节点,锚点0,0,大小32x32 (位置随意,一会我们要在代码中动态设置他们的位置)
    3. 总结
    虚拟手柄是一个比较热点的控制,现实生活中还会有圆圈控制各个角度的,也有开源项目大家可以参考
    注意节点前后层的遮挡关系

    注意:本教程部分素材来源于网络,另请大家在评论区踊跃提问发言。
    7 留言 2018-11-24 23:12:26 奖励20点积分
显示 0 到 15 ,共 15 条
eject